refactor(server): collapse tool-call mapper pass2 zod dance

Replace the duplicated 3-stage Zod pipeline (pass1 -> pass2 envelope
union -> pass2 discriminated union) in each provider mapper with a plain
resolveToolKind(name) function over readonly name sets. Opencode's
toolKind was unused; Claude/Codex used it only as a switch discriminator.
This commit is contained in:
Mohamed Boudra
2026-05-01 22:53:14 +07:00
parent 14d78327c3
commit 9800203a48
3 changed files with 160 additions and 443 deletions
@@ -13,6 +13,7 @@ interface MapperParams {
} }
const ClaudeToolCallStatusSchema = z.enum(["running", "completed", "failed", "canceled"]); const ClaudeToolCallStatusSchema = z.enum(["running", "completed", "failed", "canceled"]);
type ClaudeToolCallStatus = z.infer<typeof ClaudeToolCallStatusSchema>;
const ClaudeRawToolCallSchema = z const ClaudeRawToolCallSchema = z
.object({ .object({
@@ -26,185 +27,58 @@ const ClaudeRawToolCallSchema = z
}) })
.passthrough(); .passthrough();
const ClaudeToolCallPass1Schema = ClaudeRawToolCallSchema.transform((raw) => ({ type ClaudeToolKind =
callId: typeof raw.callId === "string" && raw.callId.trim().length > 0 ? raw.callId : null, | "shell"
name: raw.name.trim(), | "read"
input: raw.input ?? null, | "write"
output: raw.output ?? null, | "edit"
metadata: raw.metadata, | "search"
error: raw.error ?? null, | "fetch"
status: raw.status, | "speak"
})); | "unknown";
const ClaudeShellToolNameSchema = z.union([ const SHELL_NAMES: ReadonlySet<string> = new Set(["Bash", "bash", "shell", "exec_command"]);
z.literal("Bash"), const READ_NAMES: ReadonlySet<string> = new Set(["Read", "read", "read_file", "view_file"]);
z.literal("bash"), const WRITE_NAMES: ReadonlySet<string> = new Set(["Write", "write", "write_file", "create_file"]);
z.literal("shell"), const EDIT_NAMES: ReadonlySet<string> = new Set([
z.literal("exec_command"), "Edit",
]); "MultiEdit",
const ClaudeReadToolNameSchema = z.union([ "multi_edit",
z.literal("Read"),
z.literal("read"),
z.literal("read_file"),
z.literal("view_file"),
]);
const ClaudeWriteToolNameSchema = z.union([
z.literal("Write"),
z.literal("write"),
z.literal("write_file"),
z.literal("create_file"),
]);
const ClaudeEditToolNameSchema = z.union([
z.literal("Edit"),
z.literal("MultiEdit"),
z.literal("multi_edit"),
z.literal("edit"),
z.literal("apply_patch"),
z.literal("apply_diff"),
z.literal("str_replace_editor"),
]);
const ClaudeSearchToolNameSchema = z.union([
z.literal("WebSearch"),
z.literal("web_search"),
z.literal("search"),
z.literal("Grep"),
z.literal("grep"),
z.literal("Glob"),
z.literal("glob"),
]);
const ClaudeFetchToolNameSchema = z.union([
z.literal("WebFetch"),
z.literal("web_fetch"),
z.literal("WebFetchTool"),
z.literal("web_fetch_tool"),
z.literal("webfetch"),
]);
const ClaudeSpeakToolNameSchema = z
.string()
.min(1)
.refine((name) => isSpeakToolName(name.trim()));
const ClaudeToolKindSchema = z.enum([
"shell",
"read",
"write",
"edit", "edit",
"apply_patch",
"apply_diff",
"str_replace_editor",
]);
const SEARCH_NAMES: ReadonlySet<string> = new Set([
"WebSearch",
"web_search",
"search", "search",
"fetch", "Grep",
"speak", "grep",
"unknown", "Glob",
"glob",
]);
const FETCH_NAMES: ReadonlySet<string> = new Set([
"WebFetch",
"web_fetch",
"WebFetchTool",
"web_fetch_tool",
"webfetch",
]); ]);
const ClaudeToolCallPass2BaseSchema = z.object({ function resolveClaudeToolKind(name: string): ClaudeToolKind {
callId: z.string().min(1), if (SHELL_NAMES.has(name)) return "shell";
name: z.string().min(1), if (READ_NAMES.has(name)) return "read";
input: z.unknown().nullable(), if (WRITE_NAMES.has(name)) return "write";
output: z.unknown().nullable(), if (EDIT_NAMES.has(name)) return "edit";
metadata: z.record(z.string(), z.unknown()).optional(), if (SEARCH_NAMES.has(name)) return "search";
error: z.unknown().nullable(), if (FETCH_NAMES.has(name)) return "fetch";
status: ClaudeToolCallStatusSchema, if (isSpeakToolName(name)) return "speak";
toolKind: ClaudeToolKindSchema, return "unknown";
}); }
const ClaudeToolCallPass2InputSchema = ClaudeToolCallPass2BaseSchema.omit({ function resolveDetailName(toolKind: ClaudeToolKind, name: string): string {
toolKind: true, switch (toolKind) {
});
const ClaudeToolCallPass2EnvelopeSchema = z.union([
ClaudeToolCallPass2InputSchema.extend({
name: ClaudeShellToolNameSchema,
}).transform((normalized) => ({
...normalized,
name: normalized.name.trim(),
toolKind: "shell" as const,
})),
ClaudeToolCallPass2InputSchema.extend({
name: ClaudeReadToolNameSchema,
}).transform((normalized) => ({
...normalized,
name: normalized.name.trim(),
toolKind: "read" as const,
})),
ClaudeToolCallPass2InputSchema.extend({
name: ClaudeWriteToolNameSchema,
}).transform((normalized) => ({
...normalized,
name: normalized.name.trim(),
toolKind: "write" as const,
})),
ClaudeToolCallPass2InputSchema.extend({
name: ClaudeEditToolNameSchema,
}).transform((normalized) => ({
...normalized,
name: normalized.name.trim(),
toolKind: "edit" as const,
})),
ClaudeToolCallPass2InputSchema.extend({
name: ClaudeSearchToolNameSchema,
}).transform((normalized) => ({
...normalized,
name: normalized.name.trim(),
toolKind: "search" as const,
})),
ClaudeToolCallPass2InputSchema.extend({
name: ClaudeFetchToolNameSchema,
}).transform((normalized) => ({
...normalized,
name: normalized.name.trim(),
toolKind: "fetch" as const,
})),
ClaudeToolCallPass2InputSchema.extend({
name: ClaudeSpeakToolNameSchema,
}).transform((normalized) => ({
...normalized,
name: "speak" as const,
toolKind: "speak" as const,
})),
ClaudeToolCallPass2InputSchema.transform((normalized) => ({
...normalized,
name: normalized.name.trim(),
toolKind: "unknown" as const,
})),
]);
const ClaudeToolCallPass2Schema = z.discriminatedUnion("toolKind", [
ClaudeToolCallPass2BaseSchema.extend({
toolKind: z.literal("shell"),
name: ClaudeShellToolNameSchema,
}),
ClaudeToolCallPass2BaseSchema.extend({
toolKind: z.literal("read"),
name: ClaudeReadToolNameSchema,
}),
ClaudeToolCallPass2BaseSchema.extend({
toolKind: z.literal("write"),
name: ClaudeWriteToolNameSchema,
}),
ClaudeToolCallPass2BaseSchema.extend({
toolKind: z.literal("edit"),
name: ClaudeEditToolNameSchema,
}),
ClaudeToolCallPass2BaseSchema.extend({
toolKind: z.literal("search"),
name: ClaudeSearchToolNameSchema,
}),
ClaudeToolCallPass2BaseSchema.extend({
toolKind: z.literal("fetch"),
name: ClaudeFetchToolNameSchema,
}),
ClaudeToolCallPass2BaseSchema.extend({
toolKind: z.literal("speak"),
name: z.literal("speak"),
}),
ClaudeToolCallPass2BaseSchema.extend({
toolKind: z.literal("unknown"),
}),
]);
type ClaudeToolCallPass2 = z.infer<typeof ClaudeToolCallPass2Schema>;
function resolveDetailName(normalized: ClaudeToolCallPass2): string {
switch (normalized.toolKind) {
case "shell": case "shell":
return "shell"; return "shell";
case "read": case "read":
@@ -215,65 +89,56 @@ function resolveDetailName(normalized: ClaudeToolCallPass2): string {
return "apply_patch"; return "apply_patch";
case "search": case "search":
case "fetch": case "fetch":
return normalized.name; return name;
case "speak": case "speak":
return "speak"; return "speak";
default: default:
return normalized.name; return name;
} }
} }
function toToolCallTimelineItem(normalized: ClaudeToolCallPass2): ToolCallTimelineItem {
const name = normalized.toolKind === "speak" ? ("speak" as const) : normalized.name;
const detailName = resolveDetailName(normalized);
const detail = deriveClaudeToolDetail(detailName, 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 } : {}),
};
}
function mapClaudeToolCall( function mapClaudeToolCall(
params: MapperParams, params: MapperParams,
status: z.infer<typeof ClaudeToolCallStatusSchema>, status: ClaudeToolCallStatus,
error: unknown | null, error: unknown | null,
): ToolCallTimelineItem | null { ): ToolCallTimelineItem | null {
const pass1 = ClaudeToolCallPass1Schema.safeParse({ const parsed = ClaudeRawToolCallSchema.safeParse({ ...params, status, error });
...params, if (!parsed.success) {
status, return null;
error, }
}); const raw = parsed.data;
if (!pass1.success) { const callId = typeof raw.callId === "string" && raw.callId.trim().length > 0 ? raw.callId : null;
if (callId === null) {
return null; return null;
} }
const pass2Envelope = ClaudeToolCallPass2EnvelopeSchema.safeParse(pass1.data); const trimmedName = raw.name.trim();
if (!pass2Envelope.success) { const toolKind = resolveClaudeToolKind(trimmedName);
return null; const name = toolKind === "speak" ? "speak" : trimmedName;
} const input = raw.input ?? null;
const output = raw.output ?? null;
const detail = deriveClaudeToolDetail(resolveDetailName(toolKind, name), input, output);
const pass2 = ClaudeToolCallPass2Schema.safeParse(pass2Envelope.data); if (raw.status === "failed") {
if (!pass2.success) { return {
return null; type: "tool_call",
callId,
name,
detail,
status: "failed",
error: raw.error ?? { message: "Tool call failed" },
...(raw.metadata ? { metadata: raw.metadata } : {}),
};
} }
return {
return toToolCallTimelineItem(pass2.data); type: "tool_call",
callId,
name,
detail,
status: raw.status,
error: null,
...(raw.metadata ? { metadata: raw.metadata } : {}),
};
} }
export function mapClaudeRunningToolCall(params: MapperParams): ToolCallTimelineItem | null { export function mapClaudeRunningToolCall(params: MapperParams): ToolCallTimelineItem | null {
@@ -16,6 +16,7 @@ interface CodexMapperOptions {
const CodexCommandValueSchema = z.union([z.string(), z.array(z.string())]); const CodexCommandValueSchema = z.union([z.string(), z.array(z.string())]);
const CodexToolCallStatusSchema = z.enum(["running", "completed", "failed", "canceled"]); const CodexToolCallStatusSchema = z.enum(["running", "completed", "failed", "canceled"]);
type CodexToolCallStatus = z.infer<typeof CodexToolCallStatusSchema>;
const CodexRolloutToolCallParamsSchema = z const CodexRolloutToolCallParamsSchema = z
.object({ .object({
@@ -38,117 +39,44 @@ interface CodexNormalizedToolCallEnvelope {
cwd?: string | null; cwd?: string | null;
} }
const CodexNormalizedToolCallPass1Schema = z type CodexToolKind = "shell" | "read" | "write" | "edit" | "search" | "speak" | "unknown";
.object({
callId: z.string().min(1),
name: z.string().min(1),
input: z.unknown().nullable(),
output: z.unknown().nullable(),
status: CodexToolCallStatusSchema,
error: z.unknown().nullable(),
metadata: z.record(z.string(), z.unknown()).optional(),
cwd: z.string().nullable().optional(),
})
.passthrough();
const CodexShellToolNameSchema = z.union([ const CODEX_SHELL_NAMES: ReadonlySet<string> = new Set([
z.literal("Bash"), "Bash",
z.literal("shell"),
z.literal("bash"),
z.literal("exec"),
z.literal("exec_command"),
z.literal("command"),
]);
const CodexReadToolNameSchema = z.union([z.literal("read"), z.literal("read_file")]);
const CodexWriteToolNameSchema = z.union([
z.literal("write"),
z.literal("write_file"),
z.literal("create_file"),
]);
const CodexEditToolNameSchema = z.union([
z.literal("edit"),
z.literal("apply_patch"),
z.literal("apply_diff"),
]);
const CodexSearchToolNameSchema = z.union([z.literal("search"), z.literal("web_search")]);
const CodexSpeakToolNameSchema = z
.string()
.min(1)
.refine((name) => isSpeakToolName(name.trim()));
const CodexToolKindSchema = z.enum([
"shell", "shell",
"read", "bash",
"write", "exec",
"edit", "exec_command",
"search", "command",
"speak",
"unknown",
]); ]);
const CODEX_READ_NAMES: ReadonlySet<string> = new Set(["read", "read_file"]);
const CODEX_WRITE_NAMES: ReadonlySet<string> = new Set(["write", "write_file", "create_file"]);
const CODEX_EDIT_NAMES: ReadonlySet<string> = new Set(["edit", "apply_patch", "apply_diff"]);
const CODEX_SEARCH_NAMES: ReadonlySet<string> = new Set(["search", "web_search"]);
const CodexToolCallPass2BaseSchema = CodexNormalizedToolCallPass1Schema.extend({ function resolveCodexToolKind(name: string): CodexToolKind {
toolKind: CodexToolKindSchema, if (CODEX_SHELL_NAMES.has(name)) return "shell";
}); if (CODEX_READ_NAMES.has(name)) return "read";
if (CODEX_WRITE_NAMES.has(name)) return "write";
if (CODEX_EDIT_NAMES.has(name)) return "edit";
if (CODEX_SEARCH_NAMES.has(name)) return "search";
if (isSpeakToolName(name)) return "speak";
return "unknown";
}
const CodexToolCallPass2EnvelopeSchema = z.union([ interface CodexResolvedToolCall {
CodexNormalizedToolCallPass1Schema.extend({ callId: string;
name: CodexShellToolNameSchema, name: string;
}).transform((envelope) => ({ ...envelope, toolKind: "shell" as const })), toolKind: CodexToolKind;
CodexNormalizedToolCallPass1Schema.extend({ input: unknown;
name: CodexReadToolNameSchema, output: unknown;
}).transform((envelope) => ({ ...envelope, toolKind: "read" as const })), status: CodexToolCallStatus;
CodexNormalizedToolCallPass1Schema.extend({ error: unknown;
name: CodexWriteToolNameSchema, metadata?: Record<string, unknown>;
}).transform((envelope) => ({ ...envelope, toolKind: "write" as const })), cwd: string | null;
CodexNormalizedToolCallPass1Schema.extend({ }
name: CodexEditToolNameSchema,
}).transform((envelope) => ({ ...envelope, toolKind: "edit" as const })),
CodexNormalizedToolCallPass1Schema.extend({
name: CodexSearchToolNameSchema,
}).transform((envelope) => ({ ...envelope, toolKind: "search" as const })),
CodexNormalizedToolCallPass1Schema.extend({
name: CodexSpeakToolNameSchema,
}).transform((envelope) => ({ ...envelope, toolKind: "speak" as const })),
CodexNormalizedToolCallPass1Schema.transform((envelope) => ({
...envelope,
name: envelope.name.trim(),
toolKind: "unknown" as const,
})),
]);
const CodexNormalizedToolCallPass2Schema = z.discriminatedUnion("toolKind", [ function toToolCallTimelineItem(envelope: CodexResolvedToolCall): ToolCallTimelineItem {
CodexToolCallPass2BaseSchema.extend({
toolKind: z.literal("shell"),
name: CodexShellToolNameSchema,
}),
CodexToolCallPass2BaseSchema.extend({
toolKind: z.literal("read"),
name: CodexReadToolNameSchema,
}),
CodexToolCallPass2BaseSchema.extend({
toolKind: z.literal("write"),
name: CodexWriteToolNameSchema,
}),
CodexToolCallPass2BaseSchema.extend({
toolKind: z.literal("edit"),
name: CodexEditToolNameSchema,
}),
CodexToolCallPass2BaseSchema.extend({
toolKind: z.literal("search"),
name: CodexSearchToolNameSchema,
}),
CodexToolCallPass2BaseSchema.extend({
toolKind: z.literal("speak"),
name: CodexSpeakToolNameSchema,
}),
CodexToolCallPass2BaseSchema.extend({
toolKind: z.literal("unknown"),
}),
]);
type CodexNormalizedToolCallPass2 = z.infer<typeof CodexNormalizedToolCallPass2Schema>;
function toToolCallTimelineItem(envelope: CodexNormalizedToolCallPass2): ToolCallTimelineItem {
const name = envelope.toolKind === "speak" ? ("speak" as const) : envelope.name; const name = envelope.toolKind === "speak" ? ("speak" as const) : envelope.name;
const parsedDetail = deriveCodexToolDetail({ const parsedDetail = deriveCodexToolDetail({
name, name,
@@ -574,15 +502,24 @@ function toNullableObject(value: Record<string, unknown>): Record<string, unknow
function toToolCallFromNormalizedEnvelope( function toToolCallFromNormalizedEnvelope(
envelope: CodexNormalizedToolCallEnvelope, envelope: CodexNormalizedToolCallEnvelope,
): ToolCallTimelineItem | null { ): ToolCallTimelineItem | null {
const pass2Envelope = CodexToolCallPass2EnvelopeSchema.safeParse(envelope); if (envelope.callId.length === 0 || envelope.name.length === 0 || !envelope.status) {
if (!pass2Envelope.success) {
return null; return null;
} }
const parsed = CodexNormalizedToolCallPass2Schema.safeParse(pass2Envelope.data); const trimmedName = envelope.name.trim();
if (!parsed.success) { if (trimmedName.length === 0) {
return null; return null;
} }
return toToolCallTimelineItem(parsed.data); return toToolCallTimelineItem({
callId: envelope.callId,
name: trimmedName,
toolKind: resolveCodexToolKind(trimmedName),
input: envelope.input ?? null,
output: envelope.output ?? null,
status: envelope.status,
error: envelope.error ?? null,
...(envelope.metadata ? { metadata: envelope.metadata } : {}),
cwd: envelope.cwd ?? null,
});
} }
function mapCommandExecutionItem( function mapCommandExecutionItem(
@@ -905,13 +842,20 @@ export function mapCodexRolloutToolCall(params: {
} }
const normalizedName = parsed.data.name.trim(); const normalizedName = parsed.data.name.trim();
if (normalizedName.length === 0) {
return null;
}
const callId = typeof parsed.data.callId === "string" ? parsed.data.callId.trim() : "";
if (callId.length === 0) {
return null;
}
const normalizedInput = const normalizedInput =
normalizedName === "apply_patch" || normalizedName === "apply_diff" normalizedName === "apply_patch" || normalizedName === "apply_diff"
? normalizeRolloutEditInput(parsed.data.input ?? null) ? normalizeRolloutEditInput(parsed.data.input ?? null)
: (parsed.data.input ?? null); : (parsed.data.input ?? null);
const pass1 = CodexNormalizedToolCallPass1Schema.safeParse({ return toToolCallFromNormalizedEnvelope({
callId: typeof parsed.data.callId === "string" ? parsed.data.callId.trim() : "", callId,
name: normalizedName, name: normalizedName,
input: normalizedInput, input: normalizedInput,
output: parsed.data.output ?? null, output: parsed.data.output ?? null,
@@ -923,14 +867,4 @@ export function mapCodexRolloutToolCall(params: {
), ),
cwd: params.cwd ?? null, cwd: params.cwd ?? null,
}); });
if (!pass1.success) {
return null;
}
const mapped = toToolCallFromNormalizedEnvelope(pass1.data);
if (!mapped) {
return null;
}
return mapped;
} }
@@ -14,8 +14,6 @@ interface OpencodeToolCallParams {
metadata?: Record<string, unknown>; metadata?: Record<string, unknown>;
} }
const OpencodeToolCallStatusSchema = z.enum(["running", "completed", "failed", "canceled"]);
const OpencodeRawToolCallSchema = z const OpencodeRawToolCallSchema = z
.object({ .object({
toolName: z.string().min(1), toolName: z.string().min(1),
@@ -28,123 +26,43 @@ const OpencodeRawToolCallSchema = z
}) })
.passthrough(); .passthrough();
const OpencodeNormalizedToolCallPass1Schema = OpencodeRawToolCallSchema.transform((raw) => { export function mapOpencodeToolCall(params: OpencodeToolCallParams): ToolCallTimelineItem | null {
const parsed = OpencodeRawToolCallSchema.safeParse(params);
if (!parsed.success) {
return null;
}
const raw = parsed.data;
const callId =
typeof raw.callId === "string" && raw.callId.trim().length > 0 ? raw.callId.trim() : null;
if (callId === null) {
return null;
}
const name = raw.toolName.trim();
const input = raw.input ?? null; const input = raw.input ?? null;
const output = raw.output ?? null; const output = raw.output ?? null;
const error = raw.error ?? null; const error = raw.error ?? null;
const callId =
typeof raw.callId === "string" && raw.callId.trim().length > 0 ? raw.callId.trim() : null;
const rawStatus = typeof raw.status === "string" ? raw.status : undefined; const rawStatus = typeof raw.status === "string" ? raw.status : undefined;
const status = normalizeToolCallStatus(rawStatus, error, output); const status = normalizeToolCallStatus(rawStatus, error, output);
const detail = deriveOpencodeToolDetail(name, input, output);
return { if (status === "failed") {
callId,
name: raw.toolName.trim(),
input,
output,
error,
metadata: raw.metadata,
status,
};
});
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,
}),
OpencodeToolCallPass2BaseSchema.extend({
toolKind: z.literal("other"),
}),
]);
type OpencodeToolCallPass2 = z.infer<typeof OpencodeToolCallPass2Schema>;
function toToolCallTimelineItem(normalized: OpencodeToolCallPass2): ToolCallTimelineItem {
const detail = deriveOpencodeToolDetail(normalized.name, normalized.input, normalized.output);
if (normalized.status === "failed") {
return { return {
type: "tool_call", type: "tool_call",
callId: normalized.callId, callId,
name: normalized.name, name,
status: "failed", status: "failed",
detail, detail,
error: normalized.error ?? { message: "Tool call failed" }, error: error ?? { message: "Tool call failed" },
...(normalized.metadata ? { metadata: normalized.metadata } : {}), ...(raw.metadata ? { metadata: raw.metadata } : {}),
}; };
} }
return { return {
type: "tool_call", type: "tool_call",
callId: normalized.callId, callId,
name: normalized.name, name,
status: normalized.status, status,
detail, detail,
error: null, error: null,
...(normalized.metadata ? { metadata: normalized.metadata } : {}), ...(raw.metadata ? { metadata: raw.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 toToolCallTimelineItem(pass2.data);
}