From 896f88fa34f3073d1e78cfb025df815095ee302b Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Mon, 9 Feb 2026 09:07:14 +0700 Subject: [PATCH] refactor: finish strict tool-call typing and mapper dedup --- packages/app/src/stores/session-store.ts | 6 +- .../providers/claude/tool-call-mapper.ts | 593 ++---------------- .../agent/providers/codex/tool-call-mapper.ts | 524 +++------------- .../providers/opencode/tool-call-mapper.ts | 588 ++--------------- .../providers/standard-tool-call-schemas.ts | 577 +++++++++++++++++ .../agent/providers/tool-call-mapper-utils.ts | 56 ++ packages/server/src/server/messages.ts | 6 +- packages/server/src/shared/path-utils.ts | 17 + .../server/src/shared/tool-call-display.ts | 19 +- .../server/src/utils/tool-call-parsers.ts | 18 +- 10 files changed, 823 insertions(+), 1581 deletions(-) create mode 100644 packages/server/src/server/agent/providers/standard-tool-call-schemas.ts create mode 100644 packages/server/src/shared/path-utils.ts diff --git a/packages/app/src/stores/session-store.ts b/packages/app/src/stores/session-store.ts index 4e07859e6..6a9952739 100644 --- a/packages/app/src/stores/session-store.ts +++ b/packages/app/src/stores/session-store.ts @@ -54,9 +54,9 @@ export type MessageEntry = id: string; timestamp: number; toolName: string; - args: any; - result?: any; - error?: any; + args: unknown | null; + result?: unknown | null; + error?: unknown | null; status: "executing" | "completed" | "failed"; }; 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 5f1913799..0e3ed630f 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,11 +2,25 @@ 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, - commandFromValue, - flattenReadContent as flattenToolReadContent, - nonEmptyString, - truncateDiffText, + unionToolDetailSchemas, } from "../tool-call-mapper-utils.js"; type MapperParams = { @@ -17,8 +31,6 @@ type MapperParams = { metadata?: Record; }; -const MAX_DIFF_TEXT_CHARS = 12_000; - const ClaudeMapperParamsSchema = z .object({ callId: z.string().optional().nullable(), @@ -33,437 +45,6 @@ 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", @@ -473,96 +54,13 @@ function coerceCallId(callId: string | null | undefined, name: string, input: un }); } -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, +function claudeToolBranch( + name: string, inputSchema: InputSchema, outputSchema: OutputSchema, mapper: ( - input: z.infer | null, - output: z.infer | null + input: z.output | null | undefined, + output: z.output | null | undefined ) => ToolCallDetail | undefined ) { return z @@ -574,29 +72,30 @@ function claudeToolBranch mapper(input, output)); } -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)), +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)) + ), ]); 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 167cc9f7b..8ea350b8f 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 @@ -2,11 +2,29 @@ import { z } from "zod"; import type { ToolCallDetail, ToolCallTimelineItem } from "../../agent-sdk-types.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 }; @@ -27,165 +45,21 @@ const CodexRolloutToolCallParamsSchema = z const CommandValueSchema = z.union([z.string(), z.array(z.string())]); -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 CodexShellInputSchema = StandardShellInputSchema; +const CodexShellOutputSchema = StandardShellOutputSchema; +const CodexReadArgumentsSchema = StandardReadPathInputSchema; +const CodexWriteArgumentsSchema = StandardWriteInputSchema; +const CodexWriteResultSchema = StandardWriteOutputSchema; +const CodexEditArgumentsSchema = StandardEditInputSchema; +const CodexEditResultSchema = StandardEditOutputSchema; +const CodexSearchArgumentsSchema = StandardSearchInputSchema; -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 CodexReadContentSchema = z.union([ + z.string(), + StandardReadChunkSchema, + z.array(StandardReadChunkSchema), ]); -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({ @@ -260,8 +134,8 @@ const CodexReadResultWithPathSchema = z.union([ const CodexReadResultSchema = z.union([ z.string().transform((value) => ({ filePath: undefined, content: nonEmptyString(value) })), - CodexReadChunkSchema.transform((value) => ({ filePath: undefined, content: flattenReadContent(value) })), - z.array(CodexReadChunkSchema).transform((value) => ({ filePath: undefined, content: flattenReadContent(value) })), + StandardReadChunkSchema.transform((value) => ({ filePath: undefined, content: flattenReadContent(value) })), + z.array(StandardReadChunkSchema).transform((value) => ({ filePath: undefined, content: flattenReadContent(value) })), CodexReadPayloadSchema.transform((value) => ({ filePath: undefined, content: @@ -302,184 +176,6 @@ 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({ @@ -607,18 +303,7 @@ 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 } : {}), - }; + return toStandardShellDetail(input, output); } function toReadDetail( @@ -645,16 +330,7 @@ function toWriteDetail( output: z.infer | null, cwd: string | null | undefined ): ToolCallDetail | undefined { - 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 } : {}), - }; + return toStandardWriteDetail(input, output, (filePath) => normalizeCodexFilePath(filePath, cwd)); } function toEditDetail( @@ -662,32 +338,11 @@ function toEditDetail( output: z.infer | null, cwd: string | null | undefined ): ToolCallDetail | undefined { - 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 } - : {}), - }; + return toStandardEditDetail(input, output, (filePath) => normalizeCodexFilePath(filePath, cwd)); } function toSearchDetail(input: z.infer | null): ToolCallDetail | undefined { - if (!input?.query) { - return undefined; - } - return { - type: "search", - query: input.query, - }; + return toStandardSearchDetail(input); } function codexMcpToolBranch( @@ -710,32 +365,24 @@ function codexMcpToolBranch mapper(input, output, cwd)); } -const CodexKnownMcpToolDetailSchema = z.union([ - codexMcpToolBranch("shell", CodexShellInputSchema, CodexShellOutputSchema, (input, output) => - toShellDetail(input, output) +const CodexKnownMcpToolDetailSchema: z.ZodType = unionToolDetailSchemas([ + ...CODEX_MCP_KNOWN_TOOL_ALIASES.shell.map((tool) => + codexMcpToolBranch(tool, CodexShellInputSchema, CodexShellOutputSchema, (input, output) => + toShellDetail(input, output) + ) ), - codexMcpToolBranch("bash", CodexShellInputSchema, CodexShellOutputSchema, (input, output) => - toShellDetail(input, output) + ...CODEX_MCP_KNOWN_TOOL_ALIASES.read.map((tool) => + codexMcpToolBranch(tool, CodexReadArgumentsSchema, CodexReadResultSchema, toReadDetail) ), - codexMcpToolBranch("exec", CodexShellInputSchema, CodexShellOutputSchema, (input, output) => - toShellDetail(input, output) + ...CODEX_MCP_KNOWN_TOOL_ALIASES.write.map((tool) => + codexMcpToolBranch(tool, CodexWriteArgumentsSchema, CodexWriteResultSchema, toWriteDetail) ), - codexMcpToolBranch("exec_command", CodexShellInputSchema, CodexShellOutputSchema, (input, output) => - toShellDetail(input, output) + ...CODEX_MCP_KNOWN_TOOL_ALIASES.edit.map((tool) => + codexMcpToolBranch(tool, CodexEditArgumentsSchema, CodexEditResultSchema, toEditDetail) ), - codexMcpToolBranch("command", CodexShellInputSchema, CodexShellOutputSchema, (input, output) => - toShellDetail(input, output) + ...CODEX_MCP_KNOWN_TOOL_ALIASES.search.map((tool) => + codexMcpToolBranch(tool, CodexSearchArgumentsSchema, z.unknown(), (input) => toSearchDetail(input)) ), - codexMcpToolBranch("read", CodexReadArgumentsSchema, CodexReadResultSchema, toReadDetail), - codexMcpToolBranch("read_file", CodexReadArgumentsSchema, CodexReadResultSchema, toReadDetail), - codexMcpToolBranch("write", CodexWriteArgumentsSchema, CodexWriteResultSchema, toWriteDetail), - codexMcpToolBranch("write_file", CodexWriteArgumentsSchema, CodexWriteResultSchema, toWriteDetail), - codexMcpToolBranch("create_file", CodexWriteArgumentsSchema, CodexWriteResultSchema, toWriteDetail), - codexMcpToolBranch("edit", CodexEditArgumentsSchema, CodexEditResultSchema, toEditDetail), - codexMcpToolBranch("apply_patch", CodexEditArgumentsSchema, CodexEditResultSchema, toEditDetail), - codexMcpToolBranch("apply_diff", CodexEditArgumentsSchema, CodexEditResultSchema, toEditDetail), - codexMcpToolBranch("search", CodexSearchArgumentsSchema, z.unknown(), (input) => toSearchDetail(input)), - codexMcpToolBranch("web_search", CodexSearchArgumentsSchema, z.unknown(), (input) => toSearchDetail(input)), ]); function codexRolloutToolBranch( @@ -756,45 +403,30 @@ function codexRolloutToolBranch mapper(input, output)); } -const CodexKnownRolloutDetailSchema = z.union([ - codexRolloutToolBranch("Bash", CodexShellInputSchema, CodexShellOutputSchema, (input, output) => - toShellDetail(input, output) +const CodexKnownRolloutDetailSchema: z.ZodType = unionToolDetailSchemas([ + ...CODEX_ROLLOUT_KNOWN_TOOL_ALIASES.shell.map((name) => + codexRolloutToolBranch(name, CodexShellInputSchema, CodexShellOutputSchema, (input, output) => + toShellDetail(input, output) + ) ), - codexRolloutToolBranch("shell", 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) + ) ), - codexRolloutToolBranch("bash", 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) + ) ), - codexRolloutToolBranch("exec_command", 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) + ) ), - codexRolloutToolBranch("read", CodexReadArgumentsSchema, CodexReadResultSchema, (input, output) => - toReadDetail(input, output, null) + ...CODEX_ROLLOUT_KNOWN_TOOL_ALIASES.search.map((name) => + codexRolloutToolBranch(name, CodexSearchArgumentsSchema, z.unknown(), (input) => toSearchDetail(input)) ), - codexRolloutToolBranch("read_file", CodexReadArgumentsSchema, CodexReadResultSchema, (input, output) => - toReadDetail(input, output, null) - ), - codexRolloutToolBranch("write", CodexWriteArgumentsSchema, CodexWriteResultSchema, (input, output) => - toWriteDetail(input, output, null) - ), - codexRolloutToolBranch("write_file", CodexWriteArgumentsSchema, CodexWriteResultSchema, (input, output) => - toWriteDetail(input, output, null) - ), - codexRolloutToolBranch("create_file", CodexWriteArgumentsSchema, CodexWriteResultSchema, (input, output) => - toWriteDetail(input, output, null) - ), - codexRolloutToolBranch("edit", CodexEditArgumentsSchema, CodexEditResultSchema, (input, output) => - toEditDetail(input, output, null) - ), - codexRolloutToolBranch("apply_patch", CodexEditArgumentsSchema, CodexEditResultSchema, (input, output) => - toEditDetail(input, output, null) - ), - codexRolloutToolBranch("apply_diff", CodexEditArgumentsSchema, CodexEditResultSchema, (input, output) => - toEditDetail(input, output, null) - ), - codexRolloutToolBranch("search", CodexSearchArgumentsSchema, z.unknown(), (input) => toSearchDetail(input)), - codexRolloutToolBranch("web_search", CodexSearchArgumentsSchema, z.unknown(), (input) => toSearchDetail(input)), ]); function deriveMcpToolDetail( @@ -864,23 +496,9 @@ function buildToolCall(params: { }; } -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", -]); +const CODEX_BUILTIN_TOOL_NAMES = new Set( + Object.values(CODEX_MCP_KNOWN_TOOL_ALIASES).flat() +); function buildMcpToolName(server: string | undefined, tool: string): string { const trimmedTool = tool.trim(); 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 397a92df2..bc34a059a 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,12 +1,26 @@ 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, - commandFromValue, - flattenReadContent as flattenToolReadContent, - nonEmptyString, - truncateDiffText, + OPENCODE_KNOWN_TOOL_ALIASES, + unionToolDetailSchemas, } from "../tool-call-mapper-utils.js"; type OpencodeToolCallParams = { @@ -19,8 +33,6 @@ 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"]); @@ -37,435 +49,6 @@ 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", @@ -499,100 +82,13 @@ function resolveStatus(rawStatus: unknown, error: unknown, output: unknown): Too return output !== null && output !== undefined ? "completed" : "running"; } -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, +function opencodeToolBranch( + toolName: string, inputSchema: InputSchema, outputSchema: OutputSchema, mapper: ( - input: z.infer | null, - output: z.infer | null + input: z.output | null | undefined, + output: z.output | null | undefined ) => ToolCallDetail | undefined ) { return z @@ -604,20 +100,32 @@ function opencodeToolBranch< .transform(({ input, output }) => mapper(input, output)); } -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)), +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) + ) + ), ]); 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 new file mode 100644 index 000000000..679f05279 --- /dev/null +++ b/packages/server/src/server/agent/providers/standard-tool-call-schemas.ts @@ -0,0 +1,577 @@ +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 bfb4b0272..5d368bc27 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,9 +1,54 @@ +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; } @@ -91,3 +136,14 @@ 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; +} diff --git a/packages/server/src/server/messages.ts b/packages/server/src/server/messages.ts index 51809fe6c..8d0aa05c6 100644 --- a/packages/server/src/server/messages.ts +++ b/packages/server/src/server/messages.ts @@ -1,4 +1,3 @@ -import { z } from "zod"; import type { ManagedAgent } from "./agent/agent-manager.js"; import { toAgentPayload } from "./agent/agent-projections.js"; import type { AgentStreamEvent } from "./agent/agent-sdk-types.js"; @@ -9,10 +8,9 @@ import type { import { AgentStreamEventPayloadSchema as AgentStreamEventPayloadRuntimeSchema } from "../shared/messages.js"; export * from "../shared/messages.js"; -type AgentStreamEventPayloadInput = z.input; function validateStreamEventPayload( - payload: AgentStreamEventPayloadInput + payload: unknown ): AgentStreamEventPayload | null { const parsed = AgentStreamEventPayloadRuntimeSchema.safeParse(payload); if (!parsed.success) { @@ -44,5 +42,5 @@ export function serializeAgentStreamEvent( }); } - return validateStreamEventPayload(event as AgentStreamEventPayloadInput); + return validateStreamEventPayload(event); } diff --git a/packages/server/src/shared/path-utils.ts b/packages/server/src/shared/path-utils.ts new file mode 100644 index 000000000..3ef9604ea --- /dev/null +++ b/packages/server/src/shared/path-utils.ts @@ -0,0 +1,17 @@ +export function stripCwdPrefix(filePath: string, cwd?: string): string { + if (!cwd || !filePath) { + return filePath; + } + + const normalizedCwd = cwd.replace(/\\/g, "/").replace(/\/+$/, ""); + const normalizedPath = filePath.replace(/\\/g, "/"); + const prefix = `${normalizedCwd}/`; + + if (normalizedPath.startsWith(prefix)) { + return normalizedPath.slice(prefix.length); + } + if (normalizedPath === normalizedCwd) { + return "."; + } + return filePath; +} diff --git a/packages/server/src/shared/tool-call-display.ts b/packages/server/src/shared/tool-call-display.ts index 99a76a7e3..577c094bd 100644 --- a/packages/server/src/shared/tool-call-display.ts +++ b/packages/server/src/shared/tool-call-display.ts @@ -1,4 +1,5 @@ import type { ToolCallTimelineItem } from "../server/agent/agent-sdk-types.js"; +import { stripCwdPrefix } from "./path-utils.js"; export type ToolCallDisplayInput = Pick< ToolCallTimelineItem, @@ -38,24 +39,6 @@ function humanizeToolName(name: string): string { .join(" "); } -function stripCwdPrefix(filePath: string, cwd?: string): string { - if (!cwd || !filePath) { - return filePath; - } - - const normalizedCwd = cwd.replace(/\\/g, "/").replace(/\/+$/, ""); - const normalizedPath = filePath.replace(/\\/g, "/"); - const prefix = `${normalizedCwd}/`; - - if (normalizedPath.startsWith(prefix)) { - return normalizedPath.slice(prefix.length); - } - if (normalizedPath === normalizedCwd) { - return "."; - } - return filePath; -} - function formatErrorText(error: unknown): string | undefined { if (error === null || error === undefined) { return undefined; diff --git a/packages/server/src/utils/tool-call-parsers.ts b/packages/server/src/utils/tool-call-parsers.ts index 0660b057c..6ff4055b9 100644 --- a/packages/server/src/utils/tool-call-parsers.ts +++ b/packages/server/src/utils/tool-call-parsers.ts @@ -1,24 +1,10 @@ import { z } from "zod"; +import { stripCwdPrefix } from "../shared/path-utils.js"; const SHELL_WRAPPER_PREFIX_PATTERN = /^\/bin\/(?:zsh|bash|sh)\s+(?:-[a-zA-Z]+\s+)?/; const CD_AND_PATTERN = /^cd\s+(?:"[^"]+"|'[^']+'|\S+)\s+&&\s+/; - -export function stripCwdPrefix(filePath: string, cwd?: string): string { - if (!cwd || !filePath) return filePath; - - const normalizedCwd = cwd.replace(/\\/g, "/").replace(/\/+$/, ""); - const normalizedPath = filePath.replace(/\\/g, "/"); - - const prefix = `${normalizedCwd}/`; - if (normalizedPath.startsWith(prefix)) { - return normalizedPath.slice(prefix.length); - } - if (normalizedPath === normalizedCwd) { - return "."; - } - return filePath; -} +export { stripCwdPrefix }; export function stripShellWrapperPrefix(command: string): string { const prefixMatch = command.match(SHELL_WRAPPER_PREFIX_PATTERN);