This commit is contained in:
manzienkog 2026-01-29 23:26:05 -05:00 committed by GitHub
commit 07cb3875ea
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 160 additions and 3 deletions

View File

@ -55,4 +55,8 @@ export {
export type { EmbeddedContextFile, FailoverReason } from "./pi-embedded-helpers/types.js";
export type { ToolCallIdMode } from "./tool-call-id.js";
export { isValidCloudCodeAssistToolId, sanitizeToolCallId } from "./tool-call-id.js";
export {
isValidCloudCodeAssistToolId,
sanitizeToolCallId,
normalizeToolCallArguments,
} from "./tool-call-id.js";

View File

@ -9,6 +9,7 @@ import {
downgradeOpenAIReasoningBlocks,
isCompactionFailureError,
isGoogleModelApi,
normalizeToolCallArguments,
sanitizeGoogleTurnOrdering,
sanitizeSessionMessagesImages,
} from "../pi-embedded-helpers.js";
@ -336,6 +337,11 @@ export async function sanitizeSessionHistory(params: {
? sanitizeToolUseResultPairing(sanitizedThinking)
: sanitizedThinking;
// Normalize toolCall arguments to ensure they're always an object.
// Some models omit `arguments` for tools with no required params,
// but Anthropic API requires `input` to be present.
const normalizedArgs = normalizeToolCallArguments(repairedTools);
const isOpenAIResponsesApi =
params.modelApi === "openai-responses" || params.modelApi === "openai-codex-responses";
const hasSnapshot = Boolean(params.provider || params.modelApi || params.modelId);
@ -350,8 +356,8 @@ export async function sanitizeSessionHistory(params: {
: false;
const sanitizedOpenAI =
isOpenAIResponsesApi && modelChanged
? downgradeOpenAIReasoningBlocks(repairedTools)
: repairedTools;
? downgradeOpenAIReasoningBlocks(normalizedArgs)
: normalizedArgs;
if (hasSnapshot && (!priorSnapshot || modelChanged)) {
appendModelSnapshot(params.sessionManager, {

View File

@ -3,6 +3,7 @@ import { describe, expect, it } from "vitest";
import {
isValidCloudCodeAssistToolId,
normalizeToolCallArguments,
sanitizeToolCallIdsForCloudCodeAssist,
} from "./tool-call-id.js";
@ -266,3 +267,109 @@ describe("sanitizeToolCallIdsForCloudCodeAssist", () => {
});
});
});
describe("normalizeToolCallArguments", () => {
it("is a no-op when arguments are already present", () => {
const input = [
{
role: "assistant",
content: [{ type: "toolCall", id: "call1", name: "read", arguments: { path: "/tmp" } }],
},
] satisfies AgentMessage[];
const out = normalizeToolCallArguments(input);
expect(out).toBe(input);
});
it("normalizes undefined arguments to empty object", () => {
const input = [
{
role: "assistant",
content: [{ type: "toolCall", id: "call1", name: "memory_status" }],
},
] as AgentMessage[];
const out = normalizeToolCallArguments(input);
expect(out).not.toBe(input);
const assistant = out[0] as Extract<AgentMessage, { role: "assistant" }>;
const toolCall = assistant.content?.[0] as { arguments?: unknown };
expect(toolCall.arguments).toEqual({});
});
it("normalizes null arguments to empty object", () => {
const input = [
{
role: "assistant",
content: [{ type: "toolCall", id: "call1", name: "memory_status", arguments: null }],
},
] as AgentMessage[];
const out = normalizeToolCallArguments(input);
expect(out).not.toBe(input);
const assistant = out[0] as Extract<AgentMessage, { role: "assistant" }>;
const toolCall = assistant.content?.[0] as { arguments?: unknown };
expect(toolCall.arguments).toEqual({});
});
it("handles multiple toolCalls in single message", () => {
const input = [
{
role: "assistant",
content: [
{ type: "toolCall", id: "call1", name: "memory_status" },
{ type: "toolCall", id: "call2", name: "read", arguments: { path: "/tmp" } },
{ type: "toolCall", id: "call3", name: "load_memory" },
],
},
] as AgentMessage[];
const out = normalizeToolCallArguments(input);
expect(out).not.toBe(input);
const assistant = out[0] as Extract<AgentMessage, { role: "assistant" }>;
const call1 = assistant.content?.[0] as { arguments?: unknown };
const call2 = assistant.content?.[1] as { arguments?: unknown };
const call3 = assistant.content?.[2] as { arguments?: unknown };
expect(call1.arguments).toEqual({});
expect(call2.arguments).toEqual({ path: "/tmp" });
expect(call3.arguments).toEqual({});
});
it("handles toolUse and functionCall types", () => {
const input = [
{
role: "assistant",
content: [
{ type: "toolUse", id: "call1", name: "status" },
{ type: "functionCall", id: "call2", name: "ping" },
],
},
] as AgentMessage[];
const out = normalizeToolCallArguments(input);
expect(out).not.toBe(input);
const assistant = out[0] as Extract<AgentMessage, { role: "assistant" }>;
const call1 = assistant.content?.[0] as { arguments?: unknown };
const call2 = assistant.content?.[1] as { arguments?: unknown };
expect(call1.arguments).toEqual({});
expect(call2.arguments).toEqual({});
});
it("does not modify user or toolResult messages", () => {
const input = [
{ role: "user", content: [{ type: "text", text: "hi" }] },
{
role: "toolResult",
toolCallId: "call1",
toolName: "read",
content: [{ type: "text", text: "ok" }],
},
] satisfies AgentMessage[];
const out = normalizeToolCallArguments(input);
expect(out).toBe(input);
});
});

View File

@ -186,3 +186,43 @@ export function sanitizeToolCallIdsForCloudCodeAssist(
return changed ? out : messages;
}
/**
* Normalize toolCall arguments to ensure they are always an object.
* Some models (e.g., via google-antigravity) may omit the `arguments` field
* for tools with no required parameters. Anthropic API requires `input` to be present.
*/
export function normalizeToolCallArguments(messages: AgentMessage[]): AgentMessage[] {
let changed = false;
const out = messages.map((msg) => {
if (!msg || typeof msg !== "object") return msg;
const role = (msg as { role?: unknown }).role;
if (role !== "assistant") return msg;
const assistant = msg as Extract<AgentMessage, { role: "assistant" }>;
const content = assistant.content;
if (!Array.isArray(content)) return msg;
let contentChanged = false;
const nextContent = content.map((block) => {
if (!block || typeof block !== "object") return block;
const rec = block as { type?: unknown; arguments?: unknown };
const type = rec.type;
if (type !== "functionCall" && type !== "toolUse" && type !== "toolCall") {
return block;
}
// Normalize missing or undefined arguments to empty object
if (rec.arguments === undefined || rec.arguments === null) {
contentChanged = true;
return { ...(block as unknown as Record<string, unknown>), arguments: {} };
}
return block;
});
if (!contentChanged) return msg;
changed = true;
return { ...assistant, content: nextContent as typeof assistant.content };
});
return changed ? out : messages;
}