Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion packages/codemode/src/codemode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,9 @@ export const make = <const Provided extends Record<string, unknown> = {}>(
}
}
return {
catalog: prepared.catalog,
get catalog() {
return prepared.catalog
},
execute: (code) =>
executeProgram(code, prepared, limits, options.hooks ?? {}, (ctx) => extensionGlobals(ctx, extensions)),
}
Expand Down
46 changes: 31 additions & 15 deletions packages/codemode/src/tool-runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -166,15 +166,24 @@ const flattenTools = <R>(
]
}

const describeTool = <R>(visible: VisibleTool<R>): 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 = <R>(visible: VisibleTool<R>): 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<R = never> = {
readonly root: ToolNode<R>
readonly catalog: ReadonlyArray<ToolDescription>
Expand Down Expand Up @@ -286,12 +295,20 @@ const toSearchEntry = <R>(visible: VisibleTool<R>): SearchEntry => ({

export const prepare = <R>(tools: Tools<R>): Prepared<R> => {
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<SearchEntry> | undefined
let catalog: ReadonlyArray<ToolDescription> | 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))
},
}
}

Expand Down Expand Up @@ -355,7 +372,6 @@ export const make = <R>(
): ToolRuntime<R> => {
const calls: Array<ToolCall> = []
const root = prepared.root
const searchTool = makeSearchTool(prepared.searchIndex)

const recordCall = (call: ToolCall): void => {
if (maxToolCalls !== undefined && calls.length >= maxToolCalls) {
Expand Down Expand Up @@ -412,13 +428,13 @@ export const make = <R>(
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)
}),
}
Expand Down
24 changes: 18 additions & 6 deletions packages/core/src/tool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,7 @@ const layer = Layer.effect(
}
})

let catalog: { data: Data; names: string; value: CodeModeCatalog.Inventory } | undefined
const state = State.create<Data, Editor>({
name: "tool",
initial: () => ({
Expand Down Expand Up @@ -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`, {
Expand All @@ -211,23 +213,25 @@ const layer = Layer.effect(
error: error.message,
}),
{ discard: true },
),
)
},
})

return Service.of({
transform: state.transform,
reload: state.reload,
snapshot: Effect.fn("Tool.snapshot")((permissions) =>
Effect.sync(() => {
const data = state.get()
const active = new Map<string, Tool.Info>()
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
Expand All @@ -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: [
Expand Down
Loading