refactor: finish strict tool-call typing and mapper dedup

This commit is contained in:
Mohamed Boudra
2026-02-09 09:07:14 +07:00
parent 6d4992b2f5
commit 896f88fa34
10 changed files with 823 additions and 1581 deletions
@@ -2,11 +2,29 @@ import { z } from "zod";
import type { ToolCallDetail, ToolCallTimelineItem } from "../../agent-sdk-types.js";
import {
StandardEditInputSchema,
StandardEditOutputSchema,
StandardReadChunkSchema,
StandardReadPathInputSchema,
StandardSearchInputSchema,
StandardShellInputSchema,
StandardShellOutputSchema,
StandardWriteInputSchema,
StandardWriteOutputSchema,
toStandardEditDetail,
toStandardSearchDetail,
toStandardShellDetail,
toStandardWriteDetail,
} from "../standard-tool-call-schemas.js";
import {
CODEX_MCP_KNOWN_TOOL_ALIASES,
CODEX_ROLLOUT_KNOWN_TOOL_ALIASES,
coerceToolCallId,
commandFromValue,
flattenReadContent as flattenToolReadContent,
nonEmptyString,
truncateDiffText,
unionToolDetailSchemas,
} from "../tool-call-mapper-utils.js";
type CodexMapperOptions = { cwd?: string | null };
@@ -27,165 +45,21 @@ const CodexRolloutToolCallParamsSchema = z
const CommandValueSchema = z.union([z.string(), z.array(z.string())]);
const CodexShellInputSchema = z
.union([
z
.object({
command: CommandValueSchema,
cwd: z.string().optional(),
directory: z.string().optional(),
})
.passthrough(),
z
.object({
cmd: CommandValueSchema,
cwd: z.string().optional(),
directory: z.string().optional(),
})
.passthrough(),
])
.transform((value) => {
const commandValue = "command" in value ? value.command : value.cmd;
return {
command: commandFromValue(commandValue),
cwd: nonEmptyString(value.cwd) ?? nonEmptyString(value.directory),
};
});
const CodexShellInputSchema = StandardShellInputSchema;
const CodexShellOutputSchema = StandardShellOutputSchema;
const CodexReadArgumentsSchema = StandardReadPathInputSchema;
const CodexWriteArgumentsSchema = StandardWriteInputSchema;
const CodexWriteResultSchema = StandardWriteOutputSchema;
const CodexEditArgumentsSchema = StandardEditInputSchema;
const CodexEditResultSchema = StandardEditOutputSchema;
const CodexSearchArgumentsSchema = StandardSearchInputSchema;
const CodexShellOutputObjectSchema = z
.object({
command: z.string().optional(),
output: z.string().optional(),
text: z.string().optional(),
content: z.string().optional(),
aggregatedOutput: z.string().optional(),
exitCode: z.number().finite().nullable().optional(),
exit_code: z.number().finite().nullable().optional(),
metadata: z
.object({
exitCode: z.number().finite().nullable().optional(),
exit_code: z.number().finite().nullable().optional(),
})
.passthrough()
.optional(),
structuredContent: z
.object({
output: z.string().optional(),
text: z.string().optional(),
content: z.string().optional(),
})
.passthrough()
.optional(),
structured_content: z
.object({
output: z.string().optional(),
text: z.string().optional(),
content: z.string().optional(),
})
.passthrough()
.optional(),
result: z
.object({
command: z.string().optional(),
output: z.string().optional(),
text: z.string().optional(),
content: z.string().optional(),
})
.passthrough()
.optional(),
})
.passthrough();
const CodexShellOutputSchema = z.union([
z.string().transform((value) => ({
command: undefined,
output: nonEmptyString(value),
exitCode: undefined,
})),
CodexShellOutputObjectSchema.transform((value) => ({
command: nonEmptyString(value.command) ?? nonEmptyString(value.result?.command),
output:
nonEmptyString(value.output) ??
nonEmptyString(value.text) ??
nonEmptyString(value.content) ??
nonEmptyString(value.aggregatedOutput) ??
nonEmptyString(value.structuredContent?.output) ??
nonEmptyString(value.structuredContent?.text) ??
nonEmptyString(value.structuredContent?.content) ??
nonEmptyString(value.structured_content?.output) ??
nonEmptyString(value.structured_content?.text) ??
nonEmptyString(value.structured_content?.content) ??
nonEmptyString(value.result?.output) ??
nonEmptyString(value.result?.text) ??
nonEmptyString(value.result?.content),
exitCode:
value.exitCode ??
value.exit_code ??
value.metadata?.exitCode ??
value.metadata?.exit_code ??
undefined,
})),
const CodexReadContentSchema = z.union([
z.string(),
StandardReadChunkSchema,
z.array(StandardReadChunkSchema),
]);
const CodexPathSchema = z.union([
z.object({ path: z.string() }).passthrough().transform((value) => value.path),
z.object({ file_path: z.string() }).passthrough().transform((value) => value.file_path),
z.object({ filePath: z.string() }).passthrough().transform((value) => value.filePath),
]);
const CodexReadArgumentsSchema = z.union([
z
.object({
path: z.string(),
offset: z.number().finite().optional(),
limit: z.number().finite().optional(),
})
.passthrough()
.transform((value) => ({ filePath: value.path, offset: value.offset, limit: value.limit })),
z
.object({
file_path: z.string(),
offset: z.number().finite().optional(),
limit: z.number().finite().optional(),
})
.passthrough()
.transform((value) => ({ filePath: value.file_path, offset: value.offset, limit: value.limit })),
z
.object({
filePath: z.string(),
offset: z.number().finite().optional(),
limit: z.number().finite().optional(),
})
.passthrough()
.transform((value) => ({ filePath: value.filePath, offset: value.offset, limit: value.limit })),
]);
const CodexReadChunkSchema = z.union([
z
.object({
text: z.string(),
content: z.string().optional(),
output: z.string().optional(),
})
.passthrough(),
z
.object({
text: z.string().optional(),
content: z.string(),
output: z.string().optional(),
})
.passthrough(),
z
.object({
text: z.string().optional(),
content: z.string().optional(),
output: z.string(),
})
.passthrough(),
]);
const CodexReadContentSchema = z.union([z.string(), CodexReadChunkSchema, z.array(CodexReadChunkSchema)]);
const CodexReadPayloadSchema = z.union([
z
.object({
@@ -260,8 +134,8 @@ const CodexReadResultWithPathSchema = z.union([
const CodexReadResultSchema = z.union([
z.string().transform((value) => ({ filePath: undefined, content: nonEmptyString(value) })),
CodexReadChunkSchema.transform((value) => ({ filePath: undefined, content: flattenReadContent(value) })),
z.array(CodexReadChunkSchema).transform((value) => ({ filePath: undefined, content: flattenReadContent(value) })),
StandardReadChunkSchema.transform((value) => ({ filePath: undefined, content: flattenReadContent(value) })),
z.array(StandardReadChunkSchema).transform((value) => ({ filePath: undefined, content: flattenReadContent(value) })),
CodexReadPayloadSchema.transform((value) => ({
filePath: undefined,
content:
@@ -302,184 +176,6 @@ const CodexReadResultSchema = z.union([
CodexReadResultWithPathSchema,
]);
const CodexWriteContentSchema = z
.object({
content: z.string().optional(),
new_content: z.string().optional(),
newContent: z.string().optional(),
})
.passthrough();
const CodexWriteArgumentsSchema = z
.intersection(CodexPathSchema.transform((filePath) => ({ filePath })), CodexWriteContentSchema)
.transform((value) => ({
filePath: value.filePath,
content:
nonEmptyString(value.content) ??
nonEmptyString(value.new_content) ??
nonEmptyString(value.newContent),
}));
const CodexWriteResultSchema = z.union([
z
.intersection(CodexPathSchema.transform((filePath) => ({ filePath })), CodexWriteContentSchema)
.transform((value) => ({
filePath: value.filePath,
content:
nonEmptyString(value.content) ??
nonEmptyString(value.new_content) ??
nonEmptyString(value.newContent),
})),
CodexWriteContentSchema.transform((value) => ({
filePath: undefined,
content:
nonEmptyString(value.content) ??
nonEmptyString(value.new_content) ??
nonEmptyString(value.newContent),
})),
]);
const CodexEditTextSchema = z
.object({
old_string: z.string().optional(),
old_str: z.string().optional(),
oldContent: z.string().optional(),
old_content: z.string().optional(),
new_string: z.string().optional(),
new_str: z.string().optional(),
newContent: z.string().optional(),
new_content: z.string().optional(),
content: z.string().optional(),
patch: z.string().optional(),
diff: z.string().optional(),
unified_diff: z.string().optional(),
unifiedDiff: z.string().optional(),
})
.passthrough();
const CodexEditArgumentsSchema = z
.intersection(CodexPathSchema.transform((filePath) => ({ filePath })), CodexEditTextSchema)
.transform((value) => ({
filePath: value.filePath,
oldString:
nonEmptyString(value.old_string) ??
nonEmptyString(value.old_str) ??
nonEmptyString(value.oldContent) ??
nonEmptyString(value.old_content),
newString:
nonEmptyString(value.new_string) ??
nonEmptyString(value.new_str) ??
nonEmptyString(value.newContent) ??
nonEmptyString(value.new_content) ??
nonEmptyString(value.content),
unifiedDiff: truncateDiffText(
nonEmptyString(value.patch) ??
nonEmptyString(value.diff) ??
nonEmptyString(value.unified_diff) ??
nonEmptyString(value.unifiedDiff)
),
}));
const CodexEditResultFileSchema = z.union([
z
.object({
path: z.string(),
patch: z.string().optional(),
diff: z.string().optional(),
unified_diff: z.string().optional(),
unifiedDiff: z.string().optional(),
})
.passthrough()
.transform((value) => ({
filePath: value.path,
unifiedDiff: truncateDiffText(
nonEmptyString(value.patch) ??
nonEmptyString(value.diff) ??
nonEmptyString(value.unified_diff) ??
nonEmptyString(value.unifiedDiff)
),
})),
z
.object({
file_path: z.string(),
patch: z.string().optional(),
diff: z.string().optional(),
unified_diff: z.string().optional(),
unifiedDiff: z.string().optional(),
})
.passthrough()
.transform((value) => ({
filePath: value.file_path,
unifiedDiff: truncateDiffText(
nonEmptyString(value.patch) ??
nonEmptyString(value.diff) ??
nonEmptyString(value.unified_diff) ??
nonEmptyString(value.unifiedDiff)
),
})),
z
.object({
filePath: z.string(),
patch: z.string().optional(),
diff: z.string().optional(),
unified_diff: z.string().optional(),
unifiedDiff: z.string().optional(),
})
.passthrough()
.transform((value) => ({
filePath: value.filePath,
unifiedDiff: truncateDiffText(
nonEmptyString(value.patch) ??
nonEmptyString(value.diff) ??
nonEmptyString(value.unified_diff) ??
nonEmptyString(value.unifiedDiff)
),
})),
]);
const CodexEditResultSchema = z.union([
z
.intersection(CodexPathSchema.transform((filePath) => ({ filePath })), CodexEditTextSchema)
.transform((value) => ({
filePath: value.filePath,
newString:
nonEmptyString(value.newContent) ??
nonEmptyString(value.new_content) ??
nonEmptyString(value.content),
unifiedDiff: truncateDiffText(
nonEmptyString(value.patch) ??
nonEmptyString(value.diff) ??
nonEmptyString(value.unified_diff) ??
nonEmptyString(value.unifiedDiff)
),
})),
z
.object({ files: z.array(CodexEditResultFileSchema).min(1) })
.passthrough()
.transform((value) => ({
filePath: value.files[0]?.filePath,
unifiedDiff: value.files[0]?.unifiedDiff,
newString: undefined,
})),
CodexEditTextSchema.transform((value) => ({
filePath: undefined,
newString:
nonEmptyString(value.newContent) ??
nonEmptyString(value.new_content) ??
nonEmptyString(value.content),
unifiedDiff: truncateDiffText(
nonEmptyString(value.patch) ??
nonEmptyString(value.diff) ??
nonEmptyString(value.unified_diff) ??
nonEmptyString(value.unifiedDiff)
),
})),
]);
const CodexSearchArgumentsSchema = z.union([
z.object({ query: z.string() }).passthrough().transform((value) => ({ query: value.query })),
z.object({ q: z.string() }).passthrough().transform((value) => ({ query: value.q })),
]);
const CodexCommandExecutionItemSchema = z
.object({
@@ -607,18 +303,7 @@ function toShellDetail(
input: z.infer<typeof CodexShellInputSchema> | null,
output: z.infer<typeof CodexShellOutputSchema> | null
): ToolCallDetail | undefined {
const command = input?.command ?? output?.command;
if (!command) {
return undefined;
}
return {
type: "shell",
command,
...(input?.cwd ? { cwd: input.cwd } : {}),
...(output?.output ? { output: output.output } : {}),
...(output?.exitCode !== undefined ? { exitCode: output.exitCode } : {}),
};
return toStandardShellDetail(input, output);
}
function toReadDetail(
@@ -645,16 +330,7 @@ function toWriteDetail(
output: z.infer<typeof CodexWriteResultSchema> | null,
cwd: string | null | undefined
): ToolCallDetail | undefined {
const filePath = normalizeCodexFilePath(input?.filePath ?? output?.filePath, cwd);
if (!filePath) {
return undefined;
}
return {
type: "write",
filePath,
...(input?.content ? { content: input.content } : output?.content ? { content: output.content } : {}),
};
return toStandardWriteDetail(input, output, (filePath) => normalizeCodexFilePath(filePath, cwd));
}
function toEditDetail(
@@ -662,32 +338,11 @@ function toEditDetail(
output: z.infer<typeof CodexEditResultSchema> | null,
cwd: string | null | undefined
): ToolCallDetail | undefined {
const filePath = normalizeCodexFilePath(input?.filePath ?? output?.filePath, cwd);
if (!filePath) {
return undefined;
}
return {
type: "edit",
filePath,
...(input?.oldString ? { oldString: input.oldString } : {}),
...(input?.newString ? { newString: input.newString } : output?.newString ? { newString: output.newString } : {}),
...(input?.unifiedDiff
? { unifiedDiff: input.unifiedDiff }
: output?.unifiedDiff
? { unifiedDiff: output.unifiedDiff }
: {}),
};
return toStandardEditDetail(input, output, (filePath) => normalizeCodexFilePath(filePath, cwd));
}
function toSearchDetail(input: z.infer<typeof CodexSearchArgumentsSchema> | null): ToolCallDetail | undefined {
if (!input?.query) {
return undefined;
}
return {
type: "search",
query: input.query,
};
return toStandardSearchDetail(input);
}
function codexMcpToolBranch<ToolName extends string, InputSchema extends z.ZodTypeAny, OutputSchema extends z.ZodTypeAny>(
@@ -710,32 +365,24 @@ function codexMcpToolBranch<ToolName extends string, InputSchema extends z.ZodTy
.transform(({ arguments: input, result: output, cwd }) => mapper(input, output, cwd));
}
const CodexKnownMcpToolDetailSchema = z.union([
codexMcpToolBranch("shell", CodexShellInputSchema, CodexShellOutputSchema, (input, output) =>
toShellDetail(input, output)
const CodexKnownMcpToolDetailSchema: z.ZodType<ToolCallDetail | undefined> = unionToolDetailSchemas([
...CODEX_MCP_KNOWN_TOOL_ALIASES.shell.map((tool) =>
codexMcpToolBranch(tool, CodexShellInputSchema, CodexShellOutputSchema, (input, output) =>
toShellDetail(input, output)
)
),
codexMcpToolBranch("bash", CodexShellInputSchema, CodexShellOutputSchema, (input, output) =>
toShellDetail(input, output)
...CODEX_MCP_KNOWN_TOOL_ALIASES.read.map((tool) =>
codexMcpToolBranch(tool, CodexReadArgumentsSchema, CodexReadResultSchema, toReadDetail)
),
codexMcpToolBranch("exec", CodexShellInputSchema, CodexShellOutputSchema, (input, output) =>
toShellDetail(input, output)
...CODEX_MCP_KNOWN_TOOL_ALIASES.write.map((tool) =>
codexMcpToolBranch(tool, CodexWriteArgumentsSchema, CodexWriteResultSchema, toWriteDetail)
),
codexMcpToolBranch("exec_command", CodexShellInputSchema, CodexShellOutputSchema, (input, output) =>
toShellDetail(input, output)
...CODEX_MCP_KNOWN_TOOL_ALIASES.edit.map((tool) =>
codexMcpToolBranch(tool, CodexEditArgumentsSchema, CodexEditResultSchema, toEditDetail)
),
codexMcpToolBranch("command", CodexShellInputSchema, CodexShellOutputSchema, (input, output) =>
toShellDetail(input, output)
...CODEX_MCP_KNOWN_TOOL_ALIASES.search.map((tool) =>
codexMcpToolBranch(tool, CodexSearchArgumentsSchema, z.unknown(), (input) => toSearchDetail(input))
),
codexMcpToolBranch("read", CodexReadArgumentsSchema, CodexReadResultSchema, toReadDetail),
codexMcpToolBranch("read_file", CodexReadArgumentsSchema, CodexReadResultSchema, toReadDetail),
codexMcpToolBranch("write", CodexWriteArgumentsSchema, CodexWriteResultSchema, toWriteDetail),
codexMcpToolBranch("write_file", CodexWriteArgumentsSchema, CodexWriteResultSchema, toWriteDetail),
codexMcpToolBranch("create_file", CodexWriteArgumentsSchema, CodexWriteResultSchema, toWriteDetail),
codexMcpToolBranch("edit", CodexEditArgumentsSchema, CodexEditResultSchema, toEditDetail),
codexMcpToolBranch("apply_patch", CodexEditArgumentsSchema, CodexEditResultSchema, toEditDetail),
codexMcpToolBranch("apply_diff", CodexEditArgumentsSchema, CodexEditResultSchema, toEditDetail),
codexMcpToolBranch("search", CodexSearchArgumentsSchema, z.unknown(), (input) => toSearchDetail(input)),
codexMcpToolBranch("web_search", CodexSearchArgumentsSchema, z.unknown(), (input) => toSearchDetail(input)),
]);
function codexRolloutToolBranch<Name extends string, InputSchema extends z.ZodTypeAny, OutputSchema extends z.ZodTypeAny>(
@@ -756,45 +403,30 @@ function codexRolloutToolBranch<Name extends string, InputSchema extends z.ZodTy
.transform(({ input, output }) => mapper(input, output));
}
const CodexKnownRolloutDetailSchema = z.union([
codexRolloutToolBranch("Bash", CodexShellInputSchema, CodexShellOutputSchema, (input, output) =>
toShellDetail(input, output)
const CodexKnownRolloutDetailSchema: z.ZodType<ToolCallDetail | undefined> = unionToolDetailSchemas([
...CODEX_ROLLOUT_KNOWN_TOOL_ALIASES.shell.map((name) =>
codexRolloutToolBranch(name, CodexShellInputSchema, CodexShellOutputSchema, (input, output) =>
toShellDetail(input, output)
)
),
codexRolloutToolBranch("shell", CodexShellInputSchema, CodexShellOutputSchema, (input, output) =>
toShellDetail(input, output)
...CODEX_ROLLOUT_KNOWN_TOOL_ALIASES.read.map((name) =>
codexRolloutToolBranch(name, CodexReadArgumentsSchema, CodexReadResultSchema, (input, output) =>
toReadDetail(input, output, null)
)
),
codexRolloutToolBranch("bash", CodexShellInputSchema, CodexShellOutputSchema, (input, output) =>
toShellDetail(input, output)
...CODEX_ROLLOUT_KNOWN_TOOL_ALIASES.write.map((name) =>
codexRolloutToolBranch(name, CodexWriteArgumentsSchema, CodexWriteResultSchema, (input, output) =>
toWriteDetail(input, output, null)
)
),
codexRolloutToolBranch("exec_command", CodexShellInputSchema, CodexShellOutputSchema, (input, output) =>
toShellDetail(input, output)
...CODEX_ROLLOUT_KNOWN_TOOL_ALIASES.edit.map((name) =>
codexRolloutToolBranch(name, CodexEditArgumentsSchema, CodexEditResultSchema, (input, output) =>
toEditDetail(input, output, null)
)
),
codexRolloutToolBranch("read", CodexReadArgumentsSchema, CodexReadResultSchema, (input, output) =>
toReadDetail(input, output, null)
...CODEX_ROLLOUT_KNOWN_TOOL_ALIASES.search.map((name) =>
codexRolloutToolBranch(name, CodexSearchArgumentsSchema, z.unknown(), (input) => toSearchDetail(input))
),
codexRolloutToolBranch("read_file", CodexReadArgumentsSchema, CodexReadResultSchema, (input, output) =>
toReadDetail(input, output, null)
),
codexRolloutToolBranch("write", CodexWriteArgumentsSchema, CodexWriteResultSchema, (input, output) =>
toWriteDetail(input, output, null)
),
codexRolloutToolBranch("write_file", CodexWriteArgumentsSchema, CodexWriteResultSchema, (input, output) =>
toWriteDetail(input, output, null)
),
codexRolloutToolBranch("create_file", CodexWriteArgumentsSchema, CodexWriteResultSchema, (input, output) =>
toWriteDetail(input, output, null)
),
codexRolloutToolBranch("edit", CodexEditArgumentsSchema, CodexEditResultSchema, (input, output) =>
toEditDetail(input, output, null)
),
codexRolloutToolBranch("apply_patch", CodexEditArgumentsSchema, CodexEditResultSchema, (input, output) =>
toEditDetail(input, output, null)
),
codexRolloutToolBranch("apply_diff", CodexEditArgumentsSchema, CodexEditResultSchema, (input, output) =>
toEditDetail(input, output, null)
),
codexRolloutToolBranch("search", CodexSearchArgumentsSchema, z.unknown(), (input) => toSearchDetail(input)),
codexRolloutToolBranch("web_search", CodexSearchArgumentsSchema, z.unknown(), (input) => toSearchDetail(input)),
]);
function deriveMcpToolDetail(
@@ -864,23 +496,9 @@ function buildToolCall(params: {
};
}
const CODEX_BUILTIN_TOOL_NAMES = new Set([
"shell",
"bash",
"exec",
"exec_command",
"command",
"read",
"read_file",
"write",
"write_file",
"create_file",
"edit",
"apply_patch",
"apply_diff",
"web_search",
"search",
]);
const CODEX_BUILTIN_TOOL_NAMES = new Set(
Object.values(CODEX_MCP_KNOWN_TOOL_ALIASES).flat()
);
function buildMcpToolName(server: string | undefined, tool: string): string {
const trimmedTool = tool.trim();