diff --git a/packages/app/src/stores/session-store.ts b/packages/app/src/stores/session-store.ts index 4e07859e6..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, @@ -54,9 +55,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"; }; @@ -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 6495add5f..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,164 +35,25 @@ 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; - detail: ToolCallDetail; + input: unknown | null; + output: unknown | null; + detail?: ToolCallDetail; metadata?: Record; } { const parsedParams = ClaudeMapperParamsSchema.parse(params); const input = parsedParams.input ?? null; const output = parsedParams.output ?? null; - const detail = - deriveDetail(parsedParams.name, input, output) ?? { - type: "unknown", - rawInput: input, - rawOutput: output, - }; + const detail = deriveClaudeToolDetail(parsedParams.name, input, output); return { callId: coerceCallId(parsedParams.callId, parsedParams.name, input), name: parsedParams.name, - detail, + input, + output, + ...(detail ? { detail } : {}), ...(parsedParams.metadata ? { metadata: parsedParams.metadata } : {}), }; } 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 92e220fdf..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,13 +1,17 @@ import { z } from "zod"; import type { ToolCallDetail, ToolCallTimelineItem } from "../../agent-sdk-types.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 }; @@ -25,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({ @@ -547,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", @@ -562,24 +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) { - const prefix = cwd.endsWith("/") ? cwd : `${cwd}/`; - if (trimmed.startsWith(prefix)) { - return trimmed.slice(prefix.length) || "."; - } - } - 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"; } @@ -603,266 +136,26 @@ 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 codexMcpToolBranch( - tool: ToolName, - inputSchema: InputSchema, - outputSchema: OutputSchema, - mapper: ( - input: z.infer | null, - output: z.infer | null, - cwd: string | null | undefined - ) => ToolCallDetail | undefined -) { - return z - .object({ - tool: z.literal(tool), - arguments: inputSchema.nullable(), - result: outputSchema.nullable(), - cwd: z.string().optional().nullable(), - }) - .transform(({ arguments: input, result: output, cwd }) => mapper(input, output, cwd)); -} - -const CodexKnownMcpToolDetailSchema = z.union([ - codexMcpToolBranch("shell", CodexShellInputSchema, CodexShellOutputSchema, (input, output) => - toShellDetail(input, output) - ), - codexMcpToolBranch("bash", CodexShellInputSchema, CodexShellOutputSchema, (input, output) => - toShellDetail(input, output) - ), - codexMcpToolBranch("exec", CodexShellInputSchema, CodexShellOutputSchema, (input, output) => - toShellDetail(input, output) - ), - codexMcpToolBranch("exec_command", CodexShellInputSchema, CodexShellOutputSchema, (input, output) => - toShellDetail(input, output) - ), - codexMcpToolBranch("command", CodexShellInputSchema, CodexShellOutputSchema, (input, output) => - toShellDetail(input, output) - ), - 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( - 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 CodexKnownRolloutDetailSchema = z.union([ - codexRolloutToolBranch("Bash", CodexShellInputSchema, CodexShellOutputSchema, (input, output) => - toShellDetail(input, output) - ), - codexRolloutToolBranch("shell", CodexShellInputSchema, CodexShellOutputSchema, (input, output) => - toShellDetail(input, output) - ), - codexRolloutToolBranch("bash", CodexShellInputSchema, CodexShellOutputSchema, (input, output) => - toShellDetail(input, output) - ), - codexRolloutToolBranch("exec_command", CodexShellInputSchema, CodexShellOutputSchema, (input, output) => - toShellDetail(input, output) - ), - codexRolloutToolBranch("read", CodexReadArgumentsSchema, CodexReadResultSchema, (input, output) => - toReadDetail(input, output, null) - ), - 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( - tool: string, - input: unknown, - output: unknown, - options?: CodexMapperOptions -): ToolCallDetail | undefined { - const parsed = CodexKnownMcpToolDetailSchema.safeParse({ - tool, - arguments: input, - result: output, - cwd: options?.cwd ?? null, - }); - if (!parsed.success) { - return undefined; - } - return parsed.data; -} - -function deriveRolloutDetail(name: string, input: unknown, output: unknown): ToolCallDetail | undefined { - const parsed = CodexKnownRolloutDetailSchema.safeParse({ - name, - input, - output, - }); - if (!parsed.success) { - return undefined; - } - return parsed.data; -} - -function toCanonicalDetail( - knownDetail: ToolCallDetail | undefined, - rawInput: unknown | null, - rawOutput: unknown | null -): ToolCallDetail { - if (knownDetail) { - return knownDetail; - } - - return { - type: "unknown", - rawInput, - rawOutput, - }; -} - function buildToolCall(params: { callId: string; name: string; status: ToolCallTimelineItem["status"]; - rawInput: unknown | null; - rawOutput: unknown | null; + input: unknown | null; + output: unknown | null; error: unknown | null; detail?: ToolCallDetail; metadata?: Record; }): ToolCallTimelineItem { - const detail = toCanonicalDetail(params.detail, params.rawInput, params.rawOutput); - if (params.status === "failed") { return { type: "tool_call", callId: params.callId, name: params.name, status: "failed", + input: params.input, + output: params.output, error: params.error ?? { message: "Tool call failed" }, - detail, + ...(params.detail ? { detail: params.detail } : {}), ...(params.metadata ? { metadata: params.metadata } : {}), }; } @@ -872,30 +165,14 @@ function buildToolCall(params: { callId: params.callId, name: params.name, status: params.status, + input: params.input, + output: params.output, error: null, - detail, + ...(params.detail ? { detail: params.detail } : {}), ...(params.metadata ? { metadata: params.metadata } : {}), }; } -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) { @@ -955,8 +232,8 @@ function mapCommandExecutionItem( callId, name, status, - rawInput: input, - rawOutput: output, + input, + output, error, ...(detail ? { detail } : {}), }); @@ -969,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({ @@ -1017,8 +301,8 @@ function mapFileChangeItem( callId, name, status, - rawInput: input, - rawOutput: output, + input, + output, error, ...(detail ? { detail } : {}), }); @@ -1035,14 +319,19 @@ function mapMcpToolCallItem( const error = item.error ?? null; const callId = coerceCallId(item.id ?? item.callID ?? item.call_id, name, input); const status = resolveStatus(item.status, error, output); - const detail = deriveMcpToolDetail(tool, input, output, options); + const detail = deriveCodexToolDetail({ + name: tool, + input, + output, + cwd: options?.cwd ?? null, + }); return buildToolCall({ callId, name, status, - rawInput: input, - rawOutput: output, + input, + output, error, ...(detail ? { detail } : {}), }); @@ -1066,8 +355,8 @@ function mapWebSearchItem(item: z.infer): ToolC callId, name, status, - rawInput: input, - rawOutput: output, + input, + output, error, ...(detail ? { detail } : {}), }); @@ -1092,6 +381,10 @@ function createCodexThreadItemToTimelineSchema(options?: CodexMapperOptions) { }); } +// --------------------------------------------------------------------------- +// Public API +// --------------------------------------------------------------------------- + export function mapCodexToolCallFromThreadItem( item: unknown, options?: CodexMapperOptions @@ -1116,14 +409,19 @@ export function mapCodexRolloutToolCall(params: { const error = parsed.error ?? null; const status = resolveStatus("completed", error, output); const callId = coerceCallId(parsed.callId, parsed.name, input); - const detail = deriveRolloutDetail(parsed.name, input, output); + const detail = deriveCodexToolDetail({ + name: parsed.name, + input, + output, + cwd: null, + }); return buildToolCall({ callId, name: parsed.name, status, - rawInput: input, - rawOutput: output, + input, + output, error, ...(detail ? { detail } : {}), }); 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 a6098d5ac..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,162 +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; -} - -function toCanonicalDetail( - knownDetail: ToolCallDetail | undefined, - rawInput: unknown | null, - rawOutput: unknown | null -): ToolCallDetail { - if (knownDetail) { - return knownDetail; - } - - return { - type: "unknown", - rawInput, - rawOutput, - }; -} - 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 = toCanonicalDetail(deriveDetail(parsedParams.toolName, input, output), input, output); + const detail = deriveOpencodeToolDetail(parsedParams.toolName, input, output); if (status === "failed") { return { @@ -662,8 +81,10 @@ export function mapOpencodeToolCall(params: OpencodeToolCallParams): ToolCallTim callId, name: parsedParams.toolName, status: "failed", + input, + output, error: parsedParams.error ?? { message: "Tool call failed" }, - detail, + ...(detail ? { detail } : {}), ...(parsedParams.metadata ? { metadata: parsedParams.metadata } : {}), }; } @@ -673,8 +94,10 @@ export function mapOpencodeToolCall(params: OpencodeToolCallParams): ToolCallTim callId, name: parsedParams.toolName, status, + input, + output, error: null, - detail, + ...(detail ? { detail } : {}), ...(parsedParams.metadata ? { metadata: parsedParams.metadata } : {}), }; } 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)); +} 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 2d6550099..e54bf40a3 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);