From ea47407a15249d42546c6dcca71f5cdfc47aa6f0 Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Wed, 16 Sep 2026 00:26:16 -0400 Subject: [PATCH] fix(codemode): reuse tool discovery catalogs from OpenCode --- packages/codemode/src/codemode.ts | 4 ++- packages/codemode/src/tool-runtime.ts | 46 ++++++++++++++++++--------- packages/core/src/tool.ts | 24 ++++++++++---- 3 files changed, 52 insertions(+), 22 deletions(-) diff --git a/packages/codemode/src/codemode.ts b/packages/codemode/src/codemode.ts index 7bd7496a21b4..edae39aeebf9 100644 --- a/packages/codemode/src/codemode.ts +++ b/packages/codemode/src/codemode.ts @@ -156,7 +156,9 @@ export const make = = {}>( } } return { - catalog: prepared.catalog, + get catalog() { + return prepared.catalog + }, execute: (code) => executeProgram(code, prepared, limits, options.hooks ?? {}, (ctx) => extensionGlobals(ctx, extensions)), } diff --git a/packages/codemode/src/tool-runtime.ts b/packages/codemode/src/tool-runtime.ts index 5e68fc745c12..a2e27cb304b2 100644 --- a/packages/codemode/src/tool-runtime.ts +++ b/packages/codemode/src/tool-runtime.ts @@ -166,15 +166,24 @@ const flattenTools = ( ] } -const describeTool = (visible: VisibleTool): ToolDescription => ({ - path: visible.path, - description: visible.tool.description, - signature: isEmptyInput(visible.tool) - ? `${toolExpression(visible.path)}(): Promise<${outputTypeScript(visible.tool, true)}>` - : `${toolExpression(visible.path)}(input: ${inputTypeScript(visible.tool, true)}): Promise<${outputTypeScript(visible.tool, true)}>`, -}) +const describeTool = (visible: VisibleTool): ToolDescription => { + let signature: string | undefined + return { + path: visible.path, + description: visible.tool.description, + get signature() { + // Search ranks paths and descriptions first; only returned matches need their schemas rendered. + // Joining the final fragments avoids retaining the rendering's intermediate string ropes in JSC. + return (signature ??= [ + toolExpression(visible.path), + isEmptyInput(visible.tool) ? "()" : `(input: ${inputTypeScript(visible.tool, true)})`, + `: Promise<${outputTypeScript(visible.tool, true)}>`, + ].join("")) + }, + } +} -/** Tools indexed once per runtime: the lookup trie plus the model-facing catalog and search index. */ +/** Tools indexed once per runtime, with discovery materialized on demand. */ export type Prepared = { readonly root: ToolNode readonly catalog: ReadonlyArray @@ -286,12 +295,20 @@ const toSearchEntry = (visible: VisibleTool): SearchEntry => ({ export const prepare = (tools: Tools): Prepared => { const root = toolTrie(tools) - // Discovery bytes are durable instructions, so order only after canonical-path collisions settle. - const visible = flattenTools(root).sort((left, right) => compareText(left.path, right.path)) + let searchIndex: ReadonlyArray | undefined + let catalog: ReadonlyArray | undefined return { root, - catalog: visible.map(describeTool), - searchIndex: visible.map(toSearchEntry), + get catalog() { + return (catalog ??= this.searchIndex.map((entry) => entry.description)) + }, + get searchIndex() { + // Executing known tools only needs the trie. Render discovery when it is actually read, + // ordering after canonical-path collisions settle so instruction bytes stay deterministic. + return (searchIndex ??= flattenTools(root) + .sort((left, right) => compareText(left.path, right.path)) + .map(toSearchEntry)) + }, } } @@ -355,7 +372,6 @@ export const make = ( ): ToolRuntime => { const calls: Array = [] const root = prepared.root - const searchTool = makeSearchTool(prepared.searchIndex) const recordCall = (call: ToolCall): void => { if (maxToolCalls !== undefined && calls.length >= maxToolCalls) { @@ -412,13 +428,13 @@ export const make = ( calls, hooks, keys: (path) => namespaceKeys(root, path), - search: (args) => Effect.suspend(() => executeTool("search", searchTool, args)), + search: (args) => Effect.suspend(() => executeTool("search", makeSearchTool(prepared.searchIndex), args)), execute: (path, args) => Effect.suspend(() => { const segments = canonicalSegments(path) // Models often write `tools.search(...)` for the bare `search(...)`; honor it unless a tool owns that path. if (segments.length === 1 && segments[0] === "search" && lookup(root, segments) === undefined) - return executeTool("search", searchTool, args) + return executeTool("search", makeSearchTool(prepared.searchIndex), args) return executeTool(segments.join("."), resolve(root, path), args) }), } diff --git a/packages/core/src/tool.ts b/packages/core/src/tool.ts index b72155037c52..c125aee05607 100644 --- a/packages/core/src/tool.ts +++ b/packages/core/src/tool.ts @@ -154,6 +154,7 @@ const layer = Layer.effect( } }) + let catalog: { data: Data; names: string; value: CodeModeCatalog.Inventory } | undefined const state = State.create({ name: "tool", initial: () => ({ @@ -201,8 +202,9 @@ const layer = Layer.effect( editor.tools.delete(id) }, }), - notify: (value) => - Effect.forEach( + notify: (value) => { + catalog = undefined + return Effect.forEach( value.errors, ({ kind, name, namespace, error }) => Effect.logError(`Skipping invalid ${kind} registration`, { @@ -211,7 +213,8 @@ const layer = Layer.effect( error: error.message, }), { discard: true }, - ), + ) + }, }) return Service.of({ @@ -219,15 +222,16 @@ const layer = Layer.effect( reload: state.reload, snapshot: Effect.fn("Tool.snapshot")((permissions) => Effect.sync(() => { + const data = state.get() const active = new Map() const rules = permissions ?? [] - for (const [name, tool] of state.get().tools) { + for (const [name, tool] of data.tools) { if (whollyDisabled(tool.options?.permission ?? name, rules)) continue active.set(name, tool) } const direct = new Map(Array.from(active).filter(([, tool]) => tool.options?.codemode === false)) const codeModeTools = new Map(Array.from(active).filter(([, tool]) => tool.options?.codemode !== false)) - const namespaces = state.get().namespaces + const namespaces = data.namespaces const codeModeInventory = { tools: codeModeTools, namespaces } const codeModeEnabled = !whollyDisabled("execute", rules) const codeModeTool = codeModeEnabled @@ -237,7 +241,15 @@ const layer = Layer.effect( ), ) : undefined - const codeModeCatalog = codeModeEnabled ? CodeModeTool.catalog(codeModeInventory) : undefined + const names = Array.from(codeModeTools.keys()).join("\0") + // Discovery is immutable for a registry revision and visible tool set. Keep request + // definitions/executors fresh, but share the much larger rendered catalog across steps. + const codeModeCatalog = !codeModeEnabled + ? undefined + : catalog?.data === data && catalog.names === names + ? catalog.value + : CodeModeTool.catalog(codeModeInventory) + if (codeModeCatalog) catalog = { data, names, value: codeModeCatalog } return { ...(codeModeCatalog === undefined ? {} : { codeModeCatalog }), definitions: [