diff --git a/package.json b/package.json index 77211d865..b422abfbb 100644 --- a/package.json +++ b/package.json @@ -250,6 +250,9 @@ "@sinclair/typebox": "0.34.47", "hono": "4.11.4", "tar": "7.5.4" + }, + "patchedDependencies": { + "@mariozechner/pi-ai": "patches/@mariozechner__pi-ai.patch" } }, "vitest": { diff --git a/patches/@mariozechner__pi-ai.patch b/patches/@mariozechner__pi-ai.patch new file mode 100644 index 000000000..bd8113fbf --- /dev/null +++ b/patches/@mariozechner__pi-ai.patch @@ -0,0 +1,13 @@ +diff --git a/dist/providers/google-gemini-cli.js b/dist/providers/google-gemini-cli.js +index 3741764906f41e87eda9259f567b1d5332551a63..41fc6a92fc4c1f6f503b92320248391013badfb0 100644 +--- a/dist/providers/google-gemini-cli.js ++++ b/dist/providers/google-gemini-cli.js +@@ -22,7 +22,7 @@ const GEMINI_CLI_HEADERS = { + }; + // Headers for Antigravity (sandbox endpoint) - requires specific User-Agent + const ANTIGRAVITY_HEADERS = { +- "User-Agent": "antigravity/1.11.5 darwin/arm64", ++ "User-Agent": "antigravity/1.15.8 darwin/arm64", + "X-Goog-Api-Client": "google-cloud-sdk vscode_cloudshelleditor/0.1", + "Client-Metadata": JSON.stringify({ + ideType: "IDE_UNSPECIFIED", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 95b940c97..30fad4fa8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -9,6 +9,11 @@ overrides: hono: 4.11.4 tar: 7.5.4 +patchedDependencies: + '@mariozechner/pi-ai': + hash: a959dedd4f17a3a05dc9bfe16a6ad07d57d53abd992ee4526e451a80c4909c36 + path: patches/@mariozechner__pi-ai.patch + importers: .: @@ -45,7 +50,7 @@ importers: version: 0.49.3(ws@8.19.0)(zod@4.3.6) '@mariozechner/pi-ai': specifier: 0.49.3 - version: 0.49.3(ws@8.19.0)(zod@4.3.6) + version: 0.49.3(patch_hash=a959dedd4f17a3a05dc9bfe16a6ad07d57d53abd992ee4526e451a80c4909c36)(ws@8.19.0)(zod@4.3.6) '@mariozechner/pi-coding-agent': specifier: 0.49.3 version: 0.49.3(ws@8.19.0)(zod@4.3.6) @@ -6996,7 +7001,7 @@ snapshots: '@mariozechner/pi-agent-core@0.49.3(ws@8.19.0)(zod@4.3.6)': dependencies: - '@mariozechner/pi-ai': 0.49.3(ws@8.19.0)(zod@4.3.6) + '@mariozechner/pi-ai': 0.49.3(patch_hash=a959dedd4f17a3a05dc9bfe16a6ad07d57d53abd992ee4526e451a80c4909c36)(ws@8.19.0)(zod@4.3.6) '@mariozechner/pi-tui': 0.49.3 transitivePeerDependencies: - '@modelcontextprotocol/sdk' @@ -7007,7 +7012,7 @@ snapshots: - ws - zod - '@mariozechner/pi-ai@0.49.3(ws@8.19.0)(zod@4.3.6)': + '@mariozechner/pi-ai@0.49.3(patch_hash=a959dedd4f17a3a05dc9bfe16a6ad07d57d53abd992ee4526e451a80c4909c36)(ws@8.19.0)(zod@4.3.6)': dependencies: '@anthropic-ai/sdk': 0.71.2(zod@4.3.6) '@aws-sdk/client-bedrock-runtime': 3.972.0 @@ -7034,7 +7039,7 @@ snapshots: '@mariozechner/clipboard': 0.3.0 '@mariozechner/jiti': 2.6.5 '@mariozechner/pi-agent-core': 0.49.3(ws@8.19.0)(zod@4.3.6) - '@mariozechner/pi-ai': 0.49.3(ws@8.19.0)(zod@4.3.6) + '@mariozechner/pi-ai': 0.49.3(patch_hash=a959dedd4f17a3a05dc9bfe16a6ad07d57d53abd992ee4526e451a80c4909c36)(ws@8.19.0)(zod@4.3.6) '@mariozechner/pi-tui': 0.49.3 '@silvia-odwyer/photon-node': 0.3.4 chalk: 5.6.2 diff --git a/src/agents/pi-embedded-helpers.ts b/src/agents/pi-embedded-helpers.ts index 88443756f..6fe99d8ec 100644 --- a/src/agents/pi-embedded-helpers.ts +++ b/src/agents/pi-embedded-helpers.ts @@ -32,7 +32,11 @@ export { parseImageDimensionError, parseImageSizeError, } from "./pi-embedded-helpers/errors.js"; -export { isGoogleModelApi, sanitizeGoogleTurnOrdering } from "./pi-embedded-helpers/google.js"; +export { + isGoogleModelApi, + sanitizeGoogleTurnOrdering, + sanitizeToolUseInput, +} from "./pi-embedded-helpers/google.js"; export { downgradeOpenAIReasoningBlocks } from "./pi-embedded-helpers/openai.js"; export { diff --git a/src/agents/pi-embedded-helpers/google-tool-input.test.ts b/src/agents/pi-embedded-helpers/google-tool-input.test.ts new file mode 100644 index 000000000..0fa7e534e --- /dev/null +++ b/src/agents/pi-embedded-helpers/google-tool-input.test.ts @@ -0,0 +1,88 @@ +import { describe, it, expect } from "vitest"; +import type { AgentMessage } from "@mariozechner/pi-agent-core"; +import { sanitizeToolUseInput } from "./google.js"; + +describe("sanitizeToolUseInput", () => { + it("should add empty input to toolUse blocks missing it", () => { + const messages: AgentMessage[] = [ + { + role: "assistant", + content: [ + { + type: "toolUse", + id: "tool-1", + name: "readFile", + // missing input + } as any, + { + type: "text", + text: "Searching...", + }, + ], + }, + ]; + + const sanitized = sanitizeToolUseInput(messages); + const content = sanitized[0].content; + const toolUse = (Array.isArray(content) ? content[0] : null) as any; + + expect(toolUse).toBeDefined(); + expect(toolUse.input).toEqual({}); + }); + + it("should preserve existing input", () => { + const messages: AgentMessage[] = [ + { + role: "assistant", + content: [ + { + type: "toolUse", + id: "tool-2", + name: "writeFile", + input: { path: "foo.txt" }, + }, + ], + }, + ]; + + const sanitized = sanitizeToolUseInput(messages); + const content = sanitized[0].content; + const toolUse = (Array.isArray(content) ? content[0] : null) as any; + + expect(toolUse.input).toEqual({ path: "foo.txt" }); + }); + + it("should handle non-array content gracefully", () => { + const messages: AgentMessage[] = [ + { + role: "user", + content: "Hello", + }, + ]; + const sanitized = sanitizeToolUseInput(messages); + expect(sanitized).toEqual(messages); + }); + + it("should recurse through all messages", () => { + const messages: AgentMessage[] = [ + { role: "user", content: "Hi" }, + { + role: "assistant", + content: [ + { type: "toolUse", id: "1", name: "a" } as any, // fix me + ], + }, + { + role: "assistant", + content: [ + { type: "toolUse", id: "2", name: "b", input: { foo: 1 } }, // leave me + ], + }, + ]; + + const sanitized = sanitizeToolUseInput(messages); + + expect((sanitized[1].content as any[])[0].input).toEqual({}); + expect((sanitized[2].content as any[])[0].input).toEqual({ foo: 1 }); + }); +}); diff --git a/src/agents/pi-embedded-helpers/google.ts b/src/agents/pi-embedded-helpers/google.ts index 8d53901d6..7bb77ffc8 100644 --- a/src/agents/pi-embedded-helpers/google.ts +++ b/src/agents/pi-embedded-helpers/google.ts @@ -18,3 +18,25 @@ export function isAntigravityClaude(params: { } export { sanitizeGoogleTurnOrdering }; + +export function sanitizeToolUseInput(messages: any[]): any[] { + return messages.map((msg) => { + if (!msg || typeof msg !== "object") return msg; + if (msg.role !== "assistant" && msg.role !== "toolUse") return msg; + if (!Array.isArray(msg.content)) return msg; + + return { + ...msg, + content: msg.content.map((block: any) => { + if (!block || typeof block !== "object") return block; + if (block.type === "toolUse" || block.type === "toolCall") { + // If input is missing, add empty object + if (!("input" in block) || block.input === undefined) { + return { ...block, input: {} }; + } + } + return block; + }), + }; + }); +} diff --git a/src/agents/pi-embedded-runner.google-sanitize-thinking.test.ts b/src/agents/pi-embedded-runner.google-sanitize-thinking.test.ts index ed4b52940..07614b8a9 100644 --- a/src/agents/pi-embedded-runner.google-sanitize-thinking.test.ts +++ b/src/agents/pi-embedded-runner.google-sanitize-thinking.test.ts @@ -224,12 +224,14 @@ describe("sanitizeSessionHistory (google thinking)", () => { { type: "toolCall", id: "call_1", + input: {}, name: "read", arguments: { path: "/tmp/foo" }, }, { type: "toolCall", id: "call_2", + input: {}, name: "read", arguments: { path: "/tmp/bar" }, thoughtSignature: "c2ln", diff --git a/src/agents/pi-embedded-runner/google.ts b/src/agents/pi-embedded-runner/google.ts index 7b26d0d04..aec258d18 100644 --- a/src/agents/pi-embedded-runner/google.ts +++ b/src/agents/pi-embedded-runner/google.ts @@ -11,6 +11,7 @@ import { isGoogleModelApi, sanitizeGoogleTurnOrdering, sanitizeSessionMessagesImages, + sanitizeToolUseInput, } from "../pi-embedded-helpers.js"; import { sanitizeToolUseResultPairing } from "../session-transcript-repair.js"; import { log } from "./logger.js"; @@ -336,6 +337,9 @@ export async function sanitizeSessionHistory(params: { ? sanitizeToolUseResultPairing(sanitizedThinking) : sanitizedThinking; + // Ensure toolUse blocks have input field (fixes schema validation "Field required") + const sanitizedInputs = sanitizeToolUseInput(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(sanitizedInputs) + : sanitizedInputs; if (hasSnapshot && (!priorSnapshot || modelChanged)) { appendModelSnapshot(params.sessionManager, { diff --git a/src/infra/provider-usage.fetch.antigravity.ts b/src/infra/provider-usage.fetch.antigravity.ts index b40b6d91e..44cf0fb0e 100644 --- a/src/infra/provider-usage.fetch.antigravity.ts +++ b/src/infra/provider-usage.fetch.antigravity.ts @@ -174,7 +174,7 @@ export async function fetchAntigravityUsage( const headers: Record = { Authorization: `Bearer ${token}`, "Content-Type": "application/json", - "User-Agent": "antigravity", + "User-Agent": "antigravity/1.15.8", "X-Goog-Api-Client": "google-cloud-sdk vscode_cloudshelleditor/0.1", }; diff --git a/verify_logic.mjs b/verify_logic.mjs new file mode 100644 index 000000000..045067faa --- /dev/null +++ b/verify_logic.mjs @@ -0,0 +1,70 @@ + +// Logic from src/agents/pi-embedded-helpers/google.ts +function sanitizeToolUseInput(messages) { + return messages.map((msg) => { + if (!msg || typeof msg !== "object") return msg; + if (msg.role !== "assistant" && msg.role !== "toolUse") return msg; + if (!Array.isArray(msg.content)) return msg; + + return { + ...msg, + content: msg.content.map((block) => { + if (!block || typeof block !== "object") return block; + if (block.type === "toolUse" || block.type === "toolCall") { + // If input is missing, add empty object + if (!("input" in block) || block.input === undefined) { + return { ...block, input: {} }; + } + } + return block; + }), + }; + }); +} + +// Test cases +function runTests() { + console.log("Running tests..."); + let passed = 0; + let failed = 0; + + function assert(condition, message) { + if (condition) { + console.log(`✅ PASS: ${message}`); + passed++; + } else { + console.error(`❌ FAIL: ${message}`); + failed++; + } + } + + // Test 1: Add empty input + const msgs1 = [ + { + role: "assistant", + content: [{ type: "toolUse", id: "1", name: "f" }], + }, + ]; + const res1 = sanitizeToolUseInput(msgs1); + assert(res1[0].content[0].input && Object.keys(res1[0].content[0].input).length === 0, "Should add empty input"); + + // Test 2: Preserve existing input + const msgs2 = [ + { + role: "assistant", + content: [{ type: "toolUse", id: "2", name: "f", input: { a: 1 } }], + }, + ]; + const res2 = sanitizeToolUseInput(msgs2); + assert(res2[0].content[0].input.a === 1, "Should preserve existing input"); + + // Test 3: Ignore other roles + const msgs3 = [{ role: "user", content: "hi" }]; + const res3 = sanitizeToolUseInput(msgs3); + assert(res3[0] === msgs3[0], "Should ignore user messages"); + + console.log(`\nResults: ${passed} passed, ${failed} failed.`); + if (failed > 0) process.exit(1); +} + +runTests();