import { Effect } from "effect"; import type { Executor, ToolId, ToolMetadata, ToolSchema, InvokeOptions, Source } from "@executor/sdk"; import type { SandboxToolInvoker } from "@executor/codemode-core"; import { ExecutionToolError } from "./errors"; /** * Bridges QuickJS `tools.someSource.someOp(args)` calls into * `executor.tools.invoke(toolId, args)`. */ export const makeExecutorToolInvoker = ( executor: Executor, options: { readonly invokeOptions: InvokeOptions }, ): SandboxToolInvoker => ({ invoke: ({ path, args }) => Effect.gen(function* () { const result = yield* executor.tools.invoke( path as ToolId, args, options.invokeOptions, ).pipe( Effect.catchTag("ElicitationDeclinedError", (err) => Effect.fail( new ExecutionToolError({ message: `Tool "${err.toolId}" requires approval but the request was ${err.action === "cancel" ? "cancelled" : "declined"} by the user.`, cause: err, }), ), ), ); if (result.error !== null && result.error !== undefined) { return yield* Effect.fail(result.error); } return result.data; }), }); export type ToolDiscoveryResult = { readonly path: string; readonly name: string; readonly description?: string; readonly sourceId: string; readonly score: number; }; export type ExecutorSourceListItem = { readonly id: string; readonly name: string; readonly kind: string; readonly runtime?: boolean; readonly canRemove?: boolean; readonly canRefresh?: boolean; readonly toolCount: number; }; type SearchableTool = Pick; type PreparedField = { readonly raw: string; readonly tokens: readonly string[]; }; const SEARCH_FIELD_WEIGHTS = { path: 12, sourceId: 8, name: 10, description: 5, } as const; const normalizeSearchText = (value: string): string => value .replace(/([a-z0-9])([A-Z])/g, "$1 $2") .replace(/[_./:-]+/g, " ") .toLowerCase() .trim(); const tokenizeSearchText = (value: string): string[] => normalizeSearchText(value) .split(/[^a-z0-9]+/) .map((token) => token.trim()) .filter(Boolean); const prepareField = (value?: string): PreparedField => ({ raw: normalizeSearchText(value ?? ""), tokens: tokenizeSearchText(value ?? ""), }); const scorePreparedField = ( query: string, queryTokens: readonly string[], field: PreparedField, weight: number, ): { readonly score: number; readonly matchedTokens: ReadonlySet; readonly exactPhraseMatch: boolean; } => { if (field.raw.length === 0) { return { score: 0, matchedTokens: new Set(), exactPhraseMatch: false, }; } let score = 0; const matchedTokens = new Set(); const exactPhraseMatch = query.length > 0 && field.raw.includes(query); if (query.length > 0) { if (field.raw === query) { score += weight * 14; } else if (field.raw.startsWith(query)) { score += weight * 9; } else if (exactPhraseMatch) { score += weight * 6; } } for (const token of queryTokens) { if (field.tokens.includes(token)) { score += weight * 4; matchedTokens.add(token); continue; } if (field.tokens.some((candidate) => candidate.startsWith(token) || token.startsWith(candidate))) { score += weight * 2; matchedTokens.add(token); continue; } if (field.raw.includes(token)) { score += weight; matchedTokens.add(token); } } return { score, matchedTokens, exactPhraseMatch, }; }; const matchesNamespace = ( tool: SearchableTool, namespace?: string, ): boolean => { if (!namespace || normalizeSearchText(namespace).length === 0) { return true; } const namespaceTokens = tokenizeSearchText(namespace); if (namespaceTokens.length === 0) { return true; } const sourceTokens = tokenizeSearchText(tool.sourceId); const pathTokens = tokenizeSearchText(tool.id); const isPrefixMatch = (tokens: readonly string[]): boolean => namespaceTokens.every((token, index) => tokens[index] === token); return isPrefixMatch(sourceTokens) || isPrefixMatch(pathTokens); }; const scoreToolMatch = ( tool: SearchableTool, query: string, ): ToolDiscoveryResult | null => { const normalizedQuery = normalizeSearchText(query); const queryTokens = tokenizeSearchText(query); if (normalizedQuery.length === 0 || queryTokens.length === 0) { return null; } const path = prepareField(tool.id); const sourceId = prepareField(tool.sourceId); const name = prepareField(tool.name); const description = prepareField(tool.description); const fieldScores = [ scorePreparedField(normalizedQuery, queryTokens, path, SEARCH_FIELD_WEIGHTS.path), scorePreparedField(normalizedQuery, queryTokens, sourceId, SEARCH_FIELD_WEIGHTS.sourceId), scorePreparedField(normalizedQuery, queryTokens, name, SEARCH_FIELD_WEIGHTS.name), scorePreparedField(normalizedQuery, queryTokens, description, SEARCH_FIELD_WEIGHTS.description), ]; const matchedTokens = new Set(); let score = 0; let exactPhraseMatch = false; for (const fieldScore of fieldScores) { score += fieldScore.score; exactPhraseMatch ||= fieldScore.exactPhraseMatch; for (const token of fieldScore.matchedTokens) { matchedTokens.add(token); } } if (matchedTokens.size === 0) { return null; } const coverage = matchedTokens.size / queryTokens.length; const minimumCoverage = queryTokens.length <= 2 ? 1 : 0.6; if (coverage < minimumCoverage && !exactPhraseMatch) { return null; } if (coverage === 1) { score += 25; } else { score += Math.round(coverage * 10); } if (path.tokens[0] === queryTokens[0] || name.tokens[0] === queryTokens[0]) { score += 8; } if (normalizeSearchText(tool.id) === normalizedQuery || normalizeSearchText(tool.name) === normalizedQuery) { score += 20; } return { path: tool.id, name: tool.name, description: tool.description, sourceId: tool.sourceId, score, }; }; /** What `tools.search()` calls inside the sandbox. */ export const searchTools = ( executor: Executor, query: string, limit = 12, options?: { readonly namespace?: string }, ): Effect.Effect> => Effect.gen(function* () { if (normalizeSearchText(query).length === 0) { return []; } const all = yield* executor.tools.list(); return all .filter((tool: ToolMetadata) => matchesNamespace(tool, options?.namespace)) .map((tool: ToolMetadata) => scoreToolMatch(tool, query)) .filter((tool): tool is ToolDiscoveryResult => tool !== null) .sort((left, right) => right.score - left.score || left.path.localeCompare(right.path)) .slice(0, limit); }); /** What `tools.executor.sources.list()` calls inside the sandbox. */ export const listExecutorSources = ( executor: Executor, options?: { readonly query?: string; readonly limit?: number }, ): Effect.Effect> => Effect.gen(function* () { const normalizedQuery = normalizeSearchText(options?.query ?? ""); const limit = options?.limit ?? 200; const sources = yield* executor.sources.list(); const filtered = normalizedQuery.length === 0 ? sources : sources.filter((source: Source) => { const haystack = normalizeSearchText([ source.id, source.name, source.kind, ].join(" ")); return tokenizeSearchText(normalizedQuery).every((token) => haystack.includes(token)); }); const withCounts = yield* Effect.forEach( filtered, (source: Source) => executor.tools.list({ sourceId: source.id }).pipe( Effect.map((tools) => ({ id: source.id, name: source.name, kind: source.kind, runtime: source.runtime, canRemove: source.canRemove, canRefresh: source.canRefresh, toolCount: tools.length, } satisfies ExecutorSourceListItem)), ), { concurrency: "unbounded" }, ); return withCounts .sort((left, right) => left.name.localeCompare(right.name) || left.id.localeCompare(right.id)) .slice(0, limit); }); /** What `tools.describe.tool()` calls inside the sandbox. */ export const describeTool = ( executor: Executor, path: string, ): Effect.Effect< { path: string; name: string; description?: string; inputTypeScript?: string; outputTypeScript?: string; typeScriptDefinitions?: Record; }, unknown > => Effect.gen(function* () { const metadata = (yield* executor.tools.list()).find( (t: ToolMetadata) => t.id === path, ); const base = { path, name: metadata?.name ?? path, description: metadata?.description, }; const schema: ToolSchema = yield* executor.tools.schema(path); return { ...base, inputTypeScript: schema.inputTypeScript, outputTypeScript: schema.outputTypeScript, typeScriptDefinitions: schema.typeScriptDefinitions, }; });