From 25989c1e916d133b1e4de8fd8e4d461e7d419ee5 Mon Sep 17 00:00:00 2001 From: Ayush Ojha Date: Fri, 30 Jan 2026 00:53:51 -0800 Subject: [PATCH] fix(agents): normalize missing arguments on toolCall blocks When a model generates a toolCall without the arguments/input field (common for tools with only optional parameters), subsequent API calls reject with "input: Field required", permanently corrupting the session. Add normalizeToolCallArguments() to the session sanitization pipeline to default missing arguments to {} on toolCall blocks and missing input to {} on toolUse blocks. Closes #4345 --- ...ed-runner.sanitize-session-history.test.ts | 46 +++++++++++++++++ src/agents/pi-embedded-runner/google.ts | 51 ++++++++++++++++++- 2 files changed, 95 insertions(+), 2 deletions(-) diff --git a/src/agents/pi-embedded-runner.sanitize-session-history.test.ts b/src/agents/pi-embedded-runner.sanitize-session-history.test.ts index b428c3328..10f6d8a6a 100644 --- a/src/agents/pi-embedded-runner.sanitize-session-history.test.ts +++ b/src/agents/pi-embedded-runner.sanitize-session-history.test.ts @@ -206,6 +206,52 @@ describe("sanitizeSessionHistory", () => { expect(result).toEqual(messages); }); + it("normalizes toolCall blocks missing arguments to have empty object", async () => { + vi.mocked(helpers.isGoogleModelApi).mockReturnValue(false); + + const messages: AgentMessage[] = [ + { + role: "assistant", + content: [ + // Missing arguments field entirely (the bug in #4345) + { type: "toolCall", id: "call_1", name: "session_status" } as never, + { type: "toolCall", id: "call_2", name: "sessions_list", arguments: { limit: 10 } }, + ], + }, + // Include matching tool results so repair doesn't add synthetic ones + { + role: "toolResult", + toolCallId: "call_1", + toolName: "session_status", + content: [{ type: "text", text: "ok" }], + } as AgentMessage, + { + role: "toolResult", + toolCallId: "call_2", + toolName: "sessions_list", + content: [{ type: "text", text: "ok" }], + } as AgentMessage, + ]; + + const result = await sanitizeSessionHistory({ + messages, + modelApi: "anthropic-messages", + provider: "anthropic", + sessionManager: mockSessionManager, + sessionId: "test-session", + }); + + expect(result).toHaveLength(3); + const assistant = result[0] as { + role: string; + content: Array<{ type: string; arguments?: unknown }>; + }; + // The first tool call should now have arguments: {} + expect(assistant.content[0]).toHaveProperty("arguments", {}); + // The second tool call should keep its original arguments + expect(assistant.content[1]).toHaveProperty("arguments", { limit: 10 }); + }); + it("downgrades openai reasoning only when the model changes", async () => { const sessionEntries: Array<{ type: string; customType: string; data: unknown }> = [ { diff --git a/src/agents/pi-embedded-runner/google.ts b/src/agents/pi-embedded-runner/google.ts index 7b26d0d04..f29d5a2ce 100644 --- a/src/agents/pi-embedded-runner/google.ts +++ b/src/agents/pi-embedded-runner/google.ts @@ -336,6 +336,10 @@ export async function sanitizeSessionHistory(params: { ? sanitizeToolUseResultPairing(sanitizedThinking) : sanitizedThinking; + // Ensure every toolCall/toolUse block has an arguments/input field (default {}). + // Some models omit the field for optional-args tools, which causes API rejections. + const normalizedToolArgs = normalizeToolCallArguments(repairedTools); + const isOpenAIResponsesApi = params.modelApi === "openai-responses" || params.modelApi === "openai-codex-responses"; const hasSnapshot = Boolean(params.provider || params.modelApi || params.modelId); @@ -350,8 +354,8 @@ export async function sanitizeSessionHistory(params: { : false; const sanitizedOpenAI = isOpenAIResponsesApi && modelChanged - ? downgradeOpenAIReasoningBlocks(repairedTools) - : repairedTools; + ? downgradeOpenAIReasoningBlocks(normalizedToolArgs) + : normalizedToolArgs; if (hasSnapshot && (!priorSnapshot || modelChanged)) { appendModelSnapshot(params.sessionManager, { @@ -373,3 +377,46 @@ export async function sanitizeSessionHistory(params: { sessionId: params.sessionId, }).messages; } + +/** + * Ensure every toolCall/toolUse block has its arguments/input field. + * Some models omit the field entirely for tools with only optional parameters, + * which causes API rejections ("input: Field required"). + */ +function normalizeToolCallArguments(messages: AgentMessage[]): AgentMessage[] { + let changed = false; + const out: AgentMessage[] = []; + for (const msg of messages) { + if (!msg || typeof msg !== "object" || (msg as { role?: unknown }).role !== "assistant") { + out.push(msg); + continue; + } + const assistant = msg as Extract; + const content = assistant.content; + if (!Array.isArray(content)) { + out.push(msg); + continue; + } + let blockChanged = false; + const nextContent = content.map((block) => { + if (!block || typeof block !== "object") return block; + const rec = block as unknown as Record; + if (rec.type === "toolCall" && rec.arguments === undefined) { + blockChanged = true; + return { ...rec, arguments: {} }; + } + if (rec.type === "toolUse" && rec.input === undefined) { + blockChanged = true; + return { ...rec, input: {} }; + } + return block; + }); + if (blockChanged) { + changed = true; + out.push({ ...assistant, content: nextContent } as AgentMessage); + } else { + out.push(msg); + } + } + return changed ? out : messages; +}