From aa56045b492b1e01ad5836cabc43b2a4947b2bb3 Mon Sep 17 00:00:00 2001 From: Vignesh Natarajan Date: Sat, 14 Feb 2026 20:09:04 -0800 Subject: [PATCH] fix (agents): harden transcript tool-call block sanitization --- .../session-transcript-repair.e2e.test.ts | 26 +++++++++++++++++++ src/agents/session-transcript-repair.ts | 17 +++++++++++- 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/src/agents/session-transcript-repair.e2e.test.ts b/src/agents/session-transcript-repair.e2e.test.ts index 8f2a30960..f03d9f6e0 100644 --- a/src/agents/session-transcript-repair.e2e.test.ts +++ b/src/agents/session-transcript-repair.e2e.test.ts @@ -223,6 +223,32 @@ describe("sanitizeToolCallInputs", () => { expect(out.map((m) => m.role)).toEqual(["user"]); }); + it("drops tool calls with missing or blank name/id", () => { + const input: AgentMessage[] = [ + { + role: "assistant", + content: [ + { type: "toolCall", id: "call_ok", name: "read", arguments: {} }, + { type: "toolCall", id: "call_empty_name", name: "", arguments: {} }, + { type: "toolUse", id: "call_blank_name", name: " ", input: {} }, + { type: "functionCall", id: "", name: "exec", arguments: {} }, + ], + }, + ]; + + const out = sanitizeToolCallInputs(input); + const assistant = out[0] as Extract; + const toolCalls = Array.isArray(assistant.content) + ? assistant.content.filter((block) => { + const type = (block as { type?: unknown }).type; + return typeof type === "string" && ["toolCall", "toolUse", "functionCall"].includes(type); + }) + : []; + + expect(toolCalls).toHaveLength(1); + expect((toolCalls[0] as { id?: unknown }).id).toBe("call_ok"); + }); + it("keeps valid tool calls and preserves text blocks", () => { const input: AgentMessage[] = [ { diff --git a/src/agents/session-transcript-repair.ts b/src/agents/session-transcript-repair.ts index c8a6286e5..1ac325b1d 100644 --- a/src/agents/session-transcript-repair.ts +++ b/src/agents/session-transcript-repair.ts @@ -58,6 +58,18 @@ function hasToolCallInput(block: ToolCallBlock): boolean { return hasInput || hasArguments; } +function hasNonEmptyStringField(value: unknown): boolean { + return typeof value === "string" && value.trim().length > 0; +} + +function hasToolCallId(block: ToolCallBlock): boolean { + return hasNonEmptyStringField(block.id); +} + +function hasToolCallName(block: ToolCallBlock): boolean { + return hasNonEmptyStringField(block.name); +} + function extractToolResultId(msg: Extract): string | null { const toolCallId = (msg as { toolCallId?: unknown }).toolCallId; if (typeof toolCallId === "string" && toolCallId) { @@ -118,7 +130,10 @@ export function repairToolCallInputs(messages: AgentMessage[]): ToolCallInputRep let droppedInMessage = 0; for (const block of msg.content) { - if (isToolCallBlock(block) && !hasToolCallInput(block)) { + if ( + isToolCallBlock(block) && + (!hasToolCallInput(block) || !hasToolCallId(block) || !hasToolCallName(block)) + ) { droppedToolCalls += 1; droppedInMessage += 1; changed = true;