From 3082d6c9e29980bac49fccb8042497be12439e14 Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Tue, 20 Oct 2026 22:57:19 -0500 Subject: [PATCH 1/2] fix(core): tell Code Mode models built-in tools stay top-level --- packages/codemode/src/tool-runtime.ts | 25 +++++++++++++++++++ packages/codemode/test/tool-paths.test.ts | 7 ++++++ packages/core/src/codemode/instructions.ts | 8 +++--- packages/core/test/codemode/catalog.test.ts | 21 +++++++++------- .../core/test/codemode/instructions.test.ts | 8 +++--- 5 files changed, 54 insertions(+), 15 deletions(-) diff --git a/packages/codemode/src/tool-runtime.ts b/packages/codemode/src/tool-runtime.ts index 9c179bbfe070..b3e4f2f971ff 100644 --- a/packages/codemode/src/tool-runtime.ts +++ b/packages/codemode/src/tool-runtime.ts @@ -330,11 +330,36 @@ const namespaceKeys = (root: ToolNode, path: ReadonlyArray): Reado return Array.from(node.children.keys()) } +// Built-in tool names models reach for inside scripts. +const TOP_LEVEL_TOOLS = new Set([ + "read", + "write", + "edit", + "patch", + "shell", + "bash", + "grep", + "glob", + "webfetch", + "websearch", + "todowrite", + "question", + "skill", + "subagent", + "task", +]) + const resolve = (root: ToolNode, path: ReadonlyArray, index: ReadonlyArray): Tool => { const segments = canonicalSegments(path) const node = lookup(root, segments) if (node === undefined) { const name = segments.join(".") + if (!root.children.has(segments[0]) && TOP_LEVEL_TOOLS.has(segments[0])) { + throw new ToolRuntimeError( + "UnknownTool", + `'${segments[0]}' is not a Code Mode tool. It is one of your regular tools: call ${segments[0]} directly as a normal tool call, outside \`execute\`. It does not exist inside \`execute\`.`, + ) + } const ns = segments.length > 1 && root.children.has(segments[0]) ? segments[0] : undefined const closest = rank( ns ? index.filter((entry) => entry.description.path.startsWith(`${ns}.`)) : index, diff --git a/packages/codemode/test/tool-paths.test.ts b/packages/codemode/test/tool-paths.test.ts index 13d9869af6dc..c698034064cc 100644 --- a/packages/codemode/test/tool-paths.test.ts +++ b/packages/codemode/test/tool-paths.test.ts @@ -114,6 +114,13 @@ describe("callable namespaces", () => { expect(diagnostic.message).toBe("Unknown tool 'issues.get-list'. Did you mean tools.issues.list?") }) + test("a built-in tool name points back to a direct call", async () => { + const diagnostic = await failure(runtime, `return await tools.read({ path: "README.md" })`) + expect(diagnostic.kind).toBe("UnknownTool") + expect(diagnostic.message).toContain("call read directly as a normal tool call, outside `execute`") + expect(diagnostic.suggestions).toBeUndefined() + }) + test("a namespace without its own tool stays non-callable", async () => { const nested = CodeMode.make({ tools: { "issues.list": echo("List issues", "list") } }) const diagnostic = await failure(nested, `return await tools.issues({})`) diff --git a/packages/core/src/codemode/instructions.ts b/packages/core/src/codemode/instructions.ts index 859c941f2759..093847878076 100644 --- a/packages/core/src/codemode/instructions.ts +++ b/packages/core/src/codemode/instructions.ts @@ -8,13 +8,15 @@ import { CodeModeCatalog } from "./catalog.js" // prettier-ignore const prompt = (hasMoreTools: boolean) => `# Code Mode -Use the \`execute\` tool to call the tools listed below. They cannot be called directly${hasMoreTools ? ", and neither can \`search\`. Both" : ". They"} only work inside code you pass to \`execute\`. +Code Mode adds the extra tools listed below, which you call from JavaScript inside the \`execute\` tool. Your regular tools (such as \`read\`, \`edit\`, \`shell\` and \`grep\`) are separate: keep calling them directly as usual. They do not exist inside \`execute\`. -${hasMoreTools ? `The catalog is partial. Inside \`execute\`, use \`search(...)\` to find a tool, then call it by the \`path\` in the result. \`search\` is synchronous. Call it without \`await\`; it does not return a Promise. Do not guess tool names. +The tools listed below cannot be called directly${hasMoreTools ? ", and neither can \`search\`. Both" : ". They"} only work inside code you pass to \`execute\`. + +${hasMoreTools ? `The catalog is partial. Before calling a tool that is not listed below, find it with the global \`search(...)\` function (not \`tools.search\`). Do not guess tool names. \`search\` is synchronous. Call it without \`await\`; it does not return a Promise. Each result's \`path\` is the complete call expression: write it out as code, for example \`await tools.ns.tool_name({ ... })\`. Do not use it as a key such as \`tools[path]\`. - ${searchSignature}` : "The catalog is complete. Do not guess tool names."} -## Available tools` +## Code Mode tools (call only inside \`execute\`)` export function render(catalog: CodeModeCatalog.Summary) { if (catalog.total === 0) diff --git a/packages/core/test/codemode/catalog.test.ts b/packages/core/test/codemode/catalog.test.ts index 520ae8e89609..32f84b343326 100644 --- a/packages/core/test/codemode/catalog.test.ts +++ b/packages/core/test/codemode/catalog.test.ts @@ -117,21 +117,24 @@ describe("CodeModeCatalog.summarize", () => { describe("CodeModeInstructions.render", () => { test("inlines complete catalogs without search guidance", () => { const instructions = render([lookup]) - expect(instructions).toContain("## Available tools") + expect(instructions).toContain("## Code Mode tools (call only inside `execute`)") expect(instructions).toContain("- orders (1 tool)") expect(instructions).toContain(` - ${lookup.signature} // Look up an order by ID`) expect(instructions).not.toContain("search") - expect(instructions).toContain("They cannot be called directly. They only work inside code you pass to `execute`.") + expect(instructions).toContain("Your regular tools (such as `read`, `edit`, `shell` and `grep`) are separate") + expect(instructions).toContain( + "The tools listed below cannot be called directly. They only work inside code you pass to `execute`.", + ) expect(instructions).toContain("The catalog is complete. Do not guess tool names.") }) test("adds search guidance when the catalog exceeds the budget", () => { const partial = render([lookup], 0) - expect(partial).toContain("## Available tools") + expect(partial).toContain("## Code Mode tools (call only inside `execute`)") expect(partial).toContain("- orders (1 tool, none shown)") - expect(partial).toContain("They cannot be called directly, and neither can `search`.") + expect(partial).toContain("The tools listed below cannot be called directly, and neither can `search`.") expect(partial).toContain( - "The catalog is partial. Inside `execute`, use `search(...)` to find a tool, then call it by the `path` in the result. `search` is synchronous. Call it without `await`; it does not return a Promise.", + "The catalog is partial. Before calling a tool that is not listed below, find it with the global `search(...)` function (not `tools.search`). Do not guess tool names. `search` is synchronous. Call it without `await`; it does not return a Promise.", ) expect(partial).toContain("- search({") expect(partial).toContain(" /** @integer @exclusiveMinimum 0 */\n limit?: number,") @@ -194,7 +197,7 @@ describe("CodeModeInstructions.update", () => { `Changed tool listings supersede the previously listed ones:\n - ${changed.signature} // Echo text`, ) expect(text).toContain("The following tools are no longer available and must not be called: tools.orders.lookup.") - expect(text).not.toContain("## Available tools") + expect(text).not.toContain("## Code Mode tools (call only inside `execute`)") }) test("names removed tools with exact callable expressions including bracket notation", () => { @@ -225,14 +228,14 @@ describe("CodeModeInstructions.update", () => { "The Code Mode tool catalog has changed. This catalog supersedes the previous Code Mode tool catalog.", ) expect(text).toContain("The catalog is partial.") - expect(text).toContain("## Available tools") + expect(text).toContain("## Code Mode tools (call only inside `execute`)") }) test("falls back to full replacement when the delta is larger than the catalog", () => { const previous = Array.from({ length: 200 }, (_, index) => entry(`bulk.tool${index}`, `Tool ${index}`)) const text = update([...previous, echo], [echo]) expect(text).toContain("This catalog supersedes the previous Code Mode tool catalog.") - expect(text).toContain("## Available tools") + expect(text).toContain("## Code Mode tools (call only inside `execute`)") expect(text).toContain("The catalog is complete.") expect(text).not.toContain("The following tools are no longer available") }) @@ -243,6 +246,6 @@ describe("CodeModeInstructions.update", () => { expect(text).toContain("`alpha` now has 11 tools") expect(text).toContain("search them again before relying on previous results") expect(text).not.toContain("tools.alpha.tool10(input:") - expect(text).not.toContain("## Available tools") + expect(text).not.toContain("## Code Mode tools (call only inside `execute`)") }) }) diff --git a/packages/core/test/codemode/instructions.test.ts b/packages/core/test/codemode/instructions.test.ts index 5c6f760f4037..b7df6051f6cc 100644 --- a/packages/core/test/codemode/instructions.test.ts +++ b/packages/core/test/codemode/instructions.test.ts @@ -46,16 +46,18 @@ describe("CodeModeInstructions", () => { it.effect("renders the initial catalog, semantic deltas, and removal", () => Effect.gen(function* () { const initialized = yield* readInitial(CodeModeInstructions.make({ tools: [echo] })) - expect(initialized.text).toContain("Use the `execute` tool to call the tools listed below.") + expect(initialized.text).toContain( + "Code Mode adds the extra tools listed below, which you call from JavaScript inside the `execute` tool.", + ) expect(initialized.text).toContain("The catalog is complete.") - expect(initialized.text).toContain("## Available tools") + expect(initialized.text).toContain("## Code Mode tools (call only inside `execute`)") expect(initialized.text).toContain(` - ${echo.signature} // Echo text`) const added = yield* readUpdate(CodeModeInstructions.make({ tools: [echo, lookup] }), initialized) expect(added.text).toContain("The Code Mode tool catalog has changed.") expect(added.text).toContain("New tools are available in addition to those previously listed:") expect(added.text).toContain(` - ${lookup.signature} // Look up an order`) - expect(added.text).not.toContain("## Available tools") + expect(added.text).not.toContain("## Code Mode tools (call only inside `execute`)") const removed = yield* readUpdate(CodeModeInstructions.make({ tools: [echo] }), { values: added.values }) expect(removed.text).toBe( From 081ff9dccbcec2c84700dfe01cb270e2e37b5b49 Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Tue, 20 Oct 2026 23:08:32 -0500 Subject: [PATCH 2/2] fix(core): keep Code Mode change to the prompt wording --- packages/codemode/src/tool-runtime.ts | 25 ----------------------- packages/codemode/test/tool-paths.test.ts | 7 ------- 2 files changed, 32 deletions(-) diff --git a/packages/codemode/src/tool-runtime.ts b/packages/codemode/src/tool-runtime.ts index b3e4f2f971ff..9c179bbfe070 100644 --- a/packages/codemode/src/tool-runtime.ts +++ b/packages/codemode/src/tool-runtime.ts @@ -330,36 +330,11 @@ const namespaceKeys = (root: ToolNode, path: ReadonlyArray): Reado return Array.from(node.children.keys()) } -// Built-in tool names models reach for inside scripts. -const TOP_LEVEL_TOOLS = new Set([ - "read", - "write", - "edit", - "patch", - "shell", - "bash", - "grep", - "glob", - "webfetch", - "websearch", - "todowrite", - "question", - "skill", - "subagent", - "task", -]) - const resolve = (root: ToolNode, path: ReadonlyArray, index: ReadonlyArray): Tool => { const segments = canonicalSegments(path) const node = lookup(root, segments) if (node === undefined) { const name = segments.join(".") - if (!root.children.has(segments[0]) && TOP_LEVEL_TOOLS.has(segments[0])) { - throw new ToolRuntimeError( - "UnknownTool", - `'${segments[0]}' is not a Code Mode tool. It is one of your regular tools: call ${segments[0]} directly as a normal tool call, outside \`execute\`. It does not exist inside \`execute\`.`, - ) - } const ns = segments.length > 1 && root.children.has(segments[0]) ? segments[0] : undefined const closest = rank( ns ? index.filter((entry) => entry.description.path.startsWith(`${ns}.`)) : index, diff --git a/packages/codemode/test/tool-paths.test.ts b/packages/codemode/test/tool-paths.test.ts index c698034064cc..13d9869af6dc 100644 --- a/packages/codemode/test/tool-paths.test.ts +++ b/packages/codemode/test/tool-paths.test.ts @@ -114,13 +114,6 @@ describe("callable namespaces", () => { expect(diagnostic.message).toBe("Unknown tool 'issues.get-list'. Did you mean tools.issues.list?") }) - test("a built-in tool name points back to a direct call", async () => { - const diagnostic = await failure(runtime, `return await tools.read({ path: "README.md" })`) - expect(diagnostic.kind).toBe("UnknownTool") - expect(diagnostic.message).toContain("call read directly as a normal tool call, outside `execute`") - expect(diagnostic.suggestions).toBeUndefined() - }) - test("a namespace without its own tool stays non-callable", async () => { const nested = CodeMode.make({ tools: { "issues.list": echo("List issues", "list") } }) const diagnostic = await failure(nested, `return await tools.issues({})`)