refactor: enforce provider-owned canonical tool-call parsing

This commit is contained in:
Mohamed Boudra
2026-02-09 09:16:37 +07:00
parent 896f88fa34
commit 81d2e8988d
5 changed files with 1560 additions and 860 deletions

View File

@@ -2,25 +2,11 @@ import { z } from "zod";
import type { ToolCallDetail, ToolCallTimelineItem } from "../../agent-sdk-types.js";
import {
StandardEditInputSchema,
StandardEditOutputSchema,
StandardReadOutputSchema,
StandardReadPathInputSchema,
StandardSearchInputSchema,
StandardShellInputSchema,
StandardShellOutputSchema,
StandardWriteInputSchema,
StandardWriteOutputSchema,
toStandardEditDetail,
toStandardReadDetail,
toStandardSearchDetail,
toStandardShellDetail,
toStandardWriteDetail,
} from "../standard-tool-call-schemas.js";
import {
CLAUDE_KNOWN_TOOL_ALIASES,
coerceToolCallId,
unionToolDetailSchemas,
commandFromValue,
flattenReadContent as flattenToolReadContent,
nonEmptyString,
truncateDiffText,
} from "../tool-call-mapper-utils.js";
type MapperParams = {
@@ -31,6 +17,8 @@ type MapperParams = {
metadata?: Record<string, unknown>;
};
const MAX_DIFF_TEXT_CHARS = 12_000;
const ClaudeMapperParamsSchema = z
.object({
callId: z.string().optional().nullable(),
@@ -45,6 +33,437 @@ const ClaudeFailedMapperParamsSchema = ClaudeMapperParamsSchema.extend({
error: z.unknown(),
});
const CommandValueSchema = z.union([z.string(), z.array(z.string())]);
const ClaudeShellInputSchema = z
.union([
z
.object({
command: CommandValueSchema,
cwd: z.string().optional(),
directory: z.string().optional(),
})
.passthrough(),
z
.object({
cmd: CommandValueSchema,
cwd: z.string().optional(),
directory: z.string().optional(),
})
.passthrough(),
])
.transform((value) => {
const commandValue = "command" in value ? value.command : value.cmd;
return {
command: commandFromValue(commandValue),
cwd: nonEmptyString(value.cwd) ?? nonEmptyString(value.directory),
};
});
const ClaudeShellOutputObjectSchema = z
.object({
command: z.string().optional(),
output: z.string().optional(),
text: z.string().optional(),
content: z.string().optional(),
aggregated_output: z.string().optional(),
exitCode: z.number().finite().nullable().optional(),
exit_code: z.number().finite().nullable().optional(),
metadata: z
.object({
exitCode: z.number().finite().nullable().optional(),
exit_code: z.number().finite().nullable().optional(),
})
.passthrough()
.optional(),
structuredContent: z
.object({
output: z.string().optional(),
text: z.string().optional(),
content: z.string().optional(),
})
.passthrough()
.optional(),
structured_content: z
.object({
output: z.string().optional(),
text: z.string().optional(),
content: z.string().optional(),
})
.passthrough()
.optional(),
result: z
.object({
command: z.string().optional(),
output: z.string().optional(),
text: z.string().optional(),
content: z.string().optional(),
})
.passthrough()
.optional(),
})
.passthrough();
const ClaudeShellOutputSchema = z.union([
z.string().transform((value) => ({
command: undefined,
output: nonEmptyString(value),
exitCode: undefined,
})),
ClaudeShellOutputObjectSchema.transform((value) => ({
command: nonEmptyString(value.command) ?? nonEmptyString(value.result?.command),
output:
nonEmptyString(value.output) ??
nonEmptyString(value.text) ??
nonEmptyString(value.content) ??
nonEmptyString(value.aggregated_output) ??
nonEmptyString(value.structuredContent?.output) ??
nonEmptyString(value.structuredContent?.text) ??
nonEmptyString(value.structuredContent?.content) ??
nonEmptyString(value.structured_content?.output) ??
nonEmptyString(value.structured_content?.text) ??
nonEmptyString(value.structured_content?.content) ??
nonEmptyString(value.result?.output) ??
nonEmptyString(value.result?.text) ??
nonEmptyString(value.result?.content),
exitCode:
value.exitCode ??
value.exit_code ??
value.metadata?.exitCode ??
value.metadata?.exit_code ??
undefined,
})),
]);
const ClaudeReadPathInputSchema = z.union([
z
.object({
file_path: z.string(),
offset: z.number().finite().optional(),
limit: z.number().finite().optional(),
})
.passthrough()
.transform((value) => ({
filePath: value.file_path,
offset: value.offset,
limit: value.limit,
})),
z
.object({
path: z.string(),
offset: z.number().finite().optional(),
limit: z.number().finite().optional(),
})
.passthrough()
.transform((value) => ({
filePath: value.path,
offset: value.offset,
limit: value.limit,
})),
z
.object({
filePath: z.string(),
offset: z.number().finite().optional(),
limit: z.number().finite().optional(),
})
.passthrough()
.transform((value) => ({
filePath: value.filePath,
offset: value.offset,
limit: value.limit,
})),
]);
const ClaudeReadChunkSchema = z.union([
z
.object({
text: z.string(),
content: z.string().optional(),
output: z.string().optional(),
})
.passthrough(),
z
.object({
text: z.string().optional(),
content: z.string(),
output: z.string().optional(),
})
.passthrough(),
z
.object({
text: z.string().optional(),
content: z.string().optional(),
output: z.string(),
})
.passthrough(),
]);
const ClaudeReadContentSchema = z.union([
z.string(),
ClaudeReadChunkSchema,
z.array(ClaudeReadChunkSchema),
]);
const ClaudeReadPayloadSchema = z.union([
z
.object({
content: ClaudeReadContentSchema,
text: ClaudeReadContentSchema.optional(),
output: ClaudeReadContentSchema.optional(),
})
.passthrough(),
z
.object({
content: ClaudeReadContentSchema.optional(),
text: ClaudeReadContentSchema,
output: ClaudeReadContentSchema.optional(),
})
.passthrough(),
z
.object({
content: ClaudeReadContentSchema.optional(),
text: ClaudeReadContentSchema.optional(),
output: ClaudeReadContentSchema,
})
.passthrough(),
]);
const ClaudeReadOutputSchema = z.union([
z.string().transform((value) => ({ content: nonEmptyString(value) })),
ClaudeReadChunkSchema.transform((value) => ({ content: flattenReadContent(value) })),
z.array(ClaudeReadChunkSchema).transform((value) => ({ content: flattenReadContent(value) })),
ClaudeReadPayloadSchema.transform((value) => ({
content:
flattenReadContent(value.content) ??
flattenReadContent(value.text) ??
flattenReadContent(value.output),
})),
z
.object({ data: ClaudeReadPayloadSchema })
.passthrough()
.transform((value) => ({
content:
flattenReadContent(value.data.content) ??
flattenReadContent(value.data.text) ??
flattenReadContent(value.data.output),
})),
z
.object({ structuredContent: ClaudeReadPayloadSchema })
.passthrough()
.transform((value) => ({
content:
flattenReadContent(value.structuredContent.content) ??
flattenReadContent(value.structuredContent.text) ??
flattenReadContent(value.structuredContent.output),
})),
z
.object({ structured_content: ClaudeReadPayloadSchema })
.passthrough()
.transform((value) => ({
content:
flattenReadContent(value.structured_content.content) ??
flattenReadContent(value.structured_content.text) ??
flattenReadContent(value.structured_content.output),
})),
]);
const ClaudeWritePathInputSchema = z.union([
z.object({ file_path: z.string() }).passthrough().transform((value) => ({ filePath: value.file_path })),
z.object({ path: z.string() }).passthrough().transform((value) => ({ filePath: value.path })),
z.object({ filePath: z.string() }).passthrough().transform((value) => ({ filePath: value.filePath })),
]);
const ClaudeWriteContentSchema = z
.object({
content: z.string().optional(),
new_content: z.string().optional(),
newContent: z.string().optional(),
})
.passthrough();
const ClaudeWriteInputSchema = z
.intersection(ClaudeWritePathInputSchema, ClaudeWriteContentSchema)
.transform((value) => ({
filePath: value.filePath,
content:
nonEmptyString(value.content) ??
nonEmptyString(value.new_content) ??
nonEmptyString(value.newContent),
}));
const ClaudeWriteOutputSchema = z.union([
z
.intersection(ClaudeWritePathInputSchema, ClaudeWriteContentSchema)
.transform((value) => ({
filePath: value.filePath,
content:
nonEmptyString(value.content) ??
nonEmptyString(value.new_content) ??
nonEmptyString(value.newContent),
})),
ClaudeWriteContentSchema.transform((value) => ({
filePath: undefined,
content:
nonEmptyString(value.content) ??
nonEmptyString(value.new_content) ??
nonEmptyString(value.newContent),
})),
]);
const ClaudeEditTextSchema = z
.object({
old_string: z.string().optional(),
old_str: z.string().optional(),
oldContent: z.string().optional(),
old_content: z.string().optional(),
new_string: z.string().optional(),
new_str: z.string().optional(),
newContent: z.string().optional(),
new_content: z.string().optional(),
content: z.string().optional(),
patch: z.string().optional(),
diff: z.string().optional(),
unified_diff: z.string().optional(),
unifiedDiff: z.string().optional(),
})
.passthrough();
const ClaudeEditInputSchema = z
.intersection(ClaudeWritePathInputSchema, ClaudeEditTextSchema)
.transform((value) => ({
filePath: value.filePath,
oldString:
nonEmptyString(value.old_string) ??
nonEmptyString(value.old_str) ??
nonEmptyString(value.oldContent) ??
nonEmptyString(value.old_content),
newString:
nonEmptyString(value.new_string) ??
nonEmptyString(value.new_str) ??
nonEmptyString(value.newContent) ??
nonEmptyString(value.new_content) ??
nonEmptyString(value.content),
unifiedDiff: truncateDiffText(
nonEmptyString(value.patch) ??
nonEmptyString(value.diff) ??
nonEmptyString(value.unified_diff) ??
nonEmptyString(value.unifiedDiff),
MAX_DIFF_TEXT_CHARS
),
}));
const ClaudeEditOutputFileSchema = z.union([
z
.object({
path: z.string(),
patch: z.string().optional(),
diff: z.string().optional(),
unified_diff: z.string().optional(),
unifiedDiff: z.string().optional(),
})
.passthrough()
.transform((value) => ({
filePath: value.path,
unifiedDiff: truncateDiffText(
nonEmptyString(value.patch) ??
nonEmptyString(value.diff) ??
nonEmptyString(value.unified_diff) ??
nonEmptyString(value.unifiedDiff),
MAX_DIFF_TEXT_CHARS
),
})),
z
.object({
file_path: z.string(),
patch: z.string().optional(),
diff: z.string().optional(),
unified_diff: z.string().optional(),
unifiedDiff: z.string().optional(),
})
.passthrough()
.transform((value) => ({
filePath: value.file_path,
unifiedDiff: truncateDiffText(
nonEmptyString(value.patch) ??
nonEmptyString(value.diff) ??
nonEmptyString(value.unified_diff) ??
nonEmptyString(value.unifiedDiff),
MAX_DIFF_TEXT_CHARS
),
})),
z
.object({
filePath: z.string(),
patch: z.string().optional(),
diff: z.string().optional(),
unified_diff: z.string().optional(),
unifiedDiff: z.string().optional(),
})
.passthrough()
.transform((value) => ({
filePath: value.filePath,
unifiedDiff: truncateDiffText(
nonEmptyString(value.patch) ??
nonEmptyString(value.diff) ??
nonEmptyString(value.unified_diff) ??
nonEmptyString(value.unifiedDiff),
MAX_DIFF_TEXT_CHARS
),
})),
]);
const ClaudeEditOutputSchema = z.union([
z
.intersection(ClaudeWritePathInputSchema, ClaudeEditTextSchema)
.transform((value) => ({
filePath: value.filePath,
newString:
nonEmptyString(value.newContent) ??
nonEmptyString(value.new_content) ??
nonEmptyString(value.content),
unifiedDiff: truncateDiffText(
nonEmptyString(value.patch) ??
nonEmptyString(value.diff) ??
nonEmptyString(value.unified_diff) ??
nonEmptyString(value.unifiedDiff),
MAX_DIFF_TEXT_CHARS
),
})),
z
.object({ files: z.array(ClaudeEditOutputFileSchema).min(1) })
.passthrough()
.transform((value) => ({
filePath: value.files[0]?.filePath,
unifiedDiff: value.files[0]?.unifiedDiff,
newString: undefined,
})),
ClaudeEditTextSchema.transform((value) => ({
filePath: undefined,
newString:
nonEmptyString(value.newContent) ??
nonEmptyString(value.new_content) ??
nonEmptyString(value.content),
unifiedDiff: truncateDiffText(
nonEmptyString(value.patch) ??
nonEmptyString(value.diff) ??
nonEmptyString(value.unified_diff) ??
nonEmptyString(value.unifiedDiff),
MAX_DIFF_TEXT_CHARS
),
})),
]);
const ClaudeSearchInputSchema = z.union([
z.object({ query: z.string() }).passthrough().transform((value) => ({ query: value.query })),
z.object({ q: z.string() }).passthrough().transform((value) => ({ query: value.q })),
]);
function flattenReadContent(
value: z.infer<typeof ClaudeReadContentSchema> | undefined
): string | undefined {
return flattenToolReadContent(value);
}
function coerceCallId(callId: string | null | undefined, name: string, input: unknown): string {
return coerceToolCallId({
providerPrefix: "claude",
@@ -54,13 +473,96 @@ function coerceCallId(callId: string | null | undefined, name: string, input: un
});
}
function claudeToolBranch<InputSchema extends z.ZodTypeAny, OutputSchema extends z.ZodTypeAny>(
name: string,
function toShellDetail(
input: z.infer<typeof ClaudeShellInputSchema> | null,
output: z.infer<typeof ClaudeShellOutputSchema> | 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<typeof ClaudeReadPathInputSchema> | null,
output: z.infer<typeof ClaudeReadOutputSchema> | 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<typeof ClaudeWriteInputSchema> | null,
output: z.infer<typeof ClaudeWriteOutputSchema> | 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<typeof ClaudeEditInputSchema> | null,
output: z.infer<typeof ClaudeEditOutputSchema> | 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<typeof ClaudeSearchInputSchema> | null): ToolCallDetail | undefined {
if (!input?.query) {
return undefined;
}
return {
type: "search",
query: input.query,
};
}
function claudeToolBranch<Name extends string, InputSchema extends z.ZodTypeAny, OutputSchema extends z.ZodTypeAny>(
name: Name,
inputSchema: InputSchema,
outputSchema: OutputSchema,
mapper: (
input: z.output<InputSchema> | null | undefined,
output: z.output<OutputSchema> | null | undefined
input: z.infer<InputSchema> | null,
output: z.infer<OutputSchema> | null
) => ToolCallDetail | undefined
) {
return z
@@ -72,30 +574,29 @@ function claudeToolBranch<InputSchema extends z.ZodTypeAny, OutputSchema extends
.transform(({ input, output }) => mapper(input, output));
}
const ClaudeKnownToolDetailSchema: z.ZodType<ToolCallDetail | undefined> = unionToolDetailSchemas([
...CLAUDE_KNOWN_TOOL_ALIASES.shell.map((name) =>
claudeToolBranch(name, StandardShellInputSchema, StandardShellOutputSchema, (input, output) =>
toStandardShellDetail(input ?? null, output ?? null)
)
),
...CLAUDE_KNOWN_TOOL_ALIASES.read.map((name) =>
claudeToolBranch(name, StandardReadPathInputSchema, StandardReadOutputSchema, (input, output) =>
toStandardReadDetail(input ?? null, output ?? null)
)
),
...CLAUDE_KNOWN_TOOL_ALIASES.write.map((name) =>
claudeToolBranch(name, StandardWriteInputSchema, StandardWriteOutputSchema, (input, output) =>
toStandardWriteDetail(input ?? null, output ?? null)
)
),
...CLAUDE_KNOWN_TOOL_ALIASES.edit.map((name) =>
claudeToolBranch(name, StandardEditInputSchema, StandardEditOutputSchema, (input, output) =>
toStandardEditDetail(input ?? null, output ?? null)
)
),
...CLAUDE_KNOWN_TOOL_ALIASES.search.map((name) =>
claudeToolBranch(name, StandardSearchInputSchema, z.unknown(), (input) => toStandardSearchDetail(input ?? null))
),
const ClaudeKnownToolDetailSchema = z.union([
claudeToolBranch("Bash", ClaudeShellInputSchema, ClaudeShellOutputSchema, toShellDetail),
claudeToolBranch("bash", ClaudeShellInputSchema, ClaudeShellOutputSchema, toShellDetail),
claudeToolBranch("shell", ClaudeShellInputSchema, ClaudeShellOutputSchema, toShellDetail),
claudeToolBranch("exec_command", ClaudeShellInputSchema, ClaudeShellOutputSchema, toShellDetail),
claudeToolBranch("Read", ClaudeReadPathInputSchema, ClaudeReadOutputSchema, toReadDetail),
claudeToolBranch("read", ClaudeReadPathInputSchema, ClaudeReadOutputSchema, toReadDetail),
claudeToolBranch("read_file", ClaudeReadPathInputSchema, ClaudeReadOutputSchema, toReadDetail),
claudeToolBranch("view_file", ClaudeReadPathInputSchema, ClaudeReadOutputSchema, toReadDetail),
claudeToolBranch("Write", ClaudeWriteInputSchema, ClaudeWriteOutputSchema, toWriteDetail),
claudeToolBranch("write", ClaudeWriteInputSchema, ClaudeWriteOutputSchema, toWriteDetail),
claudeToolBranch("write_file", ClaudeWriteInputSchema, ClaudeWriteOutputSchema, toWriteDetail),
claudeToolBranch("create_file", ClaudeWriteInputSchema, ClaudeWriteOutputSchema, toWriteDetail),
claudeToolBranch("Edit", ClaudeEditInputSchema, ClaudeEditOutputSchema, toEditDetail),
claudeToolBranch("MultiEdit", ClaudeEditInputSchema, ClaudeEditOutputSchema, toEditDetail),
claudeToolBranch("multi_edit", ClaudeEditInputSchema, ClaudeEditOutputSchema, toEditDetail),
claudeToolBranch("edit", ClaudeEditInputSchema, ClaudeEditOutputSchema, toEditDetail),
claudeToolBranch("apply_patch", ClaudeEditInputSchema, ClaudeEditOutputSchema, toEditDetail),
claudeToolBranch("apply_diff", ClaudeEditInputSchema, ClaudeEditOutputSchema, toEditDetail),
claudeToolBranch("str_replace_editor", ClaudeEditInputSchema, ClaudeEditOutputSchema, toEditDetail),
claudeToolBranch("WebSearch", ClaudeSearchInputSchema, z.unknown(), (input) => toSearchDetail(input)),
claudeToolBranch("web_search", ClaudeSearchInputSchema, z.unknown(), (input) => toSearchDetail(input)),
claudeToolBranch("search", ClaudeSearchInputSchema, z.unknown(), (input) => toSearchDetail(input)),
]);
function deriveDetail(name: string, input: unknown, output: unknown): ToolCallDetail | undefined {

View File

@@ -1,30 +1,13 @@
import { z } from "zod";
import type { ToolCallDetail, ToolCallTimelineItem } from "../../agent-sdk-types.js";
import { stripCwdPrefix } from "../../../../shared/path-utils.js";
import {
StandardEditInputSchema,
StandardEditOutputSchema,
StandardReadChunkSchema,
StandardReadPathInputSchema,
StandardSearchInputSchema,
StandardShellInputSchema,
StandardShellOutputSchema,
StandardWriteInputSchema,
StandardWriteOutputSchema,
toStandardEditDetail,
toStandardSearchDetail,
toStandardShellDetail,
toStandardWriteDetail,
} from "../standard-tool-call-schemas.js";
import {
CODEX_MCP_KNOWN_TOOL_ALIASES,
CODEX_ROLLOUT_KNOWN_TOOL_ALIASES,
coerceToolCallId,
commandFromValue,
flattenReadContent as flattenToolReadContent,
nonEmptyString,
truncateDiffText,
unionToolDetailSchemas,
} from "../tool-call-mapper-utils.js";
type CodexMapperOptions = { cwd?: string | null };
@@ -45,21 +28,165 @@ const CodexRolloutToolCallParamsSchema = z
const CommandValueSchema = z.union([z.string(), z.array(z.string())]);
const CodexShellInputSchema = StandardShellInputSchema;
const CodexShellOutputSchema = StandardShellOutputSchema;
const CodexReadArgumentsSchema = StandardReadPathInputSchema;
const CodexWriteArgumentsSchema = StandardWriteInputSchema;
const CodexWriteResultSchema = StandardWriteOutputSchema;
const CodexEditArgumentsSchema = StandardEditInputSchema;
const CodexEditResultSchema = StandardEditOutputSchema;
const CodexSearchArgumentsSchema = StandardSearchInputSchema;
const CodexShellInputSchema = z
.union([
z
.object({
command: CommandValueSchema,
cwd: z.string().optional(),
directory: z.string().optional(),
})
.passthrough(),
z
.object({
cmd: CommandValueSchema,
cwd: z.string().optional(),
directory: z.string().optional(),
})
.passthrough(),
])
.transform((value) => {
const commandValue = "command" in value ? value.command : value.cmd;
return {
command: commandFromValue(commandValue),
cwd: nonEmptyString(value.cwd) ?? nonEmptyString(value.directory),
};
});
const CodexReadContentSchema = z.union([
z.string(),
StandardReadChunkSchema,
z.array(StandardReadChunkSchema),
const CodexShellOutputObjectSchema = z
.object({
command: z.string().optional(),
output: z.string().optional(),
text: z.string().optional(),
content: z.string().optional(),
aggregatedOutput: z.string().optional(),
exitCode: z.number().finite().nullable().optional(),
exit_code: z.number().finite().nullable().optional(),
metadata: z
.object({
exitCode: z.number().finite().nullable().optional(),
exit_code: z.number().finite().nullable().optional(),
})
.passthrough()
.optional(),
structuredContent: z
.object({
output: z.string().optional(),
text: z.string().optional(),
content: z.string().optional(),
})
.passthrough()
.optional(),
structured_content: z
.object({
output: z.string().optional(),
text: z.string().optional(),
content: z.string().optional(),
})
.passthrough()
.optional(),
result: z
.object({
command: z.string().optional(),
output: z.string().optional(),
text: z.string().optional(),
content: z.string().optional(),
})
.passthrough()
.optional(),
})
.passthrough();
const CodexShellOutputSchema = z.union([
z.string().transform((value) => ({
command: undefined,
output: nonEmptyString(value),
exitCode: undefined,
})),
CodexShellOutputObjectSchema.transform((value) => ({
command: nonEmptyString(value.command) ?? nonEmptyString(value.result?.command),
output:
nonEmptyString(value.output) ??
nonEmptyString(value.text) ??
nonEmptyString(value.content) ??
nonEmptyString(value.aggregatedOutput) ??
nonEmptyString(value.structuredContent?.output) ??
nonEmptyString(value.structuredContent?.text) ??
nonEmptyString(value.structuredContent?.content) ??
nonEmptyString(value.structured_content?.output) ??
nonEmptyString(value.structured_content?.text) ??
nonEmptyString(value.structured_content?.content) ??
nonEmptyString(value.result?.output) ??
nonEmptyString(value.result?.text) ??
nonEmptyString(value.result?.content),
exitCode:
value.exitCode ??
value.exit_code ??
value.metadata?.exitCode ??
value.metadata?.exit_code ??
undefined,
})),
]);
const CodexPathSchema = z.union([
z.object({ path: z.string() }).passthrough().transform((value) => value.path),
z.object({ file_path: z.string() }).passthrough().transform((value) => value.file_path),
z.object({ filePath: z.string() }).passthrough().transform((value) => value.filePath),
]);
const CodexReadArgumentsSchema = z.union([
z
.object({
path: z.string(),
offset: z.number().finite().optional(),
limit: z.number().finite().optional(),
})
.passthrough()
.transform((value) => ({ filePath: value.path, offset: value.offset, limit: value.limit })),
z
.object({
file_path: z.string(),
offset: z.number().finite().optional(),
limit: z.number().finite().optional(),
})
.passthrough()
.transform((value) => ({ filePath: value.file_path, offset: value.offset, limit: value.limit })),
z
.object({
filePath: z.string(),
offset: z.number().finite().optional(),
limit: z.number().finite().optional(),
})
.passthrough()
.transform((value) => ({ filePath: value.filePath, offset: value.offset, limit: value.limit })),
]);
const CodexReadChunkSchema = z.union([
z
.object({
text: z.string(),
content: z.string().optional(),
output: z.string().optional(),
})
.passthrough(),
z
.object({
text: z.string().optional(),
content: z.string(),
output: z.string().optional(),
})
.passthrough(),
z
.object({
text: z.string().optional(),
content: z.string().optional(),
output: z.string(),
})
.passthrough(),
]);
const CodexReadContentSchema = z.union([z.string(), CodexReadChunkSchema, z.array(CodexReadChunkSchema)]);
const CodexReadPayloadSchema = z.union([
z
.object({
@@ -134,8 +261,8 @@ const CodexReadResultWithPathSchema = z.union([
const CodexReadResultSchema = z.union([
z.string().transform((value) => ({ filePath: undefined, content: nonEmptyString(value) })),
StandardReadChunkSchema.transform((value) => ({ filePath: undefined, content: flattenReadContent(value) })),
z.array(StandardReadChunkSchema).transform((value) => ({ filePath: undefined, content: flattenReadContent(value) })),
CodexReadChunkSchema.transform((value) => ({ filePath: undefined, content: flattenReadContent(value) })),
z.array(CodexReadChunkSchema).transform((value) => ({ filePath: undefined, content: flattenReadContent(value) })),
CodexReadPayloadSchema.transform((value) => ({
filePath: undefined,
content:
@@ -176,6 +303,184 @@ const CodexReadResultSchema = z.union([
CodexReadResultWithPathSchema,
]);
const CodexWriteContentSchema = z
.object({
content: z.string().optional(),
new_content: z.string().optional(),
newContent: z.string().optional(),
})
.passthrough();
const CodexWriteArgumentsSchema = z
.intersection(CodexPathSchema.transform((filePath) => ({ filePath })), CodexWriteContentSchema)
.transform((value) => ({
filePath: value.filePath,
content:
nonEmptyString(value.content) ??
nonEmptyString(value.new_content) ??
nonEmptyString(value.newContent),
}));
const CodexWriteResultSchema = z.union([
z
.intersection(CodexPathSchema.transform((filePath) => ({ filePath })), CodexWriteContentSchema)
.transform((value) => ({
filePath: value.filePath,
content:
nonEmptyString(value.content) ??
nonEmptyString(value.new_content) ??
nonEmptyString(value.newContent),
})),
CodexWriteContentSchema.transform((value) => ({
filePath: undefined,
content:
nonEmptyString(value.content) ??
nonEmptyString(value.new_content) ??
nonEmptyString(value.newContent),
})),
]);
const CodexEditTextSchema = z
.object({
old_string: z.string().optional(),
old_str: z.string().optional(),
oldContent: z.string().optional(),
old_content: z.string().optional(),
new_string: z.string().optional(),
new_str: z.string().optional(),
newContent: z.string().optional(),
new_content: z.string().optional(),
content: z.string().optional(),
patch: z.string().optional(),
diff: z.string().optional(),
unified_diff: z.string().optional(),
unifiedDiff: z.string().optional(),
})
.passthrough();
const CodexEditArgumentsSchema = z
.intersection(CodexPathSchema.transform((filePath) => ({ filePath })), CodexEditTextSchema)
.transform((value) => ({
filePath: value.filePath,
oldString:
nonEmptyString(value.old_string) ??
nonEmptyString(value.old_str) ??
nonEmptyString(value.oldContent) ??
nonEmptyString(value.old_content),
newString:
nonEmptyString(value.new_string) ??
nonEmptyString(value.new_str) ??
nonEmptyString(value.newContent) ??
nonEmptyString(value.new_content) ??
nonEmptyString(value.content),
unifiedDiff: truncateDiffText(
nonEmptyString(value.patch) ??
nonEmptyString(value.diff) ??
nonEmptyString(value.unified_diff) ??
nonEmptyString(value.unifiedDiff)
),
}));
const CodexEditResultFileSchema = z.union([
z
.object({
path: z.string(),
patch: z.string().optional(),
diff: z.string().optional(),
unified_diff: z.string().optional(),
unifiedDiff: z.string().optional(),
})
.passthrough()
.transform((value) => ({
filePath: value.path,
unifiedDiff: truncateDiffText(
nonEmptyString(value.patch) ??
nonEmptyString(value.diff) ??
nonEmptyString(value.unified_diff) ??
nonEmptyString(value.unifiedDiff)
),
})),
z
.object({
file_path: z.string(),
patch: z.string().optional(),
diff: z.string().optional(),
unified_diff: z.string().optional(),
unifiedDiff: z.string().optional(),
})
.passthrough()
.transform((value) => ({
filePath: value.file_path,
unifiedDiff: truncateDiffText(
nonEmptyString(value.patch) ??
nonEmptyString(value.diff) ??
nonEmptyString(value.unified_diff) ??
nonEmptyString(value.unifiedDiff)
),
})),
z
.object({
filePath: z.string(),
patch: z.string().optional(),
diff: z.string().optional(),
unified_diff: z.string().optional(),
unifiedDiff: z.string().optional(),
})
.passthrough()
.transform((value) => ({
filePath: value.filePath,
unifiedDiff: truncateDiffText(
nonEmptyString(value.patch) ??
nonEmptyString(value.diff) ??
nonEmptyString(value.unified_diff) ??
nonEmptyString(value.unifiedDiff)
),
})),
]);
const CodexEditResultSchema = z.union([
z
.intersection(CodexPathSchema.transform((filePath) => ({ filePath })), CodexEditTextSchema)
.transform((value) => ({
filePath: value.filePath,
newString:
nonEmptyString(value.newContent) ??
nonEmptyString(value.new_content) ??
nonEmptyString(value.content),
unifiedDiff: truncateDiffText(
nonEmptyString(value.patch) ??
nonEmptyString(value.diff) ??
nonEmptyString(value.unified_diff) ??
nonEmptyString(value.unifiedDiff)
),
})),
z
.object({ files: z.array(CodexEditResultFileSchema).min(1) })
.passthrough()
.transform((value) => ({
filePath: value.files[0]?.filePath,
unifiedDiff: value.files[0]?.unifiedDiff,
newString: undefined,
})),
CodexEditTextSchema.transform((value) => ({
filePath: undefined,
newString:
nonEmptyString(value.newContent) ??
nonEmptyString(value.new_content) ??
nonEmptyString(value.content),
unifiedDiff: truncateDiffText(
nonEmptyString(value.patch) ??
nonEmptyString(value.diff) ??
nonEmptyString(value.unified_diff) ??
nonEmptyString(value.unifiedDiff)
),
})),
]);
const CodexSearchArgumentsSchema = z.union([
z.object({ query: z.string() }).passthrough().transform((value) => ({ query: value.query })),
z.object({ q: z.string() }).passthrough().transform((value) => ({ query: value.q })),
]);
const CodexCommandExecutionItemSchema = z
.object({
@@ -267,10 +572,7 @@ function normalizeCodexFilePath(filePath: string | undefined, cwd: string | null
return undefined;
}
if (typeof cwd === "string" && cwd.length > 0) {
const prefix = cwd.endsWith("/") ? cwd : `${cwd}/`;
if (trimmed.startsWith(prefix)) {
return trimmed.slice(prefix.length) || ".";
}
return stripCwdPrefix(trimmed, cwd);
}
return trimmed;
}
@@ -303,7 +605,18 @@ function toShellDetail(
input: z.infer<typeof CodexShellInputSchema> | null,
output: z.infer<typeof CodexShellOutputSchema> | null
): ToolCallDetail | undefined {
return toStandardShellDetail(input, output);
const command = input?.command ?? output?.command;
if (!command) {
return undefined;
}
return {
type: "shell",
command,
...(input?.cwd ? { cwd: input.cwd } : {}),
...(output?.output ? { output: output.output } : {}),
...(output?.exitCode !== undefined ? { exitCode: output.exitCode } : {}),
};
}
function toReadDetail(
@@ -330,7 +643,16 @@ function toWriteDetail(
output: z.infer<typeof CodexWriteResultSchema> | null,
cwd: string | null | undefined
): ToolCallDetail | undefined {
return toStandardWriteDetail(input, output, (filePath) => normalizeCodexFilePath(filePath, cwd));
const filePath = normalizeCodexFilePath(input?.filePath ?? output?.filePath, cwd);
if (!filePath) {
return undefined;
}
return {
type: "write",
filePath,
...(input?.content ? { content: input.content } : output?.content ? { content: output.content } : {}),
};
}
function toEditDetail(
@@ -338,120 +660,114 @@ function toEditDetail(
output: z.infer<typeof CodexEditResultSchema> | null,
cwd: string | null | undefined
): ToolCallDetail | undefined {
return toStandardEditDetail(input, output, (filePath) => normalizeCodexFilePath(filePath, cwd));
const filePath = normalizeCodexFilePath(input?.filePath ?? output?.filePath, cwd);
if (!filePath) {
return undefined;
}
return {
type: "edit",
filePath,
...(input?.oldString ? { oldString: input.oldString } : {}),
...(input?.newString ? { newString: input.newString } : output?.newString ? { newString: output.newString } : {}),
...(input?.unifiedDiff
? { unifiedDiff: input.unifiedDiff }
: output?.unifiedDiff
? { unifiedDiff: output.unifiedDiff }
: {}),
};
}
function toSearchDetail(input: z.infer<typeof CodexSearchArgumentsSchema> | null): ToolCallDetail | undefined {
return toStandardSearchDetail(input);
if (!input?.query) {
return undefined;
}
return {
type: "search",
query: input.query,
};
}
function codexMcpToolBranch<ToolName extends string, InputSchema extends z.ZodTypeAny, OutputSchema extends z.ZodTypeAny>(
tool: ToolName,
inputSchema: InputSchema,
outputSchema: OutputSchema,
mapper: (
input: z.infer<InputSchema> | null,
output: z.infer<OutputSchema> | null,
cwd: string | null | undefined
) => ToolCallDetail | undefined
) {
return z
.object({
tool: z.literal(tool),
arguments: inputSchema.nullable(),
result: outputSchema.nullable(),
cwd: z.string().optional().nullable(),
})
.transform(({ arguments: input, result: output, cwd }) => mapper(input, output, cwd));
}
const CodexKnownMcpToolDetailSchema: z.ZodType<ToolCallDetail | undefined> = unionToolDetailSchemas([
...CODEX_MCP_KNOWN_TOOL_ALIASES.shell.map((tool) =>
codexMcpToolBranch(tool, CodexShellInputSchema, CodexShellOutputSchema, (input, output) =>
toShellDetail(input, output)
)
),
...CODEX_MCP_KNOWN_TOOL_ALIASES.read.map((tool) =>
codexMcpToolBranch(tool, CodexReadArgumentsSchema, CodexReadResultSchema, toReadDetail)
),
...CODEX_MCP_KNOWN_TOOL_ALIASES.write.map((tool) =>
codexMcpToolBranch(tool, CodexWriteArgumentsSchema, CodexWriteResultSchema, toWriteDetail)
),
...CODEX_MCP_KNOWN_TOOL_ALIASES.edit.map((tool) =>
codexMcpToolBranch(tool, CodexEditArgumentsSchema, CodexEditResultSchema, toEditDetail)
),
...CODEX_MCP_KNOWN_TOOL_ALIASES.search.map((tool) =>
codexMcpToolBranch(tool, CodexSearchArgumentsSchema, z.unknown(), (input) => toSearchDetail(input))
),
]);
function codexRolloutToolBranch<Name extends string, InputSchema extends z.ZodTypeAny, OutputSchema extends z.ZodTypeAny>(
function codexToolDetailBranch<Name extends string, InputSchema extends z.ZodTypeAny, OutputSchema extends z.ZodTypeAny>(
name: Name,
inputSchema: InputSchema,
outputSchema: OutputSchema,
mapper: (
input: z.infer<InputSchema> | null,
output: z.infer<OutputSchema> | null
) => ToolCallDetail | undefined
mapper: (params: {
input: z.infer<InputSchema> | null;
output: z.infer<OutputSchema> | null;
cwd: string | null | undefined;
}) => ToolCallDetail | undefined
) {
return z
.object({
name: z.literal(name),
input: inputSchema.nullable(),
output: outputSchema.nullable(),
cwd: z.string().optional().nullable(),
})
.transform(({ input, output }) => mapper(input, output));
.transform(({ input, output, cwd }) => mapper({ input, output, cwd }));
}
const CodexKnownRolloutDetailSchema: z.ZodType<ToolCallDetail | undefined> = unionToolDetailSchemas([
...CODEX_ROLLOUT_KNOWN_TOOL_ALIASES.shell.map((name) =>
codexRolloutToolBranch(name, CodexShellInputSchema, CodexShellOutputSchema, (input, output) =>
toShellDetail(input, output)
)
const CodexKnownToolDetailSchema = z.union([
codexToolDetailBranch("Bash", CodexShellInputSchema, CodexShellOutputSchema, ({ input, output }) =>
toShellDetail(input, output)
),
...CODEX_ROLLOUT_KNOWN_TOOL_ALIASES.read.map((name) =>
codexRolloutToolBranch(name, CodexReadArgumentsSchema, CodexReadResultSchema, (input, output) =>
toReadDetail(input, output, null)
)
codexToolDetailBranch("shell", CodexShellInputSchema, CodexShellOutputSchema, ({ input, output }) =>
toShellDetail(input, output)
),
...CODEX_ROLLOUT_KNOWN_TOOL_ALIASES.write.map((name) =>
codexRolloutToolBranch(name, CodexWriteArgumentsSchema, CodexWriteResultSchema, (input, output) =>
toWriteDetail(input, output, null)
)
codexToolDetailBranch("bash", CodexShellInputSchema, CodexShellOutputSchema, ({ input, output }) =>
toShellDetail(input, output)
),
...CODEX_ROLLOUT_KNOWN_TOOL_ALIASES.edit.map((name) =>
codexRolloutToolBranch(name, CodexEditArgumentsSchema, CodexEditResultSchema, (input, output) =>
toEditDetail(input, output, null)
)
codexToolDetailBranch("exec", CodexShellInputSchema, CodexShellOutputSchema, ({ input, output }) =>
toShellDetail(input, output)
),
...CODEX_ROLLOUT_KNOWN_TOOL_ALIASES.search.map((name) =>
codexRolloutToolBranch(name, CodexSearchArgumentsSchema, z.unknown(), (input) => toSearchDetail(input))
codexToolDetailBranch("exec_command", CodexShellInputSchema, CodexShellOutputSchema, ({ input, output }) =>
toShellDetail(input, output)
),
codexToolDetailBranch("command", CodexShellInputSchema, CodexShellOutputSchema, ({ input, output }) =>
toShellDetail(input, output)
),
codexToolDetailBranch("read", CodexReadArgumentsSchema, CodexReadResultSchema, ({ input, output, cwd }) =>
toReadDetail(input, output, cwd)
),
codexToolDetailBranch("read_file", CodexReadArgumentsSchema, CodexReadResultSchema, ({ input, output, cwd }) =>
toReadDetail(input, output, cwd)
),
codexToolDetailBranch("write", CodexWriteArgumentsSchema, CodexWriteResultSchema, ({ input, output, cwd }) =>
toWriteDetail(input, output, cwd)
),
codexToolDetailBranch("write_file", CodexWriteArgumentsSchema, CodexWriteResultSchema, ({ input, output, cwd }) =>
toWriteDetail(input, output, cwd)
),
codexToolDetailBranch("create_file", CodexWriteArgumentsSchema, CodexWriteResultSchema, ({ input, output, cwd }) =>
toWriteDetail(input, output, cwd)
),
codexToolDetailBranch("edit", CodexEditArgumentsSchema, CodexEditResultSchema, ({ input, output, cwd }) =>
toEditDetail(input, output, cwd)
),
codexToolDetailBranch("apply_patch", CodexEditArgumentsSchema, CodexEditResultSchema, ({ input, output, cwd }) =>
toEditDetail(input, output, cwd)
),
codexToolDetailBranch("apply_diff", CodexEditArgumentsSchema, CodexEditResultSchema, ({ input, output, cwd }) =>
toEditDetail(input, output, cwd)
),
codexToolDetailBranch("search", CodexSearchArgumentsSchema, z.unknown(), ({ input }) => toSearchDetail(input)),
codexToolDetailBranch("web_search", CodexSearchArgumentsSchema, z.unknown(), ({ input }) =>
toSearchDetail(input)
),
]);
function deriveMcpToolDetail(
tool: string,
input: unknown,
output: unknown,
options?: CodexMapperOptions
): ToolCallDetail | undefined {
const parsed = CodexKnownMcpToolDetailSchema.safeParse({
tool,
arguments: input,
result: output,
cwd: options?.cwd ?? null,
});
if (!parsed.success) {
return undefined;
}
return parsed.data;
}
function deriveRolloutDetail(name: string, input: unknown, output: unknown): ToolCallDetail | undefined {
const parsed = CodexKnownRolloutDetailSchema.safeParse({
name,
input,
output,
function deriveToolDetail(params: {
name: string;
input: unknown;
output: unknown;
cwd?: string | null;
}): ToolCallDetail | undefined {
const parsed = CodexKnownToolDetailSchema.safeParse({
name: params.name,
input: params.input,
output: params.output,
cwd: params.cwd ?? null,
});
if (!parsed.success) {
return undefined;
@@ -496,9 +812,23 @@ function buildToolCall(params: {
};
}
const CODEX_BUILTIN_TOOL_NAMES = new Set(
Object.values(CODEX_MCP_KNOWN_TOOL_ALIASES).flat()
);
const CODEX_BUILTIN_TOOL_NAMES = new Set([
"shell",
"bash",
"exec",
"exec_command",
"command",
"read",
"read_file",
"write",
"write_file",
"create_file",
"edit",
"apply_patch",
"apply_diff",
"web_search",
"search",
]);
function buildMcpToolName(server: string | undefined, tool: string): string {
const trimmedTool = tool.trim();
@@ -639,7 +969,12 @@ function mapMcpToolCallItem(
const error = item.error ?? null;
const callId = coerceCallId(item.id ?? item.callID ?? item.call_id, name, input);
const status = resolveStatus(item.status, error, output);
const detail = deriveMcpToolDetail(tool, input, output, options);
const detail = deriveToolDetail({
name: tool,
input,
output,
cwd: options?.cwd ?? null,
});
return buildToolCall({
callId,
@@ -720,7 +1055,12 @@ export function mapCodexRolloutToolCall(params: {
const error = parsed.error ?? null;
const status = resolveStatus("completed", error, output);
const callId = coerceCallId(parsed.callId, parsed.name, input);
const detail = deriveRolloutDetail(parsed.name, input, output);
const detail = deriveToolDetail({
name: parsed.name,
input,
output,
cwd: null,
});
return buildToolCall({
callId,

View File

@@ -1,26 +1,12 @@
import { z } from "zod";
import type { ToolCallDetail, ToolCallTimelineItem } from "../../agent-sdk-types.js";
import {
StandardEditInputSchema,
StandardEditOutputSchema,
StandardReadOutputSchema,
StandardReadPathInputSchema,
StandardSearchInputSchema,
StandardShellInputSchema,
StandardShellOutputSchema,
StandardWriteInputSchema,
StandardWriteOutputSchema,
toStandardEditDetail,
toStandardReadDetail,
toStandardSearchDetail,
toStandardShellDetail,
toStandardWriteDetail,
} from "../standard-tool-call-schemas.js";
import {
coerceToolCallId,
OPENCODE_KNOWN_TOOL_ALIASES,
unionToolDetailSchemas,
commandFromValue,
flattenReadContent as flattenToolReadContent,
nonEmptyString,
truncateDiffText,
} from "../tool-call-mapper-utils.js";
type OpencodeToolCallParams = {
@@ -33,6 +19,8 @@ type OpencodeToolCallParams = {
metadata?: Record<string, unknown>;
};
const MAX_DIFF_TEXT_CHARS = 12_000;
const FAILED_STATUSES = new Set(["error", "failed", "failure"]);
const CANCELED_STATUSES = new Set(["canceled", "cancelled", "aborted", "interrupted"]);
const COMPLETED_STATUSES = new Set(["complete", "completed", "success", "succeeded", "done"]);
@@ -49,6 +37,435 @@ const OpencodeToolCallParamsSchema = z
})
.passthrough();
const CommandValueSchema = z.union([z.string(), z.array(z.string())]);
const OpencodeShellInputSchema = z
.union([
z
.object({
command: CommandValueSchema,
cwd: z.string().optional(),
directory: z.string().optional(),
})
.passthrough(),
z
.object({
cmd: CommandValueSchema,
cwd: z.string().optional(),
directory: z.string().optional(),
})
.passthrough(),
])
.transform((value) => {
const commandValue = "command" in value ? value.command : value.cmd;
return {
command: commandFromValue(commandValue),
cwd: nonEmptyString(value.cwd) ?? nonEmptyString(value.directory),
};
});
const OpencodeShellOutputObjectSchema = z
.object({
command: z.string().optional(),
output: z.string().optional(),
text: z.string().optional(),
content: z.string().optional(),
exitCode: z.number().finite().nullable().optional(),
exit_code: z.number().finite().nullable().optional(),
metadata: z
.object({
exitCode: z.number().finite().nullable().optional(),
exit_code: z.number().finite().nullable().optional(),
})
.passthrough()
.optional(),
structuredContent: z
.object({
output: z.string().optional(),
text: z.string().optional(),
content: z.string().optional(),
})
.passthrough()
.optional(),
structured_content: z
.object({
output: z.string().optional(),
text: z.string().optional(),
content: z.string().optional(),
})
.passthrough()
.optional(),
result: z
.object({
command: z.string().optional(),
output: z.string().optional(),
text: z.string().optional(),
content: z.string().optional(),
})
.passthrough()
.optional(),
})
.passthrough();
const OpencodeShellOutputSchema = z.union([
z.string().transform((value) => ({
command: undefined,
output: nonEmptyString(value),
exitCode: undefined,
})),
OpencodeShellOutputObjectSchema.transform((value) => ({
command: nonEmptyString(value.command) ?? nonEmptyString(value.result?.command),
output:
nonEmptyString(value.output) ??
nonEmptyString(value.text) ??
nonEmptyString(value.content) ??
nonEmptyString(value.structuredContent?.output) ??
nonEmptyString(value.structuredContent?.text) ??
nonEmptyString(value.structuredContent?.content) ??
nonEmptyString(value.structured_content?.output) ??
nonEmptyString(value.structured_content?.text) ??
nonEmptyString(value.structured_content?.content) ??
nonEmptyString(value.result?.output) ??
nonEmptyString(value.result?.text) ??
nonEmptyString(value.result?.content),
exitCode:
value.exitCode ??
value.exit_code ??
value.metadata?.exitCode ??
value.metadata?.exit_code ??
undefined,
})),
]);
const OpencodeReadPathInputSchema = z.union([
z
.object({
file_path: z.string(),
offset: z.number().finite().optional(),
limit: z.number().finite().optional(),
})
.passthrough()
.transform((value) => ({
filePath: value.file_path,
offset: value.offset,
limit: value.limit,
})),
z
.object({
path: z.string(),
offset: z.number().finite().optional(),
limit: z.number().finite().optional(),
})
.passthrough()
.transform((value) => ({
filePath: value.path,
offset: value.offset,
limit: value.limit,
})),
z
.object({
filePath: z.string(),
offset: z.number().finite().optional(),
limit: z.number().finite().optional(),
})
.passthrough()
.transform((value) => ({
filePath: value.filePath,
offset: value.offset,
limit: value.limit,
})),
]);
const OpencodeReadChunkSchema = z.union([
z
.object({
text: z.string(),
content: z.string().optional(),
output: z.string().optional(),
})
.passthrough(),
z
.object({
text: z.string().optional(),
content: z.string(),
output: z.string().optional(),
})
.passthrough(),
z
.object({
text: z.string().optional(),
content: z.string().optional(),
output: z.string(),
})
.passthrough(),
]);
const OpencodeReadContentSchema = z.union([
z.string(),
OpencodeReadChunkSchema,
z.array(OpencodeReadChunkSchema),
]);
const OpencodeReadPayloadSchema = z.union([
z
.object({
content: OpencodeReadContentSchema,
text: OpencodeReadContentSchema.optional(),
output: OpencodeReadContentSchema.optional(),
})
.passthrough(),
z
.object({
content: OpencodeReadContentSchema.optional(),
text: OpencodeReadContentSchema,
output: OpencodeReadContentSchema.optional(),
})
.passthrough(),
z
.object({
content: OpencodeReadContentSchema.optional(),
text: OpencodeReadContentSchema.optional(),
output: OpencodeReadContentSchema,
})
.passthrough(),
]);
const OpencodeReadOutputSchema = z.union([
z.string().transform((value) => ({ content: nonEmptyString(value) })),
OpencodeReadChunkSchema.transform((value) => ({ content: flattenReadContent(value) })),
z.array(OpencodeReadChunkSchema).transform((value) => ({ content: flattenReadContent(value) })),
OpencodeReadPayloadSchema.transform((value) => ({
content:
flattenReadContent(value.content) ??
flattenReadContent(value.text) ??
flattenReadContent(value.output),
})),
z
.object({ data: OpencodeReadPayloadSchema })
.passthrough()
.transform((value) => ({
content:
flattenReadContent(value.data.content) ??
flattenReadContent(value.data.text) ??
flattenReadContent(value.data.output),
})),
z
.object({ structuredContent: OpencodeReadPayloadSchema })
.passthrough()
.transform((value) => ({
content:
flattenReadContent(value.structuredContent.content) ??
flattenReadContent(value.structuredContent.text) ??
flattenReadContent(value.structuredContent.output),
})),
z
.object({ structured_content: OpencodeReadPayloadSchema })
.passthrough()
.transform((value) => ({
content:
flattenReadContent(value.structured_content.content) ??
flattenReadContent(value.structured_content.text) ??
flattenReadContent(value.structured_content.output),
})),
]);
const OpencodeWritePathInputSchema = z.union([
z.object({ file_path: z.string() }).passthrough().transform((value) => ({ filePath: value.file_path })),
z.object({ path: z.string() }).passthrough().transform((value) => ({ filePath: value.path })),
z.object({ filePath: z.string() }).passthrough().transform((value) => ({ filePath: value.filePath })),
]);
const OpencodeWriteContentSchema = z
.object({
content: z.string().optional(),
new_content: z.string().optional(),
newContent: z.string().optional(),
})
.passthrough();
const OpencodeWriteInputSchema = z
.intersection(OpencodeWritePathInputSchema, OpencodeWriteContentSchema)
.transform((value) => ({
filePath: value.filePath,
content:
nonEmptyString(value.content) ??
nonEmptyString(value.new_content) ??
nonEmptyString(value.newContent),
}));
const OpencodeWriteOutputSchema = z.union([
z
.intersection(OpencodeWritePathInputSchema, OpencodeWriteContentSchema)
.transform((value) => ({
filePath: value.filePath,
content:
nonEmptyString(value.content) ??
nonEmptyString(value.new_content) ??
nonEmptyString(value.newContent),
})),
OpencodeWriteContentSchema.transform((value) => ({
filePath: undefined,
content:
nonEmptyString(value.content) ??
nonEmptyString(value.new_content) ??
nonEmptyString(value.newContent),
})),
]);
const OpencodeEditTextSchema = z
.object({
old_string: z.string().optional(),
old_str: z.string().optional(),
oldContent: z.string().optional(),
old_content: z.string().optional(),
new_string: z.string().optional(),
new_str: z.string().optional(),
newContent: z.string().optional(),
new_content: z.string().optional(),
content: z.string().optional(),
patch: z.string().optional(),
diff: z.string().optional(),
unified_diff: z.string().optional(),
unifiedDiff: z.string().optional(),
})
.passthrough();
const OpencodeEditInputSchema = z
.intersection(OpencodeWritePathInputSchema, OpencodeEditTextSchema)
.transform((value) => ({
filePath: value.filePath,
oldString:
nonEmptyString(value.old_string) ??
nonEmptyString(value.old_str) ??
nonEmptyString(value.oldContent) ??
nonEmptyString(value.old_content),
newString:
nonEmptyString(value.new_string) ??
nonEmptyString(value.new_str) ??
nonEmptyString(value.newContent) ??
nonEmptyString(value.new_content) ??
nonEmptyString(value.content),
unifiedDiff: truncateDiffText(
nonEmptyString(value.patch) ??
nonEmptyString(value.diff) ??
nonEmptyString(value.unified_diff) ??
nonEmptyString(value.unifiedDiff),
MAX_DIFF_TEXT_CHARS
),
}));
const OpencodeEditOutputFileSchema = z.union([
z
.object({
path: z.string(),
patch: z.string().optional(),
diff: z.string().optional(),
unified_diff: z.string().optional(),
unifiedDiff: z.string().optional(),
})
.passthrough()
.transform((value) => ({
filePath: value.path,
unifiedDiff: truncateDiffText(
nonEmptyString(value.patch) ??
nonEmptyString(value.diff) ??
nonEmptyString(value.unified_diff) ??
nonEmptyString(value.unifiedDiff),
MAX_DIFF_TEXT_CHARS
),
})),
z
.object({
file_path: z.string(),
patch: z.string().optional(),
diff: z.string().optional(),
unified_diff: z.string().optional(),
unifiedDiff: z.string().optional(),
})
.passthrough()
.transform((value) => ({
filePath: value.file_path,
unifiedDiff: truncateDiffText(
nonEmptyString(value.patch) ??
nonEmptyString(value.diff) ??
nonEmptyString(value.unified_diff) ??
nonEmptyString(value.unifiedDiff),
MAX_DIFF_TEXT_CHARS
),
})),
z
.object({
filePath: z.string(),
patch: z.string().optional(),
diff: z.string().optional(),
unified_diff: z.string().optional(),
unifiedDiff: z.string().optional(),
})
.passthrough()
.transform((value) => ({
filePath: value.filePath,
unifiedDiff: truncateDiffText(
nonEmptyString(value.patch) ??
nonEmptyString(value.diff) ??
nonEmptyString(value.unified_diff) ??
nonEmptyString(value.unifiedDiff),
MAX_DIFF_TEXT_CHARS
),
})),
]);
const OpencodeEditOutputSchema = z.union([
z
.intersection(OpencodeWritePathInputSchema, OpencodeEditTextSchema)
.transform((value) => ({
filePath: value.filePath,
newString:
nonEmptyString(value.newContent) ??
nonEmptyString(value.new_content) ??
nonEmptyString(value.content),
unifiedDiff: truncateDiffText(
nonEmptyString(value.patch) ??
nonEmptyString(value.diff) ??
nonEmptyString(value.unified_diff) ??
nonEmptyString(value.unifiedDiff),
MAX_DIFF_TEXT_CHARS
),
})),
z
.object({ files: z.array(OpencodeEditOutputFileSchema).min(1) })
.passthrough()
.transform((value) => ({
filePath: value.files[0]?.filePath,
unifiedDiff: value.files[0]?.unifiedDiff,
newString: undefined,
})),
OpencodeEditTextSchema.transform((value) => ({
filePath: undefined,
newString:
nonEmptyString(value.newContent) ??
nonEmptyString(value.new_content) ??
nonEmptyString(value.content),
unifiedDiff: truncateDiffText(
nonEmptyString(value.patch) ??
nonEmptyString(value.diff) ??
nonEmptyString(value.unified_diff) ??
nonEmptyString(value.unifiedDiff),
MAX_DIFF_TEXT_CHARS
),
})),
]);
const OpencodeSearchInputSchema = z.union([
z.object({ query: z.string() }).passthrough().transform((value) => ({ query: value.query })),
z.object({ q: z.string() }).passthrough().transform((value) => ({ query: value.q })),
]);
function flattenReadContent(
value: z.infer<typeof OpencodeReadContentSchema> | undefined
): string | undefined {
return flattenToolReadContent(value);
}
function coerceCallId(callId: string | null | undefined, toolName: string, input: unknown): string {
return coerceToolCallId({
providerPrefix: "opencode",
@@ -82,13 +499,100 @@ function resolveStatus(rawStatus: unknown, error: unknown, output: unknown): Too
return output !== null && output !== undefined ? "completed" : "running";
}
function opencodeToolBranch<InputSchema extends z.ZodTypeAny, OutputSchema extends z.ZodTypeAny>(
toolName: string,
function toShellDetail(
input: z.infer<typeof OpencodeShellInputSchema> | null,
output: z.infer<typeof OpencodeShellOutputSchema> | 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<typeof OpencodeReadPathInputSchema> | null,
output: z.infer<typeof OpencodeReadOutputSchema> | 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<typeof OpencodeWriteInputSchema> | null,
output: z.infer<typeof OpencodeWriteOutputSchema> | 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<typeof OpencodeEditInputSchema> | null,
output: z.infer<typeof OpencodeEditOutputSchema> | 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<typeof OpencodeSearchInputSchema> | null): ToolCallDetail | undefined {
if (!input?.query) {
return undefined;
}
return {
type: "search",
query: input.query,
};
}
function opencodeToolBranch<
ToolName extends string,
InputSchema extends z.ZodTypeAny,
OutputSchema extends z.ZodTypeAny,
>(
toolName: ToolName,
inputSchema: InputSchema,
outputSchema: OutputSchema,
mapper: (
input: z.output<InputSchema> | null | undefined,
output: z.output<OutputSchema> | null | undefined
input: z.infer<InputSchema> | null,
output: z.infer<OutputSchema> | null
) => ToolCallDetail | undefined
) {
return z
@@ -100,32 +604,20 @@ function opencodeToolBranch<InputSchema extends z.ZodTypeAny, OutputSchema exten
.transform(({ input, output }) => mapper(input, output));
}
const OpencodeKnownToolDetailSchema: z.ZodType<ToolCallDetail | undefined> = unionToolDetailSchemas([
...OPENCODE_KNOWN_TOOL_ALIASES.shell.map((toolName) =>
opencodeToolBranch(toolName, StandardShellInputSchema, StandardShellOutputSchema, (input, output) =>
toStandardShellDetail(input ?? null, output ?? null)
)
),
...OPENCODE_KNOWN_TOOL_ALIASES.read.map((toolName) =>
opencodeToolBranch(toolName, StandardReadPathInputSchema, StandardReadOutputSchema, (input, output) =>
toStandardReadDetail(input ?? null, output ?? null)
)
),
...OPENCODE_KNOWN_TOOL_ALIASES.write.map((toolName) =>
opencodeToolBranch(toolName, StandardWriteInputSchema, StandardWriteOutputSchema, (input, output) =>
toStandardWriteDetail(input ?? null, output ?? null)
)
),
...OPENCODE_KNOWN_TOOL_ALIASES.edit.map((toolName) =>
opencodeToolBranch(toolName, StandardEditInputSchema, StandardEditOutputSchema, (input, output) =>
toStandardEditDetail(input ?? null, output ?? null)
)
),
...OPENCODE_KNOWN_TOOL_ALIASES.search.map((toolName) =>
opencodeToolBranch(toolName, StandardSearchInputSchema, z.unknown(), (input) =>
toStandardSearchDetail(input ?? null)
)
),
const OpencodeKnownToolDetailSchema = z.union([
opencodeToolBranch("shell", OpencodeShellInputSchema, OpencodeShellOutputSchema, toShellDetail),
opencodeToolBranch("bash", OpencodeShellInputSchema, OpencodeShellOutputSchema, toShellDetail),
opencodeToolBranch("exec_command", OpencodeShellInputSchema, OpencodeShellOutputSchema, toShellDetail),
opencodeToolBranch("read", OpencodeReadPathInputSchema, OpencodeReadOutputSchema, toReadDetail),
opencodeToolBranch("read_file", OpencodeReadPathInputSchema, OpencodeReadOutputSchema, toReadDetail),
opencodeToolBranch("write", OpencodeWriteInputSchema, OpencodeWriteOutputSchema, toWriteDetail),
opencodeToolBranch("write_file", OpencodeWriteInputSchema, OpencodeWriteOutputSchema, toWriteDetail),
opencodeToolBranch("create_file", OpencodeWriteInputSchema, OpencodeWriteOutputSchema, toWriteDetail),
opencodeToolBranch("edit", OpencodeEditInputSchema, OpencodeEditOutputSchema, toEditDetail),
opencodeToolBranch("apply_patch", OpencodeEditInputSchema, OpencodeEditOutputSchema, toEditDetail),
opencodeToolBranch("apply_diff", OpencodeEditInputSchema, OpencodeEditOutputSchema, toEditDetail),
opencodeToolBranch("search", OpencodeSearchInputSchema, z.unknown(), (input) => toSearchDetail(input)),
opencodeToolBranch("web_search", OpencodeSearchInputSchema, z.unknown(), (input) => toSearchDetail(input)),
]);
function deriveDetail(toolName: string, input: unknown, output: unknown): ToolCallDetail | undefined {

View File

@@ -1,577 +0,0 @@
import { z } from "zod";
import type { ToolCallDetail } from "../agent-sdk-types.js";
import {
commandFromValue,
flattenReadContent,
nonEmptyString,
truncateDiffText,
} from "./tool-call-mapper-utils.js";
export type StandardShellInput = {
command?: string;
cwd?: string;
};
export type StandardShellOutput = {
command?: string;
output?: string;
exitCode?: number | null;
};
export type StandardReadPathInput = {
filePath: string;
offset?: number;
limit?: number;
};
export type StandardReadOutput = {
content?: string;
};
export type StandardWriteInput = {
filePath: string;
content?: string;
};
export type StandardWriteOutput = {
filePath?: string;
content?: string;
};
export type StandardEditInput = {
filePath: string;
oldString?: string;
newString?: string;
unifiedDiff?: string;
};
export type StandardEditOutput = {
filePath?: string;
newString?: string;
unifiedDiff?: string;
};
export type StandardSearchInput = {
query: string;
};
const CommandValueSchema = z.union([z.string(), z.array(z.string())]);
export const StandardShellInputSchema = z
.union([
z
.object({
command: CommandValueSchema,
cwd: z.string().optional(),
directory: z.string().optional(),
})
.passthrough(),
z
.object({
cmd: CommandValueSchema,
cwd: z.string().optional(),
directory: z.string().optional(),
})
.passthrough(),
])
.transform((value) => {
const commandValue = "command" in value ? value.command : value.cmd;
return {
command: commandFromValue(commandValue),
cwd: nonEmptyString(value.cwd) ?? nonEmptyString(value.directory),
};
});
const StandardShellOutputObjectSchema = z
.object({
command: z.string().optional(),
output: z.string().optional(),
text: z.string().optional(),
content: z.string().optional(),
aggregated_output: z.string().optional(),
aggregatedOutput: z.string().optional(),
exitCode: z.number().finite().nullable().optional(),
exit_code: z.number().finite().nullable().optional(),
metadata: z
.object({
exitCode: z.number().finite().nullable().optional(),
exit_code: z.number().finite().nullable().optional(),
})
.passthrough()
.optional(),
structuredContent: z
.object({
output: z.string().optional(),
text: z.string().optional(),
content: z.string().optional(),
})
.passthrough()
.optional(),
structured_content: z
.object({
output: z.string().optional(),
text: z.string().optional(),
content: z.string().optional(),
})
.passthrough()
.optional(),
result: z
.object({
command: z.string().optional(),
output: z.string().optional(),
text: z.string().optional(),
content: z.string().optional(),
})
.passthrough()
.optional(),
})
.passthrough();
export const StandardShellOutputSchema = z.union([
z.string().transform((value) => ({
command: undefined,
output: nonEmptyString(value),
exitCode: undefined,
})),
StandardShellOutputObjectSchema.transform((value) => ({
command: nonEmptyString(value.command) ?? nonEmptyString(value.result?.command),
output:
nonEmptyString(value.output) ??
nonEmptyString(value.text) ??
nonEmptyString(value.content) ??
nonEmptyString(value.aggregated_output) ??
nonEmptyString(value.aggregatedOutput) ??
nonEmptyString(value.structuredContent?.output) ??
nonEmptyString(value.structuredContent?.text) ??
nonEmptyString(value.structuredContent?.content) ??
nonEmptyString(value.structured_content?.output) ??
nonEmptyString(value.structured_content?.text) ??
nonEmptyString(value.structured_content?.content) ??
nonEmptyString(value.result?.output) ??
nonEmptyString(value.result?.text) ??
nonEmptyString(value.result?.content),
exitCode:
value.exitCode ??
value.exit_code ??
value.metadata?.exitCode ??
value.metadata?.exit_code ??
undefined,
})),
]);
const StandardPathSchema = z.union([
z.object({ file_path: z.string() }).passthrough().transform((value) => ({ filePath: value.file_path })),
z.object({ path: z.string() }).passthrough().transform((value) => ({ filePath: value.path })),
z.object({ filePath: z.string() }).passthrough().transform((value) => ({ filePath: value.filePath })),
]);
export const StandardReadPathInputSchema = z.union([
z
.object({
file_path: z.string(),
offset: z.number().finite().optional(),
limit: z.number().finite().optional(),
})
.passthrough()
.transform((value) => ({
filePath: value.file_path,
offset: value.offset,
limit: value.limit,
})),
z
.object({
path: z.string(),
offset: z.number().finite().optional(),
limit: z.number().finite().optional(),
})
.passthrough()
.transform((value) => ({
filePath: value.path,
offset: value.offset,
limit: value.limit,
})),
z
.object({
filePath: z.string(),
offset: z.number().finite().optional(),
limit: z.number().finite().optional(),
})
.passthrough()
.transform((value) => ({
filePath: value.filePath,
offset: value.offset,
limit: value.limit,
})),
]);
export const StandardReadChunkSchema = z.union([
z
.object({
text: z.string(),
content: z.string().optional(),
output: z.string().optional(),
})
.passthrough(),
z
.object({
text: z.string().optional(),
content: z.string(),
output: z.string().optional(),
})
.passthrough(),
z
.object({
text: z.string().optional(),
content: z.string().optional(),
output: z.string(),
})
.passthrough(),
]);
const StandardReadContentSchema = z.union([
z.string(),
StandardReadChunkSchema,
z.array(StandardReadChunkSchema),
]);
const StandardReadPayloadSchema = z.union([
z
.object({
content: StandardReadContentSchema,
text: StandardReadContentSchema.optional(),
output: StandardReadContentSchema.optional(),
})
.passthrough(),
z
.object({
content: StandardReadContentSchema.optional(),
text: StandardReadContentSchema,
output: StandardReadContentSchema.optional(),
})
.passthrough(),
z
.object({
content: StandardReadContentSchema.optional(),
text: StandardReadContentSchema.optional(),
output: StandardReadContentSchema,
})
.passthrough(),
]);
export const StandardReadOutputSchema: z.ZodType<StandardReadOutput, z.ZodTypeDef, unknown> = z.union([
z.string().transform((value) => ({ content: nonEmptyString(value) })),
StandardReadChunkSchema.transform((value) => ({ content: flattenReadContent(value) })),
z.array(StandardReadChunkSchema).transform((value) => ({ content: flattenReadContent(value) })),
StandardReadPayloadSchema.transform((value) => ({
content:
flattenReadContent(value.content) ??
flattenReadContent(value.text) ??
flattenReadContent(value.output),
})),
z
.object({ data: StandardReadPayloadSchema })
.passthrough()
.transform((value) => ({
content:
flattenReadContent(value.data.content) ??
flattenReadContent(value.data.text) ??
flattenReadContent(value.data.output),
})),
z
.object({ structuredContent: StandardReadPayloadSchema })
.passthrough()
.transform((value) => ({
content:
flattenReadContent(value.structuredContent.content) ??
flattenReadContent(value.structuredContent.text) ??
flattenReadContent(value.structuredContent.output),
})),
z
.object({ structured_content: StandardReadPayloadSchema })
.passthrough()
.transform((value) => ({
content:
flattenReadContent(value.structured_content.content) ??
flattenReadContent(value.structured_content.text) ??
flattenReadContent(value.structured_content.output),
})),
]);
const StandardWriteContentSchema = z
.object({
content: z.string().optional(),
new_content: z.string().optional(),
newContent: z.string().optional(),
})
.passthrough();
export const StandardWriteInputSchema = z
.intersection(StandardPathSchema, StandardWriteContentSchema)
.transform((value) => ({
filePath: value.filePath,
content:
nonEmptyString(value.content) ??
nonEmptyString(value.new_content) ??
nonEmptyString(value.newContent),
}));
export const StandardWriteOutputSchema = z.union([
z
.intersection(StandardPathSchema, StandardWriteContentSchema)
.transform((value) => ({
filePath: value.filePath,
content:
nonEmptyString(value.content) ??
nonEmptyString(value.new_content) ??
nonEmptyString(value.newContent),
})),
StandardWriteContentSchema.transform((value) => ({
filePath: undefined,
content:
nonEmptyString(value.content) ??
nonEmptyString(value.new_content) ??
nonEmptyString(value.newContent),
})),
]);
const StandardEditTextSchema = z
.object({
old_string: z.string().optional(),
old_str: z.string().optional(),
oldContent: z.string().optional(),
old_content: z.string().optional(),
new_string: z.string().optional(),
new_str: z.string().optional(),
newContent: z.string().optional(),
new_content: z.string().optional(),
content: z.string().optional(),
patch: z.string().optional(),
diff: z.string().optional(),
unified_diff: z.string().optional(),
unifiedDiff: z.string().optional(),
})
.passthrough();
export const StandardEditInputSchema = z
.intersection(StandardPathSchema, StandardEditTextSchema)
.transform((value) => ({
filePath: value.filePath,
oldString:
nonEmptyString(value.old_string) ??
nonEmptyString(value.old_str) ??
nonEmptyString(value.oldContent) ??
nonEmptyString(value.old_content),
newString:
nonEmptyString(value.new_string) ??
nonEmptyString(value.new_str) ??
nonEmptyString(value.newContent) ??
nonEmptyString(value.new_content) ??
nonEmptyString(value.content),
unifiedDiff: truncateDiffText(
nonEmptyString(value.patch) ??
nonEmptyString(value.diff) ??
nonEmptyString(value.unified_diff) ??
nonEmptyString(value.unifiedDiff)
),
}));
const StandardEditOutputFileSchema = z.union([
z
.object({
path: z.string(),
patch: z.string().optional(),
diff: z.string().optional(),
unified_diff: z.string().optional(),
unifiedDiff: z.string().optional(),
})
.passthrough()
.transform((value) => ({
filePath: value.path,
unifiedDiff: truncateDiffText(
nonEmptyString(value.patch) ??
nonEmptyString(value.diff) ??
nonEmptyString(value.unified_diff) ??
nonEmptyString(value.unifiedDiff)
),
})),
z
.object({
file_path: z.string(),
patch: z.string().optional(),
diff: z.string().optional(),
unified_diff: z.string().optional(),
unifiedDiff: z.string().optional(),
})
.passthrough()
.transform((value) => ({
filePath: value.file_path,
unifiedDiff: truncateDiffText(
nonEmptyString(value.patch) ??
nonEmptyString(value.diff) ??
nonEmptyString(value.unified_diff) ??
nonEmptyString(value.unifiedDiff)
),
})),
z
.object({
filePath: z.string(),
patch: z.string().optional(),
diff: z.string().optional(),
unified_diff: z.string().optional(),
unifiedDiff: z.string().optional(),
})
.passthrough()
.transform((value) => ({
filePath: value.filePath,
unifiedDiff: truncateDiffText(
nonEmptyString(value.patch) ??
nonEmptyString(value.diff) ??
nonEmptyString(value.unified_diff) ??
nonEmptyString(value.unifiedDiff)
),
})),
]);
export const StandardEditOutputSchema = z.union([
z
.intersection(StandardPathSchema, StandardEditTextSchema)
.transform((value) => ({
filePath: value.filePath,
newString:
nonEmptyString(value.newContent) ??
nonEmptyString(value.new_content) ??
nonEmptyString(value.content),
unifiedDiff: truncateDiffText(
nonEmptyString(value.patch) ??
nonEmptyString(value.diff) ??
nonEmptyString(value.unified_diff) ??
nonEmptyString(value.unifiedDiff)
),
})),
z
.object({ files: z.array(StandardEditOutputFileSchema).min(1) })
.passthrough()
.transform((value) => ({
filePath: value.files[0]?.filePath,
unifiedDiff: value.files[0]?.unifiedDiff,
newString: undefined,
})),
StandardEditTextSchema.transform((value) => ({
filePath: undefined,
newString:
nonEmptyString(value.newContent) ??
nonEmptyString(value.new_content) ??
nonEmptyString(value.content),
unifiedDiff: truncateDiffText(
nonEmptyString(value.patch) ??
nonEmptyString(value.diff) ??
nonEmptyString(value.unified_diff) ??
nonEmptyString(value.unifiedDiff)
),
})),
]);
export const StandardSearchInputSchema = z.union([
z.object({ query: z.string() }).passthrough().transform((value) => ({ query: value.query })),
z.object({ q: z.string() }).passthrough().transform((value) => ({ query: value.q })),
]);
export function toStandardShellDetail(
input: StandardShellInput | null,
output: StandardShellOutput | null
): ToolCallDetail | undefined {
const command = input?.command ?? output?.command;
if (!command) {
return undefined;
}
return {
type: "shell",
command,
...(input?.cwd ? { cwd: input.cwd } : {}),
...(output?.output ? { output: output.output } : {}),
...(output?.exitCode !== undefined ? { exitCode: output.exitCode } : {}),
};
}
export function toStandardReadDetail(
input: StandardReadPathInput | null,
output: StandardReadOutput | null,
normalizePath?: (filePath: string) => string | undefined
): ToolCallDetail | undefined {
const path = input?.filePath;
if (!path) {
return undefined;
}
const filePath = normalizePath ? normalizePath(path) : path;
if (!filePath) {
return undefined;
}
return {
type: "read",
filePath,
...(output?.content ? { content: output.content } : {}),
...(input.offset !== undefined ? { offset: input.offset } : {}),
...(input.limit !== undefined ? { limit: input.limit } : {}),
};
}
export function toStandardWriteDetail(
input: StandardWriteInput | null,
output: StandardWriteOutput | null,
normalizePath?: (filePath: string) => string | undefined
): ToolCallDetail | undefined {
const rawPath = input?.filePath ?? output?.filePath;
if (!rawPath) {
return undefined;
}
const filePath = normalizePath ? normalizePath(rawPath) : rawPath;
if (!filePath) {
return undefined;
}
return {
type: "write",
filePath,
...(input?.content ? { content: input.content } : output?.content ? { content: output.content } : {}),
};
}
export function toStandardEditDetail(
input: StandardEditInput | null,
output: StandardEditOutput | null,
normalizePath?: (filePath: string) => string | undefined
): ToolCallDetail | undefined {
const rawPath = input?.filePath ?? output?.filePath;
if (!rawPath) {
return undefined;
}
const filePath = normalizePath ? normalizePath(rawPath) : rawPath;
if (!filePath) {
return undefined;
}
return {
type: "edit",
filePath,
...(input?.oldString ? { oldString: input.oldString } : {}),
...(input?.newString ? { newString: input.newString } : output?.newString ? { newString: output.newString } : {}),
...(input?.unifiedDiff
? { unifiedDiff: input.unifiedDiff }
: output?.unifiedDiff
? { unifiedDiff: output.unifiedDiff }
: {}),
};
}
export function toStandardSearchDetail(input: StandardSearchInput | null): ToolCallDetail | undefined {
if (!input?.query) {
return undefined;
}
return {
type: "search",
query: input.query,
};
}

View File

@@ -1,54 +1,9 @@
import { z } from "zod";
type ReadChunkLike = {
text?: string;
content?: string;
output?: string;
};
type ToolAliasKind = "shell" | "read" | "write" | "edit" | "search";
export type KnownToolAliases = Record<ToolAliasKind, readonly string[]>;
export const CLAUDE_KNOWN_TOOL_ALIASES: KnownToolAliases = {
shell: ["Bash", "bash", "shell", "exec_command"],
read: ["Read", "read", "read_file", "view_file"],
write: ["Write", "write", "write_file", "create_file"],
edit: [
"Edit",
"MultiEdit",
"multi_edit",
"edit",
"apply_patch",
"apply_diff",
"str_replace_editor",
],
search: ["WebSearch", "web_search", "search"],
};
export const OPENCODE_KNOWN_TOOL_ALIASES: KnownToolAliases = {
shell: ["shell", "bash", "exec_command"],
read: ["read", "read_file"],
write: ["write", "write_file", "create_file"],
edit: ["edit", "apply_patch", "apply_diff"],
search: ["search", "web_search"],
};
export const CODEX_MCP_KNOWN_TOOL_ALIASES: KnownToolAliases = {
shell: ["shell", "bash", "exec", "exec_command", "command"],
read: ["read", "read_file"],
write: ["write", "write_file", "create_file"],
edit: ["edit", "apply_patch", "apply_diff"],
search: ["search", "web_search"],
};
export const CODEX_ROLLOUT_KNOWN_TOOL_ALIASES: KnownToolAliases = {
shell: ["Bash", "shell", "bash", "exec_command"],
read: ["read", "read_file"],
write: ["write", "write_file", "create_file"],
edit: ["edit", "apply_patch", "apply_diff"],
search: ["search", "web_search"],
};
export function nonEmptyString(value: unknown): string | undefined {
return typeof value === "string" && value.length > 0 ? value : undefined;
}
@@ -136,14 +91,3 @@ export function coerceToolCallId(params: {
return `${params.providerPrefix}-${hashText(`${params.toolName}:${serialized}`)}`;
}
export function unionToolDetailSchemas(schemas: z.ZodTypeAny[]): z.ZodTypeAny {
if (schemas.length === 0) {
throw new Error("Expected at least one schema when building tool detail union");
}
let union = schemas[0];
for (let i = 1; i < schemas.length; i += 1) {
union = union.or(schemas[i]);
}
return union;
}