Browse Source

fix(codemode): require exact tool paths in guidance (#35224)

Aiden Cline 1 month ago
parent
commit
438654768c

+ 7 - 6
packages/codemode/src/tool-runtime.ts

@@ -458,8 +458,8 @@ export const discoveryPlan = <R>(
 
   // Section order is deliberate: workflow first (the top is the least likely part of a long
   // description to be truncated or skimmed away), then rules, then syntax, with the budgeted
-  // catalog at the bottom. Example call forms use explicit `<namespace>.<tool>` placeholders -
-  // never a real or fabricated tool name.
+  // catalog at the bottom. Example call forms use placeholders - never a real or fabricated
+  // tool name - and show both dot and bracket notation so non-identifier names are not normalized.
   const intro = [
     "Write a CodeMode program to answer the request. Return code only.",
     empty
@@ -467,6 +467,7 @@ export const discoveryPlan = <R>(
       : complete
         ? "Execute JavaScript in a confined runtime. Inside this program, `tools` contains only the host-provided tools listed below; surrounding agent tools are not available unless listed here."
         : "Execute JavaScript in a confined runtime. Inside this program, `tools` contains only the host-provided tools listed or searchable below; surrounding agent tools are not available unless listed here.",
+    ...(empty ? [] : ["Do not infer or normalize tool names; use only exact signatures shown below or returned by search."]),
   ]
 
   // The search step exists only when search is advertised (PARTIAL catalog); a COMPLETE
@@ -480,14 +481,14 @@ export const discoveryPlan = <R>(
         ...(complete
           ? [
               "1. Pick a tool from the list under `## Available tools` - each line is the exact call signature; use it as-is rather than guessing segments.",
-              "2. Call it using the exact signature shown: `const res = await tools.<namespace>.<tool>(input)` - bracket notation may appear for names that are not JavaScript identifiers.",
+              '2. Call it using the exact signature shown; bracket notation and quotes are part of the path.',
               '3. Parse text results: `const data = typeof res === "string" ? JSON.parse(res) : res` - most tools return JSON as a string.',
               "4. Return only the fields you need: `return { <field>: data.<field> }` - raw payloads get truncated and waste context.",
             ]
           : [
-              '1. Find a tool (skip when it is already listed below): `const { items } = await tools.$codemode.search({ query: "<intent + key nouns>" })` - short phrases like "list issues" work best.',
+              '1. If the exact signature is not listed below, first search: `const { items } = await tools.$codemode.search({ query: "<intent + key nouns>" })`.',
               "2. Read the matches: each item is `{ path, description, signature }` - read the description before using an unfamiliar tool.",
-              "3. Call it with the result's `path` as-is (never guess segments): `const res = await tools.<namespace>.<tool>(input)` - bracket notation may appear for names that are not JavaScript identifiers.",
+              "3. Call the result's `path` as-is; bracket notation and quotes are part of the path.",
               '4. Parse text results: `const data = typeof res === "string" ? JSON.parse(res) : res` - most tools return JSON as a string.',
               "5. Return only the fields you need: `return { <field>: data.<field> }` - raw payloads get truncated and waste context.",
             ]),
@@ -504,7 +505,7 @@ export const discoveryPlan = <R>(
           : "- Only tools listed here or returned by `tools.$codemode.search` are available inside `tools`; tools from the surrounding agent/runtime are not implicitly exposed.",
         "- Filter, aggregate, and transform collections in code - never return them raw or call a tool per item across messages.",
         "- A result typed `Promise<unknown>` has no guaranteed shape - verify what actually came back before relying on its fields.",
-        "- Run independent calls in parallel: `await Promise.all(items.map((item) => tools.<namespace>.<tool>(item)))`.",
+        '- Run independent calls in parallel: `await Promise.all(items.map((item) => tools.<namespace>.<tool>(item)))`, or use `tools.<namespace>["tool-name"](item)` when the listed signature uses bracket notation.',
         "- `Object.keys(tools)` lists namespaces; `Object.keys(tools.<namespace>)` lists its tools; `for...in` works on both.",
         ...(complete
           ? []

+ 5 - 5
packages/codemode/test/codemode.test.ts

@@ -622,12 +622,12 @@ describe("CodeMode public contract", () => {
     )
     expect(instructions).toContain("Return only the fields you need")
     expect(instructions).toContain("raw payloads get truncated and waste context")
-    expect(instructions).toContain("`const res = await tools.<namespace>.<tool>(input)`")
+    expect(instructions).toContain("Do not infer or normalize tool names")
+    expect(instructions).toContain("bracket notation and quotes are part of the path")
     expect(instructions).toContain("surrounding agent tools are not available unless listed here")
     expect(instructions).toContain("Only tools listed here are available inside `tools`")
-    expect(instructions).toContain("bracket notation may appear for names that are not JavaScript identifiers")
-    // Placeholders use the <namespace>.<tool>/<field> style ONLY - no fabricated tool
-    // names, and no real catalog tools cherry-picked into example lines.
+    // Placeholders use generic namespace/tool/field names only - no fabricated real tools
+    // and no real catalog tools cherry-picked into example lines.
     expect(instructions).toContain("`return { <field>: data.<field> }`")
     expect(instructions).not.toContain("total_count")
     expect(instructions).not.toContain("list_issues")
@@ -640,7 +640,7 @@ describe("CodeMode public contract", () => {
     // PARTIAL: the workflow starts with search (with query-style guidance that is clearly
     // a query string, never a tool name) and the browse-namespace rule appears.
     expect(partial).toContain(
-      '1. Find a tool (skip when it is already listed below): `const { items } = await tools.$codemode.search({ query: "<intent + key nouns>" })` - short phrases like "list issues" work best.',
+      '1. If the exact signature is not listed below, first search: `const { items } = await tools.$codemode.search({ query: "<intent + key nouns>" })`.',
     )
     expect(partial).toContain(
       "Only tools listed here or returned by `tools.$codemode.search` are available inside `tools`",

+ 40 - 0
packages/codemode/test/signature.test.ts

@@ -339,3 +339,43 @@ describe("pretty signatures in search results", () => {
     expect(instructions).not.toContain("/**")
   })
 })
+
+describe("non-identifier tool paths", () => {
+  const resolveLibrary = Tool.make({
+    description: "Resolve a Context7 library ID",
+    input: {
+      type: "object",
+      properties: {
+        query: { type: "string" },
+        libraryName: { type: "string" },
+      },
+      required: ["query", "libraryName"],
+    } as const,
+    run: () => Effect.succeed("/reactjs/react.dev"),
+  })
+  const runtime = CodeMode.make({ tools: { context7: { "resolve-library-id": resolveLibrary } } })
+
+  test("inline catalog uses bracket notation for dashed tool names", () => {
+    const instructions = runtime.instructions()
+
+    expect(instructions).toContain(
+      'tools.context7["resolve-library-id"](input: { query: string; libraryName: string }): Promise<unknown>',
+    )
+    expect(instructions).toContain("Do not infer or normalize tool names")
+    expect(instructions).toContain("bracket notation and quotes are part of the path")
+    expect(instructions).not.toContain("tools.context7.resolve-library-id")
+    expect(instructions).not.toContain("tools.context7.resolve_library_id")
+  })
+
+  test("search results return callable bracket-notation paths and signatures", async () => {
+    const result = await Effect.runPromise(
+      runtime.execute(`return await tools.$codemode.search({ query: "resolve library" })`),
+    )
+    expect(result.ok).toBe(true)
+    if (!result.ok) throw new Error("search failed")
+
+    const value = result.value as { items: Array<{ path: string; signature: string }> }
+    expect(value.items[0]?.path).toBe('tools.context7["resolve-library-id"]')
+    expect(value.items[0]?.signature).toContain('tools.context7["resolve-library-id"](input: {')
+  })
+})