334 lines · 9.1 KB · TypeScript
RawDownload
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<ToolMetadata, "id" | "sourceId" | "name" | "description">; 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<string>;  readonly exactPhraseMatch: boolean;} => {  if (field.raw.length === 0) {    return {      score: 0,      matchedTokens: new Set<string>(),      exactPhraseMatch: false,    };  }   let score = 0;  const matchedTokens = new Set<string>();  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<string>();  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<ReadonlyArray<ToolDiscoveryResult>> =>  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<ReadonlyArray<ExecutorSourceListItem>> =>  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<string, string>;  },  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,    };  });