From 81d2e8988d523acbbd6b9abea537be328723a6cf Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Mon, 9 Feb 2026 09:16:37 +0700 Subject: [PATCH] refactor: enforce provider-owned canonical tool-call parsing --- .../providers/claude/tool-call-mapper.ts | 593 +++++++++++++++-- .../agent/providers/codex/tool-call-mapper.ts | 606 ++++++++++++++---- .../providers/opencode/tool-call-mapper.ts | 588 +++++++++++++++-- .../providers/standard-tool-call-schemas.ts | 577 ----------------- .../agent/providers/tool-call-mapper-utils.ts | 56 -- 5 files changed, 1560 insertions(+), 860 deletions(-) delete mode 100644 packages/server/src/server/agent/providers/standard-tool-call-schemas.ts 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 0e3ed630f..5f1913799 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 @@ -2,25 +2,11 @@ import { z } from "zod"; import type { ToolCallDetail, ToolCallTimelineItem } from "../../agent-sdk-types.js"; import { - StandardEditInputSchema, - StandardEditOutputSchema, - StandardReadOutputSchema, - StandardReadPathInputSchema, - StandardSearchInputSchema, - StandardShellInputSchema, - StandardShellOutputSchema, - StandardWriteInputSchema, - StandardWriteOutputSchema, - toStandardEditDetail, - toStandardReadDetail, - toStandardSearchDetail, - toStandardShellDetail, - toStandardWriteDetail, -} from "../standard-tool-call-schemas.js"; -import { - CLAUDE_KNOWN_TOOL_ALIASES, coerceToolCallId, - unionToolDetailSchemas, + commandFromValue, + flattenReadContent as flattenToolReadContent, + nonEmptyString, + truncateDiffText, } from "../tool-call-mapper-utils.js"; type MapperParams = { @@ -31,6 +17,8 @@ type MapperParams = { metadata?: Record; }; +const MAX_DIFF_TEXT_CHARS = 12_000; + const ClaudeMapperParamsSchema = z .object({ callId: z.string().optional().nullable(), @@ -45,6 +33,437 @@ const ClaudeFailedMapperParamsSchema = ClaudeMapperParamsSchema.extend({ error: z.unknown(), }); +const CommandValueSchema = z.union([z.string(), z.array(z.string())]); + +const ClaudeShellInputSchema = z + .union([ + z + .object({ + command: CommandValueSchema, + cwd: z.string().optional(), + directory: z.string().optional(), + }) + .passthrough(), + z + .object({ + cmd: CommandValueSchema, + cwd: z.string().optional(), + directory: z.string().optional(), + }) + .passthrough(), + ]) + .transform((value) => { + const commandValue = "command" in value ? value.command : value.cmd; + return { + command: commandFromValue(commandValue), + cwd: nonEmptyString(value.cwd) ?? nonEmptyString(value.directory), + }; + }); + +const ClaudeShellOutputObjectSchema = z + .object({ + command: z.string().optional(), + output: z.string().optional(), + text: z.string().optional(), + content: z.string().optional(), + aggregated_output: z.string().optional(), + exitCode: z.number().finite().nullable().optional(), + exit_code: z.number().finite().nullable().optional(), + metadata: z + .object({ + exitCode: z.number().finite().nullable().optional(), + exit_code: z.number().finite().nullable().optional(), + }) + .passthrough() + .optional(), + structuredContent: z + .object({ + output: z.string().optional(), + text: z.string().optional(), + content: z.string().optional(), + }) + .passthrough() + .optional(), + structured_content: z + .object({ + output: z.string().optional(), + text: z.string().optional(), + content: z.string().optional(), + }) + .passthrough() + .optional(), + result: z + .object({ + command: z.string().optional(), + output: z.string().optional(), + text: z.string().optional(), + content: z.string().optional(), + }) + .passthrough() + .optional(), + }) + .passthrough(); + +const ClaudeShellOutputSchema = z.union([ + z.string().transform((value) => ({ + command: undefined, + output: nonEmptyString(value), + exitCode: undefined, + })), + ClaudeShellOutputObjectSchema.transform((value) => ({ + command: nonEmptyString(value.command) ?? nonEmptyString(value.result?.command), + output: + nonEmptyString(value.output) ?? + nonEmptyString(value.text) ?? + nonEmptyString(value.content) ?? + nonEmptyString(value.aggregated_output) ?? + nonEmptyString(value.structuredContent?.output) ?? + nonEmptyString(value.structuredContent?.text) ?? + nonEmptyString(value.structuredContent?.content) ?? + nonEmptyString(value.structured_content?.output) ?? + nonEmptyString(value.structured_content?.text) ?? + nonEmptyString(value.structured_content?.content) ?? + nonEmptyString(value.result?.output) ?? + nonEmptyString(value.result?.text) ?? + nonEmptyString(value.result?.content), + exitCode: + value.exitCode ?? + value.exit_code ?? + value.metadata?.exitCode ?? + value.metadata?.exit_code ?? + undefined, + })), +]); + +const ClaudeReadPathInputSchema = z.union([ + z + .object({ + file_path: z.string(), + offset: z.number().finite().optional(), + limit: z.number().finite().optional(), + }) + .passthrough() + .transform((value) => ({ + filePath: value.file_path, + offset: value.offset, + limit: value.limit, + })), + z + .object({ + path: z.string(), + offset: z.number().finite().optional(), + limit: z.number().finite().optional(), + }) + .passthrough() + .transform((value) => ({ + filePath: value.path, + offset: value.offset, + limit: value.limit, + })), + z + .object({ + filePath: z.string(), + offset: z.number().finite().optional(), + limit: z.number().finite().optional(), + }) + .passthrough() + .transform((value) => ({ + filePath: value.filePath, + offset: value.offset, + limit: value.limit, + })), +]); + +const ClaudeReadChunkSchema = z.union([ + z + .object({ + text: z.string(), + content: z.string().optional(), + output: z.string().optional(), + }) + .passthrough(), + z + .object({ + text: z.string().optional(), + content: z.string(), + output: z.string().optional(), + }) + .passthrough(), + z + .object({ + text: z.string().optional(), + content: z.string().optional(), + output: z.string(), + }) + .passthrough(), +]); + +const ClaudeReadContentSchema = z.union([ + z.string(), + ClaudeReadChunkSchema, + z.array(ClaudeReadChunkSchema), +]); + +const ClaudeReadPayloadSchema = z.union([ + z + .object({ + content: ClaudeReadContentSchema, + text: ClaudeReadContentSchema.optional(), + output: ClaudeReadContentSchema.optional(), + }) + .passthrough(), + z + .object({ + content: ClaudeReadContentSchema.optional(), + text: ClaudeReadContentSchema, + output: ClaudeReadContentSchema.optional(), + }) + .passthrough(), + z + .object({ + content: ClaudeReadContentSchema.optional(), + text: ClaudeReadContentSchema.optional(), + output: ClaudeReadContentSchema, + }) + .passthrough(), +]); + +const ClaudeReadOutputSchema = z.union([ + z.string().transform((value) => ({ content: nonEmptyString(value) })), + ClaudeReadChunkSchema.transform((value) => ({ content: flattenReadContent(value) })), + z.array(ClaudeReadChunkSchema).transform((value) => ({ content: flattenReadContent(value) })), + ClaudeReadPayloadSchema.transform((value) => ({ + content: + flattenReadContent(value.content) ?? + flattenReadContent(value.text) ?? + flattenReadContent(value.output), + })), + z + .object({ data: ClaudeReadPayloadSchema }) + .passthrough() + .transform((value) => ({ + content: + flattenReadContent(value.data.content) ?? + flattenReadContent(value.data.text) ?? + flattenReadContent(value.data.output), + })), + z + .object({ structuredContent: ClaudeReadPayloadSchema }) + .passthrough() + .transform((value) => ({ + content: + flattenReadContent(value.structuredContent.content) ?? + flattenReadContent(value.structuredContent.text) ?? + flattenReadContent(value.structuredContent.output), + })), + z + .object({ structured_content: ClaudeReadPayloadSchema }) + .passthrough() + .transform((value) => ({ + content: + flattenReadContent(value.structured_content.content) ?? + flattenReadContent(value.structured_content.text) ?? + flattenReadContent(value.structured_content.output), + })), +]); + +const ClaudeWritePathInputSchema = z.union([ + z.object({ file_path: z.string() }).passthrough().transform((value) => ({ filePath: value.file_path })), + z.object({ path: z.string() }).passthrough().transform((value) => ({ filePath: value.path })), + z.object({ filePath: z.string() }).passthrough().transform((value) => ({ filePath: value.filePath })), +]); + +const ClaudeWriteContentSchema = z + .object({ + content: z.string().optional(), + new_content: z.string().optional(), + newContent: z.string().optional(), + }) + .passthrough(); + +const ClaudeWriteInputSchema = z + .intersection(ClaudeWritePathInputSchema, ClaudeWriteContentSchema) + .transform((value) => ({ + filePath: value.filePath, + content: + nonEmptyString(value.content) ?? + nonEmptyString(value.new_content) ?? + nonEmptyString(value.newContent), + })); + +const ClaudeWriteOutputSchema = z.union([ + z + .intersection(ClaudeWritePathInputSchema, ClaudeWriteContentSchema) + .transform((value) => ({ + filePath: value.filePath, + content: + nonEmptyString(value.content) ?? + nonEmptyString(value.new_content) ?? + nonEmptyString(value.newContent), + })), + ClaudeWriteContentSchema.transform((value) => ({ + filePath: undefined, + content: + nonEmptyString(value.content) ?? + nonEmptyString(value.new_content) ?? + nonEmptyString(value.newContent), + })), +]); + +const ClaudeEditTextSchema = z + .object({ + old_string: z.string().optional(), + old_str: z.string().optional(), + oldContent: z.string().optional(), + old_content: z.string().optional(), + new_string: z.string().optional(), + new_str: z.string().optional(), + newContent: z.string().optional(), + new_content: z.string().optional(), + content: z.string().optional(), + patch: z.string().optional(), + diff: z.string().optional(), + unified_diff: z.string().optional(), + unifiedDiff: z.string().optional(), + }) + .passthrough(); + +const ClaudeEditInputSchema = z + .intersection(ClaudeWritePathInputSchema, ClaudeEditTextSchema) + .transform((value) => ({ + filePath: value.filePath, + oldString: + nonEmptyString(value.old_string) ?? + nonEmptyString(value.old_str) ?? + nonEmptyString(value.oldContent) ?? + nonEmptyString(value.old_content), + newString: + nonEmptyString(value.new_string) ?? + nonEmptyString(value.new_str) ?? + nonEmptyString(value.newContent) ?? + nonEmptyString(value.new_content) ?? + nonEmptyString(value.content), + unifiedDiff: truncateDiffText( + nonEmptyString(value.patch) ?? + nonEmptyString(value.diff) ?? + nonEmptyString(value.unified_diff) ?? + nonEmptyString(value.unifiedDiff), + MAX_DIFF_TEXT_CHARS + ), + })); + +const ClaudeEditOutputFileSchema = z.union([ + z + .object({ + path: z.string(), + patch: z.string().optional(), + diff: z.string().optional(), + unified_diff: z.string().optional(), + unifiedDiff: z.string().optional(), + }) + .passthrough() + .transform((value) => ({ + filePath: value.path, + unifiedDiff: truncateDiffText( + nonEmptyString(value.patch) ?? + nonEmptyString(value.diff) ?? + nonEmptyString(value.unified_diff) ?? + nonEmptyString(value.unifiedDiff), + MAX_DIFF_TEXT_CHARS + ), + })), + z + .object({ + file_path: z.string(), + patch: z.string().optional(), + diff: z.string().optional(), + unified_diff: z.string().optional(), + unifiedDiff: z.string().optional(), + }) + .passthrough() + .transform((value) => ({ + filePath: value.file_path, + unifiedDiff: truncateDiffText( + nonEmptyString(value.patch) ?? + nonEmptyString(value.diff) ?? + nonEmptyString(value.unified_diff) ?? + nonEmptyString(value.unifiedDiff), + MAX_DIFF_TEXT_CHARS + ), + })), + z + .object({ + filePath: z.string(), + patch: z.string().optional(), + diff: z.string().optional(), + unified_diff: z.string().optional(), + unifiedDiff: z.string().optional(), + }) + .passthrough() + .transform((value) => ({ + filePath: value.filePath, + unifiedDiff: truncateDiffText( + nonEmptyString(value.patch) ?? + nonEmptyString(value.diff) ?? + nonEmptyString(value.unified_diff) ?? + nonEmptyString(value.unifiedDiff), + MAX_DIFF_TEXT_CHARS + ), + })), +]); + +const ClaudeEditOutputSchema = z.union([ + z + .intersection(ClaudeWritePathInputSchema, ClaudeEditTextSchema) + .transform((value) => ({ + filePath: value.filePath, + newString: + nonEmptyString(value.newContent) ?? + nonEmptyString(value.new_content) ?? + nonEmptyString(value.content), + unifiedDiff: truncateDiffText( + nonEmptyString(value.patch) ?? + nonEmptyString(value.diff) ?? + nonEmptyString(value.unified_diff) ?? + nonEmptyString(value.unifiedDiff), + MAX_DIFF_TEXT_CHARS + ), + })), + z + .object({ files: z.array(ClaudeEditOutputFileSchema).min(1) }) + .passthrough() + .transform((value) => ({ + filePath: value.files[0]?.filePath, + unifiedDiff: value.files[0]?.unifiedDiff, + newString: undefined, + })), + ClaudeEditTextSchema.transform((value) => ({ + filePath: undefined, + newString: + nonEmptyString(value.newContent) ?? + nonEmptyString(value.new_content) ?? + nonEmptyString(value.content), + unifiedDiff: truncateDiffText( + nonEmptyString(value.patch) ?? + nonEmptyString(value.diff) ?? + nonEmptyString(value.unified_diff) ?? + nonEmptyString(value.unifiedDiff), + MAX_DIFF_TEXT_CHARS + ), + })), +]); + +const ClaudeSearchInputSchema = z.union([ + z.object({ query: z.string() }).passthrough().transform((value) => ({ query: value.query })), + z.object({ q: z.string() }).passthrough().transform((value) => ({ query: value.q })), +]); + +function flattenReadContent( + value: z.infer | undefined +): string | undefined { + return flattenToolReadContent(value); +} + function coerceCallId(callId: string | null | undefined, name: string, input: unknown): string { return coerceToolCallId({ providerPrefix: "claude", @@ -54,13 +473,96 @@ function coerceCallId(callId: string | null | undefined, name: string, input: un }); } -function claudeToolBranch( - name: string, +function toShellDetail( + input: z.infer | null, + output: z.infer | null +): ToolCallDetail | undefined { + const command = input?.command ?? output?.command; + if (!command) { + return undefined; + } + + return { + type: "shell", + command, + ...(input?.cwd ? { cwd: input.cwd } : {}), + ...(output?.output ? { output: output.output } : {}), + ...(output?.exitCode !== undefined ? { exitCode: output.exitCode } : {}), + }; +} + +function toReadDetail( + input: z.infer | null, + output: z.infer | null +): ToolCallDetail | undefined { + if (!input?.filePath) { + return undefined; + } + + return { + type: "read", + filePath: input.filePath, + ...(output?.content ? { content: output.content } : {}), + ...(input.offset !== undefined ? { offset: input.offset } : {}), + ...(input.limit !== undefined ? { limit: input.limit } : {}), + }; +} + +function toWriteDetail( + input: z.infer | null, + output: z.infer | null +): ToolCallDetail | undefined { + const filePath = input?.filePath ?? output?.filePath; + if (!filePath) { + return undefined; + } + + return { + type: "write", + filePath, + ...(input?.content ? { content: input.content } : output?.content ? { content: output.content } : {}), + }; +} + +function toEditDetail( + input: z.infer | null, + output: z.infer | null +): ToolCallDetail | undefined { + const filePath = input?.filePath ?? output?.filePath; + if (!filePath) { + return undefined; + } + + return { + type: "edit", + filePath, + ...(input?.oldString ? { oldString: input.oldString } : {}), + ...(input?.newString ? { newString: input.newString } : output?.newString ? { newString: output.newString } : {}), + ...(input?.unifiedDiff + ? { unifiedDiff: input.unifiedDiff } + : output?.unifiedDiff + ? { unifiedDiff: output.unifiedDiff } + : {}), + }; +} + +function toSearchDetail(input: z.infer | null): ToolCallDetail | undefined { + if (!input?.query) { + return undefined; + } + return { + type: "search", + query: input.query, + }; +} + +function claudeToolBranch( + name: Name, inputSchema: InputSchema, outputSchema: OutputSchema, mapper: ( - input: z.output | null | undefined, - output: z.output | null | undefined + input: z.infer | null, + output: z.infer | null ) => ToolCallDetail | undefined ) { return z @@ -72,30 +574,29 @@ function claudeToolBranch mapper(input, output)); } -const ClaudeKnownToolDetailSchema: z.ZodType = unionToolDetailSchemas([ - ...CLAUDE_KNOWN_TOOL_ALIASES.shell.map((name) => - claudeToolBranch(name, StandardShellInputSchema, StandardShellOutputSchema, (input, output) => - toStandardShellDetail(input ?? null, output ?? null) - ) - ), - ...CLAUDE_KNOWN_TOOL_ALIASES.read.map((name) => - claudeToolBranch(name, StandardReadPathInputSchema, StandardReadOutputSchema, (input, output) => - toStandardReadDetail(input ?? null, output ?? null) - ) - ), - ...CLAUDE_KNOWN_TOOL_ALIASES.write.map((name) => - claudeToolBranch(name, StandardWriteInputSchema, StandardWriteOutputSchema, (input, output) => - toStandardWriteDetail(input ?? null, output ?? null) - ) - ), - ...CLAUDE_KNOWN_TOOL_ALIASES.edit.map((name) => - claudeToolBranch(name, StandardEditInputSchema, StandardEditOutputSchema, (input, output) => - toStandardEditDetail(input ?? null, output ?? null) - ) - ), - ...CLAUDE_KNOWN_TOOL_ALIASES.search.map((name) => - claudeToolBranch(name, StandardSearchInputSchema, z.unknown(), (input) => toStandardSearchDetail(input ?? null)) - ), +const ClaudeKnownToolDetailSchema = z.union([ + claudeToolBranch("Bash", ClaudeShellInputSchema, ClaudeShellOutputSchema, toShellDetail), + claudeToolBranch("bash", ClaudeShellInputSchema, ClaudeShellOutputSchema, toShellDetail), + claudeToolBranch("shell", ClaudeShellInputSchema, ClaudeShellOutputSchema, toShellDetail), + claudeToolBranch("exec_command", ClaudeShellInputSchema, ClaudeShellOutputSchema, toShellDetail), + claudeToolBranch("Read", ClaudeReadPathInputSchema, ClaudeReadOutputSchema, toReadDetail), + claudeToolBranch("read", ClaudeReadPathInputSchema, ClaudeReadOutputSchema, toReadDetail), + claudeToolBranch("read_file", ClaudeReadPathInputSchema, ClaudeReadOutputSchema, toReadDetail), + claudeToolBranch("view_file", ClaudeReadPathInputSchema, ClaudeReadOutputSchema, toReadDetail), + claudeToolBranch("Write", ClaudeWriteInputSchema, ClaudeWriteOutputSchema, toWriteDetail), + claudeToolBranch("write", ClaudeWriteInputSchema, ClaudeWriteOutputSchema, toWriteDetail), + claudeToolBranch("write_file", ClaudeWriteInputSchema, ClaudeWriteOutputSchema, toWriteDetail), + claudeToolBranch("create_file", ClaudeWriteInputSchema, ClaudeWriteOutputSchema, toWriteDetail), + claudeToolBranch("Edit", ClaudeEditInputSchema, ClaudeEditOutputSchema, toEditDetail), + claudeToolBranch("MultiEdit", ClaudeEditInputSchema, ClaudeEditOutputSchema, toEditDetail), + claudeToolBranch("multi_edit", ClaudeEditInputSchema, ClaudeEditOutputSchema, toEditDetail), + claudeToolBranch("edit", ClaudeEditInputSchema, ClaudeEditOutputSchema, toEditDetail), + claudeToolBranch("apply_patch", ClaudeEditInputSchema, ClaudeEditOutputSchema, toEditDetail), + claudeToolBranch("apply_diff", ClaudeEditInputSchema, ClaudeEditOutputSchema, toEditDetail), + claudeToolBranch("str_replace_editor", ClaudeEditInputSchema, ClaudeEditOutputSchema, toEditDetail), + claudeToolBranch("WebSearch", ClaudeSearchInputSchema, z.unknown(), (input) => toSearchDetail(input)), + claudeToolBranch("web_search", ClaudeSearchInputSchema, z.unknown(), (input) => toSearchDetail(input)), + claudeToolBranch("search", ClaudeSearchInputSchema, z.unknown(), (input) => toSearchDetail(input)), ]); function deriveDetail(name: string, input: unknown, output: unknown): ToolCallDetail | undefined { 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 8ea350b8f..a8794d7ec 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 @@ -1,30 +1,13 @@ import { z } from "zod"; import type { ToolCallDetail, ToolCallTimelineItem } from "../../agent-sdk-types.js"; +import { stripCwdPrefix } from "../../../../shared/path-utils.js"; import { - StandardEditInputSchema, - StandardEditOutputSchema, - StandardReadChunkSchema, - StandardReadPathInputSchema, - StandardSearchInputSchema, - StandardShellInputSchema, - StandardShellOutputSchema, - StandardWriteInputSchema, - StandardWriteOutputSchema, - toStandardEditDetail, - toStandardSearchDetail, - toStandardShellDetail, - toStandardWriteDetail, -} from "../standard-tool-call-schemas.js"; -import { - CODEX_MCP_KNOWN_TOOL_ALIASES, - CODEX_ROLLOUT_KNOWN_TOOL_ALIASES, coerceToolCallId, commandFromValue, flattenReadContent as flattenToolReadContent, nonEmptyString, truncateDiffText, - unionToolDetailSchemas, } from "../tool-call-mapper-utils.js"; type CodexMapperOptions = { cwd?: string | null }; @@ -45,21 +28,165 @@ const CodexRolloutToolCallParamsSchema = z const CommandValueSchema = z.union([z.string(), z.array(z.string())]); -const CodexShellInputSchema = StandardShellInputSchema; -const CodexShellOutputSchema = StandardShellOutputSchema; -const CodexReadArgumentsSchema = StandardReadPathInputSchema; -const CodexWriteArgumentsSchema = StandardWriteInputSchema; -const CodexWriteResultSchema = StandardWriteOutputSchema; -const CodexEditArgumentsSchema = StandardEditInputSchema; -const CodexEditResultSchema = StandardEditOutputSchema; -const CodexSearchArgumentsSchema = StandardSearchInputSchema; +const CodexShellInputSchema = z + .union([ + z + .object({ + command: CommandValueSchema, + cwd: z.string().optional(), + directory: z.string().optional(), + }) + .passthrough(), + z + .object({ + cmd: CommandValueSchema, + cwd: z.string().optional(), + directory: z.string().optional(), + }) + .passthrough(), + ]) + .transform((value) => { + const commandValue = "command" in value ? value.command : value.cmd; + return { + command: commandFromValue(commandValue), + cwd: nonEmptyString(value.cwd) ?? nonEmptyString(value.directory), + }; + }); -const CodexReadContentSchema = z.union([ - z.string(), - StandardReadChunkSchema, - z.array(StandardReadChunkSchema), +const CodexShellOutputObjectSchema = z + .object({ + command: z.string().optional(), + output: z.string().optional(), + text: z.string().optional(), + content: z.string().optional(), + aggregatedOutput: z.string().optional(), + exitCode: z.number().finite().nullable().optional(), + exit_code: z.number().finite().nullable().optional(), + metadata: z + .object({ + exitCode: z.number().finite().nullable().optional(), + exit_code: z.number().finite().nullable().optional(), + }) + .passthrough() + .optional(), + structuredContent: z + .object({ + output: z.string().optional(), + text: z.string().optional(), + content: z.string().optional(), + }) + .passthrough() + .optional(), + structured_content: z + .object({ + output: z.string().optional(), + text: z.string().optional(), + content: z.string().optional(), + }) + .passthrough() + .optional(), + result: z + .object({ + command: z.string().optional(), + output: z.string().optional(), + text: z.string().optional(), + content: z.string().optional(), + }) + .passthrough() + .optional(), + }) + .passthrough(); + +const CodexShellOutputSchema = z.union([ + z.string().transform((value) => ({ + command: undefined, + output: nonEmptyString(value), + exitCode: undefined, + })), + CodexShellOutputObjectSchema.transform((value) => ({ + command: nonEmptyString(value.command) ?? nonEmptyString(value.result?.command), + output: + nonEmptyString(value.output) ?? + nonEmptyString(value.text) ?? + nonEmptyString(value.content) ?? + nonEmptyString(value.aggregatedOutput) ?? + nonEmptyString(value.structuredContent?.output) ?? + nonEmptyString(value.structuredContent?.text) ?? + nonEmptyString(value.structuredContent?.content) ?? + nonEmptyString(value.structured_content?.output) ?? + nonEmptyString(value.structured_content?.text) ?? + nonEmptyString(value.structured_content?.content) ?? + nonEmptyString(value.result?.output) ?? + nonEmptyString(value.result?.text) ?? + nonEmptyString(value.result?.content), + exitCode: + value.exitCode ?? + value.exit_code ?? + value.metadata?.exitCode ?? + value.metadata?.exit_code ?? + undefined, + })), ]); +const CodexPathSchema = z.union([ + z.object({ path: z.string() }).passthrough().transform((value) => value.path), + z.object({ file_path: z.string() }).passthrough().transform((value) => value.file_path), + z.object({ filePath: z.string() }).passthrough().transform((value) => value.filePath), +]); + +const CodexReadArgumentsSchema = z.union([ + z + .object({ + path: z.string(), + offset: z.number().finite().optional(), + limit: z.number().finite().optional(), + }) + .passthrough() + .transform((value) => ({ filePath: value.path, offset: value.offset, limit: value.limit })), + z + .object({ + file_path: z.string(), + offset: z.number().finite().optional(), + limit: z.number().finite().optional(), + }) + .passthrough() + .transform((value) => ({ filePath: value.file_path, offset: value.offset, limit: value.limit })), + z + .object({ + filePath: z.string(), + offset: z.number().finite().optional(), + limit: z.number().finite().optional(), + }) + .passthrough() + .transform((value) => ({ filePath: value.filePath, offset: value.offset, limit: value.limit })), +]); + +const CodexReadChunkSchema = z.union([ + z + .object({ + text: z.string(), + content: z.string().optional(), + output: z.string().optional(), + }) + .passthrough(), + z + .object({ + text: z.string().optional(), + content: z.string(), + output: z.string().optional(), + }) + .passthrough(), + z + .object({ + text: z.string().optional(), + content: z.string().optional(), + output: z.string(), + }) + .passthrough(), +]); + +const CodexReadContentSchema = z.union([z.string(), CodexReadChunkSchema, z.array(CodexReadChunkSchema)]); + const CodexReadPayloadSchema = z.union([ z .object({ @@ -134,8 +261,8 @@ const CodexReadResultWithPathSchema = z.union([ const CodexReadResultSchema = z.union([ z.string().transform((value) => ({ filePath: undefined, content: nonEmptyString(value) })), - StandardReadChunkSchema.transform((value) => ({ filePath: undefined, content: flattenReadContent(value) })), - z.array(StandardReadChunkSchema).transform((value) => ({ filePath: undefined, content: flattenReadContent(value) })), + CodexReadChunkSchema.transform((value) => ({ filePath: undefined, content: flattenReadContent(value) })), + z.array(CodexReadChunkSchema).transform((value) => ({ filePath: undefined, content: flattenReadContent(value) })), CodexReadPayloadSchema.transform((value) => ({ filePath: undefined, content: @@ -176,6 +303,184 @@ const CodexReadResultSchema = z.union([ CodexReadResultWithPathSchema, ]); +const CodexWriteContentSchema = z + .object({ + content: z.string().optional(), + new_content: z.string().optional(), + newContent: z.string().optional(), + }) + .passthrough(); + +const CodexWriteArgumentsSchema = z + .intersection(CodexPathSchema.transform((filePath) => ({ filePath })), CodexWriteContentSchema) + .transform((value) => ({ + filePath: value.filePath, + content: + nonEmptyString(value.content) ?? + nonEmptyString(value.new_content) ?? + nonEmptyString(value.newContent), + })); + +const CodexWriteResultSchema = z.union([ + z + .intersection(CodexPathSchema.transform((filePath) => ({ filePath })), CodexWriteContentSchema) + .transform((value) => ({ + filePath: value.filePath, + content: + nonEmptyString(value.content) ?? + nonEmptyString(value.new_content) ?? + nonEmptyString(value.newContent), + })), + CodexWriteContentSchema.transform((value) => ({ + filePath: undefined, + content: + nonEmptyString(value.content) ?? + nonEmptyString(value.new_content) ?? + nonEmptyString(value.newContent), + })), +]); + +const CodexEditTextSchema = z + .object({ + old_string: z.string().optional(), + old_str: z.string().optional(), + oldContent: z.string().optional(), + old_content: z.string().optional(), + new_string: z.string().optional(), + new_str: z.string().optional(), + newContent: z.string().optional(), + new_content: z.string().optional(), + content: z.string().optional(), + patch: z.string().optional(), + diff: z.string().optional(), + unified_diff: z.string().optional(), + unifiedDiff: z.string().optional(), + }) + .passthrough(); + +const CodexEditArgumentsSchema = z + .intersection(CodexPathSchema.transform((filePath) => ({ filePath })), CodexEditTextSchema) + .transform((value) => ({ + filePath: value.filePath, + oldString: + nonEmptyString(value.old_string) ?? + nonEmptyString(value.old_str) ?? + nonEmptyString(value.oldContent) ?? + nonEmptyString(value.old_content), + newString: + nonEmptyString(value.new_string) ?? + nonEmptyString(value.new_str) ?? + nonEmptyString(value.newContent) ?? + nonEmptyString(value.new_content) ?? + nonEmptyString(value.content), + unifiedDiff: truncateDiffText( + nonEmptyString(value.patch) ?? + nonEmptyString(value.diff) ?? + nonEmptyString(value.unified_diff) ?? + nonEmptyString(value.unifiedDiff) + ), + })); + +const CodexEditResultFileSchema = z.union([ + z + .object({ + path: z.string(), + patch: z.string().optional(), + diff: z.string().optional(), + unified_diff: z.string().optional(), + unifiedDiff: z.string().optional(), + }) + .passthrough() + .transform((value) => ({ + filePath: value.path, + unifiedDiff: truncateDiffText( + nonEmptyString(value.patch) ?? + nonEmptyString(value.diff) ?? + nonEmptyString(value.unified_diff) ?? + nonEmptyString(value.unifiedDiff) + ), + })), + z + .object({ + file_path: z.string(), + patch: z.string().optional(), + diff: z.string().optional(), + unified_diff: z.string().optional(), + unifiedDiff: z.string().optional(), + }) + .passthrough() + .transform((value) => ({ + filePath: value.file_path, + unifiedDiff: truncateDiffText( + nonEmptyString(value.patch) ?? + nonEmptyString(value.diff) ?? + nonEmptyString(value.unified_diff) ?? + nonEmptyString(value.unifiedDiff) + ), + })), + z + .object({ + filePath: z.string(), + patch: z.string().optional(), + diff: z.string().optional(), + unified_diff: z.string().optional(), + unifiedDiff: z.string().optional(), + }) + .passthrough() + .transform((value) => ({ + filePath: value.filePath, + unifiedDiff: truncateDiffText( + nonEmptyString(value.patch) ?? + nonEmptyString(value.diff) ?? + nonEmptyString(value.unified_diff) ?? + nonEmptyString(value.unifiedDiff) + ), + })), +]); + +const CodexEditResultSchema = z.union([ + z + .intersection(CodexPathSchema.transform((filePath) => ({ filePath })), CodexEditTextSchema) + .transform((value) => ({ + filePath: value.filePath, + newString: + nonEmptyString(value.newContent) ?? + nonEmptyString(value.new_content) ?? + nonEmptyString(value.content), + unifiedDiff: truncateDiffText( + nonEmptyString(value.patch) ?? + nonEmptyString(value.diff) ?? + nonEmptyString(value.unified_diff) ?? + nonEmptyString(value.unifiedDiff) + ), + })), + z + .object({ files: z.array(CodexEditResultFileSchema).min(1) }) + .passthrough() + .transform((value) => ({ + filePath: value.files[0]?.filePath, + unifiedDiff: value.files[0]?.unifiedDiff, + newString: undefined, + })), + CodexEditTextSchema.transform((value) => ({ + filePath: undefined, + newString: + nonEmptyString(value.newContent) ?? + nonEmptyString(value.new_content) ?? + nonEmptyString(value.content), + unifiedDiff: truncateDiffText( + nonEmptyString(value.patch) ?? + nonEmptyString(value.diff) ?? + nonEmptyString(value.unified_diff) ?? + nonEmptyString(value.unifiedDiff) + ), + })), +]); + +const CodexSearchArgumentsSchema = z.union([ + z.object({ query: z.string() }).passthrough().transform((value) => ({ query: value.query })), + z.object({ q: z.string() }).passthrough().transform((value) => ({ query: value.q })), +]); const CodexCommandExecutionItemSchema = z .object({ @@ -267,10 +572,7 @@ function normalizeCodexFilePath(filePath: string | undefined, cwd: string | null return undefined; } if (typeof cwd === "string" && cwd.length > 0) { - const prefix = cwd.endsWith("/") ? cwd : `${cwd}/`; - if (trimmed.startsWith(prefix)) { - return trimmed.slice(prefix.length) || "."; - } + return stripCwdPrefix(trimmed, cwd); } return trimmed; } @@ -303,7 +605,18 @@ function toShellDetail( input: z.infer | null, output: z.infer | null ): ToolCallDetail | undefined { - return toStandardShellDetail(input, output); + const command = input?.command ?? output?.command; + if (!command) { + return undefined; + } + + return { + type: "shell", + command, + ...(input?.cwd ? { cwd: input.cwd } : {}), + ...(output?.output ? { output: output.output } : {}), + ...(output?.exitCode !== undefined ? { exitCode: output.exitCode } : {}), + }; } function toReadDetail( @@ -330,7 +643,16 @@ function toWriteDetail( output: z.infer | null, cwd: string | null | undefined ): ToolCallDetail | undefined { - return toStandardWriteDetail(input, output, (filePath) => normalizeCodexFilePath(filePath, cwd)); + const filePath = normalizeCodexFilePath(input?.filePath ?? output?.filePath, cwd); + if (!filePath) { + return undefined; + } + + return { + type: "write", + filePath, + ...(input?.content ? { content: input.content } : output?.content ? { content: output.content } : {}), + }; } function toEditDetail( @@ -338,120 +660,114 @@ function toEditDetail( output: z.infer | null, cwd: string | null | undefined ): ToolCallDetail | undefined { - return toStandardEditDetail(input, output, (filePath) => normalizeCodexFilePath(filePath, cwd)); + const filePath = normalizeCodexFilePath(input?.filePath ?? output?.filePath, cwd); + if (!filePath) { + return undefined; + } + + return { + type: "edit", + filePath, + ...(input?.oldString ? { oldString: input.oldString } : {}), + ...(input?.newString ? { newString: input.newString } : output?.newString ? { newString: output.newString } : {}), + ...(input?.unifiedDiff + ? { unifiedDiff: input.unifiedDiff } + : output?.unifiedDiff + ? { unifiedDiff: output.unifiedDiff } + : {}), + }; } function toSearchDetail(input: z.infer | null): ToolCallDetail | undefined { - return toStandardSearchDetail(input); + if (!input?.query) { + return undefined; + } + return { + type: "search", + query: input.query, + }; } -function codexMcpToolBranch( - tool: ToolName, - inputSchema: InputSchema, - outputSchema: OutputSchema, - mapper: ( - input: z.infer | null, - output: z.infer | null, - cwd: string | null | undefined - ) => ToolCallDetail | undefined -) { - return z - .object({ - tool: z.literal(tool), - arguments: inputSchema.nullable(), - result: outputSchema.nullable(), - cwd: z.string().optional().nullable(), - }) - .transform(({ arguments: input, result: output, cwd }) => mapper(input, output, cwd)); -} - -const CodexKnownMcpToolDetailSchema: z.ZodType = unionToolDetailSchemas([ - ...CODEX_MCP_KNOWN_TOOL_ALIASES.shell.map((tool) => - codexMcpToolBranch(tool, CodexShellInputSchema, CodexShellOutputSchema, (input, output) => - toShellDetail(input, output) - ) - ), - ...CODEX_MCP_KNOWN_TOOL_ALIASES.read.map((tool) => - codexMcpToolBranch(tool, CodexReadArgumentsSchema, CodexReadResultSchema, toReadDetail) - ), - ...CODEX_MCP_KNOWN_TOOL_ALIASES.write.map((tool) => - codexMcpToolBranch(tool, CodexWriteArgumentsSchema, CodexWriteResultSchema, toWriteDetail) - ), - ...CODEX_MCP_KNOWN_TOOL_ALIASES.edit.map((tool) => - codexMcpToolBranch(tool, CodexEditArgumentsSchema, CodexEditResultSchema, toEditDetail) - ), - ...CODEX_MCP_KNOWN_TOOL_ALIASES.search.map((tool) => - codexMcpToolBranch(tool, CodexSearchArgumentsSchema, z.unknown(), (input) => toSearchDetail(input)) - ), -]); - -function codexRolloutToolBranch( +function codexToolDetailBranch( name: Name, inputSchema: InputSchema, outputSchema: OutputSchema, - mapper: ( - input: z.infer | null, - output: z.infer | null - ) => ToolCallDetail | undefined + mapper: (params: { + input: z.infer | null; + output: z.infer | null; + cwd: string | null | undefined; + }) => ToolCallDetail | undefined ) { return z .object({ name: z.literal(name), input: inputSchema.nullable(), output: outputSchema.nullable(), + cwd: z.string().optional().nullable(), }) - .transform(({ input, output }) => mapper(input, output)); + .transform(({ input, output, cwd }) => mapper({ input, output, cwd })); } -const CodexKnownRolloutDetailSchema: z.ZodType = unionToolDetailSchemas([ - ...CODEX_ROLLOUT_KNOWN_TOOL_ALIASES.shell.map((name) => - codexRolloutToolBranch(name, CodexShellInputSchema, CodexShellOutputSchema, (input, output) => - toShellDetail(input, output) - ) +const CodexKnownToolDetailSchema = z.union([ + codexToolDetailBranch("Bash", CodexShellInputSchema, CodexShellOutputSchema, ({ input, output }) => + toShellDetail(input, output) ), - ...CODEX_ROLLOUT_KNOWN_TOOL_ALIASES.read.map((name) => - codexRolloutToolBranch(name, CodexReadArgumentsSchema, CodexReadResultSchema, (input, output) => - toReadDetail(input, output, null) - ) + codexToolDetailBranch("shell", CodexShellInputSchema, CodexShellOutputSchema, ({ input, output }) => + toShellDetail(input, output) ), - ...CODEX_ROLLOUT_KNOWN_TOOL_ALIASES.write.map((name) => - codexRolloutToolBranch(name, CodexWriteArgumentsSchema, CodexWriteResultSchema, (input, output) => - toWriteDetail(input, output, null) - ) + codexToolDetailBranch("bash", CodexShellInputSchema, CodexShellOutputSchema, ({ input, output }) => + toShellDetail(input, output) ), - ...CODEX_ROLLOUT_KNOWN_TOOL_ALIASES.edit.map((name) => - codexRolloutToolBranch(name, CodexEditArgumentsSchema, CodexEditResultSchema, (input, output) => - toEditDetail(input, output, null) - ) + codexToolDetailBranch("exec", CodexShellInputSchema, CodexShellOutputSchema, ({ input, output }) => + toShellDetail(input, output) ), - ...CODEX_ROLLOUT_KNOWN_TOOL_ALIASES.search.map((name) => - codexRolloutToolBranch(name, CodexSearchArgumentsSchema, z.unknown(), (input) => toSearchDetail(input)) + codexToolDetailBranch("exec_command", CodexShellInputSchema, CodexShellOutputSchema, ({ input, output }) => + toShellDetail(input, output) + ), + codexToolDetailBranch("command", CodexShellInputSchema, CodexShellOutputSchema, ({ input, output }) => + toShellDetail(input, output) + ), + codexToolDetailBranch("read", CodexReadArgumentsSchema, CodexReadResultSchema, ({ input, output, cwd }) => + toReadDetail(input, output, cwd) + ), + codexToolDetailBranch("read_file", CodexReadArgumentsSchema, CodexReadResultSchema, ({ input, output, cwd }) => + toReadDetail(input, output, cwd) + ), + codexToolDetailBranch("write", CodexWriteArgumentsSchema, CodexWriteResultSchema, ({ input, output, cwd }) => + toWriteDetail(input, output, cwd) + ), + codexToolDetailBranch("write_file", CodexWriteArgumentsSchema, CodexWriteResultSchema, ({ input, output, cwd }) => + toWriteDetail(input, output, cwd) + ), + codexToolDetailBranch("create_file", CodexWriteArgumentsSchema, CodexWriteResultSchema, ({ input, output, cwd }) => + toWriteDetail(input, output, cwd) + ), + codexToolDetailBranch("edit", CodexEditArgumentsSchema, CodexEditResultSchema, ({ input, output, cwd }) => + toEditDetail(input, output, cwd) + ), + codexToolDetailBranch("apply_patch", CodexEditArgumentsSchema, CodexEditResultSchema, ({ input, output, cwd }) => + toEditDetail(input, output, cwd) + ), + codexToolDetailBranch("apply_diff", CodexEditArgumentsSchema, CodexEditResultSchema, ({ input, output, cwd }) => + toEditDetail(input, output, cwd) + ), + codexToolDetailBranch("search", CodexSearchArgumentsSchema, z.unknown(), ({ input }) => toSearchDetail(input)), + codexToolDetailBranch("web_search", CodexSearchArgumentsSchema, z.unknown(), ({ input }) => + toSearchDetail(input) ), ]); -function deriveMcpToolDetail( - tool: string, - input: unknown, - output: unknown, - options?: CodexMapperOptions -): ToolCallDetail | undefined { - const parsed = CodexKnownMcpToolDetailSchema.safeParse({ - tool, - arguments: input, - result: output, - cwd: options?.cwd ?? null, - }); - if (!parsed.success) { - return undefined; - } - return parsed.data; -} - -function deriveRolloutDetail(name: string, input: unknown, output: unknown): ToolCallDetail | undefined { - const parsed = CodexKnownRolloutDetailSchema.safeParse({ - name, - input, - output, +function deriveToolDetail(params: { + name: string; + input: unknown; + output: unknown; + cwd?: string | null; +}): ToolCallDetail | undefined { + const parsed = CodexKnownToolDetailSchema.safeParse({ + name: params.name, + input: params.input, + output: params.output, + cwd: params.cwd ?? null, }); if (!parsed.success) { return undefined; @@ -496,9 +812,23 @@ function buildToolCall(params: { }; } -const CODEX_BUILTIN_TOOL_NAMES = new Set( - Object.values(CODEX_MCP_KNOWN_TOOL_ALIASES).flat() -); +const CODEX_BUILTIN_TOOL_NAMES = new Set([ + "shell", + "bash", + "exec", + "exec_command", + "command", + "read", + "read_file", + "write", + "write_file", + "create_file", + "edit", + "apply_patch", + "apply_diff", + "web_search", + "search", +]); function buildMcpToolName(server: string | undefined, tool: string): string { const trimmedTool = tool.trim(); @@ -639,7 +969,12 @@ function mapMcpToolCallItem( const error = item.error ?? null; const callId = coerceCallId(item.id ?? item.callID ?? item.call_id, name, input); const status = resolveStatus(item.status, error, output); - const detail = deriveMcpToolDetail(tool, input, output, options); + const detail = deriveToolDetail({ + name: tool, + input, + output, + cwd: options?.cwd ?? null, + }); return buildToolCall({ callId, @@ -720,7 +1055,12 @@ export function mapCodexRolloutToolCall(params: { const error = parsed.error ?? null; const status = resolveStatus("completed", error, output); const callId = coerceCallId(parsed.callId, parsed.name, input); - const detail = deriveRolloutDetail(parsed.name, input, output); + const detail = deriveToolDetail({ + name: parsed.name, + input, + output, + cwd: null, + }); return buildToolCall({ callId, diff --git a/packages/server/src/server/agent/providers/opencode/tool-call-mapper.ts b/packages/server/src/server/agent/providers/opencode/tool-call-mapper.ts index bc34a059a..397a92df2 100644 --- a/packages/server/src/server/agent/providers/opencode/tool-call-mapper.ts +++ b/packages/server/src/server/agent/providers/opencode/tool-call-mapper.ts @@ -1,26 +1,12 @@ import { z } from "zod"; import type { ToolCallDetail, ToolCallTimelineItem } from "../../agent-sdk-types.js"; -import { - StandardEditInputSchema, - StandardEditOutputSchema, - StandardReadOutputSchema, - StandardReadPathInputSchema, - StandardSearchInputSchema, - StandardShellInputSchema, - StandardShellOutputSchema, - StandardWriteInputSchema, - StandardWriteOutputSchema, - toStandardEditDetail, - toStandardReadDetail, - toStandardSearchDetail, - toStandardShellDetail, - toStandardWriteDetail, -} from "../standard-tool-call-schemas.js"; import { coerceToolCallId, - OPENCODE_KNOWN_TOOL_ALIASES, - unionToolDetailSchemas, + commandFromValue, + flattenReadContent as flattenToolReadContent, + nonEmptyString, + truncateDiffText, } from "../tool-call-mapper-utils.js"; type OpencodeToolCallParams = { @@ -33,6 +19,8 @@ type OpencodeToolCallParams = { metadata?: Record; }; +const MAX_DIFF_TEXT_CHARS = 12_000; + const FAILED_STATUSES = new Set(["error", "failed", "failure"]); const CANCELED_STATUSES = new Set(["canceled", "cancelled", "aborted", "interrupted"]); const COMPLETED_STATUSES = new Set(["complete", "completed", "success", "succeeded", "done"]); @@ -49,6 +37,435 @@ const OpencodeToolCallParamsSchema = z }) .passthrough(); +const CommandValueSchema = z.union([z.string(), z.array(z.string())]); + +const OpencodeShellInputSchema = z + .union([ + z + .object({ + command: CommandValueSchema, + cwd: z.string().optional(), + directory: z.string().optional(), + }) + .passthrough(), + z + .object({ + cmd: CommandValueSchema, + cwd: z.string().optional(), + directory: z.string().optional(), + }) + .passthrough(), + ]) + .transform((value) => { + const commandValue = "command" in value ? value.command : value.cmd; + return { + command: commandFromValue(commandValue), + cwd: nonEmptyString(value.cwd) ?? nonEmptyString(value.directory), + }; + }); + +const OpencodeShellOutputObjectSchema = z + .object({ + command: z.string().optional(), + output: z.string().optional(), + text: z.string().optional(), + content: z.string().optional(), + exitCode: z.number().finite().nullable().optional(), + exit_code: z.number().finite().nullable().optional(), + metadata: z + .object({ + exitCode: z.number().finite().nullable().optional(), + exit_code: z.number().finite().nullable().optional(), + }) + .passthrough() + .optional(), + structuredContent: z + .object({ + output: z.string().optional(), + text: z.string().optional(), + content: z.string().optional(), + }) + .passthrough() + .optional(), + structured_content: z + .object({ + output: z.string().optional(), + text: z.string().optional(), + content: z.string().optional(), + }) + .passthrough() + .optional(), + result: z + .object({ + command: z.string().optional(), + output: z.string().optional(), + text: z.string().optional(), + content: z.string().optional(), + }) + .passthrough() + .optional(), + }) + .passthrough(); + +const OpencodeShellOutputSchema = z.union([ + z.string().transform((value) => ({ + command: undefined, + output: nonEmptyString(value), + exitCode: undefined, + })), + OpencodeShellOutputObjectSchema.transform((value) => ({ + command: nonEmptyString(value.command) ?? nonEmptyString(value.result?.command), + output: + nonEmptyString(value.output) ?? + nonEmptyString(value.text) ?? + nonEmptyString(value.content) ?? + nonEmptyString(value.structuredContent?.output) ?? + nonEmptyString(value.structuredContent?.text) ?? + nonEmptyString(value.structuredContent?.content) ?? + nonEmptyString(value.structured_content?.output) ?? + nonEmptyString(value.structured_content?.text) ?? + nonEmptyString(value.structured_content?.content) ?? + nonEmptyString(value.result?.output) ?? + nonEmptyString(value.result?.text) ?? + nonEmptyString(value.result?.content), + exitCode: + value.exitCode ?? + value.exit_code ?? + value.metadata?.exitCode ?? + value.metadata?.exit_code ?? + undefined, + })), +]); + +const OpencodeReadPathInputSchema = z.union([ + z + .object({ + file_path: z.string(), + offset: z.number().finite().optional(), + limit: z.number().finite().optional(), + }) + .passthrough() + .transform((value) => ({ + filePath: value.file_path, + offset: value.offset, + limit: value.limit, + })), + z + .object({ + path: z.string(), + offset: z.number().finite().optional(), + limit: z.number().finite().optional(), + }) + .passthrough() + .transform((value) => ({ + filePath: value.path, + offset: value.offset, + limit: value.limit, + })), + z + .object({ + filePath: z.string(), + offset: z.number().finite().optional(), + limit: z.number().finite().optional(), + }) + .passthrough() + .transform((value) => ({ + filePath: value.filePath, + offset: value.offset, + limit: value.limit, + })), +]); + +const OpencodeReadChunkSchema = z.union([ + z + .object({ + text: z.string(), + content: z.string().optional(), + output: z.string().optional(), + }) + .passthrough(), + z + .object({ + text: z.string().optional(), + content: z.string(), + output: z.string().optional(), + }) + .passthrough(), + z + .object({ + text: z.string().optional(), + content: z.string().optional(), + output: z.string(), + }) + .passthrough(), +]); + +const OpencodeReadContentSchema = z.union([ + z.string(), + OpencodeReadChunkSchema, + z.array(OpencodeReadChunkSchema), +]); + +const OpencodeReadPayloadSchema = z.union([ + z + .object({ + content: OpencodeReadContentSchema, + text: OpencodeReadContentSchema.optional(), + output: OpencodeReadContentSchema.optional(), + }) + .passthrough(), + z + .object({ + content: OpencodeReadContentSchema.optional(), + text: OpencodeReadContentSchema, + output: OpencodeReadContentSchema.optional(), + }) + .passthrough(), + z + .object({ + content: OpencodeReadContentSchema.optional(), + text: OpencodeReadContentSchema.optional(), + output: OpencodeReadContentSchema, + }) + .passthrough(), +]); + +const OpencodeReadOutputSchema = z.union([ + z.string().transform((value) => ({ content: nonEmptyString(value) })), + OpencodeReadChunkSchema.transform((value) => ({ content: flattenReadContent(value) })), + z.array(OpencodeReadChunkSchema).transform((value) => ({ content: flattenReadContent(value) })), + OpencodeReadPayloadSchema.transform((value) => ({ + content: + flattenReadContent(value.content) ?? + flattenReadContent(value.text) ?? + flattenReadContent(value.output), + })), + z + .object({ data: OpencodeReadPayloadSchema }) + .passthrough() + .transform((value) => ({ + content: + flattenReadContent(value.data.content) ?? + flattenReadContent(value.data.text) ?? + flattenReadContent(value.data.output), + })), + z + .object({ structuredContent: OpencodeReadPayloadSchema }) + .passthrough() + .transform((value) => ({ + content: + flattenReadContent(value.structuredContent.content) ?? + flattenReadContent(value.structuredContent.text) ?? + flattenReadContent(value.structuredContent.output), + })), + z + .object({ structured_content: OpencodeReadPayloadSchema }) + .passthrough() + .transform((value) => ({ + content: + flattenReadContent(value.structured_content.content) ?? + flattenReadContent(value.structured_content.text) ?? + flattenReadContent(value.structured_content.output), + })), +]); + +const OpencodeWritePathInputSchema = z.union([ + z.object({ file_path: z.string() }).passthrough().transform((value) => ({ filePath: value.file_path })), + z.object({ path: z.string() }).passthrough().transform((value) => ({ filePath: value.path })), + z.object({ filePath: z.string() }).passthrough().transform((value) => ({ filePath: value.filePath })), +]); + +const OpencodeWriteContentSchema = z + .object({ + content: z.string().optional(), + new_content: z.string().optional(), + newContent: z.string().optional(), + }) + .passthrough(); + +const OpencodeWriteInputSchema = z + .intersection(OpencodeWritePathInputSchema, OpencodeWriteContentSchema) + .transform((value) => ({ + filePath: value.filePath, + content: + nonEmptyString(value.content) ?? + nonEmptyString(value.new_content) ?? + nonEmptyString(value.newContent), + })); + +const OpencodeWriteOutputSchema = z.union([ + z + .intersection(OpencodeWritePathInputSchema, OpencodeWriteContentSchema) + .transform((value) => ({ + filePath: value.filePath, + content: + nonEmptyString(value.content) ?? + nonEmptyString(value.new_content) ?? + nonEmptyString(value.newContent), + })), + OpencodeWriteContentSchema.transform((value) => ({ + filePath: undefined, + content: + nonEmptyString(value.content) ?? + nonEmptyString(value.new_content) ?? + nonEmptyString(value.newContent), + })), +]); + +const OpencodeEditTextSchema = z + .object({ + old_string: z.string().optional(), + old_str: z.string().optional(), + oldContent: z.string().optional(), + old_content: z.string().optional(), + new_string: z.string().optional(), + new_str: z.string().optional(), + newContent: z.string().optional(), + new_content: z.string().optional(), + content: z.string().optional(), + patch: z.string().optional(), + diff: z.string().optional(), + unified_diff: z.string().optional(), + unifiedDiff: z.string().optional(), + }) + .passthrough(); + +const OpencodeEditInputSchema = z + .intersection(OpencodeWritePathInputSchema, OpencodeEditTextSchema) + .transform((value) => ({ + filePath: value.filePath, + oldString: + nonEmptyString(value.old_string) ?? + nonEmptyString(value.old_str) ?? + nonEmptyString(value.oldContent) ?? + nonEmptyString(value.old_content), + newString: + nonEmptyString(value.new_string) ?? + nonEmptyString(value.new_str) ?? + nonEmptyString(value.newContent) ?? + nonEmptyString(value.new_content) ?? + nonEmptyString(value.content), + unifiedDiff: truncateDiffText( + nonEmptyString(value.patch) ?? + nonEmptyString(value.diff) ?? + nonEmptyString(value.unified_diff) ?? + nonEmptyString(value.unifiedDiff), + MAX_DIFF_TEXT_CHARS + ), + })); + +const OpencodeEditOutputFileSchema = z.union([ + z + .object({ + path: z.string(), + patch: z.string().optional(), + diff: z.string().optional(), + unified_diff: z.string().optional(), + unifiedDiff: z.string().optional(), + }) + .passthrough() + .transform((value) => ({ + filePath: value.path, + unifiedDiff: truncateDiffText( + nonEmptyString(value.patch) ?? + nonEmptyString(value.diff) ?? + nonEmptyString(value.unified_diff) ?? + nonEmptyString(value.unifiedDiff), + MAX_DIFF_TEXT_CHARS + ), + })), + z + .object({ + file_path: z.string(), + patch: z.string().optional(), + diff: z.string().optional(), + unified_diff: z.string().optional(), + unifiedDiff: z.string().optional(), + }) + .passthrough() + .transform((value) => ({ + filePath: value.file_path, + unifiedDiff: truncateDiffText( + nonEmptyString(value.patch) ?? + nonEmptyString(value.diff) ?? + nonEmptyString(value.unified_diff) ?? + nonEmptyString(value.unifiedDiff), + MAX_DIFF_TEXT_CHARS + ), + })), + z + .object({ + filePath: z.string(), + patch: z.string().optional(), + diff: z.string().optional(), + unified_diff: z.string().optional(), + unifiedDiff: z.string().optional(), + }) + .passthrough() + .transform((value) => ({ + filePath: value.filePath, + unifiedDiff: truncateDiffText( + nonEmptyString(value.patch) ?? + nonEmptyString(value.diff) ?? + nonEmptyString(value.unified_diff) ?? + nonEmptyString(value.unifiedDiff), + MAX_DIFF_TEXT_CHARS + ), + })), +]); + +const OpencodeEditOutputSchema = z.union([ + z + .intersection(OpencodeWritePathInputSchema, OpencodeEditTextSchema) + .transform((value) => ({ + filePath: value.filePath, + newString: + nonEmptyString(value.newContent) ?? + nonEmptyString(value.new_content) ?? + nonEmptyString(value.content), + unifiedDiff: truncateDiffText( + nonEmptyString(value.patch) ?? + nonEmptyString(value.diff) ?? + nonEmptyString(value.unified_diff) ?? + nonEmptyString(value.unifiedDiff), + MAX_DIFF_TEXT_CHARS + ), + })), + z + .object({ files: z.array(OpencodeEditOutputFileSchema).min(1) }) + .passthrough() + .transform((value) => ({ + filePath: value.files[0]?.filePath, + unifiedDiff: value.files[0]?.unifiedDiff, + newString: undefined, + })), + OpencodeEditTextSchema.transform((value) => ({ + filePath: undefined, + newString: + nonEmptyString(value.newContent) ?? + nonEmptyString(value.new_content) ?? + nonEmptyString(value.content), + unifiedDiff: truncateDiffText( + nonEmptyString(value.patch) ?? + nonEmptyString(value.diff) ?? + nonEmptyString(value.unified_diff) ?? + nonEmptyString(value.unifiedDiff), + MAX_DIFF_TEXT_CHARS + ), + })), +]); + +const OpencodeSearchInputSchema = z.union([ + z.object({ query: z.string() }).passthrough().transform((value) => ({ query: value.query })), + z.object({ q: z.string() }).passthrough().transform((value) => ({ query: value.q })), +]); + +function flattenReadContent( + value: z.infer | undefined +): string | undefined { + return flattenToolReadContent(value); +} + function coerceCallId(callId: string | null | undefined, toolName: string, input: unknown): string { return coerceToolCallId({ providerPrefix: "opencode", @@ -82,13 +499,100 @@ function resolveStatus(rawStatus: unknown, error: unknown, output: unknown): Too return output !== null && output !== undefined ? "completed" : "running"; } -function opencodeToolBranch( - toolName: string, +function toShellDetail( + input: z.infer | null, + output: z.infer | null +): ToolCallDetail | undefined { + const command = input?.command ?? output?.command; + if (!command) { + return undefined; + } + + return { + type: "shell", + command, + ...(input?.cwd ? { cwd: input.cwd } : {}), + ...(output?.output ? { output: output.output } : {}), + ...(output?.exitCode !== undefined ? { exitCode: output.exitCode } : {}), + }; +} + +function toReadDetail( + input: z.infer | null, + output: z.infer | null +): ToolCallDetail | undefined { + if (!input?.filePath) { + return undefined; + } + + return { + type: "read", + filePath: input.filePath, + ...(output?.content ? { content: output.content } : {}), + ...(input.offset !== undefined ? { offset: input.offset } : {}), + ...(input.limit !== undefined ? { limit: input.limit } : {}), + }; +} + +function toWriteDetail( + input: z.infer | null, + output: z.infer | null +): ToolCallDetail | undefined { + const filePath = input?.filePath ?? output?.filePath; + if (!filePath) { + return undefined; + } + + return { + type: "write", + filePath, + ...(input?.content ? { content: input.content } : output?.content ? { content: output.content } : {}), + }; +} + +function toEditDetail( + input: z.infer | null, + output: z.infer | null +): ToolCallDetail | undefined { + const filePath = input?.filePath ?? output?.filePath; + if (!filePath) { + return undefined; + } + + return { + type: "edit", + filePath, + ...(input?.oldString ? { oldString: input.oldString } : {}), + ...(input?.newString ? { newString: input.newString } : output?.newString ? { newString: output.newString } : {}), + ...(input?.unifiedDiff + ? { unifiedDiff: input.unifiedDiff } + : output?.unifiedDiff + ? { unifiedDiff: output.unifiedDiff } + : {}), + }; +} + +function toSearchDetail(input: z.infer | null): ToolCallDetail | undefined { + if (!input?.query) { + return undefined; + } + return { + type: "search", + query: input.query, + }; +} + +function opencodeToolBranch< + ToolName extends string, + InputSchema extends z.ZodTypeAny, + OutputSchema extends z.ZodTypeAny, +>( + toolName: ToolName, inputSchema: InputSchema, outputSchema: OutputSchema, mapper: ( - input: z.output | null | undefined, - output: z.output | null | undefined + input: z.infer | null, + output: z.infer | null ) => ToolCallDetail | undefined ) { return z @@ -100,32 +604,20 @@ function opencodeToolBranch mapper(input, output)); } -const OpencodeKnownToolDetailSchema: z.ZodType = unionToolDetailSchemas([ - ...OPENCODE_KNOWN_TOOL_ALIASES.shell.map((toolName) => - opencodeToolBranch(toolName, StandardShellInputSchema, StandardShellOutputSchema, (input, output) => - toStandardShellDetail(input ?? null, output ?? null) - ) - ), - ...OPENCODE_KNOWN_TOOL_ALIASES.read.map((toolName) => - opencodeToolBranch(toolName, StandardReadPathInputSchema, StandardReadOutputSchema, (input, output) => - toStandardReadDetail(input ?? null, output ?? null) - ) - ), - ...OPENCODE_KNOWN_TOOL_ALIASES.write.map((toolName) => - opencodeToolBranch(toolName, StandardWriteInputSchema, StandardWriteOutputSchema, (input, output) => - toStandardWriteDetail(input ?? null, output ?? null) - ) - ), - ...OPENCODE_KNOWN_TOOL_ALIASES.edit.map((toolName) => - opencodeToolBranch(toolName, StandardEditInputSchema, StandardEditOutputSchema, (input, output) => - toStandardEditDetail(input ?? null, output ?? null) - ) - ), - ...OPENCODE_KNOWN_TOOL_ALIASES.search.map((toolName) => - opencodeToolBranch(toolName, StandardSearchInputSchema, z.unknown(), (input) => - toStandardSearchDetail(input ?? null) - ) - ), +const OpencodeKnownToolDetailSchema = z.union([ + opencodeToolBranch("shell", OpencodeShellInputSchema, OpencodeShellOutputSchema, toShellDetail), + opencodeToolBranch("bash", OpencodeShellInputSchema, OpencodeShellOutputSchema, toShellDetail), + opencodeToolBranch("exec_command", OpencodeShellInputSchema, OpencodeShellOutputSchema, toShellDetail), + opencodeToolBranch("read", OpencodeReadPathInputSchema, OpencodeReadOutputSchema, toReadDetail), + opencodeToolBranch("read_file", OpencodeReadPathInputSchema, OpencodeReadOutputSchema, toReadDetail), + opencodeToolBranch("write", OpencodeWriteInputSchema, OpencodeWriteOutputSchema, toWriteDetail), + opencodeToolBranch("write_file", OpencodeWriteInputSchema, OpencodeWriteOutputSchema, toWriteDetail), + opencodeToolBranch("create_file", OpencodeWriteInputSchema, OpencodeWriteOutputSchema, toWriteDetail), + opencodeToolBranch("edit", OpencodeEditInputSchema, OpencodeEditOutputSchema, toEditDetail), + opencodeToolBranch("apply_patch", OpencodeEditInputSchema, OpencodeEditOutputSchema, toEditDetail), + opencodeToolBranch("apply_diff", OpencodeEditInputSchema, OpencodeEditOutputSchema, toEditDetail), + opencodeToolBranch("search", OpencodeSearchInputSchema, z.unknown(), (input) => toSearchDetail(input)), + opencodeToolBranch("web_search", OpencodeSearchInputSchema, z.unknown(), (input) => toSearchDetail(input)), ]); function deriveDetail(toolName: string, input: unknown, output: unknown): ToolCallDetail | undefined { diff --git a/packages/server/src/server/agent/providers/standard-tool-call-schemas.ts b/packages/server/src/server/agent/providers/standard-tool-call-schemas.ts deleted file mode 100644 index 679f05279..000000000 --- a/packages/server/src/server/agent/providers/standard-tool-call-schemas.ts +++ /dev/null @@ -1,577 +0,0 @@ -import { z } from "zod"; - -import type { ToolCallDetail } from "../agent-sdk-types.js"; -import { - commandFromValue, - flattenReadContent, - nonEmptyString, - truncateDiffText, -} from "./tool-call-mapper-utils.js"; - -export type StandardShellInput = { - command?: string; - cwd?: string; -}; - -export type StandardShellOutput = { - command?: string; - output?: string; - exitCode?: number | null; -}; - -export type StandardReadPathInput = { - filePath: string; - offset?: number; - limit?: number; -}; - -export type StandardReadOutput = { - content?: string; -}; - -export type StandardWriteInput = { - filePath: string; - content?: string; -}; - -export type StandardWriteOutput = { - filePath?: string; - content?: string; -}; - -export type StandardEditInput = { - filePath: string; - oldString?: string; - newString?: string; - unifiedDiff?: string; -}; - -export type StandardEditOutput = { - filePath?: string; - newString?: string; - unifiedDiff?: string; -}; - -export type StandardSearchInput = { - query: string; -}; - -const CommandValueSchema = z.union([z.string(), z.array(z.string())]); - -export const StandardShellInputSchema = z - .union([ - z - .object({ - command: CommandValueSchema, - cwd: z.string().optional(), - directory: z.string().optional(), - }) - .passthrough(), - z - .object({ - cmd: CommandValueSchema, - cwd: z.string().optional(), - directory: z.string().optional(), - }) - .passthrough(), - ]) - .transform((value) => { - const commandValue = "command" in value ? value.command : value.cmd; - return { - command: commandFromValue(commandValue), - cwd: nonEmptyString(value.cwd) ?? nonEmptyString(value.directory), - }; - }); - -const StandardShellOutputObjectSchema = z - .object({ - command: z.string().optional(), - output: z.string().optional(), - text: z.string().optional(), - content: z.string().optional(), - aggregated_output: z.string().optional(), - aggregatedOutput: z.string().optional(), - exitCode: z.number().finite().nullable().optional(), - exit_code: z.number().finite().nullable().optional(), - metadata: z - .object({ - exitCode: z.number().finite().nullable().optional(), - exit_code: z.number().finite().nullable().optional(), - }) - .passthrough() - .optional(), - structuredContent: z - .object({ - output: z.string().optional(), - text: z.string().optional(), - content: z.string().optional(), - }) - .passthrough() - .optional(), - structured_content: z - .object({ - output: z.string().optional(), - text: z.string().optional(), - content: z.string().optional(), - }) - .passthrough() - .optional(), - result: z - .object({ - command: z.string().optional(), - output: z.string().optional(), - text: z.string().optional(), - content: z.string().optional(), - }) - .passthrough() - .optional(), - }) - .passthrough(); - -export const StandardShellOutputSchema = z.union([ - z.string().transform((value) => ({ - command: undefined, - output: nonEmptyString(value), - exitCode: undefined, - })), - StandardShellOutputObjectSchema.transform((value) => ({ - command: nonEmptyString(value.command) ?? nonEmptyString(value.result?.command), - output: - nonEmptyString(value.output) ?? - nonEmptyString(value.text) ?? - nonEmptyString(value.content) ?? - nonEmptyString(value.aggregated_output) ?? - nonEmptyString(value.aggregatedOutput) ?? - nonEmptyString(value.structuredContent?.output) ?? - nonEmptyString(value.structuredContent?.text) ?? - nonEmptyString(value.structuredContent?.content) ?? - nonEmptyString(value.structured_content?.output) ?? - nonEmptyString(value.structured_content?.text) ?? - nonEmptyString(value.structured_content?.content) ?? - nonEmptyString(value.result?.output) ?? - nonEmptyString(value.result?.text) ?? - nonEmptyString(value.result?.content), - exitCode: - value.exitCode ?? - value.exit_code ?? - value.metadata?.exitCode ?? - value.metadata?.exit_code ?? - undefined, - })), -]); - -const StandardPathSchema = z.union([ - z.object({ file_path: z.string() }).passthrough().transform((value) => ({ filePath: value.file_path })), - z.object({ path: z.string() }).passthrough().transform((value) => ({ filePath: value.path })), - z.object({ filePath: z.string() }).passthrough().transform((value) => ({ filePath: value.filePath })), -]); - -export const StandardReadPathInputSchema = z.union([ - z - .object({ - file_path: z.string(), - offset: z.number().finite().optional(), - limit: z.number().finite().optional(), - }) - .passthrough() - .transform((value) => ({ - filePath: value.file_path, - offset: value.offset, - limit: value.limit, - })), - z - .object({ - path: z.string(), - offset: z.number().finite().optional(), - limit: z.number().finite().optional(), - }) - .passthrough() - .transform((value) => ({ - filePath: value.path, - offset: value.offset, - limit: value.limit, - })), - z - .object({ - filePath: z.string(), - offset: z.number().finite().optional(), - limit: z.number().finite().optional(), - }) - .passthrough() - .transform((value) => ({ - filePath: value.filePath, - offset: value.offset, - limit: value.limit, - })), -]); - -export const StandardReadChunkSchema = z.union([ - z - .object({ - text: z.string(), - content: z.string().optional(), - output: z.string().optional(), - }) - .passthrough(), - z - .object({ - text: z.string().optional(), - content: z.string(), - output: z.string().optional(), - }) - .passthrough(), - z - .object({ - text: z.string().optional(), - content: z.string().optional(), - output: z.string(), - }) - .passthrough(), -]); - -const StandardReadContentSchema = z.union([ - z.string(), - StandardReadChunkSchema, - z.array(StandardReadChunkSchema), -]); - -const StandardReadPayloadSchema = z.union([ - z - .object({ - content: StandardReadContentSchema, - text: StandardReadContentSchema.optional(), - output: StandardReadContentSchema.optional(), - }) - .passthrough(), - z - .object({ - content: StandardReadContentSchema.optional(), - text: StandardReadContentSchema, - output: StandardReadContentSchema.optional(), - }) - .passthrough(), - z - .object({ - content: StandardReadContentSchema.optional(), - text: StandardReadContentSchema.optional(), - output: StandardReadContentSchema, - }) - .passthrough(), -]); - -export const StandardReadOutputSchema: z.ZodType = z.union([ - z.string().transform((value) => ({ content: nonEmptyString(value) })), - StandardReadChunkSchema.transform((value) => ({ content: flattenReadContent(value) })), - z.array(StandardReadChunkSchema).transform((value) => ({ content: flattenReadContent(value) })), - StandardReadPayloadSchema.transform((value) => ({ - content: - flattenReadContent(value.content) ?? - flattenReadContent(value.text) ?? - flattenReadContent(value.output), - })), - z - .object({ data: StandardReadPayloadSchema }) - .passthrough() - .transform((value) => ({ - content: - flattenReadContent(value.data.content) ?? - flattenReadContent(value.data.text) ?? - flattenReadContent(value.data.output), - })), - z - .object({ structuredContent: StandardReadPayloadSchema }) - .passthrough() - .transform((value) => ({ - content: - flattenReadContent(value.structuredContent.content) ?? - flattenReadContent(value.structuredContent.text) ?? - flattenReadContent(value.structuredContent.output), - })), - z - .object({ structured_content: StandardReadPayloadSchema }) - .passthrough() - .transform((value) => ({ - content: - flattenReadContent(value.structured_content.content) ?? - flattenReadContent(value.structured_content.text) ?? - flattenReadContent(value.structured_content.output), - })), -]); - -const StandardWriteContentSchema = z - .object({ - content: z.string().optional(), - new_content: z.string().optional(), - newContent: z.string().optional(), - }) - .passthrough(); - -export const StandardWriteInputSchema = z - .intersection(StandardPathSchema, StandardWriteContentSchema) - .transform((value) => ({ - filePath: value.filePath, - content: - nonEmptyString(value.content) ?? - nonEmptyString(value.new_content) ?? - nonEmptyString(value.newContent), - })); - -export const StandardWriteOutputSchema = z.union([ - z - .intersection(StandardPathSchema, StandardWriteContentSchema) - .transform((value) => ({ - filePath: value.filePath, - content: - nonEmptyString(value.content) ?? - nonEmptyString(value.new_content) ?? - nonEmptyString(value.newContent), - })), - StandardWriteContentSchema.transform((value) => ({ - filePath: undefined, - content: - nonEmptyString(value.content) ?? - nonEmptyString(value.new_content) ?? - nonEmptyString(value.newContent), - })), -]); - -const StandardEditTextSchema = z - .object({ - old_string: z.string().optional(), - old_str: z.string().optional(), - oldContent: z.string().optional(), - old_content: z.string().optional(), - new_string: z.string().optional(), - new_str: z.string().optional(), - newContent: z.string().optional(), - new_content: z.string().optional(), - content: z.string().optional(), - patch: z.string().optional(), - diff: z.string().optional(), - unified_diff: z.string().optional(), - unifiedDiff: z.string().optional(), - }) - .passthrough(); - -export const StandardEditInputSchema = z - .intersection(StandardPathSchema, StandardEditTextSchema) - .transform((value) => ({ - filePath: value.filePath, - oldString: - nonEmptyString(value.old_string) ?? - nonEmptyString(value.old_str) ?? - nonEmptyString(value.oldContent) ?? - nonEmptyString(value.old_content), - newString: - nonEmptyString(value.new_string) ?? - nonEmptyString(value.new_str) ?? - nonEmptyString(value.newContent) ?? - nonEmptyString(value.new_content) ?? - nonEmptyString(value.content), - unifiedDiff: truncateDiffText( - nonEmptyString(value.patch) ?? - nonEmptyString(value.diff) ?? - nonEmptyString(value.unified_diff) ?? - nonEmptyString(value.unifiedDiff) - ), - })); - -const StandardEditOutputFileSchema = z.union([ - z - .object({ - path: z.string(), - patch: z.string().optional(), - diff: z.string().optional(), - unified_diff: z.string().optional(), - unifiedDiff: z.string().optional(), - }) - .passthrough() - .transform((value) => ({ - filePath: value.path, - unifiedDiff: truncateDiffText( - nonEmptyString(value.patch) ?? - nonEmptyString(value.diff) ?? - nonEmptyString(value.unified_diff) ?? - nonEmptyString(value.unifiedDiff) - ), - })), - z - .object({ - file_path: z.string(), - patch: z.string().optional(), - diff: z.string().optional(), - unified_diff: z.string().optional(), - unifiedDiff: z.string().optional(), - }) - .passthrough() - .transform((value) => ({ - filePath: value.file_path, - unifiedDiff: truncateDiffText( - nonEmptyString(value.patch) ?? - nonEmptyString(value.diff) ?? - nonEmptyString(value.unified_diff) ?? - nonEmptyString(value.unifiedDiff) - ), - })), - z - .object({ - filePath: z.string(), - patch: z.string().optional(), - diff: z.string().optional(), - unified_diff: z.string().optional(), - unifiedDiff: z.string().optional(), - }) - .passthrough() - .transform((value) => ({ - filePath: value.filePath, - unifiedDiff: truncateDiffText( - nonEmptyString(value.patch) ?? - nonEmptyString(value.diff) ?? - nonEmptyString(value.unified_diff) ?? - nonEmptyString(value.unifiedDiff) - ), - })), -]); - -export const StandardEditOutputSchema = z.union([ - z - .intersection(StandardPathSchema, StandardEditTextSchema) - .transform((value) => ({ - filePath: value.filePath, - newString: - nonEmptyString(value.newContent) ?? - nonEmptyString(value.new_content) ?? - nonEmptyString(value.content), - unifiedDiff: truncateDiffText( - nonEmptyString(value.patch) ?? - nonEmptyString(value.diff) ?? - nonEmptyString(value.unified_diff) ?? - nonEmptyString(value.unifiedDiff) - ), - })), - z - .object({ files: z.array(StandardEditOutputFileSchema).min(1) }) - .passthrough() - .transform((value) => ({ - filePath: value.files[0]?.filePath, - unifiedDiff: value.files[0]?.unifiedDiff, - newString: undefined, - })), - StandardEditTextSchema.transform((value) => ({ - filePath: undefined, - newString: - nonEmptyString(value.newContent) ?? - nonEmptyString(value.new_content) ?? - nonEmptyString(value.content), - unifiedDiff: truncateDiffText( - nonEmptyString(value.patch) ?? - nonEmptyString(value.diff) ?? - nonEmptyString(value.unified_diff) ?? - nonEmptyString(value.unifiedDiff) - ), - })), -]); - -export const StandardSearchInputSchema = z.union([ - z.object({ query: z.string() }).passthrough().transform((value) => ({ query: value.query })), - z.object({ q: z.string() }).passthrough().transform((value) => ({ query: value.q })), -]); - -export function toStandardShellDetail( - input: StandardShellInput | null, - output: StandardShellOutput | null -): ToolCallDetail | undefined { - const command = input?.command ?? output?.command; - if (!command) { - return undefined; - } - - return { - type: "shell", - command, - ...(input?.cwd ? { cwd: input.cwd } : {}), - ...(output?.output ? { output: output.output } : {}), - ...(output?.exitCode !== undefined ? { exitCode: output.exitCode } : {}), - }; -} - -export function toStandardReadDetail( - input: StandardReadPathInput | null, - output: StandardReadOutput | null, - normalizePath?: (filePath: string) => string | undefined -): ToolCallDetail | undefined { - const path = input?.filePath; - if (!path) { - return undefined; - } - const filePath = normalizePath ? normalizePath(path) : path; - if (!filePath) { - return undefined; - } - - return { - type: "read", - filePath, - ...(output?.content ? { content: output.content } : {}), - ...(input.offset !== undefined ? { offset: input.offset } : {}), - ...(input.limit !== undefined ? { limit: input.limit } : {}), - }; -} - -export function toStandardWriteDetail( - input: StandardWriteInput | null, - output: StandardWriteOutput | null, - normalizePath?: (filePath: string) => string | undefined -): ToolCallDetail | undefined { - const rawPath = input?.filePath ?? output?.filePath; - if (!rawPath) { - return undefined; - } - const filePath = normalizePath ? normalizePath(rawPath) : rawPath; - if (!filePath) { - return undefined; - } - - return { - type: "write", - filePath, - ...(input?.content ? { content: input.content } : output?.content ? { content: output.content } : {}), - }; -} - -export function toStandardEditDetail( - input: StandardEditInput | null, - output: StandardEditOutput | null, - normalizePath?: (filePath: string) => string | undefined -): ToolCallDetail | undefined { - const rawPath = input?.filePath ?? output?.filePath; - if (!rawPath) { - return undefined; - } - const filePath = normalizePath ? normalizePath(rawPath) : rawPath; - if (!filePath) { - return undefined; - } - - return { - type: "edit", - filePath, - ...(input?.oldString ? { oldString: input.oldString } : {}), - ...(input?.newString ? { newString: input.newString } : output?.newString ? { newString: output.newString } : {}), - ...(input?.unifiedDiff - ? { unifiedDiff: input.unifiedDiff } - : output?.unifiedDiff - ? { unifiedDiff: output.unifiedDiff } - : {}), - }; -} - -export function toStandardSearchDetail(input: StandardSearchInput | null): ToolCallDetail | undefined { - if (!input?.query) { - return undefined; - } - return { - type: "search", - query: input.query, - }; -} diff --git a/packages/server/src/server/agent/providers/tool-call-mapper-utils.ts b/packages/server/src/server/agent/providers/tool-call-mapper-utils.ts index 5d368bc27..bfb4b0272 100644 --- a/packages/server/src/server/agent/providers/tool-call-mapper-utils.ts +++ b/packages/server/src/server/agent/providers/tool-call-mapper-utils.ts @@ -1,54 +1,9 @@ -import { z } from "zod"; - type ReadChunkLike = { text?: string; content?: string; output?: string; }; -type ToolAliasKind = "shell" | "read" | "write" | "edit" | "search"; -export type KnownToolAliases = Record; - -export const CLAUDE_KNOWN_TOOL_ALIASES: KnownToolAliases = { - shell: ["Bash", "bash", "shell", "exec_command"], - read: ["Read", "read", "read_file", "view_file"], - write: ["Write", "write", "write_file", "create_file"], - edit: [ - "Edit", - "MultiEdit", - "multi_edit", - "edit", - "apply_patch", - "apply_diff", - "str_replace_editor", - ], - search: ["WebSearch", "web_search", "search"], -}; - -export const OPENCODE_KNOWN_TOOL_ALIASES: KnownToolAliases = { - shell: ["shell", "bash", "exec_command"], - read: ["read", "read_file"], - write: ["write", "write_file", "create_file"], - edit: ["edit", "apply_patch", "apply_diff"], - search: ["search", "web_search"], -}; - -export const CODEX_MCP_KNOWN_TOOL_ALIASES: KnownToolAliases = { - shell: ["shell", "bash", "exec", "exec_command", "command"], - read: ["read", "read_file"], - write: ["write", "write_file", "create_file"], - edit: ["edit", "apply_patch", "apply_diff"], - search: ["search", "web_search"], -}; - -export const CODEX_ROLLOUT_KNOWN_TOOL_ALIASES: KnownToolAliases = { - shell: ["Bash", "shell", "bash", "exec_command"], - read: ["read", "read_file"], - write: ["write", "write_file", "create_file"], - edit: ["edit", "apply_patch", "apply_diff"], - search: ["search", "web_search"], -}; - export function nonEmptyString(value: unknown): string | undefined { return typeof value === "string" && value.length > 0 ? value : undefined; } @@ -136,14 +91,3 @@ export function coerceToolCallId(params: { return `${params.providerPrefix}-${hashText(`${params.toolName}:${serialized}`)}`; } - -export function unionToolDetailSchemas(schemas: z.ZodTypeAny[]): z.ZodTypeAny { - if (schemas.length === 0) { - throw new Error("Expected at least one schema when building tool detail union"); - } - let union = schemas[0]; - for (let i = 1; i < schemas.length; i += 1) { - union = union.or(schemas[i]); - } - return union; -}