diff --git a/packages/codemode/src/tool-runtime.ts b/packages/codemode/src/tool-runtime.ts index fa3ddc6c2f..d79d0182b5 100644 --- a/packages/codemode/src/tool-runtime.ts +++ b/packages/codemode/src/tool-runtime.ts @@ -458,8 +458,8 @@ export const discoveryPlan = ( // 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 `.` 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 = ( : 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 = ( ...(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..(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 { : data. }` - 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: "" })` - 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: "" })`.', "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..(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 { : data. }` - raw payloads get truncated and waste context.", ]), @@ -504,7 +505,7 @@ export const discoveryPlan = ( : "- 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` 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..(item)))`.", + '- Run independent calls in parallel: `await Promise.all(items.map((item) => tools..(item)))`, or use `tools.["tool-name"](item)` when the listed signature uses bracket notation.', "- `Object.keys(tools)` lists namespaces; `Object.keys(tools.)` lists its tools; `for...in` works on both.", ...(complete ? [] diff --git a/packages/codemode/test/codemode.test.ts b/packages/codemode/test/codemode.test.ts index eaa834e0a4..4fda88b384 100644 --- a/packages/codemode/test/codemode.test.ts +++ b/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..(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 ./ 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 { : data. }`") 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: "" })` - 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: "" })`.', ) expect(partial).toContain( "Only tools listed here or returned by `tools.$codemode.search` are available inside `tools`", diff --git a/packages/codemode/test/signature.test.ts b/packages/codemode/test/signature.test.ts index 9c45371d93..edf4c442c4 100644 --- a/packages/codemode/test/signature.test.ts +++ b/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', + ) + 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: {') + }) +})