From 8405b7c9eabd92179dbedc8947d158a5518af6d0 Mon Sep 17 00:00:00 2001 From: Saatvik Arya Date: Sun, 24 May 2026 16:55:10 +0530 Subject: [PATCH 1/2] feat(execution): allow custom tool discovery providers Add an optional ToolDiscoveryProvider to createExecutionEngine so hosts can override sandbox tools.search without replacing the code executor or normal tool invoker. Preserve the existing lexical search as the default provider and document the SDK usage. --- packages/core/execution/README.md | 19 +++++ packages/core/execution/src/engine.ts | 37 +++++++-- packages/core/execution/src/index.ts | 5 ++ .../core/execution/src/tool-invoker.test.ts | 80 ++++++++++++++++++- packages/core/execution/src/tool-invoker.ts | 19 +++++ 5 files changed, 151 insertions(+), 9 deletions(-) diff --git a/packages/core/execution/README.md b/packages/core/execution/README.md index 7c75dfe83..cf2b73d40 100644 --- a/packages/core/execution/README.md +++ b/packages/core/execution/README.md @@ -51,6 +51,25 @@ console.log(result); // { result: 12, logs: [...] } ``` +## Custom tool discovery + +`tools.search(...)` uses Executor's built-in lexical tool discovery by default. Hosts can provide their own implementation, such as an indexed or semantic search provider, without replacing the sandbox runtime: + +```ts +import { createExecutionEngine, type ToolDiscoveryProvider } from "@executor-js/execution"; + +const toolDiscoveryProvider: ToolDiscoveryProvider = { + searchTools: ({ query, namespace, limit, offset }) => + mySearchIndex.searchTools({ query, namespace, limit, offset }), +}; + +const engine = createExecutionEngine({ + executor, + codeExecutor: makeQuickJsExecutor(), + toolDiscoveryProvider, +}); +``` + ## Pause/resume for elicitation When the host doesn't support inline elicitation, use `executeWithPause` to intercept the first request as a pause point: diff --git a/packages/core/execution/src/engine.ts b/packages/core/execution/src/engine.ts index aacb048fe..8bd5541d4 100644 --- a/packages/core/execution/src/engine.ts +++ b/packages/core/execution/src/engine.ts @@ -12,10 +12,11 @@ import { CodeExecutionError } from "@executor-js/codemode-core"; import type { CodeExecutor, ExecuteResult, SandboxToolInvoker } from "@executor-js/codemode-core"; import { + defaultToolDiscoveryProvider, makeExecutorToolInvoker, - searchTools, listExecutorSources, describeTool, + type ToolDiscoveryProvider, } from "./tool-invoker"; import { ExecutionToolError } from "./errors"; import { buildExecuteDescription } from "./description"; @@ -27,6 +28,7 @@ import { buildExecuteDescription } from "./description"; export type ExecutionEngineConfig = { readonly executor: Executor; readonly codeExecutor: CodeExecutor; + readonly toolDiscoveryProvider?: ToolDiscoveryProvider; }; export type ExecutionResult = @@ -186,7 +188,11 @@ const readOptionalOffset = (value: unknown, toolName: string): number | Executio return Math.floor(value); }; -const makeFullInvoker = (executor: Executor, invokeOptions: InvokeOptions): SandboxToolInvoker => { +const makeFullInvoker = ( + executor: Executor, + invokeOptions: InvokeOptions, + toolDiscoveryProvider: ToolDiscoveryProvider, +): SandboxToolInvoker => { const base = makeExecutorToolInvoker(executor, { invokeOptions }); return { invoke: ({ path, args }) => { @@ -226,7 +232,10 @@ const makeFullInvoker = (executor: Executor, invokeOptions: InvokeOptions): Sand return Effect.fail(offset); } - return searchTools(executor, args.query ?? "", limit, { + return toolDiscoveryProvider.searchTools({ + executor, + query: args.query ?? "", + limit, namespace: args.namespace, offset, }).pipe( @@ -364,7 +373,11 @@ export type ExecutionEngine export const createExecutionEngine = ( config: ExecutionEngineConfig, ): ExecutionEngine => { - const { executor, codeExecutor } = config; + const { + executor, + codeExecutor, + toolDiscoveryProvider = defaultToolDiscoveryProvider, + } = config; const pausedExecutions = new Map>(); let nextId = 0; @@ -433,7 +446,11 @@ export const createExecutionEngine = { }), ); + it.effect("lets execution hosts provide custom tool discovery", () => + Effect.gen(function* () { + const executor = yield* makeSearchExecutor(); + const calls: Array<{ + readonly query: string; + readonly namespace?: string; + readonly limit: number; + readonly offset: number; + }> = []; + const provider: ToolDiscoveryProvider = { + searchTools: ({ query, namespace, limit, offset }) => + Effect.sync(() => { + calls.push({ query, namespace, limit, offset }); + return { + items: [ + { + path: "custom.searchResult", + name: "searchResult", + description: "Provided by the host", + sourceId: "custom", + score: 999, + }, + ], + total: 1, + hasMore: false, + nextOffset: null, + }; + }), + }; + const engine = createExecutionEngine({ + executor, + codeExecutor, + toolDiscoveryProvider: provider, + }); + + const result = yield* engine.execute( + [ + "return await tools.search({", + ' query: "calendar events",', + ' namespace: "calendar",', + " limit: 7,", + " offset: 2,", + "});", + ].join("\n"), + { onElicitation: acceptAll }, + ); + + expect(result.error).toBeUndefined(); + expect(result.result).toEqual({ + items: [ + { + path: "custom.searchResult", + name: "searchResult", + description: "Provided by the host", + sourceId: "custom", + score: 999, + }, + ], + total: 1, + hasMore: false, + nextOffset: null, + }); + expect(calls).toEqual([ + { + query: "calendar events", + namespace: "calendar", + limit: 7, + offset: 2, + }, + ]); + }), + ); + it.effect("supports executor-scoped source listing and tool search", () => Effect.gen(function* () { const executor = yield* makeSearchExecutor(); diff --git a/packages/core/execution/src/tool-invoker.ts b/packages/core/execution/src/tool-invoker.ts index 1e878ef97..edf390682 100644 --- a/packages/core/execution/src/tool-invoker.ts +++ b/packages/core/execution/src/tool-invoker.ts @@ -249,6 +249,20 @@ export type ExecutorSourceListItem = { readonly toolCount: number; }; +export type ToolDiscoveryInput = { + readonly executor: Executor; + readonly query: string; + readonly namespace?: string; + readonly limit: number; + readonly offset: number; +}; + +export interface ToolDiscoveryProvider { + readonly searchTools: ( + input: ToolDiscoveryInput, + ) => Effect.Effect, ExecutionToolError>; +} + /** * Page of results from a list-style discovery tool. Shared by * `tools.search` and `tools.executor.sources.list` so the model sees one @@ -515,6 +529,11 @@ export const searchTools = Effect.fn("executor.tools.search")(function* ( return page; }); +export const defaultToolDiscoveryProvider: ToolDiscoveryProvider = { + searchTools: ({ executor, query, namespace, limit, offset }) => + searchTools(executor, query, limit, { namespace, offset }), +}; + /** What `tools.executor.sources.list()` calls inside the sandbox. */ export const listExecutorSources = Effect.fn("executor.sources.list")(function* ( executor: Executor, From eb53a88ea7164c80e512e7f7944251aa169dd4a4 Mon Sep 17 00:00:00 2001 From: Saatvik Arya Date: Sun, 24 May 2026 17:39:21 +0530 Subject: [PATCH 2/2] format --- packages/core/execution/src/engine.ts | 30 +++++++++++++-------------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/packages/core/execution/src/engine.ts b/packages/core/execution/src/engine.ts index 8bd5541d4..bc173eec2 100644 --- a/packages/core/execution/src/engine.ts +++ b/packages/core/execution/src/engine.ts @@ -232,17 +232,19 @@ const makeFullInvoker = ( return Effect.fail(offset); } - return toolDiscoveryProvider.searchTools({ - executor, - query: args.query ?? "", - limit, - namespace: args.namespace, - offset, - }).pipe( - Effect.withSpan("mcp.tool.dispatch", { - attributes: { "mcp.tool.name": path, "executor.tool.builtin": true }, - }), - ); + return toolDiscoveryProvider + .searchTools({ + executor, + query: args.query ?? "", + limit, + namespace: args.namespace, + offset, + }) + .pipe( + Effect.withSpan("mcp.tool.dispatch", { + attributes: { "mcp.tool.name": path, "executor.tool.builtin": true }, + }), + ); } if (path === "executor.sources.list") { if (args !== undefined && !isRecord(args)) { @@ -373,11 +375,7 @@ export type ExecutionEngine export const createExecutionEngine = ( config: ExecutionEngineConfig, ): ExecutionEngine => { - const { - executor, - codeExecutor, - toolDiscoveryProvider = defaultToolDiscoveryProvider, - } = config; + const { executor, codeExecutor, toolDiscoveryProvider = defaultToolDiscoveryProvider } = config; const pausedExecutions = new Map>(); let nextId = 0;