From bc96c2794238bc32338cd486ecc6f0079f0b3cda Mon Sep 17 00:00:00 2001 From: Mimi Date: Thu, 29 Jan 2026 00:13:01 +0800 Subject: [PATCH] fix: add missing arguments/input field to tool calls When models call tools without parameters, they sometimes omit the arguments/input field entirely. Anthropic/Cloud Code Assist APIs require this field to be present (even if empty {}), causing the error: messages.X.content.Y.tool_use.input: Field required This corrupts the session history and prevents further messages. Fix: Add sanitizeToolCallArguments() in images.ts that ensures all toolCall, toolUse, and functionCall blocks have the required field. Closes #issue-number --- ...ssistant-text-blocks-but-preserves.test.ts | 44 +++++++++++++++++++ src/agents/pi-embedded-helpers/images.ts | 27 ++++++++++-- 2 files changed, 68 insertions(+), 3 deletions(-) diff --git a/src/agents/pi-embedded-helpers.sanitize-session-messages-images.removes-empty-assistant-text-blocks-but-preserves.test.ts b/src/agents/pi-embedded-helpers.sanitize-session-messages-images.removes-empty-assistant-text-blocks-but-preserves.test.ts index 4d03c3ffe..d648f321a 100644 --- a/src/agents/pi-embedded-helpers.sanitize-session-messages-images.removes-empty-assistant-text-blocks-but-preserves.test.ts +++ b/src/agents/pi-embedded-helpers.sanitize-session-messages-images.removes-empty-assistant-text-blocks-but-preserves.test.ts @@ -117,4 +117,48 @@ describe("sanitizeSessionMessagesImages", () => { expect(out[0]?.role).toBe("user"); expect(out[1]?.role).toBe("toolResult"); }); + + it("adds missing arguments field to tool calls", async () => { + const input = [ + { + role: "assistant", + content: [ + { type: "toolCall", id: "call_1", name: "session_status" }, // Missing arguments! + ], + }, + ] satisfies AgentMessage[]; + + const out = await sanitizeSessionMessagesImages(input, "test"); + + expect(out).toHaveLength(1); + const content = (out[0] as { content?: unknown }).content as Array<{ + type?: string; + arguments?: unknown; + }>; + expect(content).toHaveLength(1); + expect(content[0]?.type).toBe("toolCall"); + expect(content[0]?.arguments).toEqual({}); + }); + + it("adds missing input field to toolUse blocks", async () => { + const input = [ + { + role: "assistant", + content: [ + { type: "toolUse", id: "call_1", name: "session_status" }, // Missing input! + ], + }, + ] satisfies AgentMessage[]; + + const out = await sanitizeSessionMessagesImages(input, "test"); + + expect(out).toHaveLength(1); + const content = (out[0] as { content?: unknown }).content as Array<{ + type?: string; + input?: unknown; + }>; + expect(content).toHaveLength(1); + expect(content[0]?.type).toBe("toolUse"); + expect(content[0]?.input).toEqual({}); + }); }); diff --git a/src/agents/pi-embedded-helpers/images.ts b/src/agents/pi-embedded-helpers/images.ts index 518226ae0..cd97c7eb3 100644 --- a/src/agents/pi-embedded-helpers/images.ts +++ b/src/agents/pi-embedded-helpers/images.ts @@ -21,6 +21,25 @@ export function isEmptyAssistantMessageContent( }); } +/** + * Ensures tool call blocks have the required arguments/input field. + * Some models omit this field when calling tools with no parameters, + * but Anthropic/Cloud Code Assist APIs require it (even if empty `{}`). + */ +function sanitizeToolCallArguments(content: unknown[]): unknown[] { + return content.map((block) => { + if (!block || typeof block !== "object") return block; + const rec = block as { type?: unknown; arguments?: unknown; input?: unknown }; + if (rec.type === "toolCall" && rec.arguments === undefined) { + return { ...rec, arguments: {} }; + } + if ((rec.type === "toolUse" || rec.type === "functionCall") && rec.input === undefined) { + return { ...rec, input: {} }; + } + return block; + }); +} + export async function sanitizeSessionMessagesImages( messages: AgentMessage[], label: string, @@ -97,17 +116,19 @@ export async function sanitizeSessionMessagesImages( } const content = assistantMsg.content; if (Array.isArray(content)) { + // Ensure all tool calls have required arguments/input field + const sanitizedContent = sanitizeToolCallArguments(content); if (!allowNonImageSanitization) { const nextContent = (await sanitizeContentBlocksImages( - content as unknown as ContentBlock[], + sanitizedContent as unknown as ContentBlock[], label, )) as unknown as typeof assistantMsg.content; out.push({ ...assistantMsg, content: nextContent }); continue; } const strippedContent = options?.preserveSignatures - ? content // Keep signatures for Antigravity Claude - : stripThoughtSignatures(content, options?.sanitizeThoughtSignatures); // Strip for Gemini + ? sanitizedContent // Keep signatures for Antigravity Claude + : stripThoughtSignatures(sanitizedContent, options?.sanitizeThoughtSignatures); // Strip for Gemini const filteredContent = strippedContent.filter((block) => { if (!block || typeof block !== "object") return true;