History

feat(execution): allow custom tool discovery providers

Saatvik Arya committed 5 months ago

Commit c598d641 parent eefb8b55 files · +156 −16Browse files

Unified · Split

5 files
  1. packages/core/execution/README.md +19 −0
  2. packages/core/execution/src/engine.ts +34 −15
  3. packages/core/execution/src/index.ts +5 −0
  4. packages/core/execution/src/tool-invoker.test.ts +79 −1
  5. packages/core/execution/src/tool-invoker.ts +19 −0
packages/core/execution/README.md+19−0
@@ -51,6 +51,25 @@ console.log(result); · 50 unmodified lines
5151 // { result: 12, logs: [...] }
5252 ```
5353
54+## Custom tool discovery
55+
56+`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:
57+
58+```ts
59+import { createExecutionEngine, type ToolDiscoveryProvider } from "@executor-js/execution";
60+
61+const toolDiscoveryProvider: ToolDiscoveryProvider = {
62+ searchTools: ({ query, namespace, limit, offset }) =>
63+ mySearchIndex.searchTools({ query, namespace, limit, offset }),
64+};
65+
66+const engine = createExecutionEngine({
67+ executor,
68+ codeExecutor: makeQuickJsExecutor(),
69+ toolDiscoveryProvider,
70+});
71+```
72+
5473 ## Pause/resume for elicitation
5574
5675 When the host doesn't support inline elicitation, use `executeWithPause` to intercept the first request as a pause point:
packages/core/execution/src/engine.ts+34−15
@@ -12,10 +12,11 @@ import { CodeExecutionError } from "@executor-js/codemode-core"; · 11 unmodified lines
1212 import type { CodeExecutor, ExecuteResult, SandboxToolInvoker } from "@executor-js/codemode-core";
1313
1414 import {
15+ defaultToolDiscoveryProvider,
1516 makeExecutorToolInvoker,
16− searchTools,
1717 listExecutorSources,
1818 describeTool,
19+ type ToolDiscoveryProvider,
1920 } from "./tool-invoker";
2021 import { ExecutionToolError } from "./errors";
2122 import { buildExecuteDescription } from "./description";
@@ -27,6 +28,7 @@ import { buildExecuteDescription } from "./description"; · 5 unmodified lines
2728 export type ExecutionEngineConfig<E extends Cause.YieldableError = CodeExecutionError> = {
2829 readonly executor: Executor;
2930 readonly codeExecutor: CodeExecutor<E>;
31+ readonly toolDiscoveryProvider?: ToolDiscoveryProvider;
3032 };
3133
3234 export type ExecutionResult =
@@ -186,7 +188,11 @@ const readOptionalOffset = (value: unknown, toolName: string): number | Executio · 153 unmodified lines
186188 return Math.floor(value);
187189 };
188190
189−const makeFullInvoker = (executor: Executor, invokeOptions: InvokeOptions): SandboxToolInvoker => {
191+const makeFullInvoker = (
192+ executor: Executor,
193+ invokeOptions: InvokeOptions,
194+ toolDiscoveryProvider: ToolDiscoveryProvider,
195+): SandboxToolInvoker => {
190196 const base = makeExecutorToolInvoker(executor, { invokeOptions });
191197 return {
192198 invoke: ({ path, args }) => {
@@ -226,14 +232,19 @@ const makeFullInvoker = (executor: Executor, invokeOptions: InvokeOptions): Sand · 33 unmodified lines
226232 return Effect.fail(offset);
227233 }
228234
229− return searchTools(executor, args.query ?? "", limit, {
230− namespace: args.namespace,
231− offset,
232− }).pipe(
233− Effect.withSpan("mcp.tool.dispatch", {
234− attributes: { "mcp.tool.name": path, "executor.tool.builtin": true },
235− }),
236− );
235+ return toolDiscoveryProvider
236+ .searchTools({
237+ executor,
238+ query: args.query ?? "",
239+ limit,
240+ namespace: args.namespace,
241+ offset,
242+ })
243+ .pipe(
244+ Effect.withSpan("mcp.tool.dispatch", {
245+ attributes: { "mcp.tool.name": path, "executor.tool.builtin": true },
246+ }),
247+ );
237248 }
238249 if (path === "executor.sources.list") {
239250 if (args !== undefined && !isRecord(args)) {
@@ -364,7 +375,7 @@ export type ExecutionEngine<E extends Cause.YieldableError = CodeExecutionError> · 124 unmodified lines
364375 export const createExecutionEngine = <E extends Cause.YieldableError = CodeExecutionError>(
365376 config: ExecutionEngineConfig<E>,
366377 ): ExecutionEngine<E> => {
367− const { executor, codeExecutor } = config;
378+ const { executor, codeExecutor, toolDiscoveryProvider = defaultToolDiscoveryProvider } = config;
368379 const pausedExecutions = new Map<string, InternalPausedExecution<E>>();
369380 let nextId = 0;
370381
@@ -433,7 +444,11 @@ export const createExecutionEngine = <E extends Cause.YieldableError = CodeExecu · 62 unmodified lines
433444 return yield* Deferred.await(responseDeferred);
434445 });
435446
436− const invoker = makeFullInvoker(executor, { onElicitation: elicitationHandler });
447+ const invoker = makeFullInvoker(
448+ executor,
449+ { onElicitation: elicitationHandler },
450+ toolDiscoveryProvider,
451+ );
437452 fiber = yield* Effect.forkDetach(
438453 codeExecutor.execute(code, invoker).pipe(Effect.withSpan("executor.code.exec")),
439454 );
@@ -477,9 +492,13 @@ export const createExecutionEngine = <E extends Cause.YieldableError = CodeExecu · 37 unmodified lines
477492 "mcp.execute.mode": "inline",
478493 "mcp.execute.code_length": code.length,
479494 });
480− const invoker = makeFullInvoker(executor, {
481− onElicitation: options.onElicitation,
482− });
495+ const invoker = makeFullInvoker(
496+ executor,
497+ {
498+ onElicitation: options.onElicitation,
499+ },
500+ toolDiscoveryProvider,
501+ );
483502 return yield* codeExecutor.execute(code, invoker).pipe(Effect.withSpan("executor.code.exec"));
484503 });
485504
packages/core/execution/src/index.ts+5−0
@@ -12,8 +12,13 @@ export { · 11 unmodified lines
1212 export { buildExecuteDescription } from "./description";
1313 export { ExecutionToolError } from "./errors";
1414 export {
15+ defaultToolDiscoveryProvider,
1516 makeExecutorToolInvoker,
1617 searchTools,
1718 listExecutorSources,
1819 describeTool,
20+ type ToolDiscoveryInput,
21+ type ToolDiscoveryProvider,
22+ type PagedResult,
23+ type ToolDiscoveryResult,
1924 } from "./tool-invoker";
packages/core/execution/src/tool-invoker.test.ts+79−1
@@ -12,7 +12,12 @@ import { · 11 unmodified lines
1212 import { makeTestConfig, typeCheckOutputTypeScript } from "@executor-js/sdk/testing";
1313 import { makeQuickJsExecutor } from "@executor-js/runtime-quickjs";
1414 import { createExecutionEngine } from "./engine";
15−import { describeTool, makeExecutorToolInvoker, searchTools } from "./tool-invoker";
15+import {
16+ describeTool,
17+ makeExecutorToolInvoker,
18+ searchTools,
19+ type ToolDiscoveryProvider,
20+} from "./tool-invoker";
1621
1722 const codeExecutor = makeQuickJsExecutor();
1823
@@ -334,6 +339,79 @@ describe("tool discovery", () => { · 315 unmodified lines
334339 }),
335340 );
336341
342+ it.effect("lets execution hosts provide custom tool discovery", () =>
343+ Effect.gen(function* () {
344+ const executor = yield* makeSearchExecutor();
345+ const calls: Array<{
346+ readonly query: string;
347+ readonly namespace?: string;
348+ readonly limit: number;
349+ readonly offset: number;
350+ }> = [];
351+ const provider: ToolDiscoveryProvider = {
352+ searchTools: ({ query, namespace, limit, offset }) =>
353+ Effect.sync(() => {
354+ calls.push({ query, namespace, limit, offset });
355+ return {
356+ items: [
357+ {
358+ path: "custom.searchResult",
359+ name: "searchResult",
360+ description: "Provided by the host",
361+ sourceId: "custom",
362+ score: 999,
363+ },
364+ ],
365+ total: 1,
366+ hasMore: false,
367+ nextOffset: null,
368+ };
369+ }),
370+ };
371+ const engine = createExecutionEngine({
372+ executor,
373+ codeExecutor,
374+ toolDiscoveryProvider: provider,
375+ });
376+
377+ const result = yield* engine.execute(
378+ [
379+ "return await tools.search({",
380+ ' query: "calendar events",',
381+ ' namespace: "calendar",',
382+ " limit: 7,",
383+ " offset: 2,",
384+ "});",
385+ ].join("\n"),
386+ { onElicitation: acceptAll },
387+ );
388+
389+ expect(result.error).toBeUndefined();
390+ expect(result.result).toEqual({
391+ items: [
392+ {
393+ path: "custom.searchResult",
394+ name: "searchResult",
395+ description: "Provided by the host",
396+ sourceId: "custom",
397+ score: 999,
398+ },
399+ ],
400+ total: 1,
401+ hasMore: false,
402+ nextOffset: null,
403+ });
404+ expect(calls).toEqual([
405+ {
406+ query: "calendar events",
407+ namespace: "calendar",
408+ limit: 7,
409+ offset: 2,
410+ },
411+ ]);
412+ }),
413+ );
414+
337415 it.effect("supports executor-scoped source listing and tool search", () =>
338416 Effect.gen(function* () {
339417 const executor = yield* makeSearchExecutor();
packages/core/execution/src/tool-invoker.ts+19−0
@@ -249,6 +249,20 @@ export type ExecutorSourceListItem = { · 248 unmodified lines
249249 readonly toolCount: number;
250250 };
251251
252+export type ToolDiscoveryInput = {
253+ readonly executor: Executor;
254+ readonly query: string;
255+ readonly namespace?: string;
256+ readonly limit: number;
257+ readonly offset: number;
258+};
259+
260+export interface ToolDiscoveryProvider {
261+ readonly searchTools: (
262+ input: ToolDiscoveryInput,
263+ ) => Effect.Effect<PagedResult<ToolDiscoveryResult>, ExecutionToolError>;
264+}
265+
252266 /**
253267 * Page of results from a list-style discovery tool. Shared by
254268 * `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* ( · 260 unmodified lines
515529 return page;
516530 });
517531
532+export const defaultToolDiscoveryProvider: ToolDiscoveryProvider = {
533+ searchTools: ({ executor, query, namespace, limit, offset }) =>
534+ searchTools(executor, query, limit, { namespace, offset }),
535+};
536+
518537 /** What `tools.executor.sources.list()` calls inside the sandbox. */
519538 export const listExecutorSources = Effect.fn("executor.sources.list")(function* (
520539 executor: Executor,

Keyboard shortcuts

General

Show keyboard shortcuts
?
Focus the current search
/

Issues and pull requests

Open filters
f

Issues, pull requests and Inbox

Select next item
j↓
Select previous item
k↑
Open selected item
Enter
Dismiss filters or clear selection
Esc

Repository code

Go to file
t