diff --git a/packages/app/src/stores/session-store.ts b/packages/app/src/stores/session-store.ts index 6a9952739..fc8de8bfb 100644 --- a/packages/app/src/stores/session-store.ts +++ b/packages/app/src/stores/session-store.ts @@ -8,6 +8,7 @@ import type { PendingPermission } from "@/types/shared"; import type { AgentLifecycleStatus } from "@server/shared/agent-lifecycle"; import type { AgentPermissionResponse, + AgentPermissionRequest, AgentSessionConfig, AgentProvider, AgentMode, @@ -80,7 +81,7 @@ export interface Agent { capabilities: AgentCapabilityFlags; currentModeId: string | null; availableModes: AgentMode[]; - pendingPermissions: any[]; + pendingPermissions: AgentPermissionRequest[]; persistence: AgentPersistenceHandle | null; runtimeInfo?: AgentRuntimeInfo; lastUsage?: AgentUsage; diff --git a/packages/server/src/server/agent/providers/claude/tool-call-detail-parser.ts b/packages/server/src/server/agent/providers/claude/tool-call-detail-parser.ts new file mode 100644 index 000000000..73ecdf4da --- /dev/null +++ b/packages/server/src/server/agent/providers/claude/tool-call-detail-parser.ts @@ -0,0 +1,67 @@ +import { z } from "zod"; + +import type { ToolCallDetail } from "../../agent-sdk-types.js"; +import { + ToolEditInputSchema, + ToolEditOutputSchema, + ToolReadInputSchema, + ToolReadOutputSchema, + ToolSearchInputSchema, + ToolShellInputSchema, + ToolShellOutputSchema, + ToolWriteInputSchema, + ToolWriteOutputSchema, + toEditToolDetail, + toReadToolDetail, + toSearchToolDetail, + toShellToolDetail, + toWriteToolDetail, + toolDetailBranchByName, +} from "../tool-call-detail-primitives.js"; + +const ClaudeKnownToolDetailSchema = z.union([ + toolDetailBranchByName("Bash", ToolShellInputSchema, ToolShellOutputSchema, toShellToolDetail), + toolDetailBranchByName("bash", ToolShellInputSchema, ToolShellOutputSchema, toShellToolDetail), + toolDetailBranchByName("shell", ToolShellInputSchema, ToolShellOutputSchema, toShellToolDetail), + toolDetailBranchByName("exec_command", ToolShellInputSchema, ToolShellOutputSchema, toShellToolDetail), + toolDetailBranchByName("Read", ToolReadInputSchema, ToolReadOutputSchema, toReadToolDetail), + toolDetailBranchByName("read", ToolReadInputSchema, ToolReadOutputSchema, toReadToolDetail), + toolDetailBranchByName("read_file", ToolReadInputSchema, ToolReadOutputSchema, toReadToolDetail), + toolDetailBranchByName("view_file", ToolReadInputSchema, ToolReadOutputSchema, toReadToolDetail), + toolDetailBranchByName("Write", ToolWriteInputSchema, ToolWriteOutputSchema, toWriteToolDetail), + toolDetailBranchByName("write", ToolWriteInputSchema, ToolWriteOutputSchema, toWriteToolDetail), + toolDetailBranchByName("write_file", ToolWriteInputSchema, ToolWriteOutputSchema, toWriteToolDetail), + toolDetailBranchByName("create_file", ToolWriteInputSchema, ToolWriteOutputSchema, toWriteToolDetail), + toolDetailBranchByName("Edit", ToolEditInputSchema, ToolEditOutputSchema, toEditToolDetail), + toolDetailBranchByName("MultiEdit", ToolEditInputSchema, ToolEditOutputSchema, toEditToolDetail), + toolDetailBranchByName("multi_edit", ToolEditInputSchema, ToolEditOutputSchema, toEditToolDetail), + toolDetailBranchByName("edit", ToolEditInputSchema, ToolEditOutputSchema, toEditToolDetail), + toolDetailBranchByName("apply_patch", ToolEditInputSchema, ToolEditOutputSchema, toEditToolDetail), + toolDetailBranchByName("apply_diff", ToolEditInputSchema, ToolEditOutputSchema, toEditToolDetail), + toolDetailBranchByName("str_replace_editor", ToolEditInputSchema, ToolEditOutputSchema, toEditToolDetail), + toolDetailBranchByName("WebSearch", ToolSearchInputSchema, z.unknown(), (input) => + toSearchToolDetail(input) + ), + toolDetailBranchByName("web_search", ToolSearchInputSchema, z.unknown(), (input) => + toSearchToolDetail(input) + ), + toolDetailBranchByName("search", ToolSearchInputSchema, z.unknown(), (input) => + toSearchToolDetail(input) + ), +]); + +export function deriveClaudeToolDetail( + name: string, + input: unknown, + output: unknown +): ToolCallDetail | undefined { + const parsed = ClaudeKnownToolDetailSchema.safeParse({ + name, + input, + output, + }); + if (!parsed.success) { + return undefined; + } + return parsed.data; +} 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..53eb0e949 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 @@ -1,13 +1,8 @@ import { z } from "zod"; import type { ToolCallDetail, ToolCallTimelineItem } from "../../agent-sdk-types.js"; -import { - coerceToolCallId, - commandFromValue, - flattenReadContent as flattenToolReadContent, - nonEmptyString, - truncateDiffText, -} from "../tool-call-mapper-utils.js"; +import { coerceToolCallId } from "../tool-call-mapper-utils.js"; +import { deriveClaudeToolDetail } from "./tool-call-detail-parser.js"; type MapperParams = { callId?: string | null; @@ -17,8 +12,6 @@ type MapperParams = { metadata?: Record; }; -const MAX_DIFF_TEXT_CHARS = 12_000; - const ClaudeMapperParamsSchema = z .object({ callId: z.string().optional().nullable(), @@ -33,437 +26,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,144 +35,6 @@ 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, - inputSchema: InputSchema, - outputSchema: OutputSchema, - mapper: ( - input: z.infer | null, - output: z.infer | null - ) => ToolCallDetail | undefined -) { - return z - .object({ - name: z.literal(name), - input: inputSchema.nullable(), - output: outputSchema.nullable(), - }) - .transform(({ input, output }) => 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)), -]); - -function deriveDetail(name: string, input: unknown, output: unknown): ToolCallDetail | undefined { - const parsed = ClaudeKnownToolDetailSchema.safeParse({ - name, - input, - output, - }); - if (!parsed.success) { - return undefined; - } - return parsed.data; -} - function buildBase(params: MapperParams): { callId: string; name: string; @@ -622,7 +46,7 @@ function buildBase(params: MapperParams): { const parsedParams = ClaudeMapperParamsSchema.parse(params); const input = parsedParams.input ?? null; const output = parsedParams.output ?? null; - const detail = deriveDetail(parsedParams.name, input, output); + const detail = deriveClaudeToolDetail(parsedParams.name, input, output); return { callId: coerceCallId(parsedParams.callId, parsedParams.name, input), diff --git a/packages/server/src/server/agent/providers/codex/tool-call-detail-parser.ts b/packages/server/src/server/agent/providers/codex/tool-call-detail-parser.ts new file mode 100644 index 000000000..545d722e3 --- /dev/null +++ b/packages/server/src/server/agent/providers/codex/tool-call-detail-parser.ts @@ -0,0 +1,148 @@ +import { z } from "zod"; + +import type { ToolCallDetail } from "../../agent-sdk-types.js"; +import { stripCwdPrefix } from "../../../../shared/path-utils.js"; +import { + ToolEditInputSchema, + ToolEditOutputSchema, + ToolReadInputSchema, + ToolReadOutputWithPathSchema, + ToolSearchInputSchema, + ToolShellInputSchema, + ToolShellOutputSchema, + ToolWriteInputSchema, + ToolWriteOutputSchema, + toEditToolDetail, + toReadToolDetail, + toSearchToolDetail, + toShellToolDetail, + toWriteToolDetail, + toolDetailBranchByNameWithCwd, +} from "../tool-call-detail-primitives.js"; + +export type CodexToolDetailContext = { + cwd?: string | null; +}; + +export 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", +]); + +export function normalizeCodexFilePath( + filePath: string, + cwd: string | null | undefined +): string | undefined { + if (!filePath) { + return undefined; + } + + if (typeof cwd === "string" && cwd.length > 0) { + return stripCwdPrefix(filePath, cwd); + } + return filePath; +} + +function normalizePathForCwd(cwd: string | null): (filePath: string) => string | undefined { + return (filePath) => normalizeCodexFilePath(filePath, cwd); +} + +const CodexKnownToolDetailSchema = z.union([ + toolDetailBranchByNameWithCwd("Bash", ToolShellInputSchema, ToolShellOutputSchema, (input, output) => + toShellToolDetail(input, output) + ), + toolDetailBranchByNameWithCwd("shell", ToolShellInputSchema, ToolShellOutputSchema, (input, output) => + toShellToolDetail(input, output) + ), + toolDetailBranchByNameWithCwd("bash", ToolShellInputSchema, ToolShellOutputSchema, (input, output) => + toShellToolDetail(input, output) + ), + toolDetailBranchByNameWithCwd("exec", ToolShellInputSchema, ToolShellOutputSchema, (input, output) => + toShellToolDetail(input, output) + ), + toolDetailBranchByNameWithCwd( + "exec_command", + ToolShellInputSchema, + ToolShellOutputSchema, + (input, output) => toShellToolDetail(input, output) + ), + toolDetailBranchByNameWithCwd("command", ToolShellInputSchema, ToolShellOutputSchema, (input, output) => + toShellToolDetail(input, output) + ), + toolDetailBranchByNameWithCwd("read", ToolReadInputSchema, ToolReadOutputWithPathSchema, (input, output, cwd) => + toReadToolDetail(input, output, { normalizePath: normalizePathForCwd(cwd) }) + ), + toolDetailBranchByNameWithCwd( + "read_file", + ToolReadInputSchema, + ToolReadOutputWithPathSchema, + (input, output, cwd) => toReadToolDetail(input, output, { normalizePath: normalizePathForCwd(cwd) }) + ), + toolDetailBranchByNameWithCwd("write", ToolWriteInputSchema, ToolWriteOutputSchema, (input, output, cwd) => + toWriteToolDetail(input, output, { normalizePath: normalizePathForCwd(cwd) }) + ), + toolDetailBranchByNameWithCwd( + "write_file", + ToolWriteInputSchema, + ToolWriteOutputSchema, + (input, output, cwd) => toWriteToolDetail(input, output, { normalizePath: normalizePathForCwd(cwd) }) + ), + toolDetailBranchByNameWithCwd( + "create_file", + ToolWriteInputSchema, + ToolWriteOutputSchema, + (input, output, cwd) => toWriteToolDetail(input, output, { normalizePath: normalizePathForCwd(cwd) }) + ), + toolDetailBranchByNameWithCwd("edit", ToolEditInputSchema, ToolEditOutputSchema, (input, output, cwd) => + toEditToolDetail(input, output, { normalizePath: normalizePathForCwd(cwd) }) + ), + toolDetailBranchByNameWithCwd( + "apply_patch", + ToolEditInputSchema, + ToolEditOutputSchema, + (input, output, cwd) => toEditToolDetail(input, output, { normalizePath: normalizePathForCwd(cwd) }) + ), + toolDetailBranchByNameWithCwd( + "apply_diff", + ToolEditInputSchema, + ToolEditOutputSchema, + (input, output, cwd) => toEditToolDetail(input, output, { normalizePath: normalizePathForCwd(cwd) }) + ), + toolDetailBranchByNameWithCwd("search", ToolSearchInputSchema, z.unknown(), (input) => + toSearchToolDetail(input) + ), + toolDetailBranchByNameWithCwd("web_search", ToolSearchInputSchema, z.unknown(), (input) => + toSearchToolDetail(input) + ), +]); + +export function deriveCodexToolDetail(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; + } + return parsed.data; +} 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 a8794d7ec..02e5182dc 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,14 +1,17 @@ import { z } from "zod"; import type { ToolCallDetail, ToolCallTimelineItem } from "../../agent-sdk-types.js"; -import { stripCwdPrefix } from "../../../../shared/path-utils.js"; +import { CommandValueSchema } from "../tool-call-detail-primitives.js"; import { coerceToolCallId, commandFromValue, - flattenReadContent as flattenToolReadContent, - nonEmptyString, truncateDiffText, } from "../tool-call-mapper-utils.js"; +import { + CODEX_BUILTIN_TOOL_NAMES, + deriveCodexToolDetail, + normalizeCodexFilePath, +} from "./tool-call-detail-parser.js"; type CodexMapperOptions = { cwd?: string | null }; @@ -26,461 +29,9 @@ const CodexRolloutToolCallParamsSchema = z }) .passthrough(); -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 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({ - content: CodexReadContentSchema, - text: CodexReadContentSchema.optional(), - output: CodexReadContentSchema.optional(), - }) - .passthrough(), - z - .object({ - content: CodexReadContentSchema.optional(), - text: CodexReadContentSchema, - output: CodexReadContentSchema.optional(), - }) - .passthrough(), - z - .object({ - content: CodexReadContentSchema.optional(), - text: CodexReadContentSchema.optional(), - output: CodexReadContentSchema, - }) - .passthrough(), -]); - -const CodexReadResultWithPathSchema = z.union([ - z - .object({ - path: z.string(), - content: CodexReadContentSchema.optional(), - text: CodexReadContentSchema.optional(), - output: CodexReadContentSchema.optional(), - }) - .passthrough() - .transform((value) => ({ - filePath: value.path, - content: - flattenReadContent(value.content) ?? - flattenReadContent(value.text) ?? - flattenReadContent(value.output), - })), - z - .object({ - file_path: z.string(), - content: CodexReadContentSchema.optional(), - text: CodexReadContentSchema.optional(), - output: CodexReadContentSchema.optional(), - }) - .passthrough() - .transform((value) => ({ - filePath: value.file_path, - content: - flattenReadContent(value.content) ?? - flattenReadContent(value.text) ?? - flattenReadContent(value.output), - })), - z - .object({ - filePath: z.string(), - content: CodexReadContentSchema.optional(), - text: CodexReadContentSchema.optional(), - output: CodexReadContentSchema.optional(), - }) - .passthrough() - .transform((value) => ({ - filePath: value.filePath, - content: - flattenReadContent(value.content) ?? - flattenReadContent(value.text) ?? - flattenReadContent(value.output), - })), -]); - -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) })), - CodexReadPayloadSchema.transform((value) => ({ - filePath: undefined, - content: - flattenReadContent(value.content) ?? - flattenReadContent(value.text) ?? - flattenReadContent(value.output), - })), - z - .object({ data: CodexReadPayloadSchema }) - .passthrough() - .transform((value) => ({ - filePath: undefined, - content: - flattenReadContent(value.data.content) ?? - flattenReadContent(value.data.text) ?? - flattenReadContent(value.data.output), - })), - z - .object({ structuredContent: CodexReadPayloadSchema }) - .passthrough() - .transform((value) => ({ - filePath: undefined, - content: - flattenReadContent(value.structuredContent.content) ?? - flattenReadContent(value.structuredContent.text) ?? - flattenReadContent(value.structuredContent.output), - })), - z - .object({ structured_content: CodexReadPayloadSchema }) - .passthrough() - .transform((value) => ({ - filePath: undefined, - content: - flattenReadContent(value.structured_content.content) ?? - flattenReadContent(value.structured_content.text) ?? - flattenReadContent(value.structured_content.output), - })), - 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 })), -]); +// --------------------------------------------------------------------------- +// Thread-item parsing +// --------------------------------------------------------------------------- const CodexCommandExecutionItemSchema = z .object({ @@ -548,12 +99,6 @@ const CodexThreadItemSchema = z.discriminatedUnion("type", [ CodexWebSearchItemSchema, ]); -function flattenReadContent( - value: z.infer | undefined -): string | undefined { - return flattenToolReadContent(value); -} - function coerceCallId(raw: string | null | undefined, name: string, input: unknown): string { return coerceToolCallId({ providerPrefix: "codex", @@ -563,21 +108,11 @@ function coerceCallId(raw: string | null | undefined, name: string, input: unkno }); } -function normalizeCodexFilePath(filePath: string | undefined, cwd: string | null | undefined): string | undefined { - if (typeof filePath !== "string") { - return undefined; - } - const trimmed = filePath.trim(); - if (!trimmed) { - return undefined; - } - if (typeof cwd === "string" && cwd.length > 0) { - return stripCwdPrefix(trimmed, cwd); - } - return trimmed; -} - -function resolveStatus(rawStatus: string | undefined, error: unknown, output: unknown): ToolCallTimelineItem["status"] { +function resolveStatus( + rawStatus: string | undefined, + error: unknown, + output: unknown +): ToolCallTimelineItem["status"] { if (error !== undefined && error !== null) { return "failed"; } @@ -601,180 +136,6 @@ function resolveStatus(rawStatus: string | undefined, error: unknown, output: un 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, - cwd: string | null | undefined -): ToolCallDetail | undefined { - const filePath = normalizeCodexFilePath(input?.filePath ?? output?.filePath, cwd); - 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 } : {}), - }; -} - -function toWriteDetail( - input: z.infer | null, - 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 } : {}), - }; -} - -function toEditDetail( - input: z.infer | null, - 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 } - : {}), - }; -} - -function toSearchDetail(input: z.infer | null): ToolCallDetail | undefined { - if (!input?.query) { - return undefined; - } - return { - type: "search", - query: input.query, - }; -} - -function codexToolDetailBranch( - name: Name, - inputSchema: InputSchema, - outputSchema: OutputSchema, - 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, cwd }) => mapper({ input, output, cwd })); -} - -const CodexKnownToolDetailSchema = z.union([ - codexToolDetailBranch("Bash", CodexShellInputSchema, CodexShellOutputSchema, ({ input, output }) => - toShellDetail(input, output) - ), - codexToolDetailBranch("shell", CodexShellInputSchema, CodexShellOutputSchema, ({ input, output }) => - toShellDetail(input, output) - ), - codexToolDetailBranch("bash", CodexShellInputSchema, CodexShellOutputSchema, ({ input, output }) => - toShellDetail(input, output) - ), - codexToolDetailBranch("exec", CodexShellInputSchema, CodexShellOutputSchema, ({ input, output }) => - toShellDetail(input, output) - ), - 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 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; - } - return parsed.data; -} - function buildToolCall(params: { callId: string; name: string; @@ -812,24 +173,6 @@ 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", -]); - function buildMcpToolName(server: string | undefined, tool: string): string { const trimmedTool = tool.trim(); if (!trimmedTool) { @@ -903,11 +246,18 @@ function mapFileChangeItem( const changes = item.changes ?? []; const files = changes - .map((change) => ({ - path: normalizeCodexFilePath(change.path, options?.cwd), - kind: change.kind, - diff: change.diff, - })) + .map((change) => { + const pathValue = + typeof change.path === "string" + ? normalizeCodexFilePath(change.path.trim(), options?.cwd) + : undefined; + + return { + path: pathValue, + kind: change.kind, + diff: change.diff, + }; + }) .filter((change) => change.path !== undefined); const input = toNullableObject({ @@ -969,7 +319,7 @@ 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 = deriveToolDetail({ + const detail = deriveCodexToolDetail({ name: tool, input, output, @@ -1031,6 +381,10 @@ function createCodexThreadItemToTimelineSchema(options?: CodexMapperOptions) { }); } +// --------------------------------------------------------------------------- +// Public API +// --------------------------------------------------------------------------- + export function mapCodexToolCallFromThreadItem( item: unknown, options?: CodexMapperOptions @@ -1055,7 +409,7 @@ 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 = deriveToolDetail({ + const detail = deriveCodexToolDetail({ name: parsed.name, input, output, diff --git a/packages/server/src/server/agent/providers/opencode/tool-call-detail-parser.ts b/packages/server/src/server/agent/providers/opencode/tool-call-detail-parser.ts new file mode 100644 index 000000000..9b85c78c6 --- /dev/null +++ b/packages/server/src/server/agent/providers/opencode/tool-call-detail-parser.ts @@ -0,0 +1,56 @@ +import { z } from "zod"; + +import type { ToolCallDetail } from "../../agent-sdk-types.js"; +import { + ToolEditInputSchema, + ToolEditOutputSchema, + ToolReadInputSchema, + ToolReadOutputSchema, + ToolSearchInputSchema, + ToolShellInputSchema, + ToolShellOutputSchema, + ToolWriteInputSchema, + ToolWriteOutputSchema, + toEditToolDetail, + toReadToolDetail, + toSearchToolDetail, + toShellToolDetail, + toWriteToolDetail, + toolDetailBranchByToolName, +} from "../tool-call-detail-primitives.js"; + +const OpencodeKnownToolDetailSchema = z.union([ + toolDetailBranchByToolName("shell", ToolShellInputSchema, ToolShellOutputSchema, toShellToolDetail), + toolDetailBranchByToolName("bash", ToolShellInputSchema, ToolShellOutputSchema, toShellToolDetail), + toolDetailBranchByToolName("exec_command", ToolShellInputSchema, ToolShellOutputSchema, toShellToolDetail), + toolDetailBranchByToolName("read", ToolReadInputSchema, ToolReadOutputSchema, toReadToolDetail), + toolDetailBranchByToolName("read_file", ToolReadInputSchema, ToolReadOutputSchema, toReadToolDetail), + toolDetailBranchByToolName("write", ToolWriteInputSchema, ToolWriteOutputSchema, toWriteToolDetail), + toolDetailBranchByToolName("write_file", ToolWriteInputSchema, ToolWriteOutputSchema, toWriteToolDetail), + toolDetailBranchByToolName("create_file", ToolWriteInputSchema, ToolWriteOutputSchema, toWriteToolDetail), + toolDetailBranchByToolName("edit", ToolEditInputSchema, ToolEditOutputSchema, toEditToolDetail), + toolDetailBranchByToolName("apply_patch", ToolEditInputSchema, ToolEditOutputSchema, toEditToolDetail), + toolDetailBranchByToolName("apply_diff", ToolEditInputSchema, ToolEditOutputSchema, toEditToolDetail), + toolDetailBranchByToolName("search", ToolSearchInputSchema, z.unknown(), (input) => + toSearchToolDetail(input) + ), + toolDetailBranchByToolName("web_search", ToolSearchInputSchema, z.unknown(), (input) => + toSearchToolDetail(input) + ), +]); + +export function deriveOpencodeToolDetail( + toolName: string, + input: unknown, + output: unknown +): ToolCallDetail | undefined { + const parsed = OpencodeKnownToolDetailSchema.safeParse({ + toolName, + input, + output, + }); + if (!parsed.success) { + return undefined; + } + return parsed.data; +} 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..593bf114d 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,13 +1,8 @@ import { z } from "zod"; -import type { ToolCallDetail, ToolCallTimelineItem } from "../../agent-sdk-types.js"; -import { - coerceToolCallId, - commandFromValue, - flattenReadContent as flattenToolReadContent, - nonEmptyString, - truncateDiffText, -} from "../tool-call-mapper-utils.js"; +import type { ToolCallTimelineItem } from "../../agent-sdk-types.js"; +import { coerceToolCallId } from "../tool-call-mapper-utils.js"; +import { deriveOpencodeToolDetail } from "./tool-call-detail-parser.js"; type OpencodeToolCallParams = { toolName: string; @@ -19,8 +14,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 +30,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", @@ -475,7 +39,11 @@ function coerceCallId(callId: string | null | undefined, toolName: string, input }); } -function resolveStatus(rawStatus: unknown, error: unknown, output: unknown): ToolCallTimelineItem["status"] { +function resolveStatus( + rawStatus: unknown, + error: unknown, + output: unknown +): ToolCallTimelineItem["status"] { if (error !== null && error !== undefined) { return "failed"; } @@ -499,146 +67,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, - inputSchema: InputSchema, - outputSchema: OutputSchema, - mapper: ( - input: z.infer | null, - output: z.infer | null - ) => ToolCallDetail | undefined -) { - return z - .object({ - toolName: z.literal(toolName), - input: inputSchema.nullable(), - output: outputSchema.nullable(), - }) - .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)), -]); - -function deriveDetail(toolName: string, input: unknown, output: unknown): ToolCallDetail | undefined { - const parsed = OpencodeKnownToolDetailSchema.safeParse({ - toolName, - input, - output, - }); - if (!parsed.success) { - return undefined; - } - return parsed.data; -} - export function mapOpencodeToolCall(params: OpencodeToolCallParams): ToolCallTimelineItem { const parsedParams = OpencodeToolCallParamsSchema.parse(params); const input = parsedParams.input ?? null; const output = parsedParams.output ?? null; const status = resolveStatus(parsedParams.status, parsedParams.error, output); const callId = coerceCallId(parsedParams.callId, parsedParams.toolName, input); - const detail = deriveDetail(parsedParams.toolName, input, output); + const detail = deriveOpencodeToolDetail(parsedParams.toolName, input, output); if (status === "failed") { return { diff --git a/packages/server/src/server/agent/providers/tool-call-detail-primitives.ts b/packages/server/src/server/agent/providers/tool-call-detail-primitives.ts new file mode 100644 index 000000000..a04e6b22f --- /dev/null +++ b/packages/server/src/server/agent/providers/tool-call-detail-primitives.ts @@ -0,0 +1,694 @@ +import { z } from "zod"; + +import type { ToolCallDetail } from "../agent-sdk-types.js"; +import { + commandFromValue, + flattenReadContent as flattenToolReadContent, + nonEmptyString, + truncateDiffText, +} from "./tool-call-mapper-utils.js"; + +export const CommandValueSchema = z.union([z.string(), z.array(z.string())]); + +export const ToolShellInputSchema = 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 ToolShellOutputObjectSchema = 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 ToolShellOutputSchema = z.union([ + z.string().transform((value) => ({ + command: undefined, + output: nonEmptyString(value), + exitCode: undefined, + })), + ToolShellOutputObjectSchema.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, + })), +]); + +export const ToolPathInputSchema = z.union([ + z.object({ path: z.string() }).passthrough().transform((value) => ({ filePath: value.path })), + z + .object({ file_path: z.string() }) + .passthrough() + .transform((value) => ({ filePath: value.file_path })), + z + .object({ filePath: z.string() }) + .passthrough() + .transform((value) => ({ filePath: value.filePath })), +]); + +export const ToolReadInputSchema = 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 ToolReadChunkSchema = 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 ToolReadContentSchema = z.union([z.string(), ToolReadChunkSchema, z.array(ToolReadChunkSchema)]); + +const ToolReadPayloadSchema = z.union([ + z + .object({ + content: ToolReadContentSchema, + text: ToolReadContentSchema.optional(), + output: ToolReadContentSchema.optional(), + }) + .passthrough(), + z + .object({ + content: ToolReadContentSchema.optional(), + text: ToolReadContentSchema, + output: ToolReadContentSchema.optional(), + }) + .passthrough(), + z + .object({ + content: ToolReadContentSchema.optional(), + text: ToolReadContentSchema.optional(), + output: ToolReadContentSchema, + }) + .passthrough(), +]); + +function flattenReadContent( + value: z.infer | undefined +): string | undefined { + return flattenToolReadContent(value); +} + +const ToolReadOutputContentSchema = z.union([ + z.string().transform((value) => ({ filePath: undefined, content: nonEmptyString(value) })), + ToolReadChunkSchema.transform((value) => ({ + filePath: undefined, + content: flattenReadContent(value), + })), + z.array(ToolReadChunkSchema).transform((value) => ({ + filePath: undefined, + content: flattenReadContent(value), + })), + ToolReadPayloadSchema.transform((value) => ({ + filePath: undefined, + content: + flattenReadContent(value.content) ?? + flattenReadContent(value.text) ?? + flattenReadContent(value.output), + })), + z + .object({ data: ToolReadPayloadSchema }) + .passthrough() + .transform((value) => ({ + filePath: undefined, + content: + flattenReadContent(value.data.content) ?? + flattenReadContent(value.data.text) ?? + flattenReadContent(value.data.output), + })), + z + .object({ structuredContent: ToolReadPayloadSchema }) + .passthrough() + .transform((value) => ({ + filePath: undefined, + content: + flattenReadContent(value.structuredContent.content) ?? + flattenReadContent(value.structuredContent.text) ?? + flattenReadContent(value.structuredContent.output), + })), + z + .object({ structured_content: ToolReadPayloadSchema }) + .passthrough() + .transform((value) => ({ + filePath: undefined, + content: + flattenReadContent(value.structured_content.content) ?? + flattenReadContent(value.structured_content.text) ?? + flattenReadContent(value.structured_content.output), + })), +]); + +const ToolReadOutputPathSchema = z.union([ + z + .object({ + path: z.string(), + content: ToolReadContentSchema.optional(), + text: ToolReadContentSchema.optional(), + output: ToolReadContentSchema.optional(), + }) + .passthrough() + .transform((value) => ({ + filePath: value.path, + content: + flattenReadContent(value.content) ?? + flattenReadContent(value.text) ?? + flattenReadContent(value.output), + })), + z + .object({ + file_path: z.string(), + content: ToolReadContentSchema.optional(), + text: ToolReadContentSchema.optional(), + output: ToolReadContentSchema.optional(), + }) + .passthrough() + .transform((value) => ({ + filePath: value.file_path, + content: + flattenReadContent(value.content) ?? + flattenReadContent(value.text) ?? + flattenReadContent(value.output), + })), + z + .object({ + filePath: z.string(), + content: ToolReadContentSchema.optional(), + text: ToolReadContentSchema.optional(), + output: ToolReadContentSchema.optional(), + }) + .passthrough() + .transform((value) => ({ + filePath: value.filePath, + content: + flattenReadContent(value.content) ?? + flattenReadContent(value.text) ?? + flattenReadContent(value.output), + })), +]); + +type ToolReadOutputValue = { + filePath?: string; + content?: string; +}; + +export const ToolReadOutputSchema: z.ZodType< + ToolReadOutputValue, + z.ZodTypeDef, + unknown +> = ToolReadOutputContentSchema; + +export const ToolReadOutputWithPathSchema: z.ZodType< + ToolReadOutputValue, + z.ZodTypeDef, + unknown +> = z.union([ToolReadOutputContentSchema, ToolReadOutputPathSchema]); + +export const ToolWriteContentSchema = z + .object({ + content: z.string().optional(), + new_content: z.string().optional(), + newContent: z.string().optional(), + }) + .passthrough(); + +export const ToolWriteInputSchema = z + .intersection(ToolPathInputSchema, ToolWriteContentSchema) + .transform((value) => ({ + filePath: value.filePath, + content: + nonEmptyString(value.content) ?? + nonEmptyString(value.new_content) ?? + nonEmptyString(value.newContent), + })); + +export const ToolWriteOutputSchema = z.union([ + z + .intersection(ToolPathInputSchema, ToolWriteContentSchema) + .transform((value) => ({ + filePath: value.filePath, + content: + nonEmptyString(value.content) ?? + nonEmptyString(value.new_content) ?? + nonEmptyString(value.newContent), + })), + ToolWriteContentSchema.transform((value) => ({ + filePath: undefined, + content: + nonEmptyString(value.content) ?? + nonEmptyString(value.new_content) ?? + nonEmptyString(value.newContent), + })), +]); + +export const ToolEditTextSchema = 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 ToolEditInputSchema = z + .intersection(ToolPathInputSchema, ToolEditTextSchema) + .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 ToolEditOutputFileSchema = 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 ToolEditOutputSchema = z.union([ + z + .intersection(ToolPathInputSchema, ToolEditTextSchema) + .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(ToolEditOutputFileSchema).min(1) }) + .passthrough() + .transform((value) => ({ + filePath: value.files[0]?.filePath, + unifiedDiff: value.files[0]?.unifiedDiff, + newString: undefined, + })), + ToolEditTextSchema.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 ToolSearchInputSchema = 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 type ParsedToolShellInput = z.infer; +export type ParsedToolShellOutput = z.infer; +export type ParsedToolReadInput = z.infer; +export type ParsedToolReadOutput = z.infer; +export type ParsedToolReadOutputWithPath = z.infer; +export type ParsedToolWriteInput = z.infer; +export type ParsedToolWriteOutput = z.infer; +export type ParsedToolEditInput = z.infer; +export type ParsedToolEditOutput = z.infer; +export type ParsedToolSearchInput = z.infer; + +type NormalizePathFn = (filePath: string) => string | undefined; + +function normalizeDetailPath( + filePath: string | undefined, + normalizePath?: NormalizePathFn +): string | undefined { + if (typeof filePath !== "string") { + return undefined; + } + const trimmed = filePath.trim(); + if (!trimmed) { + return undefined; + } + return normalizePath ? normalizePath(trimmed) : trimmed; +} + +export function toShellToolDetail( + input: ParsedToolShellInput | null, + output: ParsedToolShellOutput | 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 toReadToolDetail( + input: ParsedToolReadInput | null, + output: ParsedToolReadOutput | ParsedToolReadOutputWithPath | null, + options?: { normalizePath?: NormalizePathFn } +): ToolCallDetail | undefined { + const filePath = normalizeDetailPath( + input?.filePath ?? output?.filePath, + options?.normalizePath + ); + 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 toWriteToolDetail( + input: ParsedToolWriteInput | null, + output: ParsedToolWriteOutput | null, + options?: { normalizePath?: NormalizePathFn } +): ToolCallDetail | undefined { + const filePath = normalizeDetailPath( + input?.filePath ?? output?.filePath, + options?.normalizePath + ); + if (!filePath) { + return undefined; + } + + return { + type: "write", + filePath, + ...(input?.content ? { content: input.content } : output?.content ? { content: output.content } : {}), + }; +} + +export function toEditToolDetail( + input: ParsedToolEditInput | null, + output: ParsedToolEditOutput | null, + options?: { normalizePath?: NormalizePathFn } +): ToolCallDetail | undefined { + const filePath = normalizeDetailPath( + input?.filePath ?? output?.filePath, + options?.normalizePath + ); + 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 toSearchToolDetail( + input: ParsedToolSearchInput | null +): ToolCallDetail | undefined { + if (!input?.query) { + return undefined; + } + return { + type: "search", + query: input.query, + }; +} + +export function toolDetailBranchByName< + Name extends string, + InputSchema extends z.ZodTypeAny, + OutputSchema extends z.ZodTypeAny, +>( + name: Name, + inputSchema: InputSchema, + outputSchema: OutputSchema, + mapper: ( + input: z.infer | null, + output: z.infer | null + ) => ToolCallDetail | undefined +) { + return z + .object({ + name: z.literal(name), + input: inputSchema.nullable(), + output: outputSchema.nullable(), + }) + .transform(({ input, output }) => mapper(input, output)); +} + +export function toolDetailBranchByToolName< + Name extends string, + InputSchema extends z.ZodTypeAny, + OutputSchema extends z.ZodTypeAny, +>( + toolName: Name, + inputSchema: InputSchema, + outputSchema: OutputSchema, + mapper: ( + input: z.infer | null, + output: z.infer | null + ) => ToolCallDetail | undefined +) { + return z + .object({ + toolName: z.literal(toolName), + input: inputSchema.nullable(), + output: outputSchema.nullable(), + }) + .transform(({ input, output }) => mapper(input, output)); +} + +export function toolDetailBranchByNameWithCwd< + Name extends string, + InputSchema extends z.ZodTypeAny, + OutputSchema extends z.ZodTypeAny, +>( + name: Name, + inputSchema: InputSchema, + outputSchema: OutputSchema, + mapper: ( + input: z.infer | null, + output: z.infer | null, + cwd: string | null + ) => ToolCallDetail | undefined +) { + return z + .object({ + name: z.literal(name), + input: inputSchema.nullable(), + output: outputSchema.nullable(), + cwd: z.string().optional().nullable(), + }) + .transform(({ input, output, cwd }) => mapper(input, output, cwd ?? null)); +}