/** * Embedding-retrieval fast-path for pre-call tool routing (ITEM B / L2). * * For large tool catalogs, ranking by semantic similarity is far cheaper than * an LLM router call and benchmarks at sub-10 ms when embedding vectors are * cached. This module is intentionally PURE with no provider imports: the * caller injects an `embedFn` so the retriever stays testable and free of * circular dependencies. * * Hybrid scoring formula: * score = weights.cosine * cosineSimilarity(queryVec, toolVec) * + weights.bm25 * bm25Score(query, toolText) * * Both components are independently normalized to [0, 1] before combination * so neither scale dominates. * * Fail-safe contract: * `ToolEmbeddingIndex.rank()` and `selectRelevantToolNames()` propagate any * error thrown by the injected `embedFn`. The CALLER (tool-routing * orchestration) is responsible for catching that error and degrading * gracefully to the LLM-router / server-granularity path. * * Determinism: * All public functions are deterministic given the same inputs and embedFn. * No Math.random(), no argless Date, no global mutable state outside the * instance-level vector cache (which is keyed by text content). */ import type { ToolRetrievalItem, ToolRetrievalRankedResult, ToolRetrievalSelectOptions, ToolRetrievalWeights } from "../types/index.js"; /** * Cosine similarity between two vectors. * * Returns 0 when either vector is zero-length, has zero magnitude, or the * lengths differ — all of which indicate the comparison is meaningless. */ export declare function cosineSimilarity(a: number[], b: number[]): number; /** * An in-process index that ranks tool catalog items by hybrid semantic + * lexical relevance to a query. * * Embedding vectors for tool descriptions are computed lazily on the first * `rank()` call and cached by description text, so subsequent turns that * share the same catalog pay only the cost of embedding the query itself. * * ### Fail-safe * Any error thrown by `embedFn` propagates out of `rank()`. The CALLER must * catch it and degrade to the LLM-router path; `ToolEmbeddingIndex` itself * never silently swallows embedFn errors. * * ### Thread-safety * The internal cache is a plain `Map`. Node.js is single-threaded so there * are no data races, but if the same index instance is used concurrently * (e.g. two turns in parallel) both calls will race to populate the cache; * the last writer wins (same content either way because `embedFn` is * deterministic for a given text). */ export declare class ToolEmbeddingIndex { private readonly items; private readonly embedFn; private bm25CorpusCache?; /** * Cached embedding vectors keyed by the item's `text` field. * Populated on first `rank()` call for texts not yet seen. * * Callers can share a single Map across multiple index instances (e.g. * across turns in the same NeuroLink session) so tool vectors are computed * once and reused. Pass the shared Map via the constructor's third argument. */ private readonly vectorCache; constructor(items: ToolRetrievalItem[], embedFn: (texts: string[]) => Promise, /** * Optional shared vector cache. When supplied, cached vectors from * previous turns are reused and any newly-computed vectors are stored * into this same Map, making it warm for the next call. */ sharedVectorCache?: Map); /** * Returns the top-K catalog items ranked by hybrid score descending. * * @throws If `embedFn` throws — propagated verbatim so the caller can fail * open. No wrapping, no swallowing. */ rank(query: string, opts: { topK: number; weights?: ToolRetrievalWeights; timeoutMs?: number; }): Promise; } /** * Selects the most relevant tool names from a catalog given a query. * * Creates a temporary `ToolEmbeddingIndex`, runs `rank()`, and returns just * the tool names. Use `ToolEmbeddingIndex` directly when you want to reuse * cached tool vectors across multiple queries (e.g. multiple turns with the * same catalog). * * @throws If `opts.embedFn` throws — propagated so the caller can degrade to * the LLM-router path. * * @example * ```ts * const tools = await selectRelevantToolNames( * "show me yesterday's sales", * catalog.flatMap(server => server.toolNames.map(name => ({ * name, * text: `${server.description} — ${name}`, * }))), * { topK: 5, embedFn: myEmbedFn }, * ); * // => ["analytics_getSales", "analytics_getRevenue", ...] * ``` */ export declare function selectRelevantToolNames(query: string, items: ToolRetrievalItem[], opts: ToolRetrievalSelectOptions): Promise;