diff --git a/package.json b/package.json index 3ef2fae28..483b4761d 100644 --- a/package.json +++ b/package.json @@ -117,10 +117,10 @@ "@grammyjs/runner": "^2.0.3", "@grammyjs/transformer-throttler": "^1.2.1", "@homebridge/ciao": "^1.3.4", - "@mariozechner/pi-agent-core": "^0.42.1", - "@mariozechner/pi-ai": "^0.42.1", - "@mariozechner/pi-coding-agent": "^0.42.1", - "@mariozechner/pi-tui": "^0.42.1", + "@mariozechner/pi-agent-core": "^0.42.2", + "@mariozechner/pi-ai": "^0.42.2", + "@mariozechner/pi-coding-agent": "^0.42.2", + "@mariozechner/pi-tui": "^0.42.2", "@microsoft/agents-hosting": "^1.1.1", "@microsoft/agents-hosting-express": "^1.1.1", "@microsoft/agents-hosting-extensions-teams": "^1.1.1", @@ -190,7 +190,7 @@ "patchedDependencies": { "@buape/carbon@0.0.0-beta-20260109194934": "patches/@buape__carbon@0.0.0-beta-20260109194934.patch", "@mariozechner/pi-agent-core": "patches/@mariozechner__pi-agent-core.patch", - "@mariozechner/pi-ai@0.42.1": "patches/@mariozechner__pi-ai@0.42.1.patch", + "@mariozechner/pi-ai@0.42.2": "patches/@mariozechner__pi-ai@0.42.2.patch", "@mariozechner/pi-coding-agent": "patches/@mariozechner__pi-coding-agent.patch", "playwright-core@1.57.0": "patches/playwright-core@1.57.0.patch", "qrcode-terminal": "patches/qrcode-terminal.patch" diff --git a/patches/@mariozechner__pi-ai@0.42.2.patch b/patches/@mariozechner__pi-ai@0.42.2.patch new file mode 100644 index 000000000..e4a1be4c1 --- /dev/null +++ b/patches/@mariozechner__pi-ai@0.42.2.patch @@ -0,0 +1,57 @@ +diff --git a/dist/providers/google-gemini-cli.js b/dist/providers/google-gemini-cli.js +index 93aa26c395e9bd0df64376408a13d15ee9e7cce7..41a439e5fc370038a5febef9e8f021ee279cf8aa 100644 +--- a/dist/providers/google-gemini-cli.js ++++ b/dist/providers/google-gemini-cli.js +@@ -248,6 +248,11 @@ export const streamGoogleGeminiCli = (model, context, options) => { + break; // Success, exit retry loop + } + const errorText = await response.text(); ++ // Fail immediately on 429 for Antigravity to let callers rotate accounts. ++ // Antigravity rate limits can have very long retry delays (10+ minutes). ++ if (isAntigravity && response.status === 429) { ++ throw new Error(`Cloud Code Assist API error (${response.status}): ${errorText}`); ++ } + // Check if retryable + if (attempt < MAX_RETRIES && isRetryableError(response.status, errorText)) { + // Use server-provided delay or exponential backoff +diff --git a/dist/providers/openai-codex-responses.js b/dist/providers/openai-codex-responses.js +index 188a8294f26fe1bfe3fb298a7f58e4d8eaf2a529..3fd8027edafdad4ca364af53f0a1811139705b21 100644 +--- a/dist/providers/openai-codex-responses.js ++++ b/dist/providers/openai-codex-responses.js +@@ -433,9 +433,15 @@ function convertMessages(model, context) { + } + else if (msg.role === "assistant") { + const output = []; ++ // OpenAI Responses rejects `reasoning` items that are not followed by a `message`. ++ // Tool-call-only turns (thinking + function_call) are valid assistant turns, but ++ // their stored reasoning items must not be replayed as standalone `reasoning` input. ++ const hasTextBlock = msg.content.some((b) => b.type === "text"); + for (const block of msg.content) { + if (block.type === "thinking" && msg.stopReason !== "error") { + if (block.thinkingSignature) { ++ if (!hasTextBlock) ++ continue; + const reasoningItem = JSON.parse(block.thinkingSignature); + output.push(reasoningItem); + } +diff --git a/dist/providers/openai-responses.js b/dist/providers/openai-responses.js +index 20fb0a22aaa28f7ff7c2f44a8b628fa1d9d7d936..0bf46bfb4a6fac5a0304652e42566b2c991bab48 100644 +--- a/dist/providers/openai-responses.js ++++ b/dist/providers/openai-responses.js +@@ -396,10 +396,16 @@ function convertMessages(model, context) { + } + else if (msg.role === "assistant") { + const output = []; ++ // OpenAI Responses rejects `reasoning` items that are not followed by a `message`. ++ // Tool-call-only turns (thinking + function_call) are valid assistant turns, but ++ // their stored reasoning items must not be replayed as standalone `reasoning` input. ++ const hasTextBlock = msg.content.some((b) => b.type === "text"); + for (const block of msg.content) { + // Do not submit thinking blocks if the completion had an error (i.e. abort) + if (block.type === "thinking" && msg.stopReason !== "error") { + if (block.thinkingSignature) { ++ if (!hasTextBlock) ++ continue; + const reasoningItem = JSON.parse(block.thinkingSignature); + output.push(reasoningItem); + } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b58e0a206..877d4081c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -14,13 +14,13 @@ patchedDependencies: '@mariozechner/pi-agent-core': hash: 01312ceb1f6be7e42822c24c9a7a4f7db56b24ae114a364855bd3819779d1cf4 path: patches/@mariozechner__pi-agent-core.patch - '@mariozechner/pi-ai@0.42.1': + '@mariozechner/pi-ai@0.42.2': hash: 0786e946616db65fea37764750c64745131880092ccd082a0016cc9a8788702c - path: patches/@mariozechner__pi-ai@0.42.1.patch + path: patches/@mariozechner__pi-ai@0.42.2.patch '@mariozechner/pi-coding-agent': hash: 58af7c712ebe270527c2ad9d3351fac39d6cd4b81cc475a258d87840b446b90e path: patches/@mariozechner__pi-coding-agent.patch - 'playwright-core@1.57.0': + playwright-core@1.57.0: hash: 66f1f266424dbe354068aaa5bba87bfb0e1d7d834a938c25dd70d43cdf1c1b02 path: patches/playwright-core@1.57.0.patch qrcode-terminal: @@ -47,17 +47,17 @@ importers: specifier: ^1.3.4 version: 1.3.4 '@mariozechner/pi-agent-core': - specifier: ^0.42.1 - version: 0.42.1(patch_hash=01312ceb1f6be7e42822c24c9a7a4f7db56b24ae114a364855bd3819779d1cf4)(ws@8.19.0)(zod@4.3.5) + specifier: ^0.42.2 + version: 0.42.2(patch_hash=01312ceb1f6be7e42822c24c9a7a4f7db56b24ae114a364855bd3819779d1cf4)(ws@8.19.0)(zod@4.3.5) '@mariozechner/pi-ai': - specifier: ^0.42.1 - version: 0.42.1(patch_hash=0786e946616db65fea37764750c64745131880092ccd082a0016cc9a8788702c)(ws@8.19.0)(zod@4.3.5) + specifier: ^0.42.2 + version: 0.42.2(patch_hash=0786e946616db65fea37764750c64745131880092ccd082a0016cc9a8788702c)(ws@8.19.0)(zod@4.3.5) '@mariozechner/pi-coding-agent': - specifier: ^0.42.1 - version: 0.42.1(patch_hash=58af7c712ebe270527c2ad9d3351fac39d6cd4b81cc475a258d87840b446b90e)(ws@8.19.0)(zod@4.3.5) + specifier: ^0.42.2 + version: 0.42.2(patch_hash=58af7c712ebe270527c2ad9d3351fac39d6cd4b81cc475a258d87840b446b90e)(ws@8.19.0)(zod@4.3.5) '@mariozechner/pi-tui': - specifier: ^0.42.1 - version: 0.42.1 + specifier: ^0.42.2 + version: 0.42.2 '@microsoft/agents-hosting': specifier: ^1.1.1 version: 1.1.1 @@ -132,13 +132,13 @@ importers: version: 0.2.0 playwright-core: specifier: 1.57.0 - version: 1.57.0 + version: 1.57.0(patch_hash=66f1f266424dbe354068aaa5bba87bfb0e1d7d834a938c25dd70d43cdf1c1b02) proper-lockfile: specifier: ^4.1.2 version: 4.1.2 qrcode-terminal: specifier: ^0.12.0 - version: 0.12.0 + version: 0.12.0(patch_hash=ed82029850dbdf551f5df1de320945af52b8ea8500cc7bd4f39258e7a3d92e12) sharp: specifier: ^0.34.5 version: 0.34.5 @@ -860,22 +860,22 @@ packages: peerDependencies: lit: ^3.3.1 - '@mariozechner/pi-agent-core@0.42.1': - resolution: {integrity: sha512-sIRB1jHheQSGONoVorrQ3X9vc1JFLoAe+48k7UJmpLAhJXnQmRtq1+CtzvKOVe5qVRyNBNuNdXipH+seicbwvQ==} + '@mariozechner/pi-agent-core@0.42.2': + resolution: {integrity: sha512-j81u9v6FhNgYXTTcgM5FB7f2hLQPc/73oTM0LuRDlybJakDRM3z/BBGkRk5csCfjYN1OeEGMjqj61pR30WqOZg==} engines: {node: '>=20.0.0'} - '@mariozechner/pi-ai@0.42.1': - resolution: {integrity: sha512-uRiH+s7EPDz9Q7hjQJ4Mm8lU4e4/C8pB4rnqGds73B5/0rqZb8DeSBukKU6uOwMNkXol/tYgnkEqXy7qEoHLJQ==} + '@mariozechner/pi-ai@0.42.2': + resolution: {integrity: sha512-uIPfOCGSWm8Uo4kJ0nWKClJlIhBFxDasBbbAzzEgR9NrsyZMHuFYn4Y2XsbNKHmc4KnoS6DZvaO6IP+/+IS9rw==} engines: {node: '>=20.0.0'} hasBin: true - '@mariozechner/pi-coding-agent@0.42.1': - resolution: {integrity: sha512-BsLgFBC//98EdQCCoNYPfxKi2TEJ64a9w/CQC8/DyeAVhjrdJELUd+3EoRV6iiodbpUspUASW9w3ELoqQGAtoA==} + '@mariozechner/pi-coding-agent@0.42.2': + resolution: {integrity: sha512-Rlxo2rWU4RjwBe4jr/CnqqknbDF3fnb3RzCe9u1mMm4nb2T9qLP3QGDo2c5DhWEpcRZ54hrR6HTennlEazfXvA==} engines: {node: '>=20.0.0'} hasBin: true - '@mariozechner/pi-tui@0.42.1': - resolution: {integrity: sha512-dsmzcOjD+9MNgqnJ6/BJ255Sc9KQWN4Asyh1ron7bMA4Q9lqSWl6q8dk78DBp2QcVeWHtvqbwdZwNnDwWyxw3g==} + '@mariozechner/pi-tui@0.42.2': + resolution: {integrity: sha512-im3HwwKvSlh+N1hJpKVoZYW1JrXz0N3eN/PfpWX8DJYYTTG4RDaT0Mbbf0vUbkKSqCfeJQZCN5bfCocTXlU8wA==} engines: {node: '>=20.0.0'} '@microsoft/agents-activity@1.1.1': @@ -3790,10 +3790,10 @@ snapshots: transitivePeerDependencies: - tailwindcss - '@mariozechner/pi-agent-core@0.42.1(patch_hash=01312ceb1f6be7e42822c24c9a7a4f7db56b24ae114a364855bd3819779d1cf4)(ws@8.19.0)(zod@4.3.5)': + '@mariozechner/pi-agent-core@0.42.2(patch_hash=01312ceb1f6be7e42822c24c9a7a4f7db56b24ae114a364855bd3819779d1cf4)(ws@8.19.0)(zod@4.3.5)': dependencies: - '@mariozechner/pi-ai': 0.42.1(patch_hash=0786e946616db65fea37764750c64745131880092ccd082a0016cc9a8788702c)(ws@8.19.0)(zod@4.3.5) - '@mariozechner/pi-tui': 0.42.1 + '@mariozechner/pi-ai': 0.42.2(patch_hash=0786e946616db65fea37764750c64745131880092ccd082a0016cc9a8788702c)(ws@8.19.0)(zod@4.3.5) + '@mariozechner/pi-tui': 0.42.2 transitivePeerDependencies: - '@modelcontextprotocol/sdk' - bufferutil @@ -3802,7 +3802,7 @@ snapshots: - ws - zod - '@mariozechner/pi-ai@0.42.1(patch_hash=0786e946616db65fea37764750c64745131880092ccd082a0016cc9a8788702c)(ws@8.19.0)(zod@4.3.5)': + '@mariozechner/pi-ai@0.42.2(patch_hash=0786e946616db65fea37764750c64745131880092ccd082a0016cc9a8788702c)(ws@8.19.0)(zod@4.3.5)': dependencies: '@anthropic-ai/sdk': 0.71.2(zod@4.3.5) '@google/genai': 1.34.0 @@ -3822,12 +3822,12 @@ snapshots: - ws - zod - '@mariozechner/pi-coding-agent@0.42.1(patch_hash=58af7c712ebe270527c2ad9d3351fac39d6cd4b81cc475a258d87840b446b90e)(ws@8.19.0)(zod@4.3.5)': + '@mariozechner/pi-coding-agent@0.42.2(patch_hash=58af7c712ebe270527c2ad9d3351fac39d6cd4b81cc475a258d87840b446b90e)(ws@8.19.0)(zod@4.3.5)': dependencies: '@mariozechner/clipboard': 0.3.0 - '@mariozechner/pi-agent-core': 0.42.1(patch_hash=01312ceb1f6be7e42822c24c9a7a4f7db56b24ae114a364855bd3819779d1cf4)(ws@8.19.0)(zod@4.3.5) - '@mariozechner/pi-ai': 0.42.1(patch_hash=0786e946616db65fea37764750c64745131880092ccd082a0016cc9a8788702c)(ws@8.19.0)(zod@4.3.5) - '@mariozechner/pi-tui': 0.42.1 + '@mariozechner/pi-agent-core': 0.42.2(patch_hash=01312ceb1f6be7e42822c24c9a7a4f7db56b24ae114a364855bd3819779d1cf4)(ws@8.19.0)(zod@4.3.5) + '@mariozechner/pi-ai': 0.42.2(patch_hash=0786e946616db65fea37764750c64745131880092ccd082a0016cc9a8788702c)(ws@8.19.0)(zod@4.3.5) + '@mariozechner/pi-tui': 0.42.2 chalk: 5.6.2 cli-highlight: 2.1.11 diff: 8.0.2 @@ -3846,7 +3846,7 @@ snapshots: - ws - zod - '@mariozechner/pi-tui@0.42.1': + '@mariozechner/pi-tui@0.42.2': dependencies: '@types/mime-types': 2.1.4 chalk: 5.6.2 @@ -5689,11 +5689,11 @@ snapshots: dependencies: pngjs: 7.0.0 - playwright-core@1.57.0: {} + playwright-core@1.57.0(patch_hash=66f1f266424dbe354068aaa5bba87bfb0e1d7d834a938c25dd70d43cdf1c1b02): {} playwright@1.57.0: dependencies: - playwright-core: 1.57.0 + playwright-core: 1.57.0(patch_hash=66f1f266424dbe354068aaa5bba87bfb0e1d7d834a938c25dd70d43cdf1c1b02) optionalDependencies: fsevents: 2.3.2 @@ -5778,7 +5778,7 @@ snapshots: '@thi.ng/bitstream': 2.4.37 optional: true - qrcode-terminal@0.12.0: {} + qrcode-terminal@0.12.0(patch_hash=ed82029850dbdf551f5df1de320945af52b8ea8500cc7bd4f39258e7a3d92e12): {} qs@6.14.1: dependencies: diff --git a/src/agents/bash-tools.ts b/src/agents/bash-tools.ts index b71d1c5ac..cf9ba5055 100644 --- a/src/agents/bash-tools.ts +++ b/src/agents/bash-tools.ts @@ -44,7 +44,7 @@ const DEFAULT_PATH = // because Claude API on Vertex AI rejects nested anyOf schemas as invalid JSON Schema. // Type.Union of literals compiles to { anyOf: [{enum:["a"]}, {enum:["b"]}, ...] } // which is valid but not accepted. A flat enum { type: "string", enum: [...] } works. -const stringEnum = ( +const _stringEnum = ( values: T, options?: { description?: string }, ) => @@ -453,12 +453,7 @@ export function createBashTool( export const bashTool = createBashTool(); const processSchema = Type.Object({ - action: stringEnum( - ["list", "poll", "log", "write", "kill", "clear", "remove"] as const, - { - description: "Process action", - }, - ), + action: Type.String({ description: "Process action" }), sessionId: Type.Optional( Type.String({ description: "Session id for actions other than list" }), ), diff --git a/src/agents/pi-embedded-runner.test.ts b/src/agents/pi-embedded-runner.test.ts index 6d8a5f213..1704cd1ae 100644 --- a/src/agents/pi-embedded-runner.test.ts +++ b/src/agents/pi-embedded-runner.test.ts @@ -118,12 +118,11 @@ function createStubTool(name: string): AgentTool { } describe("splitSdkTools", () => { - // Tool names are now capitalized (Bash, Read, etc.) to bypass Anthropic OAuth blocking const tools = [ - createStubTool("Read"), - createStubTool("Bash"), - createStubTool("Edit"), - createStubTool("Write"), + createStubTool("read"), + createStubTool("bash"), + createStubTool("edit"), + createStubTool("write"), createStubTool("browser"), ]; @@ -134,27 +133,25 @@ describe("splitSdkTools", () => { }); expect(builtInTools).toEqual([]); expect(customTools.map((tool) => tool.name)).toEqual([ - "Read", - "Bash", - "Edit", - "Write", + "read", + "bash", + "edit", + "write", "browser", ]); }); - it("routes all tools to customTools even when not sandboxed (for OAuth compatibility)", () => { - // All tools are now passed as customTools to bypass pi-coding-agent's - // built-in tool filtering, which expects lowercase names. + it("routes all tools to customTools even when not sandboxed", () => { const { builtInTools, customTools } = splitSdkTools({ tools, sandboxEnabled: false, }); expect(builtInTools).toEqual([]); expect(customTools.map((tool) => tool.name)).toEqual([ - "Read", - "Bash", - "Edit", - "Write", + "read", + "bash", + "edit", + "write", "browser", ]); }); diff --git a/src/agents/pi-embedded-runner.ts b/src/agents/pi-embedded-runner.ts index 04b21cc43..46eb7e2e7 100644 --- a/src/agents/pi-embedded-runner.ts +++ b/src/agents/pi-embedded-runner.ts @@ -605,10 +605,8 @@ export function createSystemPromptOverride( return () => trimmed; } -// Tool names are now capitalized (Bash, Read, Write, Edit) to bypass Anthropic's -// OAuth token blocking of lowercase names. However, pi-coding-agent's SDK has -// hardcoded lowercase names in its built-in tool registry, so we must pass ALL -// tools as customTools to bypass the SDK's filtering. +// We always pass tools via `customTools` so our policy filtering, sandbox integration, +// and extended toolset remain consistent across providers. type AnyAgentTool = AgentTool; @@ -619,9 +617,8 @@ export function splitSdkTools(options: { builtInTools: AnyAgentTool[]; customTools: ReturnType; } { - // Always pass all tools as customTools to bypass pi-coding-agent's built-in - // tool filtering, which expects lowercase names (bash, read, write, edit). - // Our tools are now capitalized (Bash, Read, Write, Edit) for OAuth compatibility. + // Always pass all tools as customTools so the SDK doesn't "helpfully" swap in + // its own built-in implementations (we need our tool wrappers + policy). const { tools } = options; return { builtInTools: [], diff --git a/src/agents/pi-tools-agent-config.test.ts b/src/agents/pi-tools-agent-config.test.ts index 7e4ae4ab9..4ed26ff96 100644 --- a/src/agents/pi-tools-agent-config.test.ts +++ b/src/agents/pi-tools-agent-config.test.ts @@ -28,9 +28,9 @@ describe("Agent-specific tool filtering", () => { }); const toolNames = tools.map((t) => t.name); - expect(toolNames).toContain("Read"); - expect(toolNames).toContain("Write"); - expect(toolNames).not.toContain("Bash"); + expect(toolNames).toContain("read"); + expect(toolNames).toContain("write"); + expect(toolNames).not.toContain("bash"); }); it("should keep global tool policy when agent only sets tools.elevated", () => { @@ -62,9 +62,9 @@ describe("Agent-specific tool filtering", () => { }); const toolNames = tools.map((t) => t.name); - expect(toolNames).toContain("Bash"); - expect(toolNames).toContain("Read"); - expect(toolNames).not.toContain("Write"); + expect(toolNames).toContain("bash"); + expect(toolNames).toContain("read"); + expect(toolNames).not.toContain("write"); }); it("should apply agent-specific tool policy", () => { @@ -95,10 +95,10 @@ describe("Agent-specific tool filtering", () => { }); const toolNames = tools.map((t) => t.name); - expect(toolNames).toContain("Read"); - expect(toolNames).not.toContain("Bash"); - expect(toolNames).not.toContain("Write"); - expect(toolNames).not.toContain("Edit"); + expect(toolNames).toContain("read"); + expect(toolNames).not.toContain("bash"); + expect(toolNames).not.toContain("write"); + expect(toolNames).not.toContain("edit"); }); it("should allow different tool policies for different agents", () => { @@ -130,9 +130,9 @@ describe("Agent-specific tool filtering", () => { agentDir: "/tmp/agent-main", }); const mainToolNames = mainTools.map((t) => t.name); - expect(mainToolNames).toContain("Bash"); - expect(mainToolNames).toContain("Write"); - expect(mainToolNames).toContain("Edit"); + expect(mainToolNames).toContain("bash"); + expect(mainToolNames).toContain("write"); + expect(mainToolNames).toContain("edit"); // family agent: restricted const familyTools = createClawdbotCodingTools({ @@ -142,10 +142,10 @@ describe("Agent-specific tool filtering", () => { agentDir: "/tmp/agent-family", }); const familyToolNames = familyTools.map((t) => t.name); - expect(familyToolNames).toContain("Read"); - expect(familyToolNames).not.toContain("Bash"); - expect(familyToolNames).not.toContain("Write"); - expect(familyToolNames).not.toContain("Edit"); + expect(familyToolNames).toContain("read"); + expect(familyToolNames).not.toContain("bash"); + expect(familyToolNames).not.toContain("write"); + expect(familyToolNames).not.toContain("edit"); }); it("should prefer agent-specific tool policy over global", () => { @@ -176,7 +176,7 @@ describe("Agent-specific tool filtering", () => { const toolNames = tools.map((t) => t.name); // Agent policy overrides global: browser is allowed again expect(toolNames).toContain("browser"); - expect(toolNames).not.toContain("Bash"); + expect(toolNames).not.toContain("bash"); expect(toolNames).not.toContain("process"); }); @@ -247,9 +247,9 @@ describe("Agent-specific tool filtering", () => { // Agent policy should be applied first, then sandbox // Agent allows only "read", sandbox allows ["read", "write", "bash"] // Result: only "read" (most restrictive wins) - expect(toolNames).toContain("Read"); - expect(toolNames).not.toContain("Bash"); - expect(toolNames).not.toContain("Write"); + expect(toolNames).toContain("read"); + expect(toolNames).not.toContain("bash"); + expect(toolNames).not.toContain("write"); }); it("should run bash synchronously when process is denied", async () => { @@ -265,7 +265,7 @@ describe("Agent-specific tool filtering", () => { workspaceDir: "/tmp/test-main", agentDir: "/tmp/agent-main", }); - const bash = tools.find((tool) => tool.name === "Bash"); + const bash = tools.find((tool) => tool.name === "bash"); expect(bash).toBeDefined(); const result = await bash?.execute("call1", { diff --git a/src/agents/pi-tools.test.ts b/src/agents/pi-tools.test.ts index 8f34136af..e891f758e 100644 --- a/src/agents/pi-tools.test.ts +++ b/src/agents/pi-tools.test.ts @@ -170,8 +170,7 @@ describe("createClawdbotCodingTools", () => { it("includes bash and process tools", () => { const tools = createClawdbotCodingTools(); - // NOTE: bash/read/write/edit are capitalized to bypass Anthropic OAuth blocking - expect(tools.some((tool) => tool.name === "Bash")).toBe(true); + expect(tools.some((tool) => tool.name === "bash")).toBe(true); expect(tools.some((tool) => tool.name === "process")).toBe(true); }); @@ -213,9 +212,8 @@ describe("createClawdbotCodingTools", () => { expect(names.has("sessions_send")).toBe(false); expect(names.has("sessions_spawn")).toBe(false); - // NOTE: bash/read/write/edit are capitalized to bypass Anthropic OAuth blocking - expect(names.has("Read")).toBe(true); - expect(names.has("Bash")).toBe(true); + expect(names.has("read")).toBe(true); + expect(names.has("bash")).toBe(true); expect(names.has("process")).toBe(true); }); @@ -234,14 +232,12 @@ describe("createClawdbotCodingTools", () => { }, }, }); - // Tool names are capitalized for OAuth compatibility - expect(tools.map((tool) => tool.name)).toEqual(["Read"]); + expect(tools.map((tool) => tool.name)).toEqual(["read"]); }); it("keeps read tool image metadata intact", async () => { const tools = createClawdbotCodingTools(); - // NOTE: read is capitalized to bypass Anthropic OAuth blocking - const readTool = tools.find((tool) => tool.name === "Read"); + const readTool = tools.find((tool) => tool.name === "read"); expect(readTool).toBeDefined(); const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-read-")); @@ -281,8 +277,7 @@ describe("createClawdbotCodingTools", () => { it("returns text content without image blocks for text files", async () => { const tools = createClawdbotCodingTools(); - // NOTE: read is capitalized to bypass Anthropic OAuth blocking - const readTool = tools.find((tool) => tool.name === "Read"); + const readTool = tools.find((tool) => tool.name === "read"); expect(readTool).toBeDefined(); const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-read-")); @@ -337,10 +332,8 @@ describe("createClawdbotCodingTools", () => { }, }; const tools = createClawdbotCodingTools({ sandbox }); - // NOTE: bash/read are capitalized to bypass Anthropic OAuth blocking - // Policy matching is case-insensitive, so allow: ["bash"] matches tool named "Bash" - expect(tools.some((tool) => tool.name === "Bash")).toBe(true); - expect(tools.some((tool) => tool.name === "Read")).toBe(false); + expect(tools.some((tool) => tool.name === "bash")).toBe(true); + expect(tools.some((tool) => tool.name === "read")).toBe(false); expect(tools.some((tool) => tool.name === "browser")).toBe(false); }); @@ -370,18 +363,16 @@ describe("createClawdbotCodingTools", () => { }, }; const tools = createClawdbotCodingTools({ sandbox }); - // NOTE: read/write/edit are capitalized to bypass Anthropic OAuth blocking - expect(tools.some((tool) => tool.name === "Read")).toBe(true); - expect(tools.some((tool) => tool.name === "Write")).toBe(false); - expect(tools.some((tool) => tool.name === "Edit")).toBe(false); + expect(tools.some((tool) => tool.name === "read")).toBe(true); + expect(tools.some((tool) => tool.name === "write")).toBe(false); + expect(tools.some((tool) => tool.name === "edit")).toBe(false); }); it("filters tools by agent tool policy even without sandbox", () => { const tools = createClawdbotCodingTools({ config: { tools: { deny: ["browser"] } }, }); - // NOTE: bash is capitalized to bypass Anthropic OAuth blocking - expect(tools.some((tool) => tool.name === "Bash")).toBe(true); + expect(tools.some((tool) => tool.name === "bash")).toBe(true); expect(tools.some((tool) => tool.name === "browser")).toBe(false); }); diff --git a/src/agents/pi-tools.ts b/src/agents/pi-tools.ts index e68b530d7..0de7ed7bb 100644 --- a/src/agents/pi-tools.ts +++ b/src/agents/pi-tools.ts @@ -283,28 +283,6 @@ function normalizeToolNames(list?: string[]) { return list.map((entry) => entry.trim().toLowerCase()).filter(Boolean); } -/** - * Anthropic blocks specific lowercase tool names (bash, read, write, edit) with OAuth tokens. - * Renaming to capitalized versions bypasses the block while maintaining compatibility - * with regular API keys. - */ -const OAUTH_BLOCKED_TOOL_NAMES: Record = { - bash: "Bash", - read: "Read", - write: "Write", - edit: "Edit", -}; - -function renameBlockedToolsForOAuth(tools: AnyAgentTool[]): AnyAgentTool[] { - return tools.map((tool) => { - const newName = OAUTH_BLOCKED_TOOL_NAMES[tool.name]; - if (newName) { - return { ...tool, name: newName }; - } - return tool; - }); -} - const DEFAULT_SUBAGENT_TOOL_DENY = [ "sessions_list", "sessions_history", @@ -656,7 +634,8 @@ export function createClawdbotCodingTools(options?: { ) : normalized; - // Anthropic blocks specific lowercase tool names (bash, read, write, edit) with OAuth tokens. - // Always use capitalized versions for compatibility with both OAuth and regular API keys. - return renameBlockedToolsForOAuth(withAbort); + // NOTE: Keep canonical (lowercase) tool names here. + // pi-ai's Anthropic OAuth transport remaps tool names to Claude Code-style names + // on the wire and maps them back for tool dispatch. + return withAbort; } diff --git a/src/agents/system-prompt.test.ts b/src/agents/system-prompt.test.ts index 09b7c95cf..7d20383b1 100644 --- a/src/agents/system-prompt.test.ts +++ b/src/agents/system-prompt.test.ts @@ -46,6 +46,21 @@ describe("buildAgentSystemPrompt", () => { expect(prompt).toContain("sessions_send"); }); + it("preserves tool casing in the prompt", () => { + const prompt = buildAgentSystemPrompt({ + workspaceDir: "/tmp/clawd", + toolNames: ["Read", "Bash", "process"], + skillsPrompt: + "\n \n demo\n \n", + }); + + expect(prompt).toContain("- Read: Read file contents"); + expect(prompt).toContain("- Bash: Run shell commands"); + expect(prompt).toContain( + "Use `Read` to load the SKILL.md at the location listed for that skill.", + ); + }); + it("includes user time when provided", () => { const prompt = buildAgentSystemPrompt({ workspaceDir: "/tmp/clawd", diff --git a/src/agents/system-prompt.ts b/src/agents/system-prompt.ts index 9c78aa018..ead95e2c5 100644 --- a/src/agents/system-prompt.ts +++ b/src/agents/system-prompt.ts @@ -84,9 +84,19 @@ export function buildAgentSystemPrompt(params: { "image", ]; - const normalizedTools = (params.toolNames ?? []) - .map((tool) => tool.trim().toLowerCase()) - .filter(Boolean); + const rawToolNames = (params.toolNames ?? []).map((tool) => tool.trim()); + const canonicalToolNames = rawToolNames.filter(Boolean); + const canonicalByNormalized = new Map(); + for (const name of canonicalToolNames) { + const normalized = name.toLowerCase(); + if (!canonicalByNormalized.has(normalized)) { + canonicalByNormalized.set(normalized, name); + } + } + const resolveToolName = (normalized: string) => + canonicalByNormalized.get(normalized) ?? normalized; + + const normalizedTools = canonicalToolNames.map((tool) => tool.toLowerCase()); const availableTools = new Set(normalizedTools); const extraTools = Array.from( new Set(normalizedTools.filter((tool) => !toolOrder.includes(tool))), @@ -94,13 +104,17 @@ export function buildAgentSystemPrompt(params: { const enabledTools = toolOrder.filter((tool) => availableTools.has(tool)); const toolLines = enabledTools.map((tool) => { const summary = toolSummaries[tool]; - return summary ? `- ${tool}: ${summary}` : `- ${tool}`; + const name = resolveToolName(tool); + return summary ? `- ${name}: ${summary}` : `- ${name}`; }); for (const tool of extraTools.sort()) { - toolLines.push(`- ${tool}`); + toolLines.push(`- ${resolveToolName(tool)}`); } const hasGateway = availableTools.has("gateway"); + const readToolName = resolveToolName("read"); + const bashToolName = resolveToolName("bash"); + const processToolName = resolveToolName("process"); const extraSystemPrompt = params.extraSystemPrompt?.trim(); const ownerNumbers = (params.ownerNumbers ?? []) .map((value) => value.trim()) @@ -143,7 +157,7 @@ export function buildAgentSystemPrompt(params: { const skillsSection = skillsPrompt ? [ "## Skills", - "Skills provide task-specific instructions. Use `read` to load the SKILL.md at the location listed for that skill.", + `Skills provide task-specific instructions. Use \`${readToolName}\` to load the SKILL.md at the location listed for that skill.`, ...skillsLines, "", ] @@ -154,6 +168,7 @@ export function buildAgentSystemPrompt(params: { "", "## Tooling", "Tool availability (filtered by policy):", + "Tool names are case-sensitive. Call tools exactly as listed.", toolLines.length > 0 ? toolLines.join("\n") : [ @@ -161,8 +176,8 @@ export function buildAgentSystemPrompt(params: { "- grep: search file contents for patterns", "- find: find files by glob pattern", "- ls: list directory contents", - "- bash: run shell commands (supports background via yieldMs/background)", - "- process: manage background bash sessions", + `- ${bashToolName}: run shell commands (supports background via yieldMs/background)`, + `- ${processToolName}: manage background bash sessions`, "- whatsapp_login: generate a WhatsApp QR code and wait for linking", "- browser: control clawd's dedicated browser", "- canvas: present/eval/snapshot the Canvas",