1import type { OrchestrationV2TurnItem, OrchestrationV2TurnItemStatus } from "@t3tools/contracts";
2import * as Option from "effect/Option";
3import * as Schema from "effect/Schema";
4
5import type { MuseItem } from "./protocol.ts";
6
7const decodeToolArgs = Schema.decodeUnknownSync(
8 Schema.fromJsonString(Schema.Record(Schema.String, Schema.Unknown)),
9);
10/** Muse's `web_search` tool output: the query and its hits, as JSON. */
11const decodeWebSearchOutput = Schema.decodeUnknownOption(
12 Schema.fromJsonString(
13 Schema.Struct({
14 results: Schema.Array(
15 Schema.Struct({
16 url: Schema.optional(Schema.String),
17 title: Schema.optional(Schema.String),
18 snippet: Schema.optional(Schema.String),
19 }),
20 ),
21 }),
22 ),
23);
24
25type Fields<
26 T extends OrchestrationV2TurnItem["type"],
27 K extends keyof Extract<OrchestrationV2TurnItem, { type: T }>,
28> = Pick<Extract<OrchestrationV2TurnItem, { type: T }>, K>;
29type MuseToolPresentation = {
30 readonly status: OrchestrationV2TurnItemStatus;
31 readonly title: string | null;
32} & (
33 | Fields<"command_execution", "type" | "input" | "output" | "exitCode">
34 | Fields<"file_change", "type" | "fileName" | "oldStr" | "newStr" | "diffStr">
35 | Fields<"web_search", "type" | "patterns" | "results">
36 | Fields<"dynamic_tool", "type" | "toolName" | "input" | "output">
37);
38
39/** Unrecognized native terminal states must not appear as successful work. */
40export function museItemStatus(item: MuseItem): OrchestrationV2TurnItemStatus {
41 if (item.status === "inProgress") return "running";
42 if (item.status === "cancelled") return "cancelled";
43 if (
44 item.status === "completed" &&
45 !(item.kind === "compaction" && item.outcome && item.outcome !== "compacted")
46 )
47 return "completed";
48 return "failed";
49}
50
51/** Converts native tool details into the shared timeline fields used by every client. */
52export function museToolPresentation(
53 item: MuseItem,
54 status = museItemStatus(item),
55): MuseToolPresentation {
56 let args: Record<string, unknown> | undefined;
57 if (item.args) {
58 try {
59 args = decodeToolArgs(item.args);
60 } catch {
61 // Streaming model-authored arguments may not be valid JSON yet.
62 }
63 }
64 const stringArg = (...keys: string[]) => {
65 for (const key of keys) {
66 const value = args?.[key];
67 if (typeof value === "string") return value;
68 }
69 return undefined;
70 };
71 const toolName = item.tool ?? item.kind;
72 const title =
73 item.kind === "subagent"
74 ? item.objective || item.role || "Muse agent"
75 : item.kind === "workflow"
76 ? item.entryId || "Workflow"
77 : toolName;
78 const detail =
79 status === "failed" ? item.failureReason || `Muse reported ${item.status}.` : undefined;
80 const shared = { status, title: detail ? `${title}: ${detail}` : title };
81 const text =
82 item.visibleOutput ??
83 item.result?.summary ??
84 item.text ??
85 item.summary?.join("\n") ??
86 item.finalSummary ??
87 item.fallbackText ??
88 item.objective ??
89 "";
90 const output = detail ? [detail, text].filter(Boolean).join("\n") : text;
91
92 if (item.kind === "userShell" || /shell|bash|exec_command/i.test(item.tool ?? "")) {
93 return {
94 ...shared,
95 type: "command_execution",
96 input: item.commandText ?? stringArg("command", "cmd") ?? item.args ?? "",
97 output,
98 ...(item.exitCode === undefined ? {} : { exitCode: item.exitCode }),
99 };
100 }
101 if (item.kind === "toolCall" && /write|edit|patch/i.test(toolName)) {
102 const fileName = stringArg("file_path", "path", "filePath")?.trim();
103 if (fileName) {
104 const oldStr = stringArg("old_string", "old_str", "oldStr", "oldText");
105 const newStr = stringArg("new_string", "new_str", "newStr", "newText", "content");
106 const diffStr = stringArg("diff", "patch", "diffStr");
107 return {
108 ...shared,
109 type: "file_change",
110 fileName,
111 ...(oldStr === undefined ? {} : { oldStr }),
112 ...(newStr === undefined ? {} : { newStr }),
113 ...(diffStr === undefined ? {} : { diffStr }),
114 };
115 }
116 }
117 if (item.kind === "toolCall" && /web|browse|fetch/i.test(toolName)) {
118 const query = stringArg("query", "url", "pattern")?.trim();
119 const pageUrl = stringArg("url")?.trim();
120 // A search lists its hits; a page fetch (or a failure) shows its text under the page's link.
121 const hits =
122 item.tool === "web_search" && !detail
123 ? Option.getOrUndefined(decodeWebSearchOutput(text))?.results.map(
124 ({ url, title, snippet }) => ({
125 ...(url?.trim() ? { url: url.trim() } : {}),
126 ...(title?.trim() ? { title: title.trim() } : {}),
127 ...(snippet ? { snippet } : {}),
128 }),
129 )
130 : undefined;
131 const results =
132 hits ?? (output ? [{ ...(pageUrl ? { url: pageUrl } : {}), snippet: output }] : []);
133 return {
134 ...shared,
135 type: "web_search",
136 ...(query ? { patterns: [query] } : {}),
137 ...(results.length ? { results } : {}),
138 };
139 }
140 return {
141 ...shared,
142 type: "dynamic_tool",
143 toolName,
144 input: args ?? item.args ?? "",
145 output,
146 };
147}