Merge branch 'main' of github.com:getpaseo/paseo

This commit is contained in:
Mohamed Boudra
2026-02-12 18:04:13 +07:00
17 changed files with 1610 additions and 872 deletions
@@ -177,7 +177,7 @@ export function ToolCallDetailsContent({
if (plainInputText !== null) { if (plainInputText !== null) {
sections.push( sections.push(
<View key="unknown-plain-text" style={styles.plainTextSection}> <View key="unknown-plain-text" style={styles.plainTextSection}>
<Text selectable style={styles.scrollText}>{plainInputText}</Text> <Text selectable style={styles.plainText}>{plainInputText}</Text>
</View> </View>
); );
} else { } else {
@@ -298,6 +298,13 @@ const styles = StyleSheet.create((theme) => {
gap: theme.spacing[2], gap: theme.spacing[2],
padding: theme.spacing[3], padding: theme.spacing[3],
}, },
plainText: {
fontFamily: Fonts.sans,
fontSize: theme.fontSize.base,
color: theme.colors.foreground,
lineHeight: 22,
overflowWrap: "anywhere",
},
sectionTitle: { sectionTitle: {
color: theme.colors.foregroundMuted, color: theme.colors.foregroundMuted,
fontSize: theme.fontSize.xs, fontSize: theme.fontSize.xs,
+13 -1
View File
@@ -1,5 +1,14 @@
import type { ComponentType } from "react"; import type { ComponentType } from "react";
import { Bot, Brain, Eye, Pencil, Search, SquareTerminal, Wrench } from "lucide-react-native"; import {
Bot,
Brain,
Eye,
MicVocal,
Pencil,
Search,
SquareTerminal,
Wrench,
} from "lucide-react-native";
import type { ToolCallDetail } from "@server/server/agent/agent-sdk-types"; import type { ToolCallDetail } from "@server/server/agent/agent-sdk-types";
export type ToolCallIconComponent = ComponentType<{ size?: number; color?: string }>; export type ToolCallIconComponent = ComponentType<{ size?: number; color?: string }>;
@@ -20,6 +29,9 @@ export function resolveToolCallIcon(toolName: string, detail?: ToolCallDetail):
if (lowerName === "thinking" && (!detail || detail.type === "unknown")) { if (lowerName === "thinking" && (!detail || detail.type === "unknown")) {
return Brain; return Brain;
} }
if (lowerName === "speak") {
return MicVocal;
}
if (detail) { if (detail) {
return TOOL_DETAIL_ICONS[detail.type]; return TOOL_DETAIL_ICONS[detail.type];
@@ -1300,16 +1300,21 @@ class ClaudeAgentSession implements AgentSession {
} }
this.activeSidechains.set(parentToolUseId, toolName); this.activeSidechains.set(parentToolUseId, toolName);
const toolCall = mapClaudeRunningToolCall({
name: "Task",
callId: parentToolUseId,
input: null,
output: null,
metadata: { subAgentActivity: toolName },
});
if (!toolCall) {
return [];
}
return [ return [
{ {
type: "timeline", type: "timeline",
item: mapClaudeRunningToolCall({ item: toolCall,
name: "Task",
callId: parentToolUseId,
input: null,
output: null,
metadata: { subAgentActivity: toolName },
}),
provider: "claude", provider: "claude",
}, },
]; ];
@@ -1497,14 +1502,14 @@ class ClaudeAgentSession implements AgentSession {
if (toolName === "ExitPlanMode" && typeof input.plan === "string") { if (toolName === "ExitPlanMode" && typeof input.plan === "string") {
metadata.planText = input.plan; metadata.planText = input.plan;
} }
const detail = const toolDetail =
kind === "tool" kind === "tool"
? mapClaudeRunningToolCall({ ? mapClaudeRunningToolCall({
name: toolName, name: toolName,
callId: options.toolUseID ?? requestId, callId: options.toolUseID ?? requestId,
input, input,
output: null, output: null,
}).detail })?.detail
: undefined; : undefined;
const request: AgentPermissionRequest = { const request: AgentPermissionRequest = {
@@ -1513,7 +1518,7 @@ class ClaudeAgentSession implements AgentSession {
name: toolName, name: toolName,
kind, kind,
input, input,
detail, detail: toolDetail,
suggestions: options.suggestions?.map((suggestion) => ({ ...suggestion })), suggestions: options.suggestions?.map((suggestion) => ({ ...suggestion })),
metadata: Object.keys(metadata).length ? metadata : undefined, metadata: Object.keys(metadata).length ? metadata : undefined,
}; };
@@ -1591,9 +1596,12 @@ class ClaudeAgentSession implements AgentSession {
} }
private pushToolCall( private pushToolCall(
item: Extract<AgentTimelineItem, { type: "tool_call" }>, item: Extract<AgentTimelineItem, { type: "tool_call" }> | null,
target?: AgentTimelineItem[] target?: AgentTimelineItem[]
) { ) {
if (!item) {
return;
}
if (target) { if (target) {
target.push(item); target.push(item);
return; return;
@@ -19,15 +19,58 @@ import {
toolDetailBranchByName, toolDetailBranchByName,
} from "../tool-call-detail-primitives.js"; } from "../tool-call-detail-primitives.js";
const ClaudeKnownToolDetailSchema = z.union([ const ClaudeToolEnvelopeSchema = z
.object({
name: z.string().min(1),
input: z.unknown().nullable(),
output: z.unknown().nullable(),
})
.passthrough();
const ClaudeSpeakToolDetailSchema = z
.object({
name: z.literal("speak"),
input: z
.union([
z.string().transform((text) => ({ text })),
z.object({ text: z.string() }).passthrough(),
])
.nullable(),
output: z.unknown().nullable(),
})
.transform(({ input }) => {
const text = input?.text?.trim() ?? "";
if (!text) {
return undefined;
}
return {
type: "unknown",
input: text,
output: null,
} satisfies ToolCallDetail;
});
const ClaudeToolDetailPass2Schema = z.union([
toolDetailBranchByName("Bash", ToolShellInputSchema, ToolShellOutputSchema, toShellToolDetail), toolDetailBranchByName("Bash", ToolShellInputSchema, ToolShellOutputSchema, toShellToolDetail),
toolDetailBranchByName("bash", ToolShellInputSchema, ToolShellOutputSchema, toShellToolDetail), toolDetailBranchByName("bash", ToolShellInputSchema, ToolShellOutputSchema, toShellToolDetail),
toolDetailBranchByName("shell", ToolShellInputSchema, ToolShellOutputSchema, toShellToolDetail), toolDetailBranchByName("shell", ToolShellInputSchema, ToolShellOutputSchema, toShellToolDetail),
toolDetailBranchByName("exec_command", ToolShellInputSchema, ToolShellOutputSchema, toShellToolDetail), toolDetailBranchByName("exec_command", ToolShellInputSchema, ToolShellOutputSchema, toShellToolDetail),
toolDetailBranchByName("Read", ToolReadInputSchema, ToolReadOutputSchema, toReadToolDetail), toolDetailBranchByName("Read", ToolReadInputSchema, z.unknown(), (input, output) => {
toolDetailBranchByName("read", ToolReadInputSchema, ToolReadOutputSchema, toReadToolDetail), const parsedOutput = ToolReadOutputSchema.safeParse(output);
toolDetailBranchByName("read_file", ToolReadInputSchema, ToolReadOutputSchema, toReadToolDetail), return toReadToolDetail(input, parsedOutput.success ? parsedOutput.data : null);
toolDetailBranchByName("view_file", ToolReadInputSchema, ToolReadOutputSchema, toReadToolDetail), }),
toolDetailBranchByName("read", ToolReadInputSchema, z.unknown(), (input, output) => {
const parsedOutput = ToolReadOutputSchema.safeParse(output);
return toReadToolDetail(input, parsedOutput.success ? parsedOutput.data : null);
}),
toolDetailBranchByName("read_file", ToolReadInputSchema, z.unknown(), (input, output) => {
const parsedOutput = ToolReadOutputSchema.safeParse(output);
return toReadToolDetail(input, parsedOutput.success ? parsedOutput.data : null);
}),
toolDetailBranchByName("view_file", ToolReadInputSchema, z.unknown(), (input, output) => {
const parsedOutput = ToolReadOutputSchema.safeParse(output);
return toReadToolDetail(input, parsedOutput.success ? parsedOutput.data : null);
}),
toolDetailBranchByName("Write", ToolWriteInputSchema, ToolWriteOutputSchema, toWriteToolDetail), toolDetailBranchByName("Write", ToolWriteInputSchema, ToolWriteOutputSchema, toWriteToolDetail),
toolDetailBranchByName("write", ToolWriteInputSchema, ToolWriteOutputSchema, toWriteToolDetail), toolDetailBranchByName("write", ToolWriteInputSchema, ToolWriteOutputSchema, toWriteToolDetail),
toolDetailBranchByName("write_file", ToolWriteInputSchema, ToolWriteOutputSchema, toWriteToolDetail), toolDetailBranchByName("write_file", ToolWriteInputSchema, ToolWriteOutputSchema, toWriteToolDetail),
@@ -38,7 +81,12 @@ const ClaudeKnownToolDetailSchema = z.union([
toolDetailBranchByName("edit", ToolEditInputSchema, ToolEditOutputSchema, toEditToolDetail), toolDetailBranchByName("edit", ToolEditInputSchema, ToolEditOutputSchema, toEditToolDetail),
toolDetailBranchByName("apply_patch", ToolEditInputSchema, ToolEditOutputSchema, toEditToolDetail), toolDetailBranchByName("apply_patch", ToolEditInputSchema, ToolEditOutputSchema, toEditToolDetail),
toolDetailBranchByName("apply_diff", ToolEditInputSchema, ToolEditOutputSchema, toEditToolDetail), toolDetailBranchByName("apply_diff", ToolEditInputSchema, ToolEditOutputSchema, toEditToolDetail),
toolDetailBranchByName("str_replace_editor", ToolEditInputSchema, ToolEditOutputSchema, toEditToolDetail), toolDetailBranchByName(
"str_replace_editor",
ToolEditInputSchema,
ToolEditOutputSchema,
toEditToolDetail
),
toolDetailBranchByName("WebSearch", ToolSearchInputSchema, z.unknown(), (input) => toolDetailBranchByName("WebSearch", ToolSearchInputSchema, z.unknown(), (input) =>
toSearchToolDetail(input) toSearchToolDetail(input)
), ),
@@ -48,6 +96,7 @@ const ClaudeKnownToolDetailSchema = z.union([
toolDetailBranchByName("search", ToolSearchInputSchema, z.unknown(), (input) => toolDetailBranchByName("search", ToolSearchInputSchema, z.unknown(), (input) =>
toSearchToolDetail(input) toSearchToolDetail(input)
), ),
ClaudeSpeakToolDetailSchema,
]); ]);
export function deriveClaudeToolDetail( export function deriveClaudeToolDetail(
@@ -55,17 +104,27 @@ export function deriveClaudeToolDetail(
input: unknown, input: unknown,
output: unknown output: unknown
): ToolCallDetail { ): ToolCallDetail {
const parsed = ClaudeKnownToolDetailSchema.safeParse({ const pass1 = ClaudeToolEnvelopeSchema.safeParse({
name, name,
input,
output,
});
if (parsed.success && parsed.data) {
return parsed.data;
}
return {
type: "unknown",
input: input ?? null, input: input ?? null,
output: output ?? null, output: output ?? null,
});
if (!pass1.success) {
return {
type: "unknown",
input: input ?? null,
output: output ?? null,
};
}
const pass2 = ClaudeToolDetailPass2Schema.safeParse(pass1.data);
if (pass2.success && pass2.data) {
return pass2.data;
}
return {
type: "unknown",
input: pass1.data.input,
output: pass1.data.output,
}; };
} }
@@ -6,14 +6,24 @@ import {
mapClaudeRunningToolCall, mapClaudeRunningToolCall,
} from "./tool-call-mapper.js"; } from "./tool-call-mapper.js";
function expectMapped<T>(item: T | null): T {
expect(item).toBeTruthy();
if (!item) {
throw new Error("Expected mapped tool call");
}
return item;
}
describe("claude tool-call mapper", () => { describe("claude tool-call mapper", () => {
it("maps running shell calls with canonical fields", () => { it("maps running shell calls with canonical fields", () => {
const item = mapClaudeRunningToolCall({ const item = expectMapped(
callId: "claude-call-1", mapClaudeRunningToolCall({
name: "Bash", callId: "claude-call-1",
input: { command: "pwd", cwd: "/tmp/repo" }, name: "Bash",
output: null, input: { command: "pwd", cwd: "/tmp/repo" },
}); output: null,
})
);
expect(item.type).toBe("tool_call"); expect(item.type).toBe("tool_call");
expect(item.status).toBe("running"); expect(item.status).toBe("running");
@@ -27,45 +37,53 @@ describe("claude tool-call mapper", () => {
}); });
it("maps running known tool variants with detail for early summaries", () => { it("maps running known tool variants with detail for early summaries", () => {
const readItem = mapClaudeRunningToolCall({ const readItem = expectMapped(
callId: "claude-running-read", mapClaudeRunningToolCall({
name: "read_file", callId: "claude-running-read",
input: { file_path: "README.md" }, name: "read_file",
output: null, input: { file_path: "README.md" },
}); output: null,
})
);
expect(readItem.detail).toEqual({ expect(readItem.detail).toEqual({
type: "read", type: "read",
filePath: "README.md", filePath: "README.md",
}); });
const writeItem = mapClaudeRunningToolCall({ const writeItem = expectMapped(
callId: "claude-running-write", mapClaudeRunningToolCall({
name: "write_file", callId: "claude-running-write",
input: { file_path: "src/new.ts" }, name: "write_file",
output: null, input: { file_path: "src/new.ts" },
}); output: null,
})
);
expect(writeItem.detail).toEqual({ expect(writeItem.detail).toEqual({
type: "write", type: "write",
filePath: "src/new.ts", filePath: "src/new.ts",
}); });
const editItem = mapClaudeRunningToolCall({ const editItem = expectMapped(
callId: "claude-running-edit", mapClaudeRunningToolCall({
name: "apply_patch", callId: "claude-running-edit",
input: { file_path: "src/index.ts" }, name: "apply_patch",
output: null, input: { file_path: "src/index.ts" },
}); output: null,
})
);
expect(editItem.detail).toEqual({ expect(editItem.detail).toEqual({
type: "edit", type: "edit",
filePath: "src/index.ts", filePath: "src/index.ts",
}); });
const searchItem = mapClaudeRunningToolCall({ const searchItem = expectMapped(
callId: "claude-running-search", mapClaudeRunningToolCall({
name: "web_search", callId: "claude-running-search",
input: { query: "tool call mapping" }, name: "web_search",
output: null, input: { query: "tool call mapping" },
}); output: null,
})
);
expect(searchItem.detail).toEqual({ expect(searchItem.detail).toEqual({
type: "search", type: "search",
query: "tool call mapping", query: "tool call mapping",
@@ -73,12 +91,14 @@ describe("claude tool-call mapper", () => {
}); });
it("maps completed read calls with detail enrichment", () => { it("maps completed read calls with detail enrichment", () => {
const item = mapClaudeCompletedToolCall({ const item = expectMapped(
callId: "claude-call-2", mapClaudeCompletedToolCall({
name: "read_file", callId: "claude-call-2",
input: { file_path: "README.md" }, name: "read_file",
output: { content: "hello" }, input: { file_path: "README.md" },
}); output: { content: "hello" },
})
);
expect(item.status).toBe("completed"); expect(item.status).toBe("completed");
expect(item.error).toBeNull(); expect(item.error).toBeNull();
@@ -91,33 +111,37 @@ describe("claude tool-call mapper", () => {
}); });
it("preserves read content from array/object output variants", () => { it("preserves read content from array/object output variants", () => {
const arrayContent = mapClaudeCompletedToolCall({ const arrayContent = expectMapped(
callId: "claude-read-array", mapClaudeCompletedToolCall({
name: "read_file", callId: "claude-read-array",
input: { file_path: "README.md" }, name: "read_file",
output: { input: { file_path: "README.md" },
content: [ output: {
{ type: "output_text", text: "alpha" }, content: [
{ type: "output_text", content: "beta" }, { type: "output_text", text: "alpha" },
], { type: "output_text", content: "beta" },
}, ],
}); },
})
);
expect(arrayContent.detail?.type).toBe("read"); expect(arrayContent.detail?.type).toBe("read");
if (arrayContent.detail?.type === "read") { if (arrayContent.detail?.type === "read") {
expect(arrayContent.detail.content).toBe("alpha\nbeta"); expect(arrayContent.detail.content).toBe("alpha\nbeta");
} }
const objectContent = mapClaudeCompletedToolCall({ const objectContent = expectMapped(
callId: "claude-read-object", mapClaudeCompletedToolCall({
name: "read_file", callId: "claude-read-object",
input: { file_path: "README.md" }, name: "read_file",
output: { input: { file_path: "README.md" },
structured_content: { output: {
content: { type: "output_text", text: "gamma" }, structured_content: {
content: { type: "output_text", text: "gamma" },
},
}, },
}, })
}); );
expect(objectContent.detail?.type).toBe("read"); expect(objectContent.detail?.type).toBe("read");
if (objectContent.detail?.type === "read") { if (objectContent.detail?.type === "read") {
@@ -126,13 +150,15 @@ describe("claude tool-call mapper", () => {
}); });
it("maps failed calls with required error", () => { it("maps failed calls with required error", () => {
const item = mapClaudeFailedToolCall({ const item = expectMapped(
callId: "claude-call-3", mapClaudeFailedToolCall({
name: "shell", callId: "claude-call-3",
input: { command: "false" }, name: "shell",
output: null, input: { command: "false" },
error: { message: "Command failed" }, output: null,
}); error: { message: "Command failed" },
})
);
expect(item.status).toBe("failed"); expect(item.status).toBe("failed");
expect(item.error).toEqual({ message: "Command failed" }); expect(item.error).toEqual({ message: "Command failed" });
@@ -140,35 +166,41 @@ describe("claude tool-call mapper", () => {
}); });
it("maps write/edit/search known shapes with distinct detail types", () => { it("maps write/edit/search known shapes with distinct detail types", () => {
const writeItem = mapClaudeCompletedToolCall({ const writeItem = expectMapped(
callId: "claude-write-1", mapClaudeCompletedToolCall({
name: "write_file", callId: "claude-write-1",
input: { file_path: "src/new.ts", content: "export const x = 1;" }, name: "write_file",
output: null, input: { file_path: "src/new.ts", content: "export const x = 1;" },
}); output: null,
})
);
expect(writeItem.detail?.type).toBe("write"); expect(writeItem.detail?.type).toBe("write");
if (writeItem.detail?.type === "write") { if (writeItem.detail?.type === "write") {
expect(writeItem.detail.filePath).toBe("src/new.ts"); expect(writeItem.detail.filePath).toBe("src/new.ts");
} }
const editItem = mapClaudeCompletedToolCall({ const editItem = expectMapped(
callId: "claude-edit-1", mapClaudeCompletedToolCall({
name: "apply_patch", callId: "claude-edit-1",
input: { file_path: "src/index.ts", patch: "@@\\n-old\\n+new\\n" }, name: "apply_patch",
output: null, input: { file_path: "src/index.ts", patch: "@@\\n-old\\n+new\\n" },
}); output: null,
})
);
expect(editItem.detail?.type).toBe("edit"); expect(editItem.detail?.type).toBe("edit");
if (editItem.detail?.type === "edit") { if (editItem.detail?.type === "edit") {
expect(editItem.detail.filePath).toBe("src/index.ts"); expect(editItem.detail.filePath).toBe("src/index.ts");
expect(editItem.detail.unifiedDiff).toContain("@@"); expect(editItem.detail.unifiedDiff).toContain("@@");
} }
const searchItem = mapClaudeCompletedToolCall({ const searchItem = expectMapped(
callId: "claude-search-1", mapClaudeCompletedToolCall({
name: "web_search", callId: "claude-search-1",
input: { query: "tool call mapping" }, name: "web_search",
output: null, input: { query: "tool call mapping" },
}); output: null,
})
);
expect(searchItem.detail).toEqual({ expect(searchItem.detail).toEqual({
type: "search", type: "search",
query: "tool call mapping", query: "tool call mapping",
@@ -176,12 +208,14 @@ describe("claude tool-call mapper", () => {
}); });
it("maps unknown tools to unknown detail with raw payloads", () => { it("maps unknown tools to unknown detail with raw payloads", () => {
const item = mapClaudeCompletedToolCall({ const item = expectMapped(
callId: "claude-call-4", mapClaudeCompletedToolCall({
name: "my_custom_tool", callId: "claude-call-4",
input: { foo: "bar" }, name: "my_custom_tool",
output: { ok: true }, input: { foo: "bar" },
}); output: { ok: true },
})
);
expect(item.status).toBe("completed"); expect(item.status).toBe("completed");
expect(item.error).toBeNull(); expect(item.error).toBeNull();
@@ -191,4 +225,33 @@ describe("claude tool-call mapper", () => {
output: { ok: true }, output: { ok: true },
}); });
}); });
it("normalizes claude speak tool names through schema transforms", () => {
const item = expectMapped(
mapClaudeCompletedToolCall({
callId: "claude-speak-1",
name: "mcp__paseo__speak",
input: { text: "Voice response from Claude." },
output: { ok: true },
})
);
expect(item.name).toBe("speak");
expect(item.detail).toEqual({
type: "unknown",
input: "Voice response from Claude.",
output: null,
});
});
it("drops tool calls when callId is missing", () => {
const item = mapClaudeCompletedToolCall({
callId: null,
name: "read_file",
input: { file_path: "README.md" },
output: { content: "hello" },
});
expect(item).toBeNull();
});
}); });
@@ -1,7 +1,6 @@
import { z } from "zod"; import { z } from "zod";
import type { ToolCallTimelineItem } from "../../agent-sdk-types.js"; import type { ToolCallTimelineItem } from "../../agent-sdk-types.js";
import { coerceToolCallId } from "../tool-call-mapper-utils.js";
import { deriveClaudeToolDetail } from "./tool-call-detail-parser.js"; import { deriveClaudeToolDetail } from "./tool-call-detail-parser.js";
type MapperParams = { type MapperParams = {
@@ -12,87 +11,170 @@ type MapperParams = {
metadata?: Record<string, unknown>; metadata?: Record<string, unknown>;
}; };
const ClaudeMapperParamsSchema = z const ClaudeToolCallStatusSchema = z.enum([
"running",
"completed",
"failed",
"canceled",
]);
const ClaudeRawToolCallSchema = z
.object({ .object({
callId: z.string().optional().nullable(), callId: z.string().optional().nullable(),
name: z.string().min(1), name: z.string().min(1),
input: z.unknown().optional(), input: z.unknown().optional(),
output: z.unknown().optional(), output: z.unknown().optional(),
metadata: z.record(z.unknown()).optional(), metadata: z.record(z.string(), z.unknown()).optional(),
error: z.unknown().nullable().optional(),
status: ClaudeToolCallStatusSchema,
}) })
.passthrough(); .passthrough();
const ClaudeFailedMapperParamsSchema = ClaudeMapperParamsSchema.extend({ const ClaudeToolCallPass1Schema = ClaudeRawToolCallSchema.transform((raw) => ({
error: z.unknown(), callId:
typeof raw.callId === "string" && raw.callId.trim().length > 0
? raw.callId
: null,
name: raw.name.trim(),
input: raw.input ?? null,
output: raw.output ?? null,
metadata: raw.metadata,
error: raw.error ?? null,
status: raw.status,
}));
const ClaudeToolCallPass2BaseSchema = z.object({
callId: z.string().min(1),
name: z.string().min(1),
input: z.unknown().nullable(),
output: z.unknown().nullable(),
metadata: z.record(z.string(), z.unknown()).optional(),
error: z.unknown().nullable(),
status: ClaudeToolCallStatusSchema,
toolKind: z.enum(["speak", "other"]),
}); });
function coerceCallId(callId: string | null | undefined, name: string, input: unknown): string { const ClaudeToolCallPass2InputSchema = ClaudeToolCallPass2BaseSchema.omit({
return coerceToolCallId({ toolKind: true,
providerPrefix: "claude", });
rawCallId: callId,
toolName: name, const ClaudeToolCallPass2EnvelopeSchema = z.union([
input, ClaudeToolCallPass2InputSchema.extend({
name: z.literal("mcp__paseo__speak"),
}).transform((normalized) => ({
...normalized,
name: normalized.name.trim(),
toolKind: "speak" as const,
})),
ClaudeToolCallPass2InputSchema.transform((normalized) => ({
...normalized,
name: normalized.name.trim(),
toolKind: "other" as const,
})),
]);
const ClaudeToolCallPass2Schema = z.discriminatedUnion("toolKind", [
ClaudeToolCallPass2BaseSchema.extend({
toolKind: z.literal("speak"),
name: z.literal("mcp__paseo__speak"),
}).transform((normalized): ToolCallTimelineItem => {
const name = "speak" as const;
const detail = deriveClaudeToolDetail(name, normalized.input, normalized.output);
if (normalized.status === "failed") {
return {
type: "tool_call",
callId: normalized.callId,
name,
detail,
status: "failed",
error: normalized.error ?? { message: "Tool call failed" },
...(normalized.metadata ? { metadata: normalized.metadata } : {}),
};
}
return {
type: "tool_call",
callId: normalized.callId,
name,
detail,
status: normalized.status,
error: null,
...(normalized.metadata ? { metadata: normalized.metadata } : {}),
};
}),
ClaudeToolCallPass2BaseSchema.extend({
toolKind: z.literal("other"),
}).transform((normalized): ToolCallTimelineItem => {
const detail = deriveClaudeToolDetail(normalized.name, normalized.input, normalized.output);
if (normalized.status === "failed") {
return {
type: "tool_call",
callId: normalized.callId,
name: normalized.name,
detail,
status: "failed",
error: normalized.error ?? { message: "Tool call failed" },
...(normalized.metadata ? { metadata: normalized.metadata } : {}),
};
}
return {
type: "tool_call",
callId: normalized.callId,
name: normalized.name,
detail,
status: normalized.status,
error: null,
...(normalized.metadata ? { metadata: normalized.metadata } : {}),
};
}),
]);
function mapClaudeToolCall(
params: MapperParams,
status: z.infer<typeof ClaudeToolCallStatusSchema>,
error: unknown | null
): ToolCallTimelineItem | null {
const pass1 = ClaudeToolCallPass1Schema.safeParse({
...params,
status,
error,
}); });
if (!pass1.success) {
return null;
}
const pass2Envelope = ClaudeToolCallPass2EnvelopeSchema.safeParse(pass1.data);
if (!pass2Envelope.success) {
return null;
}
const pass2 = ClaudeToolCallPass2Schema.safeParse(pass2Envelope.data);
if (!pass2.success) {
return null;
}
return pass2.data;
} }
function buildBase(params: MapperParams): { export function mapClaudeRunningToolCall(
callId: string; params: MapperParams
name: string; ): ToolCallTimelineItem | null {
detail: Extract<ToolCallTimelineItem, { type: "tool_call" }>["detail"]; return mapClaudeToolCall(params, "running", null);
metadata?: Record<string, unknown>;
} {
const parsedParams = ClaudeMapperParamsSchema.parse(params);
const input = parsedParams.input ?? null;
const output = parsedParams.output ?? null;
const detail = deriveClaudeToolDetail(parsedParams.name, input, output);
return {
callId: coerceCallId(parsedParams.callId, parsedParams.name, input),
name: parsedParams.name,
detail,
...(parsedParams.metadata ? { metadata: parsedParams.metadata } : {}),
};
} }
export function mapClaudeRunningToolCall(params: MapperParams): ToolCallTimelineItem { export function mapClaudeCompletedToolCall(
const base = buildBase(params); params: MapperParams
return { ): ToolCallTimelineItem | null {
type: "tool_call", return mapClaudeToolCall(params, "completed", null);
...base,
status: "running",
error: null,
};
}
export function mapClaudeCompletedToolCall(params: MapperParams): ToolCallTimelineItem {
const base = buildBase(params);
return {
type: "tool_call",
...base,
status: "completed",
error: null,
};
} }
export function mapClaudeFailedToolCall( export function mapClaudeFailedToolCall(
params: MapperParams & { error: unknown } params: MapperParams & { error: unknown }
): ToolCallTimelineItem { ): ToolCallTimelineItem | null {
const parsedParams = ClaudeFailedMapperParamsSchema.parse(params); return mapClaudeToolCall(params, "failed", params.error);
const base = buildBase(parsedParams);
return {
type: "tool_call",
...base,
status: "failed",
error: parsedParams.error,
};
} }
export function mapClaudeCanceledToolCall(params: MapperParams): ToolCallTimelineItem { export function mapClaudeCanceledToolCall(
const base = buildBase(params); params: MapperParams
return { ): ToolCallTimelineItem | null {
type: "tool_call", return mapClaudeToolCall(params, "canceled", null);
...base,
status: "canceled",
error: null,
};
} }
@@ -982,6 +982,9 @@ function mapCodexExecNotificationToToolCall(params: {
: null, : null,
cwd: params.cwd ?? null, cwd: params.cwd ?? null,
}); });
if (!mapped) {
return null;
}
return params.running ? toRunningToolCall(mapped) : mapped; return params.running ? toRunningToolCall(mapped) : mapped;
} }
@@ -993,7 +996,7 @@ function mapCodexPatchNotificationToToolCall(params: {
stderr?: string | null; stderr?: string | null;
success?: boolean | null; success?: boolean | null;
running: boolean; running: boolean;
}): ToolCallTimelineItem { }): ToolCallTimelineItem | null {
const files = parseCodexPatchChanges(params.changes); const files = parseCodexPatchChanges(params.changes);
const firstPath = files[0]?.path; const firstPath = files[0]?.path;
const firstPatchText = files const firstPatchText = files
@@ -1038,6 +1041,9 @@ function mapCodexPatchNotificationToToolCall(params: {
: { message: params.stderr?.trim() || "Patch apply failed" }, : { message: params.stderr?.trim() || "Patch apply failed" },
cwd: params.cwd ?? null, cwd: params.cwd ?? null,
}); });
if (!mapped) {
return null;
}
return params.running ? toRunningToolCall(mapped) : mapped; return params.running ? toRunningToolCall(mapped) : mapped;
} }
@@ -2565,11 +2571,13 @@ class CodexAppServerAgentSession implements AgentSession {
cwd: this.config.cwd ?? null, cwd: this.config.cwd ?? null,
running: true, running: true,
}); });
this.warnOnIncompleteEditToolCall(timelineItem, "patch_apply_started", { if (timelineItem) {
callId: parsed.callId, this.warnOnIncompleteEditToolCall(timelineItem, "patch_apply_started", {
changes: parsed.changes, callId: parsed.callId,
}); changes: parsed.changes,
this.emitEvent({ type: "timeline", provider: CODEX_PROVIDER, item: timelineItem }); });
this.emitEvent({ type: "timeline", provider: CODEX_PROVIDER, item: timelineItem });
}
return; return;
} }
@@ -2587,12 +2595,14 @@ class CodexAppServerAgentSession implements AgentSession {
success: parsed.success, success: parsed.success,
running: false, running: false,
}); });
this.warnOnIncompleteEditToolCall(timelineItem, "patch_apply_completed", { if (timelineItem) {
callId: parsed.callId, this.warnOnIncompleteEditToolCall(timelineItem, "patch_apply_completed", {
changes: parsed.changes, callId: parsed.callId,
stdout: parsed.stdout, changes: parsed.changes,
}); stdout: parsed.stdout,
this.emitEvent({ type: "timeline", provider: CODEX_PROVIDER, item: timelineItem }); });
this.emitEvent({ type: "timeline", provider: CODEX_PROVIDER, item: timelineItem });
}
return; return;
} }
@@ -503,14 +503,15 @@ export async function parseRolloutFile(
record.kind === "timeline" record.kind === "timeline"
? [record.item] ? [record.item]
: record.kind === "call" : record.kind === "call"
? [ ? (() => {
mapCodexRolloutToolCall({ const mapped = mapCodexRolloutToolCall({
callId: record.callId ?? null, callId: record.callId ?? null,
name: record.name, name: record.name,
input: record.input ?? null, input: record.input ?? null,
output: record.callId ? outputsByCallId.get(record.callId) ?? null : null, output: record.callId ? outputsByCallId.get(record.callId) ?? null : null,
}), });
] return mapped ? [mapped] : [];
})()
: [] : []
); );
return dedupeMirroredTextTimelineItems(timeline); return dedupeMirroredTextTimelineItems(timeline);
@@ -24,23 +24,43 @@ export type CodexToolDetailContext = {
cwd?: string | null; cwd?: string | null;
}; };
export const CODEX_BUILTIN_TOOL_NAMES = new Set([ const CodexToolEnvelopeSchema = z
"shell", .object({
"bash", name: z.string().min(1),
"exec", input: z.unknown().nullable(),
"exec_command", output: z.unknown().nullable(),
"command", cwd: z.string().nullable().optional(),
"read", })
"read_file", .passthrough();
"write",
"write_file", const CodexSpeakToolDetailSchema = z
"create_file", .object({
"edit", name: z.literal("speak"),
"apply_patch", input: z
"apply_diff", .union([
"web_search", z.string().transform((text) => ({ text })),
"search", z.object({ text: z.string() }).passthrough(),
]); ])
.nullable(),
output: z.unknown().nullable(),
cwd: z.string().nullable().optional(),
})
.transform(({ input }) => {
const text = input?.text?.trim() ?? "";
if (!text) {
return undefined;
}
return {
type: "unknown",
input: text,
output: null,
} satisfies ToolCallDetail;
});
const CodexLooseEditOutputSchema = z.unknown().transform((value) => {
const parsed = ToolEditOutputSchema.safeParse(value);
return parsed.success ? parsed.data : null;
});
export function normalizeCodexFilePath( export function normalizeCodexFilePath(
filePath: string, filePath: string,
@@ -60,7 +80,7 @@ function normalizePathForCwd(cwd: string | null): (filePath: string) => string |
return (filePath) => normalizeCodexFilePath(filePath, cwd); return (filePath) => normalizeCodexFilePath(filePath, cwd);
} }
const CodexKnownToolDetailSchema = z.union([ const CodexToolDetailPass2Schema = z.union([
toolDetailBranchByNameWithCwd("Bash", ToolShellInputSchema, ToolShellOutputSchema, (input, output) => toolDetailBranchByNameWithCwd("Bash", ToolShellInputSchema, ToolShellOutputSchema, (input, output) =>
toShellToolDetail(input, output) toShellToolDetail(input, output)
), ),
@@ -82,14 +102,22 @@ const CodexKnownToolDetailSchema = z.union([
toolDetailBranchByNameWithCwd("command", ToolShellInputSchema, ToolShellOutputSchema, (input, output) => toolDetailBranchByNameWithCwd("command", ToolShellInputSchema, ToolShellOutputSchema, (input, output) =>
toShellToolDetail(input, output) toShellToolDetail(input, output)
), ),
toolDetailBranchByNameWithCwd("read", ToolReadInputSchema, ToolReadOutputWithPathSchema, (input, output, cwd) => toolDetailBranchByNameWithCwd("read", ToolReadInputSchema, z.unknown(), (input, output, cwd) => {
toReadToolDetail(input, output, { normalizePath: normalizePathForCwd(cwd) }) const parsedOutput = ToolReadOutputWithPathSchema.safeParse(output);
), return toReadToolDetail(input, parsedOutput.success ? parsedOutput.data : null, {
normalizePath: normalizePathForCwd(cwd),
});
}),
toolDetailBranchByNameWithCwd( toolDetailBranchByNameWithCwd(
"read_file", "read_file",
ToolReadInputSchema, ToolReadInputSchema,
ToolReadOutputWithPathSchema, z.unknown(),
(input, output, cwd) => toReadToolDetail(input, output, { normalizePath: normalizePathForCwd(cwd) }) (input, output, cwd) => {
const parsedOutput = ToolReadOutputWithPathSchema.safeParse(output);
return toReadToolDetail(input, parsedOutput.success ? parsedOutput.data : null, {
normalizePath: normalizePathForCwd(cwd),
});
}
), ),
toolDetailBranchByNameWithCwd("write", ToolWriteInputSchema, ToolWriteOutputSchema, (input, output, cwd) => toolDetailBranchByNameWithCwd("write", ToolWriteInputSchema, ToolWriteOutputSchema, (input, output, cwd) =>
toWriteToolDetail(input, output, { normalizePath: normalizePathForCwd(cwd) }) toWriteToolDetail(input, output, { normalizePath: normalizePathForCwd(cwd) })
@@ -106,19 +134,19 @@ const CodexKnownToolDetailSchema = z.union([
ToolWriteOutputSchema, ToolWriteOutputSchema,
(input, output, cwd) => toWriteToolDetail(input, output, { normalizePath: normalizePathForCwd(cwd) }) (input, output, cwd) => toWriteToolDetail(input, output, { normalizePath: normalizePathForCwd(cwd) })
), ),
toolDetailBranchByNameWithCwd("edit", ToolEditInputSchema, ToolEditOutputSchema, (input, output, cwd) => toolDetailBranchByNameWithCwd("edit", ToolEditInputSchema, CodexLooseEditOutputSchema, (input, output, cwd) =>
toEditToolDetail(input, output, { normalizePath: normalizePathForCwd(cwd) }) toEditToolDetail(input, output, { normalizePath: normalizePathForCwd(cwd) })
), ),
toolDetailBranchByNameWithCwd( toolDetailBranchByNameWithCwd(
"apply_patch", "apply_patch",
ToolEditInputSchema, ToolEditInputSchema,
ToolEditOutputSchema, CodexLooseEditOutputSchema,
(input, output, cwd) => toEditToolDetail(input, output, { normalizePath: normalizePathForCwd(cwd) }) (input, output, cwd) => toEditToolDetail(input, output, { normalizePath: normalizePathForCwd(cwd) })
), ),
toolDetailBranchByNameWithCwd( toolDetailBranchByNameWithCwd(
"apply_diff", "apply_diff",
ToolEditInputSchema, ToolEditInputSchema,
ToolEditOutputSchema, CodexLooseEditOutputSchema,
(input, output, cwd) => toEditToolDetail(input, output, { normalizePath: normalizePathForCwd(cwd) }) (input, output, cwd) => toEditToolDetail(input, output, { normalizePath: normalizePathForCwd(cwd) })
), ),
toolDetailBranchByNameWithCwd("search", ToolSearchInputSchema, z.unknown(), (input) => toolDetailBranchByNameWithCwd("search", ToolSearchInputSchema, z.unknown(), (input) =>
@@ -127,6 +155,7 @@ const CodexKnownToolDetailSchema = z.union([
toolDetailBranchByNameWithCwd("web_search", ToolSearchInputSchema, z.unknown(), (input) => toolDetailBranchByNameWithCwd("web_search", ToolSearchInputSchema, z.unknown(), (input) =>
toSearchToolDetail(input) toSearchToolDetail(input)
), ),
CodexSpeakToolDetailSchema,
]); ]);
export function deriveCodexToolDetail(params: { export function deriveCodexToolDetail(params: {
@@ -135,18 +164,28 @@ export function deriveCodexToolDetail(params: {
output: unknown; output: unknown;
cwd?: string | null; cwd?: string | null;
}): ToolCallDetail { }): ToolCallDetail {
const parsed = CodexKnownToolDetailSchema.safeParse({ const pass1 = CodexToolEnvelopeSchema.safeParse({
name: params.name, name: params.name,
input: params.input,
output: params.output,
cwd: params.cwd ?? null,
});
if (parsed.success && parsed.data) {
return parsed.data;
}
return {
type: "unknown",
input: params.input ?? null, input: params.input ?? null,
output: params.output ?? null, output: params.output ?? null,
cwd: params.cwd ?? null,
});
if (!pass1.success) {
return {
type: "unknown",
input: params.input ?? null,
output: params.output ?? null,
};
}
const pass2 = CodexToolDetailPass2Schema.safeParse(pass1.data);
if (pass2.success && pass2.data) {
return pass2.data;
}
return {
type: "unknown",
input: pass1.data.input,
output: pass1.data.output,
}; };
} }
@@ -5,6 +5,14 @@ import {
mapCodexToolCallFromThreadItem, mapCodexToolCallFromThreadItem,
} from "./tool-call-mapper.js"; } from "./tool-call-mapper.js";
function expectMapped<T>(item: T | null): T {
expect(item).toBeTruthy();
if (!item) {
throw new Error("Expected mapped tool call");
}
return item;
}
describe("codex tool-call mapper", () => { describe("codex tool-call mapper", () => {
it("maps commandExecution start into running canonical call", () => { it("maps commandExecution start into running canonical call", () => {
const item = mapCodexToolCallFromThreadItem({ const item = mapCodexToolCallFromThreadItem({
@@ -301,12 +309,14 @@ describe("codex tool-call mapper", () => {
}); });
it("maps unknown tools to unknown detail with raw payloads", () => { it("maps unknown tools to unknown detail with raw payloads", () => {
const item = mapCodexRolloutToolCall({ const item = expectMapped(
callId: "codex-call-4", mapCodexRolloutToolCall({
name: "my_custom_tool", callId: "codex-call-4",
input: { foo: "bar" }, name: "my_custom_tool",
output: { ok: true }, input: { foo: "bar" },
}); output: { ok: true },
})
);
expect(item.status).toBe("completed"); expect(item.status).toBe("completed");
expect(item.error).toBeNull(); expect(item.error).toBeNull();
@@ -327,13 +337,15 @@ describe("codex tool-call mapper", () => {
"+new", "+new",
"*** End Patch", "*** End Patch",
].join("\n"); ].join("\n");
const item = mapCodexRolloutToolCall({ const item = expectMapped(
callId: "codex-call-apply", mapCodexRolloutToolCall({
name: "apply_patch", callId: "codex-call-apply",
input: patch, name: "apply_patch",
output: '{"output":"Success. Updated the following files:\\nM src/index.ts\\n"}', input: patch,
cwd: "/tmp/repo", output: '{"output":"Success. Updated the following files:\\nM src/index.ts\\n"}',
}); cwd: "/tmp/repo",
})
);
expect(item.status).toBe("completed"); expect(item.status).toBe("completed");
expect(item.error).toBeNull(); expect(item.error).toBeNull();
@@ -359,16 +371,18 @@ describe("codex tool-call mapper", () => {
"*** End Patch", "*** End Patch",
].join("\n"); ].join("\n");
const item = mapCodexRolloutToolCall({ const item = expectMapped(
callId: "codex-call-apply-object", mapCodexRolloutToolCall({
name: "apply_patch", callId: "codex-call-apply-object",
input: { name: "apply_patch",
path: "/tmp/repo/src/object.ts", input: {
content: patch, path: "/tmp/repo/src/object.ts",
}, content: patch,
output: null, },
cwd: "/tmp/repo", output: null,
}); cwd: "/tmp/repo",
})
);
expect(item.detail.type).toBe("edit"); expect(item.detail.type).toBe("edit");
if (item.detail.type === "edit") { if (item.detail.type === "edit") {
@@ -412,58 +426,6 @@ describe("codex tool-call mapper", () => {
} }
}); });
it("maps fileChange patch alias fields into edit unified diff detail", () => {
const item = mapCodexToolCallFromThreadItem(
{
type: "fileChange",
id: "codex-file-change-patch-alias",
status: "completed",
changes: [
{
path: "/tmp/repo/src/from-patch-alias.ts",
kind: "modify",
patch: "@@\n-oldAlias\n+newAlias\n",
},
],
},
{ cwd: "/tmp/repo" }
);
expect(item?.detail?.type).toBe("edit");
if (item?.detail?.type === "edit") {
expect(item.detail.filePath).toBe("src/from-patch-alias.ts");
expect(item.detail.unifiedDiff).toContain("-oldAlias");
expect(item.detail.unifiedDiff).toContain("+newAlias");
expect(item.detail.newString).toBeUndefined();
}
});
it("maps fileChange unifiedDiff alias fields into edit unified diff detail", () => {
const item = mapCodexToolCallFromThreadItem(
{
type: "fileChange",
id: "codex-file-change-unified-diff-alias",
status: "completed",
changes: [
{
path: "/tmp/repo/src/from-unified-diff-alias.ts",
kind: "modify",
unified_diff: "@@\n-beforeAlias\n+afterAlias\n",
},
],
},
{ cwd: "/tmp/repo" }
);
expect(item?.detail?.type).toBe("edit");
if (item?.detail?.type === "edit") {
expect(item.detail.filePath).toBe("src/from-unified-diff-alias.ts");
expect(item.detail.unifiedDiff).toContain("-beforeAlias");
expect(item.detail.unifiedDiff).toContain("+afterAlias");
expect(item.detail.newString).toBeUndefined();
}
});
it("maps path-only fileChange payloads to unknown detail instead of empty edit detail", () => { it("maps path-only fileChange payloads to unknown detail instead of empty edit detail", () => {
const item = mapCodexToolCallFromThreadItem( const item = mapCodexToolCallFromThreadItem(
{ {
@@ -484,17 +446,80 @@ describe("codex tool-call mapper", () => {
}); });
it("maps path-only apply_patch rollout payloads to unknown detail instead of empty edit detail", () => { it("maps path-only apply_patch rollout payloads to unknown detail instead of empty edit detail", () => {
const item = mapCodexRolloutToolCall({ const item = expectMapped(
callId: "codex-call-apply-path-only", mapCodexRolloutToolCall({
name: "apply_patch", callId: "codex-call-apply-path-only",
input: { path: "/tmp/repo/src/path-only-rollout.ts" }, name: "apply_patch",
output: { files: [{ path: "/tmp/repo/src/path-only-rollout.ts", kind: "modify" }] }, input: { path: "/tmp/repo/src/path-only-rollout.ts" },
cwd: "/tmp/repo", output: { files: [{ path: "/tmp/repo/src/path-only-rollout.ts", kind: "modify" }] },
}); cwd: "/tmp/repo",
})
);
expect(item.detail.type).toBe("unknown"); expect(item.detail.type).toBe("unknown");
if (item.detail.type === "unknown") { if (item.detail.type === "unknown") {
expect(item.detail.input).toEqual({ path: "/tmp/repo/src/path-only-rollout.ts" }); expect(item.detail.input).toEqual({ path: "/tmp/repo/src/path-only-rollout.ts" });
} }
}); });
it("normalizes codex paseo speak mcp calls and extracts spoken text", () => {
const item = mapCodexToolCallFromThreadItem({
type: "mcpToolCall",
id: "codex-speak-thread-1",
status: "completed",
server: "paseo",
tool: "speak",
arguments: { text: "Voice response from Codex." },
result: { ok: true },
});
expect(item).toBeTruthy();
expect(item?.name).toBe("speak");
expect(item?.detail).toEqual({
type: "unknown",
input: "Voice response from Codex.",
output: null,
});
});
it("normalizes codex paseo speak rollout names and extracts spoken text", () => {
const item = expectMapped(
mapCodexRolloutToolCall({
callId: "codex-speak-rollout-1",
name: "paseo.speak",
input: { text: "Rollout speech text." },
output: { ok: true },
})
);
expect(item.name).toBe("speak");
expect(item.detail).toEqual({
type: "unknown",
input: "Rollout speech text.",
output: null,
});
});
it("drops rollout tool calls when callId is missing", () => {
const item = mapCodexRolloutToolCall({
callId: null,
name: "read_file",
input: { path: "/tmp/repo/README.md" },
output: { content: "hello" },
});
expect(item).toBeNull();
});
it("drops thread mcp tool calls when id is missing", () => {
const item = mapCodexToolCallFromThreadItem({
type: "mcpToolCall",
status: "completed",
tool: "read_file",
arguments: { path: "/tmp/repo/README.md" },
result: { content: "hello" },
});
expect(item).toBeNull();
});
}); });
File diff suppressed because it is too large Load Diff
@@ -2,6 +2,7 @@ import { execSync, spawn, type ChildProcess } from "node:child_process";
import { createOpencodeClient, type OpencodeClient } from "@opencode-ai/sdk/v2/client"; import { createOpencodeClient, type OpencodeClient } from "@opencode-ai/sdk/v2/client";
import net from "node:net"; import net from "node:net";
import type { Logger } from "pino"; import type { Logger } from "pino";
import { z } from "zod";
import type { import type {
AgentCapabilityFlags, AgentCapabilityFlags,
@@ -69,6 +70,84 @@ type OpenCodeMcpConfig =
const MCP_ALREADY_PRESENT_ERROR_TOKENS = ["already", "exists", "connected"] as const; const MCP_ALREADY_PRESENT_ERROR_TOKENS = ["already", "exists", "connected"] as const;
const OpencodeToolStateSchema = z
.object({
status: z.string().optional(),
input: z.unknown().optional(),
output: z.unknown().optional(),
error: z.unknown().optional(),
})
.passthrough();
const OpencodeToolPartBaseSchema = z
.object({
tool: z.string().trim().min(1),
state: OpencodeToolStateSchema.optional(),
})
.passthrough();
const OpencodeToolPartWithCallIdSchema = OpencodeToolPartBaseSchema.extend({
callID: z.string().trim().min(1),
id: z.string().optional(),
}).transform((part) => ({
toolName: part.tool,
callId: part.callID,
status: part.state?.status,
input: part.state?.input,
output: part.state?.output,
error: part.state?.error,
}));
const OpencodeToolPartWithIdSchema = OpencodeToolPartBaseSchema.extend({
id: z.string().trim().min(1),
callID: z.string().optional(),
}).transform((part) => ({
toolName: part.tool,
callId: part.id,
status: part.state?.status,
input: part.state?.input,
output: part.state?.output,
error: part.state?.error,
}));
const OpencodeToolPartWithoutIdSchema = OpencodeToolPartBaseSchema.extend({
id: z.string().optional(),
callID: z.string().optional(),
}).transform((part) => ({
toolName: part.tool,
callId: undefined,
status: part.state?.status,
input: part.state?.input,
output: part.state?.output,
error: part.state?.error,
}));
const OpencodeToolPartSchema = z.union([
OpencodeToolPartWithCallIdSchema,
OpencodeToolPartWithIdSchema,
OpencodeToolPartWithoutIdSchema,
]);
const OpencodeToolPartTimelineEnvelopeSchema = OpencodeToolPartSchema.transform((part) => ({
toolName: part.toolName,
callId: part.callId,
status: part.status,
input: part.input,
output: part.output,
error: part.error,
}));
const OpencodeToolPartToTimelineItemSchema = OpencodeToolPartTimelineEnvelopeSchema.transform((part) =>
mapOpencodeToolCall({
toolName: part.toolName,
callId: part.callId,
status: part.status,
input: part.input,
output: part.output,
error: part.error,
})
);
function resolveOpenCodeBinary(): string { function resolveOpenCodeBinary(): string {
try { try {
const opencodePath = execSync("which opencode", { encoding: "utf8" }).trim(); const opencodePath = execSync("which opencode", { encoding: "utf8" }).trim();
@@ -650,33 +729,15 @@ class OpenCodeAgentSession implements AgentSession {
}; };
} }
} else if (partType === "tool") { } else if (partType === "tool") {
const toolPart = part as { const parsedToolPart = OpencodeToolPartToTimelineItemSchema.safeParse(part);
id?: string; if (parsedToolPart.success) {
tool?: string; if (parsedToolPart.data) {
callID?: string; yield {
state?: { type: "timeline",
status?: string; provider: "opencode",
input?: AgentMetadata; item: parsedToolPart.data,
output?: string; };
error?: string; }
};
};
const toolName = toolPart.tool;
const state = toolPart.state;
if (toolName) {
yield {
type: "timeline",
provider: "opencode",
item: mapOpencodeToolCall({
toolName,
callId: toolPart.callID ?? toolPart.id,
status: state?.status,
input: state?.input,
output: state?.output,
error: state?.error,
}),
};
} }
} }
} }
@@ -931,26 +992,15 @@ class OpenCodeAgentSession implements AgentSession {
} }
} else if (partType === "tool") { } else if (partType === "tool") {
// Tool parts: { tool: string, state: { status, input, output?, error? } } // Tool parts: { tool: string, state: { status, input, output?, error? } }
const toolName = part.tool as string | undefined; const parsedToolPart = OpencodeToolPartToTimelineItemSchema.safeParse(part);
const state = part.state as AgentMetadata | undefined; if (parsedToolPart.success) {
const status = state?.status as string | undefined; if (parsedToolPart.data) {
const input = state?.input as AgentMetadata | undefined; events.push({
const output = state?.output as string | undefined; type: "timeline",
const error = state?.error as string | undefined; provider: "opencode",
item: parsedToolPart.data,
if (toolName) { });
events.push({ }
type: "timeline",
provider: "opencode",
item: mapOpencodeToolCall({
toolName,
callId: (part.callID as string | undefined) ?? (part.id as string | undefined),
status,
input,
output,
error,
}),
});
} }
} else if (partType === "step-finish") { } else if (partType === "step-finish") {
// Extract usage from step-finish parts // Extract usage from step-finish parts
@@ -23,8 +23,14 @@ const OpencodeKnownToolDetailSchema = z.union([
toolDetailBranchByToolName("shell", ToolShellInputSchema, ToolShellOutputSchema, toShellToolDetail), toolDetailBranchByToolName("shell", ToolShellInputSchema, ToolShellOutputSchema, toShellToolDetail),
toolDetailBranchByToolName("bash", ToolShellInputSchema, ToolShellOutputSchema, toShellToolDetail), toolDetailBranchByToolName("bash", ToolShellInputSchema, ToolShellOutputSchema, toShellToolDetail),
toolDetailBranchByToolName("exec_command", ToolShellInputSchema, ToolShellOutputSchema, toShellToolDetail), toolDetailBranchByToolName("exec_command", ToolShellInputSchema, ToolShellOutputSchema, toShellToolDetail),
toolDetailBranchByToolName("read", ToolReadInputSchema, ToolReadOutputSchema, toReadToolDetail), toolDetailBranchByToolName("read", ToolReadInputSchema, z.unknown(), (input, output) => {
toolDetailBranchByToolName("read_file", ToolReadInputSchema, ToolReadOutputSchema, toReadToolDetail), const parsedOutput = ToolReadOutputSchema.safeParse(output);
return toReadToolDetail(input, parsedOutput.success ? parsedOutput.data : null);
}),
toolDetailBranchByToolName("read_file", ToolReadInputSchema, z.unknown(), (input, output) => {
const parsedOutput = ToolReadOutputSchema.safeParse(output);
return toReadToolDetail(input, parsedOutput.success ? parsedOutput.data : null);
}),
toolDetailBranchByToolName("write", ToolWriteInputSchema, ToolWriteOutputSchema, toWriteToolDetail), toolDetailBranchByToolName("write", ToolWriteInputSchema, ToolWriteOutputSchema, toWriteToolDetail),
toolDetailBranchByToolName("write_file", ToolWriteInputSchema, ToolWriteOutputSchema, toWriteToolDetail), toolDetailBranchByToolName("write_file", ToolWriteInputSchema, ToolWriteOutputSchema, toWriteToolDetail),
toolDetailBranchByToolName("create_file", ToolWriteInputSchema, ToolWriteOutputSchema, toWriteToolDetail), toolDetailBranchByToolName("create_file", ToolWriteInputSchema, ToolWriteOutputSchema, toWriteToolDetail),
@@ -2,15 +2,25 @@ import { describe, expect, it } from "vitest";
import { mapOpencodeToolCall } from "./tool-call-mapper.js"; import { mapOpencodeToolCall } from "./tool-call-mapper.js";
function expectMapped<T>(item: T | null): T {
expect(item).toBeTruthy();
if (!item) {
throw new Error("Expected mapped tool call");
}
return item;
}
describe("opencode tool-call mapper", () => { describe("opencode tool-call mapper", () => {
it("maps running shell calls", () => { it("maps running shell calls", () => {
const item = mapOpencodeToolCall({ const item = expectMapped(
toolName: "shell", mapOpencodeToolCall({
callId: "opencode-call-1", toolName: "shell",
status: "running", callId: "opencode-call-1",
input: { command: "pwd", cwd: "/tmp/repo" }, status: "running",
output: null, input: { command: "pwd", cwd: "/tmp/repo" },
}); output: null,
})
);
expect(item.status).toBe("running"); expect(item.status).toBe("running");
expect(item.error).toBeNull(); expect(item.error).toBeNull();
@@ -22,49 +32,57 @@ describe("opencode tool-call mapper", () => {
}); });
it("maps running known tool variants with detail for early summaries", () => { it("maps running known tool variants with detail for early summaries", () => {
const readItem = mapOpencodeToolCall({ const readItem = expectMapped(
toolName: "read_file", mapOpencodeToolCall({
callId: "opencode-running-read", toolName: "read_file",
status: "running", callId: "opencode-running-read",
input: { file_path: "README.md" }, status: "running",
output: null, input: { file_path: "README.md" },
}); output: null,
})
);
expect(readItem.detail).toEqual({ expect(readItem.detail).toEqual({
type: "read", type: "read",
filePath: "README.md", filePath: "README.md",
}); });
const writeItem = mapOpencodeToolCall({ const writeItem = expectMapped(
toolName: "write_file", mapOpencodeToolCall({
callId: "opencode-running-write", toolName: "write_file",
status: "running", callId: "opencode-running-write",
input: { file_path: "src/new.ts" }, status: "running",
output: null, input: { file_path: "src/new.ts" },
}); output: null,
})
);
expect(writeItem.detail).toEqual({ expect(writeItem.detail).toEqual({
type: "write", type: "write",
filePath: "src/new.ts", filePath: "src/new.ts",
}); });
const editItem = mapOpencodeToolCall({ const editItem = expectMapped(
toolName: "apply_patch", mapOpencodeToolCall({
callId: "opencode-running-edit", toolName: "apply_patch",
status: "running", callId: "opencode-running-edit",
input: { file_path: "src/index.ts" }, status: "running",
output: null, input: { file_path: "src/index.ts" },
}); output: null,
})
);
expect(editItem.detail).toEqual({ expect(editItem.detail).toEqual({
type: "edit", type: "edit",
filePath: "src/index.ts", filePath: "src/index.ts",
}); });
const searchItem = mapOpencodeToolCall({ const searchItem = expectMapped(
toolName: "web_search", mapOpencodeToolCall({
callId: "opencode-running-search", toolName: "web_search",
status: "running", callId: "opencode-running-search",
input: { query: "opencode mapper" }, status: "running",
output: null, input: { query: "opencode mapper" },
}); output: null,
})
);
expect(searchItem.detail).toEqual({ expect(searchItem.detail).toEqual({
type: "search", type: "search",
query: "opencode mapper", query: "opencode mapper",
@@ -72,13 +90,15 @@ describe("opencode tool-call mapper", () => {
}); });
it("maps completed read calls", () => { it("maps completed read calls", () => {
const item = mapOpencodeToolCall({ const item = expectMapped(
toolName: "read_file", mapOpencodeToolCall({
callId: "opencode-call-2", toolName: "read_file",
status: "complete", callId: "opencode-call-2",
input: { file_path: "README.md" }, status: "complete",
output: { content: "hello" }, input: { file_path: "README.md" },
}); output: { content: "hello" },
})
);
expect(item.status).toBe("completed"); expect(item.status).toBe("completed");
expect(item.error).toBeNull(); expect(item.error).toBeNull();
@@ -91,35 +111,39 @@ describe("opencode tool-call mapper", () => {
}); });
it("preserves read content from array/object output variants", () => { it("preserves read content from array/object output variants", () => {
const arrayContent = mapOpencodeToolCall({ const arrayContent = expectMapped(
toolName: "read_file", mapOpencodeToolCall({
callId: "opencode-read-array", toolName: "read_file",
status: "completed", callId: "opencode-read-array",
input: { file_path: "README.md" }, status: "completed",
output: { input: { file_path: "README.md" },
content: [ output: {
{ type: "output_text", text: "alpha" }, content: [
{ type: "output_text", output: "beta" }, { type: "output_text", text: "alpha" },
], { type: "output_text", output: "beta" },
}, ],
}); },
})
);
expect(arrayContent.detail?.type).toBe("read"); expect(arrayContent.detail?.type).toBe("read");
if (arrayContent.detail?.type === "read") { if (arrayContent.detail?.type === "read") {
expect(arrayContent.detail.content).toBe("alpha\nbeta"); expect(arrayContent.detail.content).toBe("alpha\nbeta");
} }
const objectContent = mapOpencodeToolCall({ const objectContent = expectMapped(
toolName: "read_file", mapOpencodeToolCall({
callId: "opencode-read-object", toolName: "read_file",
status: "completed", callId: "opencode-read-object",
input: { file_path: "README.md" }, status: "completed",
output: { input: { file_path: "README.md" },
data: { output: {
content: { type: "output_text", text: "gamma" }, data: {
content: { type: "output_text", text: "gamma" },
},
}, },
}, })
}); );
expect(objectContent.detail?.type).toBe("read"); expect(objectContent.detail?.type).toBe("read");
if (objectContent.detail?.type === "read") { if (objectContent.detail?.type === "read") {
@@ -128,14 +152,16 @@ describe("opencode tool-call mapper", () => {
}); });
it("maps failed calls with required error", () => { it("maps failed calls with required error", () => {
const item = mapOpencodeToolCall({ const item = expectMapped(
toolName: "shell", mapOpencodeToolCall({
callId: "opencode-call-3", toolName: "shell",
status: "error", callId: "opencode-call-3",
input: { command: "false" }, status: "error",
output: null, input: { command: "false" },
error: "command failed", output: null,
}); error: "command failed",
})
);
expect(item.status).toBe("failed"); expect(item.status).toBe("failed");
expect(item.error).toBe("command failed"); expect(item.error).toBe("command failed");
@@ -143,38 +169,44 @@ describe("opencode tool-call mapper", () => {
}); });
it("maps write/edit/search known variants into canonical detail", () => { it("maps write/edit/search known variants into canonical detail", () => {
const writeItem = mapOpencodeToolCall({ const writeItem = expectMapped(
toolName: "write_file", mapOpencodeToolCall({
callId: "opencode-write-1", toolName: "write_file",
status: "completed", callId: "opencode-write-1",
input: { file_path: "src/new.ts", content: "const x = 1;" }, status: "completed",
output: null, input: { file_path: "src/new.ts", content: "const x = 1;" },
}); output: null,
})
);
expect(writeItem.detail?.type).toBe("write"); expect(writeItem.detail?.type).toBe("write");
if (writeItem.detail?.type === "write") { if (writeItem.detail?.type === "write") {
expect(writeItem.detail.filePath).toBe("src/new.ts"); expect(writeItem.detail.filePath).toBe("src/new.ts");
} }
const editItem = mapOpencodeToolCall({ const editItem = expectMapped(
toolName: "apply_patch", mapOpencodeToolCall({
callId: "opencode-edit-1", toolName: "apply_patch",
status: "completed", callId: "opencode-edit-1",
input: { file_path: "src/index.ts", diff: "@@\\n-old\\n+new\\n" }, status: "completed",
output: null, input: { file_path: "src/index.ts", diff: "@@\\n-old\\n+new\\n" },
}); output: null,
})
);
expect(editItem.detail?.type).toBe("edit"); expect(editItem.detail?.type).toBe("edit");
if (editItem.detail?.type === "edit") { if (editItem.detail?.type === "edit") {
expect(editItem.detail.filePath).toBe("src/index.ts"); expect(editItem.detail.filePath).toBe("src/index.ts");
expect(editItem.detail.unifiedDiff).toContain("@@"); expect(editItem.detail.unifiedDiff).toContain("@@");
} }
const searchItem = mapOpencodeToolCall({ const searchItem = expectMapped(
toolName: "web_search", mapOpencodeToolCall({
callId: "opencode-search-1", toolName: "web_search",
status: "completed", callId: "opencode-search-1",
input: { query: "opencode mapper" }, status: "completed",
output: null, input: { query: "opencode mapper" },
}); output: null,
})
);
expect(searchItem.detail).toEqual({ expect(searchItem.detail).toEqual({
type: "search", type: "search",
query: "opencode mapper", query: "opencode mapper",
@@ -182,13 +214,15 @@ describe("opencode tool-call mapper", () => {
}); });
it("maps unknown tools to unknown detail with raw payloads", () => { it("maps unknown tools to unknown detail with raw payloads", () => {
const item = mapOpencodeToolCall({ const item = expectMapped(
toolName: "my_custom_tool", mapOpencodeToolCall({
callId: "opencode-call-4", toolName: "my_custom_tool",
status: "completed", callId: "opencode-call-4",
input: { foo: "bar" }, status: "completed",
output: { ok: true }, input: { foo: "bar" },
}); output: { ok: true },
})
);
expect(item.status).toBe("completed"); expect(item.status).toBe("completed");
expect(item.error).toBeNull(); expect(item.error).toBeNull();
@@ -198,4 +232,35 @@ describe("opencode tool-call mapper", () => {
output: { ok: true }, output: { ok: true },
}); });
}); });
it("does not apply cross-provider speak normalization in opencode mapper", () => {
const item = expectMapped(
mapOpencodeToolCall({
toolName: "paseo_voice.speak",
callId: "opencode-call-voice-1",
status: "completed",
input: { text: "Voice response from OpenCode." },
output: { ok: true },
})
);
expect(item.name).toBe("paseo_voice.speak");
expect(item.detail).toEqual({
type: "unknown",
input: { text: "Voice response from OpenCode." },
output: { ok: true },
});
});
it("drops tool calls when callId is missing", () => {
const item = mapOpencodeToolCall({
toolName: "read_file",
callId: null,
status: "completed",
input: { file_path: "README.md" },
output: { content: "hello" },
});
expect(item).toBeNull();
});
}); });
@@ -1,7 +1,6 @@
import { z } from "zod"; import { z } from "zod";
import type { ToolCallTimelineItem } from "../../agent-sdk-types.js"; import type { ToolCallTimelineItem } from "../../agent-sdk-types.js";
import { coerceToolCallId } from "../tool-call-mapper-utils.js";
import { deriveOpencodeToolDetail } from "./tool-call-detail-parser.js"; import { deriveOpencodeToolDetail } from "./tool-call-detail-parser.js";
type OpencodeToolCallParams = { type OpencodeToolCallParams = {
@@ -18,7 +17,14 @@ const FAILED_STATUSES = new Set(["error", "failed", "failure"]);
const CANCELED_STATUSES = new Set(["canceled", "cancelled", "aborted", "interrupted"]); const CANCELED_STATUSES = new Set(["canceled", "cancelled", "aborted", "interrupted"]);
const COMPLETED_STATUSES = new Set(["complete", "completed", "success", "succeeded", "done"]); const COMPLETED_STATUSES = new Set(["complete", "completed", "success", "succeeded", "done"]);
const OpencodeToolCallParamsSchema = z const OpencodeToolCallStatusSchema = z.enum([
"running",
"completed",
"failed",
"canceled",
]);
const OpencodeRawToolCallSchema = z
.object({ .object({
toolName: z.string().min(1), toolName: z.string().min(1),
callId: z.string().optional().nullable(), callId: z.string().optional().nullable(),
@@ -26,74 +32,163 @@ const OpencodeToolCallParamsSchema = z
input: z.unknown().optional(), input: z.unknown().optional(),
output: z.unknown().optional(), output: z.unknown().optional(),
error: z.unknown().optional(), error: z.unknown().optional(),
metadata: z.record(z.unknown()).optional(), metadata: z.record(z.string(), z.unknown()).optional(),
}) })
.passthrough(); .passthrough();
function coerceCallId(callId: string | null | undefined, toolName: string, input: unknown): string { const OpencodeNormalizedToolCallPass1Schema = OpencodeRawToolCallSchema.transform((raw) => {
return coerceToolCallId({ const input = raw.input ?? null;
providerPrefix: "opencode", const output = raw.output ?? null;
rawCallId: callId, const error = raw.error ?? null;
toolName, const callId =
input, typeof raw.callId === "string" && raw.callId.trim().length > 0
}); ? raw.callId.trim()
} : null;
let status: z.infer<typeof OpencodeToolCallStatusSchema>;
function resolveStatus( if (error !== null) {
rawStatus: unknown, status = "failed";
error: unknown, } else if (typeof raw.status === "string") {
output: unknown const normalized = raw.status.trim().toLowerCase();
): ToolCallTimelineItem["status"] { if (FAILED_STATUSES.has(normalized)) {
if (error !== null && error !== undefined) { status = "failed";
return "failed"; } else if (CANCELED_STATUSES.has(normalized)) {
} status = "canceled";
} else if (COMPLETED_STATUSES.has(normalized)) {
if (typeof rawStatus === "string") { status = "completed";
const normalized = rawStatus.trim().toLowerCase(); } else {
if (normalized.length > 0) { status = "running";
if (FAILED_STATUSES.has(normalized)) {
return "failed";
}
if (CANCELED_STATUSES.has(normalized)) {
return "canceled";
}
if (COMPLETED_STATUSES.has(normalized)) {
return "completed";
}
return "running";
} }
} } else {
status = output !== null ? "completed" : "running";
return output !== null && output !== undefined ? "completed" : "running";
}
export function mapOpencodeToolCall(params: OpencodeToolCallParams): ToolCallTimelineItem {
const parsedParams = OpencodeToolCallParamsSchema.parse(params);
const input = parsedParams.input ?? null;
const output = parsedParams.output ?? null;
const status = resolveStatus(parsedParams.status, parsedParams.error, output);
const callId = coerceCallId(parsedParams.callId, parsedParams.toolName, input);
const detail = deriveOpencodeToolDetail(parsedParams.toolName, input, output);
if (status === "failed") {
return {
type: "tool_call",
callId,
name: parsedParams.toolName,
status: "failed",
detail,
error: parsedParams.error ?? { message: "Tool call failed" },
...(parsedParams.metadata ? { metadata: parsedParams.metadata } : {}),
};
} }
return { return {
type: "tool_call",
callId, callId,
name: parsedParams.toolName, name: raw.toolName.trim(),
input,
output,
error,
metadata: raw.metadata,
status, status,
detail,
error: null,
...(parsedParams.metadata ? { metadata: parsedParams.metadata } : {}),
}; };
});
const OpencodeKnownToolNameSchema = z.union([
z.literal("shell"),
z.literal("bash"),
z.literal("exec_command"),
z.literal("read"),
z.literal("read_file"),
z.literal("write"),
z.literal("write_file"),
z.literal("create_file"),
z.literal("edit"),
z.literal("apply_patch"),
z.literal("apply_diff"),
z.literal("search"),
z.literal("web_search"),
]);
const OpencodeToolCallPass2BaseSchema = z.object({
callId: z.string().min(1),
name: z.string().min(1),
input: z.unknown().nullable(),
output: z.unknown().nullable(),
error: z.unknown().nullable(),
metadata: z.record(z.string(), z.unknown()).optional(),
status: OpencodeToolCallStatusSchema,
toolKind: z.enum(["known", "other"]),
});
const OpencodeToolCallPass2InputSchema = OpencodeToolCallPass2BaseSchema.omit({
toolKind: true,
});
const OpencodeToolCallPass2EnvelopeSchema = z.union([
OpencodeToolCallPass2InputSchema.extend({
name: OpencodeKnownToolNameSchema,
}).transform((normalized) => ({
...normalized,
name: normalized.name.trim(),
toolKind: "known" as const,
})),
OpencodeToolCallPass2InputSchema.transform((normalized) => ({
...normalized,
name: normalized.name.trim(),
toolKind: "other" as const,
})),
]);
const OpencodeToolCallPass2Schema = z.discriminatedUnion("toolKind", [
OpencodeToolCallPass2BaseSchema.extend({
toolKind: z.literal("known"),
name: OpencodeKnownToolNameSchema,
}).transform((normalized): ToolCallTimelineItem => {
const detail = deriveOpencodeToolDetail(normalized.name, normalized.input, normalized.output);
if (normalized.status === "failed") {
return {
type: "tool_call",
callId: normalized.callId,
name: normalized.name,
status: "failed",
detail,
error: normalized.error ?? { message: "Tool call failed" },
...(normalized.metadata ? { metadata: normalized.metadata } : {}),
};
}
return {
type: "tool_call",
callId: normalized.callId,
name: normalized.name,
status: normalized.status,
detail,
error: null,
...(normalized.metadata ? { metadata: normalized.metadata } : {}),
};
}),
OpencodeToolCallPass2BaseSchema.extend({
toolKind: z.literal("other"),
}).transform((normalized): ToolCallTimelineItem => {
const detail = deriveOpencodeToolDetail(normalized.name, normalized.input, normalized.output);
if (normalized.status === "failed") {
return {
type: "tool_call",
callId: normalized.callId,
name: normalized.name,
status: "failed",
detail,
error: normalized.error ?? { message: "Tool call failed" },
...(normalized.metadata ? { metadata: normalized.metadata } : {}),
};
}
return {
type: "tool_call",
callId: normalized.callId,
name: normalized.name,
status: normalized.status,
detail,
error: null,
...(normalized.metadata ? { metadata: normalized.metadata } : {}),
};
}),
]);
export function mapOpencodeToolCall(params: OpencodeToolCallParams): ToolCallTimelineItem | null {
const pass1 = OpencodeNormalizedToolCallPass1Schema.safeParse(params);
if (!pass1.success) {
return null;
}
const pass2Envelope = OpencodeToolCallPass2EnvelopeSchema.safeParse(pass1.data);
if (!pass2Envelope.success) {
return null;
}
const pass2 = OpencodeToolCallPass2Schema.safeParse(pass2Envelope.data);
if (!pass2.success) {
return null;
}
return pass2.data;
} }
@@ -2,7 +2,6 @@ import { z } from "zod";
import type { ToolCallDetail } from "../agent-sdk-types.js"; import type { ToolCallDetail } from "../agent-sdk-types.js";
import { import {
commandFromValue,
extractCodexShellOutput, extractCodexShellOutput,
flattenReadContent as flattenToolReadContent, flattenReadContent as flattenToolReadContent,
nonEmptyString, nonEmptyString,
@@ -29,9 +28,19 @@ export const ToolShellInputSchema = z
.passthrough(), .passthrough(),
]) ])
.transform((value) => { .transform((value) => {
const commandValue = "command" in value ? value.command : value.cmd; const parsedCommand = CommandValueSchema.safeParse(
"command" in value ? value.command : value.cmd
);
const command = parsedCommand.success
? typeof parsedCommand.data === "string"
? nonEmptyString(parsedCommand.data)
: parsedCommand.data
.map((token) => token.trim())
.filter((token) => token.length > 0)
.join(" ") || undefined
: undefined;
return { return {
command: commandFromValue(commandValue), command,
cwd: nonEmptyString(value.cwd) ?? nonEmptyString(value.directory), cwd: nonEmptyString(value.cwd) ?? nonEmptyString(value.directory),
}; };
}); });
@@ -314,17 +323,12 @@ type ToolReadOutputValue = {
content?: string; content?: string;
}; };
export const ToolReadOutputSchema: z.ZodType< export const ToolReadOutputSchema = ToolReadOutputContentSchema;
ToolReadOutputValue,
z.ZodTypeDef,
unknown
> = ToolReadOutputContentSchema;
export const ToolReadOutputWithPathSchema: z.ZodType< export const ToolReadOutputWithPathSchema = z.union([
ToolReadOutputValue, ToolReadOutputContentSchema,
z.ZodTypeDef, ToolReadOutputPathSchema,
unknown ]);
> = z.union([ToolReadOutputContentSchema, ToolReadOutputPathSchema]);
export const ToolWriteContentSchema = z export const ToolWriteContentSchema = z
.object({ .object({
@@ -508,8 +512,8 @@ export const ToolSearchInputSchema = z.union([
export type ParsedToolShellInput = z.infer<typeof ToolShellInputSchema>; export type ParsedToolShellInput = z.infer<typeof ToolShellInputSchema>;
export type ParsedToolShellOutput = z.infer<typeof ToolShellOutputSchema>; export type ParsedToolShellOutput = z.infer<typeof ToolShellOutputSchema>;
export type ParsedToolReadInput = z.infer<typeof ToolReadInputSchema>; export type ParsedToolReadInput = z.infer<typeof ToolReadInputSchema>;
export type ParsedToolReadOutput = z.infer<typeof ToolReadOutputSchema>; export type ParsedToolReadOutput = ToolReadOutputValue;
export type ParsedToolReadOutputWithPath = z.infer<typeof ToolReadOutputWithPathSchema>; export type ParsedToolReadOutputWithPath = ToolReadOutputValue;
export type ParsedToolWriteInput = z.infer<typeof ToolWriteInputSchema>; export type ParsedToolWriteInput = z.infer<typeof ToolWriteInputSchema>;
export type ParsedToolWriteOutput = z.infer<typeof ToolWriteOutputSchema>; export type ParsedToolWriteOutput = z.infer<typeof ToolWriteOutputSchema>;
export type ParsedToolEditInput = z.infer<typeof ToolEditInputSchema>; export type ParsedToolEditInput = z.infer<typeof ToolEditInputSchema>;
@@ -649,7 +653,13 @@ export function toolDetailBranchByName<
input: inputSchema.nullable(), input: inputSchema.nullable(),
output: outputSchema.nullable(), output: outputSchema.nullable(),
}) })
.transform(({ input, output }) => mapper(input, output)); .transform((value) => {
const parsed = value as unknown as {
input: z.infer<InputSchema> | null;
output: z.infer<OutputSchema> | null;
};
return mapper(parsed.input, parsed.output);
});
} }
export function toolDetailBranchByToolName< export function toolDetailBranchByToolName<
@@ -671,7 +681,13 @@ export function toolDetailBranchByToolName<
input: inputSchema.nullable(), input: inputSchema.nullable(),
output: outputSchema.nullable(), output: outputSchema.nullable(),
}) })
.transform(({ input, output }) => mapper(input, output)); .transform((value) => {
const parsed = value as unknown as {
input: z.infer<InputSchema> | null;
output: z.infer<OutputSchema> | null;
};
return mapper(parsed.input, parsed.output);
});
} }
export function toolDetailBranchByNameWithCwd< export function toolDetailBranchByNameWithCwd<
@@ -695,5 +711,12 @@ export function toolDetailBranchByNameWithCwd<
output: outputSchema.nullable(), output: outputSchema.nullable(),
cwd: z.string().optional().nullable(), cwd: z.string().optional().nullable(),
}) })
.transform(({ input, output, cwd }) => mapper(input, output, cwd ?? null)); .transform((value) => {
const parsed = value as unknown as {
input: z.infer<InputSchema> | null;
output: z.infer<OutputSchema> | null;
cwd?: string | null;
};
return mapper(parsed.input, parsed.output, parsed.cwd ?? null);
});
} }
@@ -8,19 +8,6 @@ export function nonEmptyString(value: unknown): string | undefined {
return typeof value === "string" && value.length > 0 ? value : undefined; return typeof value === "string" && value.length > 0 ? value : undefined;
} }
export function commandFromValue(value: unknown): string | undefined {
if (typeof value === "string") {
return nonEmptyString(value);
}
if (!Array.isArray(value)) {
return undefined;
}
const tokens = value.filter(
(token): token is string => typeof token === "string" && token.length > 0
);
return tokens.length > 0 ? tokens.join(" ") : undefined;
}
const CODEX_SHELL_ENVELOPE_HEADER_LINES = new Set([ const CODEX_SHELL_ENVELOPE_HEADER_LINES = new Set([
"chunk id:", "chunk id:",
"wall time:", "wall time:",