From 9800203a486079cad2b2ce62fde4185677d9dcb0 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Fri, 1 May 2026 22:53:14 +0700 Subject: [PATCH] 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. --- .../providers/claude/tool-call-mapper.ts | 299 +++++------------- .../agent/providers/codex/tool-call-mapper.ts | 178 ++++------- .../providers/opencode/tool-call-mapper.ts | 126 ++------ 3 files changed, 160 insertions(+), 443 deletions(-) diff --git a/packages/server/src/server/agent/providers/claude/tool-call-mapper.ts b/packages/server/src/server/agent/providers/claude/tool-call-mapper.ts index 9de28a245..7aef1ea80 100644 --- a/packages/server/src/server/agent/providers/claude/tool-call-mapper.ts +++ b/packages/server/src/server/agent/providers/claude/tool-call-mapper.ts @@ -13,6 +13,7 @@ interface MapperParams { } const ClaudeToolCallStatusSchema = z.enum(["running", "completed", "failed", "canceled"]); +type ClaudeToolCallStatus = z.infer; const ClaudeRawToolCallSchema = z .object({ @@ -26,185 +27,58 @@ const ClaudeRawToolCallSchema = z }) .passthrough(); -const ClaudeToolCallPass1Schema = ClaudeRawToolCallSchema.transform((raw) => ({ - 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, -})); +type ClaudeToolKind = + | "shell" + | "read" + | "write" + | "edit" + | "search" + | "fetch" + | "speak" + | "unknown"; -const ClaudeShellToolNameSchema = z.union([ - z.literal("Bash"), - z.literal("bash"), - z.literal("shell"), - z.literal("exec_command"), -]); -const ClaudeReadToolNameSchema = z.union([ - 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", +const SHELL_NAMES: ReadonlySet = new Set(["Bash", "bash", "shell", "exec_command"]); +const READ_NAMES: ReadonlySet = new Set(["Read", "read", "read_file", "view_file"]); +const WRITE_NAMES: ReadonlySet = new Set(["Write", "write", "write_file", "create_file"]); +const EDIT_NAMES: ReadonlySet = new Set([ + "Edit", + "MultiEdit", + "multi_edit", "edit", + "apply_patch", + "apply_diff", + "str_replace_editor", +]); +const SEARCH_NAMES: ReadonlySet = new Set([ + "WebSearch", + "web_search", "search", - "fetch", - "speak", - "unknown", + "Grep", + "grep", + "Glob", + "glob", +]); +const FETCH_NAMES: ReadonlySet = new Set([ + "WebFetch", + "web_fetch", + "WebFetchTool", + "web_fetch_tool", + "webfetch", ]); -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: ClaudeToolKindSchema, -}); +function resolveClaudeToolKind(name: string): ClaudeToolKind { + if (SHELL_NAMES.has(name)) return "shell"; + if (READ_NAMES.has(name)) return "read"; + if (WRITE_NAMES.has(name)) return "write"; + if (EDIT_NAMES.has(name)) return "edit"; + if (SEARCH_NAMES.has(name)) return "search"; + if (FETCH_NAMES.has(name)) return "fetch"; + if (isSpeakToolName(name)) return "speak"; + return "unknown"; +} -const ClaudeToolCallPass2InputSchema = ClaudeToolCallPass2BaseSchema.omit({ - toolKind: true, -}); - -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; - -function resolveDetailName(normalized: ClaudeToolCallPass2): string { - switch (normalized.toolKind) { +function resolveDetailName(toolKind: ClaudeToolKind, name: string): string { + switch (toolKind) { case "shell": return "shell"; case "read": @@ -215,65 +89,56 @@ function resolveDetailName(normalized: ClaudeToolCallPass2): string { return "apply_patch"; case "search": case "fetch": - return normalized.name; + return name; case "speak": return "speak"; 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( params: MapperParams, - status: z.infer, + status: ClaudeToolCallStatus, error: unknown | null, ): ToolCallTimelineItem | null { - const pass1 = ClaudeToolCallPass1Schema.safeParse({ - ...params, - status, - error, - }); - if (!pass1.success) { + const parsed = ClaudeRawToolCallSchema.safeParse({ ...params, status, error }); + if (!parsed.success) { + return null; + } + const raw = parsed.data; + const callId = typeof raw.callId === "string" && raw.callId.trim().length > 0 ? raw.callId : null; + if (callId === null) { return null; } - const pass2Envelope = ClaudeToolCallPass2EnvelopeSchema.safeParse(pass1.data); - if (!pass2Envelope.success) { - return null; - } + const trimmedName = raw.name.trim(); + const toolKind = resolveClaudeToolKind(trimmedName); + 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 (!pass2.success) { - return null; + if (raw.status === "failed") { + return { + type: "tool_call", + callId, + name, + detail, + status: "failed", + error: raw.error ?? { message: "Tool call failed" }, + ...(raw.metadata ? { metadata: raw.metadata } : {}), + }; } - - return toToolCallTimelineItem(pass2.data); + return { + type: "tool_call", + callId, + name, + detail, + status: raw.status, + error: null, + ...(raw.metadata ? { metadata: raw.metadata } : {}), + }; } export function mapClaudeRunningToolCall(params: MapperParams): ToolCallTimelineItem | null { diff --git a/packages/server/src/server/agent/providers/codex/tool-call-mapper.ts b/packages/server/src/server/agent/providers/codex/tool-call-mapper.ts index 5a5ab86a6..760d757ff 100644 --- a/packages/server/src/server/agent/providers/codex/tool-call-mapper.ts +++ b/packages/server/src/server/agent/providers/codex/tool-call-mapper.ts @@ -16,6 +16,7 @@ interface CodexMapperOptions { const CodexCommandValueSchema = z.union([z.string(), z.array(z.string())]); const CodexToolCallStatusSchema = z.enum(["running", "completed", "failed", "canceled"]); +type CodexToolCallStatus = z.infer; const CodexRolloutToolCallParamsSchema = z .object({ @@ -38,117 +39,44 @@ interface CodexNormalizedToolCallEnvelope { cwd?: string | null; } -const CodexNormalizedToolCallPass1Schema = z - .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(); +type CodexToolKind = "shell" | "read" | "write" | "edit" | "search" | "speak" | "unknown"; -const CodexShellToolNameSchema = z.union([ - z.literal("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([ +const CODEX_SHELL_NAMES: ReadonlySet = new Set([ + "Bash", "shell", - "read", - "write", - "edit", - "search", - "speak", - "unknown", + "bash", + "exec", + "exec_command", + "command", ]); +const CODEX_READ_NAMES: ReadonlySet = new Set(["read", "read_file"]); +const CODEX_WRITE_NAMES: ReadonlySet = new Set(["write", "write_file", "create_file"]); +const CODEX_EDIT_NAMES: ReadonlySet = new Set(["edit", "apply_patch", "apply_diff"]); +const CODEX_SEARCH_NAMES: ReadonlySet = new Set(["search", "web_search"]); -const CodexToolCallPass2BaseSchema = CodexNormalizedToolCallPass1Schema.extend({ - toolKind: CodexToolKindSchema, -}); +function resolveCodexToolKind(name: string): CodexToolKind { + 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([ - CodexNormalizedToolCallPass1Schema.extend({ - name: CodexShellToolNameSchema, - }).transform((envelope) => ({ ...envelope, toolKind: "shell" as const })), - CodexNormalizedToolCallPass1Schema.extend({ - name: CodexReadToolNameSchema, - }).transform((envelope) => ({ ...envelope, toolKind: "read" as const })), - CodexNormalizedToolCallPass1Schema.extend({ - name: CodexWriteToolNameSchema, - }).transform((envelope) => ({ ...envelope, toolKind: "write" as const })), - 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, - })), -]); +interface CodexResolvedToolCall { + callId: string; + name: string; + toolKind: CodexToolKind; + input: unknown; + output: unknown; + status: CodexToolCallStatus; + error: unknown; + metadata?: Record; + cwd: string | null; +} -const CodexNormalizedToolCallPass2Schema = z.discriminatedUnion("toolKind", [ - 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; - -function toToolCallTimelineItem(envelope: CodexNormalizedToolCallPass2): ToolCallTimelineItem { +function toToolCallTimelineItem(envelope: CodexResolvedToolCall): ToolCallTimelineItem { const name = envelope.toolKind === "speak" ? ("speak" as const) : envelope.name; const parsedDetail = deriveCodexToolDetail({ name, @@ -574,15 +502,24 @@ function toNullableObject(value: Record): Record; } -const OpencodeToolCallStatusSchema = z.enum(["running", "completed", "failed", "canceled"]); - const OpencodeRawToolCallSchema = z .object({ toolName: z.string().min(1), @@ -28,123 +26,43 @@ const OpencodeRawToolCallSchema = z }) .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 output = raw.output ?? 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 status = normalizeToolCallStatus(rawStatus, error, output); + const detail = deriveOpencodeToolDetail(name, input, output); - return { - 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; - -function toToolCallTimelineItem(normalized: OpencodeToolCallPass2): ToolCallTimelineItem { - const detail = deriveOpencodeToolDetail(normalized.name, normalized.input, normalized.output); - if (normalized.status === "failed") { + if (status === "failed") { return { type: "tool_call", - callId: normalized.callId, - name: normalized.name, + callId, + name, status: "failed", detail, - error: normalized.error ?? { message: "Tool call failed" }, - ...(normalized.metadata ? { metadata: normalized.metadata } : {}), + error: error ?? { message: "Tool call failed" }, + ...(raw.metadata ? { metadata: raw.metadata } : {}), }; } return { type: "tool_call", - callId: normalized.callId, - name: normalized.name, - status: normalized.status, + callId, + name, + status, detail, 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); -}