From 6c2ccab9513e0fd12c9ca30275f5fc4948c2ef3c Mon Sep 17 00:00:00 2001 From: Andrew Date: Mon, 31 Aug 2026 13:58:40 -0700 Subject: [PATCH] fix(codemode): recover tool names that miss by namespace convention Co-Authored-By: Claude Fable 5 --- packages/codemode/src/tool-runtime.ts | 49 +++++++++++++++++++++-- packages/codemode/test/codemode.test.ts | 53 +++++++++++++++++++++++++ 2 files changed, 98 insertions(+), 4 deletions(-) diff --git a/packages/codemode/src/tool-runtime.ts b/packages/codemode/src/tool-runtime.ts index f4ccc61d4c49..701acd3e9245 100644 --- a/packages/codemode/src/tool-runtime.ts +++ b/packages/codemode/src/tool-runtime.ts @@ -673,7 +673,42 @@ const namespaceKeys = (tools: HostTools, path: ReadonlyArray): Rea return Object.keys(value) } -const resolve = (tools: HostTools, path: ReadonlyArray): HostTool | Definition => { +/** Walks a path to a callable tool, returning undefined instead of throwing when it misses. */ +const lookup = (tools: HostTools, path: ReadonlyArray): HostTool | Definition | undefined => { + let value: HostTool | Definition | HostTools = tools + for (const segment of path) { + if ( + isBlockedMember(segment) || + typeof value === "function" || + isDefinition(value) || + !Object.hasOwn(value, segment) + ) + return undefined + value = value[segment] + } + return typeof value === "function" || isDefinition(value) ? value : undefined +} + +type Resolved = { readonly tool: HostTool | Definition; readonly path: ReadonlyArray } + +/** Real tools a missed path plausibly meant: its namespace repeated inside the leaf, or the leaf under another namespace. */ +const recoveryCandidates = (tools: HostTools, path: ReadonlyArray): Array> => { + const leaf = path.at(-1) + const parent = path.at(-2) + if (leaf === undefined || parent === undefined) return [] + const guesses: Array> = [] + if (leaf.startsWith(`${parent}_`)) guesses.push([...path.slice(0, -1), leaf.slice(parent.length + 1)]) + if (path.length === 2) { + for (const key of Object.keys(tools)) if (key !== parent) guesses.push([key, leaf]) + } + return guesses.flatMap((guess) => { + const tool = lookup(tools, guess) + return tool === undefined ? [] : [{ tool, path: guess }] + }) +} + +/** Resolves a written path to its tool, recovering a unique convention miss under the real path. */ +const resolve = (tools: HostTools, path: ReadonlyArray): Resolved => { let value: HostTool | Definition | HostTools = tools for (const segment of path) { @@ -683,7 +718,12 @@ const resolve = (tools: HostTools, path: ReadonlyArray): HostTool< isDefinition(value) || !Object.hasOwn(value, segment) ) { + const candidates = recoveryCandidates(tools, path) + if (candidates.length === 1 && candidates[0] !== undefined) return candidates[0] throw new ToolRuntimeError("UnknownTool", `Unknown tool '${path.join(".")}'.`, [ + ...(candidates.length > 1 + ? [`Did you mean ${candidates.map((candidate) => `tools.${candidate.path.join(".")}`).join(" or ")}?`] + : []), "Use tools.$codemode.search({ query }) to find available described tools.", ]) } @@ -694,7 +734,7 @@ const resolve = (tools: HostTools, path: ReadonlyArray): HostTool< throw new ToolRuntimeError("UnknownTool", `Tool '${path.join(".")}' is not callable.`) } - return value + return { tool: value, path } } export type ToolRuntime = { @@ -758,7 +798,9 @@ export const make = ( keys: (path) => namespaceKeys(callableTools, path), invoke: (path, args) => Effect.gen(function* () { - const name = path.join(".") + const { tool, path: resolvedPath } = resolve(callableTools, path) + // The resolved path, not the written one, so telemetry names the tool that actually ran. + const name = resolvedPath.join(".") const externalArgs = args.map((arg) => copyOut(copyIn(arg, `Arguments for tool '${name}'`))) const call = { name } const recordAndObserve = (input: unknown) => @@ -766,7 +808,6 @@ export const make = ( recordCall(call) return calls.length - 1 }).pipe(Effect.tap((index) => hooks?.onToolCallStart?.({ index, name, input }) ?? Effect.void)) - const tool = resolve(callableTools, path) let describedInput: unknown if (isDefinition(tool)) { if (externalArgs.length !== 1) diff --git a/packages/codemode/test/codemode.test.ts b/packages/codemode/test/codemode.test.ts index 221b5e07dfe1..97f9f836d111 100644 --- a/packages/codemode/test/codemode.test.ts +++ b/packages/codemode/test/codemode.test.ts @@ -1161,3 +1161,56 @@ describe("CodeMode public contract", () => { expect(() => CodeMode.make({ tools: { $codemode: { lookup } } })).toThrow(/reserved for CodeMode discovery tools/) }) }) + +describe("tool name recovery", () => { + const echo = Tool.make({ + description: "Echo the value", + input: Schema.Struct({ value: Schema.String }), + output: Schema.String, + run: ({ value }) => Effect.succeed(value), + }) + const tools = { + bedrock: { generate_image: echo, bedrock_database_query_records: echo, shopify_products_search: echo }, + alpha: { shared_tool: echo }, + beta: { shared_tool: echo }, + } + + test("recovers convention-missed tool names and records the tool that actually ran", async () => { + const calls: Array = [] + const runtime = CodeMode.make({ + tools, + onToolCallStart: ({ name }) => + Effect.sync(() => { + calls.push(name) + }), + }) + + const result = await Effect.runPromise( + runtime.execute(`return [ + await tools.bedrock.bedrock_generate_image({ value: "a" }), + await tools.shopify.shopify_products_search({ value: "b" }), + await tools.bedrock.bedrock_database_query_records({ value: "c" }), + ]`), + ) + + expect(result.ok).toBe(true) + if (result.ok) expect(result.value).toStrictEqual(["a", "b", "c"]) + expect(calls).toStrictEqual([ + "bedrock.generate_image", + "bedrock.shopify_products_search", + "bedrock.bedrock_database_query_records", + ]) + }) + + test("refuses to guess between equally plausible tools and names them", async () => { + const result = await Effect.runPromise( + CodeMode.execute({ tools, code: `return await tools.gamma.shared_tool({ value: "x" })` }), + ) + + expect(result.ok).toBe(false) + if (!result.ok) { + expect(result.error.kind).toBe("UnknownTool") + expect(result.error.suggestions?.join(" ")).toContain("tools.alpha.shared_tool or tools.beta.shared_tool") + } + }) +})