refactor(server): collapse tool-call mapper pass2 zod dance
Replace the duplicated 3-stage Zod pipeline (pass1 -> pass2 envelope union -> pass2 discriminated union) in each provider mapper with a plain resolveToolKind(name) function over readonly name sets. Opencode's toolKind was unused; Claude/Codex used it only as a switch discriminator.
This commit is contained in:
@@ -13,6 +13,7 @@ interface MapperParams {
|
||||
}
|
||||
|
||||
const ClaudeToolCallStatusSchema = z.enum(["running", "completed", "failed", "canceled"]);
|
||||
type ClaudeToolCallStatus = z.infer<typeof ClaudeToolCallStatusSchema>;
|
||||
|
||||
const ClaudeRawToolCallSchema = z
|
||||
.object({
|
||||
@@ -26,185 +27,58 @@ const ClaudeRawToolCallSchema = z
|
||||
})
|
||||
.passthrough();
|
||||
|
||||
const ClaudeToolCallPass1Schema = ClaudeRawToolCallSchema.transform((raw) => ({
|
||||
callId: typeof raw.callId === "string" && raw.callId.trim().length > 0 ? raw.callId : null,
|
||||
name: raw.name.trim(),
|
||||
input: raw.input ?? null,
|
||||
output: raw.output ?? null,
|
||||
metadata: raw.metadata,
|
||||
error: raw.error ?? null,
|
||||
status: raw.status,
|
||||
}));
|
||||
type ClaudeToolKind =
|
||||
| "shell"
|
||||
| "read"
|
||||
| "write"
|
||||
| "edit"
|
||||
| "search"
|
||||
| "fetch"
|
||||
| "speak"
|
||||
| "unknown";
|
||||
|
||||
const ClaudeShellToolNameSchema = z.union([
|
||||
z.literal("Bash"),
|
||||
z.literal("bash"),
|
||||
z.literal("shell"),
|
||||
z.literal("exec_command"),
|
||||
]);
|
||||
const ClaudeReadToolNameSchema = z.union([
|
||||
z.literal("Read"),
|
||||
z.literal("read"),
|
||||
z.literal("read_file"),
|
||||
z.literal("view_file"),
|
||||
]);
|
||||
const ClaudeWriteToolNameSchema = z.union([
|
||||
z.literal("Write"),
|
||||
z.literal("write"),
|
||||
z.literal("write_file"),
|
||||
z.literal("create_file"),
|
||||
]);
|
||||
const ClaudeEditToolNameSchema = z.union([
|
||||
z.literal("Edit"),
|
||||
z.literal("MultiEdit"),
|
||||
z.literal("multi_edit"),
|
||||
z.literal("edit"),
|
||||
z.literal("apply_patch"),
|
||||
z.literal("apply_diff"),
|
||||
z.literal("str_replace_editor"),
|
||||
]);
|
||||
const ClaudeSearchToolNameSchema = z.union([
|
||||
z.literal("WebSearch"),
|
||||
z.literal("web_search"),
|
||||
z.literal("search"),
|
||||
z.literal("Grep"),
|
||||
z.literal("grep"),
|
||||
z.literal("Glob"),
|
||||
z.literal("glob"),
|
||||
]);
|
||||
const ClaudeFetchToolNameSchema = z.union([
|
||||
z.literal("WebFetch"),
|
||||
z.literal("web_fetch"),
|
||||
z.literal("WebFetchTool"),
|
||||
z.literal("web_fetch_tool"),
|
||||
z.literal("webfetch"),
|
||||
]);
|
||||
const ClaudeSpeakToolNameSchema = z
|
||||
.string()
|
||||
.min(1)
|
||||
.refine((name) => isSpeakToolName(name.trim()));
|
||||
|
||||
const ClaudeToolKindSchema = z.enum([
|
||||
"shell",
|
||||
"read",
|
||||
"write",
|
||||
const SHELL_NAMES: ReadonlySet<string> = new Set(["Bash", "bash", "shell", "exec_command"]);
|
||||
const READ_NAMES: ReadonlySet<string> = new Set(["Read", "read", "read_file", "view_file"]);
|
||||
const WRITE_NAMES: ReadonlySet<string> = new Set(["Write", "write", "write_file", "create_file"]);
|
||||
const EDIT_NAMES: ReadonlySet<string> = new Set([
|
||||
"Edit",
|
||||
"MultiEdit",
|
||||
"multi_edit",
|
||||
"edit",
|
||||
"apply_patch",
|
||||
"apply_diff",
|
||||
"str_replace_editor",
|
||||
]);
|
||||
const SEARCH_NAMES: ReadonlySet<string> = new Set([
|
||||
"WebSearch",
|
||||
"web_search",
|
||||
"search",
|
||||
"fetch",
|
||||
"speak",
|
||||
"unknown",
|
||||
"Grep",
|
||||
"grep",
|
||||
"Glob",
|
||||
"glob",
|
||||
]);
|
||||
const FETCH_NAMES: ReadonlySet<string> = new Set([
|
||||
"WebFetch",
|
||||
"web_fetch",
|
||||
"WebFetchTool",
|
||||
"web_fetch_tool",
|
||||
"webfetch",
|
||||
]);
|
||||
|
||||
const ClaudeToolCallPass2BaseSchema = z.object({
|
||||
callId: z.string().min(1),
|
||||
name: z.string().min(1),
|
||||
input: z.unknown().nullable(),
|
||||
output: z.unknown().nullable(),
|
||||
metadata: z.record(z.string(), z.unknown()).optional(),
|
||||
error: z.unknown().nullable(),
|
||||
status: ClaudeToolCallStatusSchema,
|
||||
toolKind: ClaudeToolKindSchema,
|
||||
});
|
||||
function resolveClaudeToolKind(name: string): ClaudeToolKind {
|
||||
if (SHELL_NAMES.has(name)) return "shell";
|
||||
if (READ_NAMES.has(name)) return "read";
|
||||
if (WRITE_NAMES.has(name)) return "write";
|
||||
if (EDIT_NAMES.has(name)) return "edit";
|
||||
if (SEARCH_NAMES.has(name)) return "search";
|
||||
if (FETCH_NAMES.has(name)) return "fetch";
|
||||
if (isSpeakToolName(name)) return "speak";
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
const ClaudeToolCallPass2InputSchema = ClaudeToolCallPass2BaseSchema.omit({
|
||||
toolKind: true,
|
||||
});
|
||||
|
||||
const ClaudeToolCallPass2EnvelopeSchema = z.union([
|
||||
ClaudeToolCallPass2InputSchema.extend({
|
||||
name: ClaudeShellToolNameSchema,
|
||||
}).transform((normalized) => ({
|
||||
...normalized,
|
||||
name: normalized.name.trim(),
|
||||
toolKind: "shell" as const,
|
||||
})),
|
||||
ClaudeToolCallPass2InputSchema.extend({
|
||||
name: ClaudeReadToolNameSchema,
|
||||
}).transform((normalized) => ({
|
||||
...normalized,
|
||||
name: normalized.name.trim(),
|
||||
toolKind: "read" as const,
|
||||
})),
|
||||
ClaudeToolCallPass2InputSchema.extend({
|
||||
name: ClaudeWriteToolNameSchema,
|
||||
}).transform((normalized) => ({
|
||||
...normalized,
|
||||
name: normalized.name.trim(),
|
||||
toolKind: "write" as const,
|
||||
})),
|
||||
ClaudeToolCallPass2InputSchema.extend({
|
||||
name: ClaudeEditToolNameSchema,
|
||||
}).transform((normalized) => ({
|
||||
...normalized,
|
||||
name: normalized.name.trim(),
|
||||
toolKind: "edit" as const,
|
||||
})),
|
||||
ClaudeToolCallPass2InputSchema.extend({
|
||||
name: ClaudeSearchToolNameSchema,
|
||||
}).transform((normalized) => ({
|
||||
...normalized,
|
||||
name: normalized.name.trim(),
|
||||
toolKind: "search" as const,
|
||||
})),
|
||||
ClaudeToolCallPass2InputSchema.extend({
|
||||
name: ClaudeFetchToolNameSchema,
|
||||
}).transform((normalized) => ({
|
||||
...normalized,
|
||||
name: normalized.name.trim(),
|
||||
toolKind: "fetch" as const,
|
||||
})),
|
||||
ClaudeToolCallPass2InputSchema.extend({
|
||||
name: ClaudeSpeakToolNameSchema,
|
||||
}).transform((normalized) => ({
|
||||
...normalized,
|
||||
name: "speak" as const,
|
||||
toolKind: "speak" as const,
|
||||
})),
|
||||
ClaudeToolCallPass2InputSchema.transform((normalized) => ({
|
||||
...normalized,
|
||||
name: normalized.name.trim(),
|
||||
toolKind: "unknown" as const,
|
||||
})),
|
||||
]);
|
||||
|
||||
const ClaudeToolCallPass2Schema = z.discriminatedUnion("toolKind", [
|
||||
ClaudeToolCallPass2BaseSchema.extend({
|
||||
toolKind: z.literal("shell"),
|
||||
name: ClaudeShellToolNameSchema,
|
||||
}),
|
||||
ClaudeToolCallPass2BaseSchema.extend({
|
||||
toolKind: z.literal("read"),
|
||||
name: ClaudeReadToolNameSchema,
|
||||
}),
|
||||
ClaudeToolCallPass2BaseSchema.extend({
|
||||
toolKind: z.literal("write"),
|
||||
name: ClaudeWriteToolNameSchema,
|
||||
}),
|
||||
ClaudeToolCallPass2BaseSchema.extend({
|
||||
toolKind: z.literal("edit"),
|
||||
name: ClaudeEditToolNameSchema,
|
||||
}),
|
||||
ClaudeToolCallPass2BaseSchema.extend({
|
||||
toolKind: z.literal("search"),
|
||||
name: ClaudeSearchToolNameSchema,
|
||||
}),
|
||||
ClaudeToolCallPass2BaseSchema.extend({
|
||||
toolKind: z.literal("fetch"),
|
||||
name: ClaudeFetchToolNameSchema,
|
||||
}),
|
||||
ClaudeToolCallPass2BaseSchema.extend({
|
||||
toolKind: z.literal("speak"),
|
||||
name: z.literal("speak"),
|
||||
}),
|
||||
ClaudeToolCallPass2BaseSchema.extend({
|
||||
toolKind: z.literal("unknown"),
|
||||
}),
|
||||
]);
|
||||
|
||||
type ClaudeToolCallPass2 = z.infer<typeof ClaudeToolCallPass2Schema>;
|
||||
|
||||
function resolveDetailName(normalized: ClaudeToolCallPass2): string {
|
||||
switch (normalized.toolKind) {
|
||||
function resolveDetailName(toolKind: ClaudeToolKind, name: string): string {
|
||||
switch (toolKind) {
|
||||
case "shell":
|
||||
return "shell";
|
||||
case "read":
|
||||
@@ -215,65 +89,56 @@ function resolveDetailName(normalized: ClaudeToolCallPass2): string {
|
||||
return "apply_patch";
|
||||
case "search":
|
||||
case "fetch":
|
||||
return normalized.name;
|
||||
return name;
|
||||
case "speak":
|
||||
return "speak";
|
||||
default:
|
||||
return normalized.name;
|
||||
return name;
|
||||
}
|
||||
}
|
||||
|
||||
function toToolCallTimelineItem(normalized: ClaudeToolCallPass2): ToolCallTimelineItem {
|
||||
const name = normalized.toolKind === "speak" ? ("speak" as const) : normalized.name;
|
||||
const detailName = resolveDetailName(normalized);
|
||||
const detail = deriveClaudeToolDetail(detailName, normalized.input, normalized.output);
|
||||
if (normalized.status === "failed") {
|
||||
return {
|
||||
type: "tool_call",
|
||||
callId: normalized.callId,
|
||||
name,
|
||||
detail,
|
||||
status: "failed",
|
||||
error: normalized.error ?? { message: "Tool call failed" },
|
||||
...(normalized.metadata ? { metadata: normalized.metadata } : {}),
|
||||
};
|
||||
}
|
||||
return {
|
||||
type: "tool_call",
|
||||
callId: normalized.callId,
|
||||
name,
|
||||
detail,
|
||||
status: normalized.status,
|
||||
error: null,
|
||||
...(normalized.metadata ? { metadata: normalized.metadata } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
function mapClaudeToolCall(
|
||||
params: MapperParams,
|
||||
status: z.infer<typeof ClaudeToolCallStatusSchema>,
|
||||
status: ClaudeToolCallStatus,
|
||||
error: unknown | null,
|
||||
): ToolCallTimelineItem | null {
|
||||
const pass1 = ClaudeToolCallPass1Schema.safeParse({
|
||||
...params,
|
||||
status,
|
||||
error,
|
||||
});
|
||||
if (!pass1.success) {
|
||||
const parsed = ClaudeRawToolCallSchema.safeParse({ ...params, status, error });
|
||||
if (!parsed.success) {
|
||||
return null;
|
||||
}
|
||||
const raw = parsed.data;
|
||||
const callId = typeof raw.callId === "string" && raw.callId.trim().length > 0 ? raw.callId : null;
|
||||
if (callId === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const pass2Envelope = ClaudeToolCallPass2EnvelopeSchema.safeParse(pass1.data);
|
||||
if (!pass2Envelope.success) {
|
||||
return null;
|
||||
}
|
||||
const trimmedName = raw.name.trim();
|
||||
const toolKind = resolveClaudeToolKind(trimmedName);
|
||||
const name = toolKind === "speak" ? "speak" : trimmedName;
|
||||
const input = raw.input ?? null;
|
||||
const output = raw.output ?? null;
|
||||
const detail = deriveClaudeToolDetail(resolveDetailName(toolKind, name), input, output);
|
||||
|
||||
const pass2 = ClaudeToolCallPass2Schema.safeParse(pass2Envelope.data);
|
||||
if (!pass2.success) {
|
||||
return null;
|
||||
if (raw.status === "failed") {
|
||||
return {
|
||||
type: "tool_call",
|
||||
callId,
|
||||
name,
|
||||
detail,
|
||||
status: "failed",
|
||||
error: raw.error ?? { message: "Tool call failed" },
|
||||
...(raw.metadata ? { metadata: raw.metadata } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
return toToolCallTimelineItem(pass2.data);
|
||||
return {
|
||||
type: "tool_call",
|
||||
callId,
|
||||
name,
|
||||
detail,
|
||||
status: raw.status,
|
||||
error: null,
|
||||
...(raw.metadata ? { metadata: raw.metadata } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
export function mapClaudeRunningToolCall(params: MapperParams): ToolCallTimelineItem | null {
|
||||
|
||||
@@ -16,6 +16,7 @@ interface CodexMapperOptions {
|
||||
const CodexCommandValueSchema = z.union([z.string(), z.array(z.string())]);
|
||||
|
||||
const CodexToolCallStatusSchema = z.enum(["running", "completed", "failed", "canceled"]);
|
||||
type CodexToolCallStatus = z.infer<typeof CodexToolCallStatusSchema>;
|
||||
|
||||
const CodexRolloutToolCallParamsSchema = z
|
||||
.object({
|
||||
@@ -38,117 +39,44 @@ interface CodexNormalizedToolCallEnvelope {
|
||||
cwd?: string | null;
|
||||
}
|
||||
|
||||
const CodexNormalizedToolCallPass1Schema = z
|
||||
.object({
|
||||
callId: z.string().min(1),
|
||||
name: z.string().min(1),
|
||||
input: z.unknown().nullable(),
|
||||
output: z.unknown().nullable(),
|
||||
status: CodexToolCallStatusSchema,
|
||||
error: z.unknown().nullable(),
|
||||
metadata: z.record(z.string(), z.unknown()).optional(),
|
||||
cwd: z.string().nullable().optional(),
|
||||
})
|
||||
.passthrough();
|
||||
type CodexToolKind = "shell" | "read" | "write" | "edit" | "search" | "speak" | "unknown";
|
||||
|
||||
const CodexShellToolNameSchema = z.union([
|
||||
z.literal("Bash"),
|
||||
z.literal("shell"),
|
||||
z.literal("bash"),
|
||||
z.literal("exec"),
|
||||
z.literal("exec_command"),
|
||||
z.literal("command"),
|
||||
]);
|
||||
const CodexReadToolNameSchema = z.union([z.literal("read"), z.literal("read_file")]);
|
||||
const CodexWriteToolNameSchema = z.union([
|
||||
z.literal("write"),
|
||||
z.literal("write_file"),
|
||||
z.literal("create_file"),
|
||||
]);
|
||||
const CodexEditToolNameSchema = z.union([
|
||||
z.literal("edit"),
|
||||
z.literal("apply_patch"),
|
||||
z.literal("apply_diff"),
|
||||
]);
|
||||
const CodexSearchToolNameSchema = z.union([z.literal("search"), z.literal("web_search")]);
|
||||
const CodexSpeakToolNameSchema = z
|
||||
.string()
|
||||
.min(1)
|
||||
.refine((name) => isSpeakToolName(name.trim()));
|
||||
|
||||
const CodexToolKindSchema = z.enum([
|
||||
const CODEX_SHELL_NAMES: ReadonlySet<string> = new Set([
|
||||
"Bash",
|
||||
"shell",
|
||||
"read",
|
||||
"write",
|
||||
"edit",
|
||||
"search",
|
||||
"speak",
|
||||
"unknown",
|
||||
"bash",
|
||||
"exec",
|
||||
"exec_command",
|
||||
"command",
|
||||
]);
|
||||
const CODEX_READ_NAMES: ReadonlySet<string> = new Set(["read", "read_file"]);
|
||||
const CODEX_WRITE_NAMES: ReadonlySet<string> = new Set(["write", "write_file", "create_file"]);
|
||||
const CODEX_EDIT_NAMES: ReadonlySet<string> = new Set(["edit", "apply_patch", "apply_diff"]);
|
||||
const CODEX_SEARCH_NAMES: ReadonlySet<string> = new Set(["search", "web_search"]);
|
||||
|
||||
const CodexToolCallPass2BaseSchema = CodexNormalizedToolCallPass1Schema.extend({
|
||||
toolKind: CodexToolKindSchema,
|
||||
});
|
||||
function resolveCodexToolKind(name: string): CodexToolKind {
|
||||
if (CODEX_SHELL_NAMES.has(name)) return "shell";
|
||||
if (CODEX_READ_NAMES.has(name)) return "read";
|
||||
if (CODEX_WRITE_NAMES.has(name)) return "write";
|
||||
if (CODEX_EDIT_NAMES.has(name)) return "edit";
|
||||
if (CODEX_SEARCH_NAMES.has(name)) return "search";
|
||||
if (isSpeakToolName(name)) return "speak";
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
const CodexToolCallPass2EnvelopeSchema = z.union([
|
||||
CodexNormalizedToolCallPass1Schema.extend({
|
||||
name: CodexShellToolNameSchema,
|
||||
}).transform((envelope) => ({ ...envelope, toolKind: "shell" as const })),
|
||||
CodexNormalizedToolCallPass1Schema.extend({
|
||||
name: CodexReadToolNameSchema,
|
||||
}).transform((envelope) => ({ ...envelope, toolKind: "read" as const })),
|
||||
CodexNormalizedToolCallPass1Schema.extend({
|
||||
name: CodexWriteToolNameSchema,
|
||||
}).transform((envelope) => ({ ...envelope, toolKind: "write" as const })),
|
||||
CodexNormalizedToolCallPass1Schema.extend({
|
||||
name: CodexEditToolNameSchema,
|
||||
}).transform((envelope) => ({ ...envelope, toolKind: "edit" as const })),
|
||||
CodexNormalizedToolCallPass1Schema.extend({
|
||||
name: CodexSearchToolNameSchema,
|
||||
}).transform((envelope) => ({ ...envelope, toolKind: "search" as const })),
|
||||
CodexNormalizedToolCallPass1Schema.extend({
|
||||
name: CodexSpeakToolNameSchema,
|
||||
}).transform((envelope) => ({ ...envelope, toolKind: "speak" as const })),
|
||||
CodexNormalizedToolCallPass1Schema.transform((envelope) => ({
|
||||
...envelope,
|
||||
name: envelope.name.trim(),
|
||||
toolKind: "unknown" as const,
|
||||
})),
|
||||
]);
|
||||
interface CodexResolvedToolCall {
|
||||
callId: string;
|
||||
name: string;
|
||||
toolKind: CodexToolKind;
|
||||
input: unknown;
|
||||
output: unknown;
|
||||
status: CodexToolCallStatus;
|
||||
error: unknown;
|
||||
metadata?: Record<string, unknown>;
|
||||
cwd: string | null;
|
||||
}
|
||||
|
||||
const CodexNormalizedToolCallPass2Schema = z.discriminatedUnion("toolKind", [
|
||||
CodexToolCallPass2BaseSchema.extend({
|
||||
toolKind: z.literal("shell"),
|
||||
name: CodexShellToolNameSchema,
|
||||
}),
|
||||
CodexToolCallPass2BaseSchema.extend({
|
||||
toolKind: z.literal("read"),
|
||||
name: CodexReadToolNameSchema,
|
||||
}),
|
||||
CodexToolCallPass2BaseSchema.extend({
|
||||
toolKind: z.literal("write"),
|
||||
name: CodexWriteToolNameSchema,
|
||||
}),
|
||||
CodexToolCallPass2BaseSchema.extend({
|
||||
toolKind: z.literal("edit"),
|
||||
name: CodexEditToolNameSchema,
|
||||
}),
|
||||
CodexToolCallPass2BaseSchema.extend({
|
||||
toolKind: z.literal("search"),
|
||||
name: CodexSearchToolNameSchema,
|
||||
}),
|
||||
CodexToolCallPass2BaseSchema.extend({
|
||||
toolKind: z.literal("speak"),
|
||||
name: CodexSpeakToolNameSchema,
|
||||
}),
|
||||
CodexToolCallPass2BaseSchema.extend({
|
||||
toolKind: z.literal("unknown"),
|
||||
}),
|
||||
]);
|
||||
|
||||
type CodexNormalizedToolCallPass2 = z.infer<typeof CodexNormalizedToolCallPass2Schema>;
|
||||
|
||||
function toToolCallTimelineItem(envelope: CodexNormalizedToolCallPass2): ToolCallTimelineItem {
|
||||
function toToolCallTimelineItem(envelope: CodexResolvedToolCall): ToolCallTimelineItem {
|
||||
const name = envelope.toolKind === "speak" ? ("speak" as const) : envelope.name;
|
||||
const parsedDetail = deriveCodexToolDetail({
|
||||
name,
|
||||
@@ -574,15 +502,24 @@ function toNullableObject(value: Record<string, unknown>): Record<string, unknow
|
||||
function toToolCallFromNormalizedEnvelope(
|
||||
envelope: CodexNormalizedToolCallEnvelope,
|
||||
): ToolCallTimelineItem | null {
|
||||
const pass2Envelope = CodexToolCallPass2EnvelopeSchema.safeParse(envelope);
|
||||
if (!pass2Envelope.success) {
|
||||
if (envelope.callId.length === 0 || envelope.name.length === 0 || !envelope.status) {
|
||||
return null;
|
||||
}
|
||||
const parsed = CodexNormalizedToolCallPass2Schema.safeParse(pass2Envelope.data);
|
||||
if (!parsed.success) {
|
||||
const trimmedName = envelope.name.trim();
|
||||
if (trimmedName.length === 0) {
|
||||
return null;
|
||||
}
|
||||
return toToolCallTimelineItem(parsed.data);
|
||||
return toToolCallTimelineItem({
|
||||
callId: envelope.callId,
|
||||
name: trimmedName,
|
||||
toolKind: resolveCodexToolKind(trimmedName),
|
||||
input: envelope.input ?? null,
|
||||
output: envelope.output ?? null,
|
||||
status: envelope.status,
|
||||
error: envelope.error ?? null,
|
||||
...(envelope.metadata ? { metadata: envelope.metadata } : {}),
|
||||
cwd: envelope.cwd ?? null,
|
||||
});
|
||||
}
|
||||
|
||||
function mapCommandExecutionItem(
|
||||
@@ -905,13 +842,20 @@ export function mapCodexRolloutToolCall(params: {
|
||||
}
|
||||
|
||||
const normalizedName = parsed.data.name.trim();
|
||||
if (normalizedName.length === 0) {
|
||||
return null;
|
||||
}
|
||||
const callId = typeof parsed.data.callId === "string" ? parsed.data.callId.trim() : "";
|
||||
if (callId.length === 0) {
|
||||
return null;
|
||||
}
|
||||
const normalizedInput =
|
||||
normalizedName === "apply_patch" || normalizedName === "apply_diff"
|
||||
? normalizeRolloutEditInput(parsed.data.input ?? null)
|
||||
: (parsed.data.input ?? null);
|
||||
|
||||
const pass1 = CodexNormalizedToolCallPass1Schema.safeParse({
|
||||
callId: typeof parsed.data.callId === "string" ? parsed.data.callId.trim() : "",
|
||||
return toToolCallFromNormalizedEnvelope({
|
||||
callId,
|
||||
name: normalizedName,
|
||||
input: normalizedInput,
|
||||
output: parsed.data.output ?? null,
|
||||
@@ -923,14 +867,4 @@ export function mapCodexRolloutToolCall(params: {
|
||||
),
|
||||
cwd: params.cwd ?? null,
|
||||
});
|
||||
if (!pass1.success) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const mapped = toToolCallFromNormalizedEnvelope(pass1.data);
|
||||
if (!mapped) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return mapped;
|
||||
}
|
||||
|
||||
@@ -14,8 +14,6 @@ interface OpencodeToolCallParams {
|
||||
metadata?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
const OpencodeToolCallStatusSchema = z.enum(["running", "completed", "failed", "canceled"]);
|
||||
|
||||
const OpencodeRawToolCallSchema = z
|
||||
.object({
|
||||
toolName: z.string().min(1),
|
||||
@@ -28,123 +26,43 @@ const OpencodeRawToolCallSchema = z
|
||||
})
|
||||
.passthrough();
|
||||
|
||||
const OpencodeNormalizedToolCallPass1Schema = OpencodeRawToolCallSchema.transform((raw) => {
|
||||
export function mapOpencodeToolCall(params: OpencodeToolCallParams): ToolCallTimelineItem | null {
|
||||
const parsed = OpencodeRawToolCallSchema.safeParse(params);
|
||||
if (!parsed.success) {
|
||||
return null;
|
||||
}
|
||||
const raw = parsed.data;
|
||||
const callId =
|
||||
typeof raw.callId === "string" && raw.callId.trim().length > 0 ? raw.callId.trim() : null;
|
||||
if (callId === null) {
|
||||
return null;
|
||||
}
|
||||
const name = raw.toolName.trim();
|
||||
const input = raw.input ?? null;
|
||||
const output = raw.output ?? null;
|
||||
const error = raw.error ?? null;
|
||||
const callId =
|
||||
typeof raw.callId === "string" && raw.callId.trim().length > 0 ? raw.callId.trim() : null;
|
||||
const rawStatus = typeof raw.status === "string" ? raw.status : undefined;
|
||||
const status = normalizeToolCallStatus(rawStatus, error, output);
|
||||
const detail = deriveOpencodeToolDetail(name, input, output);
|
||||
|
||||
return {
|
||||
callId,
|
||||
name: raw.toolName.trim(),
|
||||
input,
|
||||
output,
|
||||
error,
|
||||
metadata: raw.metadata,
|
||||
status,
|
||||
};
|
||||
});
|
||||
|
||||
const OpencodeKnownToolNameSchema = z.union([
|
||||
z.literal("shell"),
|
||||
z.literal("bash"),
|
||||
z.literal("exec_command"),
|
||||
z.literal("read"),
|
||||
z.literal("read_file"),
|
||||
z.literal("write"),
|
||||
z.literal("write_file"),
|
||||
z.literal("create_file"),
|
||||
z.literal("edit"),
|
||||
z.literal("apply_patch"),
|
||||
z.literal("apply_diff"),
|
||||
z.literal("search"),
|
||||
z.literal("web_search"),
|
||||
]);
|
||||
|
||||
const OpencodeToolCallPass2BaseSchema = z.object({
|
||||
callId: z.string().min(1),
|
||||
name: z.string().min(1),
|
||||
input: z.unknown().nullable(),
|
||||
output: z.unknown().nullable(),
|
||||
error: z.unknown().nullable(),
|
||||
metadata: z.record(z.string(), z.unknown()).optional(),
|
||||
status: OpencodeToolCallStatusSchema,
|
||||
toolKind: z.enum(["known", "other"]),
|
||||
});
|
||||
|
||||
const OpencodeToolCallPass2InputSchema = OpencodeToolCallPass2BaseSchema.omit({
|
||||
toolKind: true,
|
||||
});
|
||||
|
||||
const OpencodeToolCallPass2EnvelopeSchema = z.union([
|
||||
OpencodeToolCallPass2InputSchema.extend({
|
||||
name: OpencodeKnownToolNameSchema,
|
||||
}).transform((normalized) => ({
|
||||
...normalized,
|
||||
name: normalized.name.trim(),
|
||||
toolKind: "known" as const,
|
||||
})),
|
||||
OpencodeToolCallPass2InputSchema.transform((normalized) => ({
|
||||
...normalized,
|
||||
name: normalized.name.trim(),
|
||||
toolKind: "other" as const,
|
||||
})),
|
||||
]);
|
||||
|
||||
const OpencodeToolCallPass2Schema = z.discriminatedUnion("toolKind", [
|
||||
OpencodeToolCallPass2BaseSchema.extend({
|
||||
toolKind: z.literal("known"),
|
||||
name: OpencodeKnownToolNameSchema,
|
||||
}),
|
||||
OpencodeToolCallPass2BaseSchema.extend({
|
||||
toolKind: z.literal("other"),
|
||||
}),
|
||||
]);
|
||||
|
||||
type OpencodeToolCallPass2 = z.infer<typeof OpencodeToolCallPass2Schema>;
|
||||
|
||||
function toToolCallTimelineItem(normalized: OpencodeToolCallPass2): ToolCallTimelineItem {
|
||||
const detail = deriveOpencodeToolDetail(normalized.name, normalized.input, normalized.output);
|
||||
if (normalized.status === "failed") {
|
||||
if (status === "failed") {
|
||||
return {
|
||||
type: "tool_call",
|
||||
callId: normalized.callId,
|
||||
name: normalized.name,
|
||||
callId,
|
||||
name,
|
||||
status: "failed",
|
||||
detail,
|
||||
error: normalized.error ?? { message: "Tool call failed" },
|
||||
...(normalized.metadata ? { metadata: normalized.metadata } : {}),
|
||||
error: error ?? { message: "Tool call failed" },
|
||||
...(raw.metadata ? { metadata: raw.metadata } : {}),
|
||||
};
|
||||
}
|
||||
return {
|
||||
type: "tool_call",
|
||||
callId: normalized.callId,
|
||||
name: normalized.name,
|
||||
status: normalized.status,
|
||||
callId,
|
||||
name,
|
||||
status,
|
||||
detail,
|
||||
error: null,
|
||||
...(normalized.metadata ? { metadata: normalized.metadata } : {}),
|
||||
...(raw.metadata ? { metadata: raw.metadata } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
export function mapOpencodeToolCall(params: OpencodeToolCallParams): ToolCallTimelineItem | null {
|
||||
const pass1 = OpencodeNormalizedToolCallPass1Schema.safeParse(params);
|
||||
if (!pass1.success) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const pass2Envelope = OpencodeToolCallPass2EnvelopeSchema.safeParse(pass1.data);
|
||||
if (!pass2Envelope.success) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const pass2 = OpencodeToolCallPass2Schema.safeParse(pass2Envelope.data);
|
||||
if (!pass2.success) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return toToolCallTimelineItem(pass2.data);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user