@juspay/neurolink 9.77.0 → 9.78.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -219,6 +219,7 @@ export { logger } from "./utils/logger.js";
219
219
  export { getPoolStats } from "./utils/redis.js";
220
220
  export { ToolRoutingCache } from "./core/toolRoutingCache.js";
221
221
  export { resolveToolRoutingExclusions, buildToolRoutingCatalog, buildRoutingQueryFromHistory, DEFAULT_ROUTER_PROMPT_PREFIX, } from "./core/toolRouting.js";
222
+ export { cosineSimilarity, ToolEmbeddingIndex, selectRelevantToolNames, } from "./core/toolRoutingEmbedding.js";
222
223
  export declare function initializeTelemetry(): Promise<boolean>;
223
224
  export declare function getTelemetryStatus(): Promise<{
224
225
  enabled: boolean;
package/dist/lib/index.js CHANGED
@@ -362,6 +362,8 @@ export { getPoolStats } from "./utils/redis.js";
362
362
  export { ToolRoutingCache } from "./core/toolRoutingCache.js";
363
363
  // Pre-call tool routing resolver — exported for host integrations and testing
364
364
  export { resolveToolRoutingExclusions, buildToolRoutingCatalog, buildRoutingQueryFromHistory, DEFAULT_ROUTER_PROMPT_PREFIX, } from "./core/toolRouting.js";
365
+ // Pre-call tool routing — embedding fast-path (ITEM B: L2 semantic retrieval)
366
+ export { cosineSimilarity, ToolEmbeddingIndex, selectRelevantToolNames, } from "./core/toolRoutingEmbedding.js";
365
367
  // ============================================================================
366
368
  // REAL-TIME SERVICES & TELEMETRY - Enterprise Platform Features
367
369
  // ============================================================================
@@ -102,6 +102,7 @@ export declare class NeuroLink {
102
102
  private conversationMemoryConfig?;
103
103
  private toolRoutingConfig?;
104
104
  private toolRoutingCacheInstance?;
105
+ private toolRoutingVectorCache?;
105
106
  private toolDedupConfig?;
106
107
  private enableOrchestration;
107
108
  private authProvider?;
@@ -363,6 +363,11 @@ export class NeuroLink {
363
363
  // Lazy-initialized routing decision cache (ITEM C). Created on first use so
364
364
  // instances that don't use routing pay no overhead.
365
365
  toolRoutingCacheInstance;
366
+ // Persisted vector cache for the L2 embedding fast-path (ITEM B). Populated
367
+ // on the first turn and reused across subsequent turns so tool embedding
368
+ // vectors are computed once. Cleared when the catalog changes via
369
+ // setToolRoutingServers() so stale vectors are never reused.
370
+ toolRoutingVectorCache;
366
371
  // Opt-in tool-signature deduplication config.
367
372
  toolDedupConfig;
368
373
  // Add orchestration property
@@ -5934,7 +5939,11 @@ Current user's request: ${currentInput}`;
5934
5939
  // Require a non-empty sessionId to avoid anonymous sessions sharing a
5935
5940
  // ":query" namespace (cross-session cache leak).
5936
5941
  const cacheKey = cacheEnabled && sessionId ? `${sessionId}:${routingQuery}` : undefined;
5942
+ const routableServerCount = catalog.filter((s) => !(routingConfig.alwaysIncludeServerIds ?? []).includes(s.id)).length;
5937
5943
  // --- ITEM E: build the emitDecision callback ---
5944
+ // Constructed BEFORE the cache-hit check so it is available for all
5945
+ // outcome paths, including cache-hit turns (Finding 4).
5946
+ const routingStartTime = Date.now();
5938
5947
  const emitDecision = (decision) => {
5939
5948
  try {
5940
5949
  const activeSpan = trace.getActiveSpan();
@@ -5949,100 +5958,176 @@ Current user's request: ${currentInput}`;
5949
5958
  activeSpan.setAttribute("tool_routing.excluded_tool_count", decision.excludedToolCount);
5950
5959
  activeSpan.setAttribute("tool_routing.cache_hit", decision.cacheHit);
5951
5960
  activeSpan.setAttribute("tool_routing.duration_ms", decision.durationMs);
5961
+ // Optional L2 / tool-granularity fields (present only when embedding ran).
5962
+ if (decision.embeddingActivated !== undefined) {
5963
+ activeSpan.setAttribute("tool_routing.embedding_activated", decision.embeddingActivated);
5964
+ }
5965
+ if (decision.candidateToolCount !== undefined) {
5966
+ activeSpan.setAttribute("tool_routing.candidate_tool_count", decision.candidateToolCount);
5967
+ }
5968
+ if (decision.granularity !== undefined) {
5969
+ activeSpan.setAttribute("tool_routing.granularity", decision.granularity);
5970
+ }
5952
5971
  }
5953
5972
  catch {
5954
5973
  // Telemetry must never affect routing behaviour.
5955
5974
  }
5956
5975
  };
5957
- // Unified hit/miss block: always produces raw (pre-stickiness) exclusions
5958
- // so stickiness can be applied identically on both paths below.
5976
+ // Check the cache first.
5977
+ if (cacheEnabled && cache && cacheKey !== undefined) {
5978
+ const cached = cache.get(cacheKey);
5979
+ if (cached) {
5980
+ // Decrement stickiness turn counter even on a cache hit so the window
5981
+ // advances correctly regardless of whether the LLM router ran
5982
+ // (Finding 3). Re-apply stickiness to the cached exclusion list so
5983
+ // the live stickiness window, not the one at write-time, is honoured
5984
+ // (Finding 2 complement: we stored the pre-stickiness list, so
5985
+ // re-applying here gives the correct per-turn view).
5986
+ let cachedExcluded = cached.excludedToolNames;
5987
+ if (stickinessEnabled && sessionId) {
5988
+ try {
5989
+ const stickyIds = cache.getStickyServerIds(sessionId);
5990
+ if (stickyIds.length > 0) {
5991
+ cachedExcluded = cachedExcluded.filter((toolName) => !stickyIds.some((id) => toolName.startsWith(`${id}_`)));
5992
+ }
5993
+ }
5994
+ catch {
5995
+ // Stickiness failure is non-fatal.
5996
+ }
5997
+ }
5998
+ // Notify the emitDecision callback so custom telemetry sinks observe
5999
+ // every routing outcome, not just non-cached turns (Finding 4).
6000
+ const cachedSelectedSet = new Set(cached.selectedServerIds);
6001
+ const cachedExcludedServerIds = catalog
6002
+ .map((e) => e.id)
6003
+ .filter((id) => !cachedSelectedSet.has(id));
6004
+ emitDecision({
6005
+ outcome: "cache-hit",
6006
+ selectedServerIds: cached.selectedServerIds,
6007
+ excludedServerIds: cachedExcludedServerIds,
6008
+ hallucinatedIds: [],
6009
+ excludedToolCount: cachedExcluded.length,
6010
+ routableServerCount,
6011
+ cacheHit: true,
6012
+ durationMs: Date.now() - routingStartTime,
6013
+ });
6014
+ logger.debug("[ToolRouting] Cache hit, skipping router LLM", {
6015
+ hasSessionId: !!sessionId,
6016
+ routingQueryLength: routingQuery.length,
6017
+ });
6018
+ if (cachedExcluded.length > 0) {
6019
+ options.excludeTools = [
6020
+ ...(options.excludeTools ?? []),
6021
+ ...cachedExcluded,
6022
+ ];
6023
+ }
6024
+ return;
6025
+ }
6026
+ }
6027
+ // The router call below re-enters the public generate(), whose finally
6028
+ // block resets _disableToolCacheForCurrentRequest to false. That flag is
6029
+ // turn-scoped (set at the top of this turn) and read by the main tool
6030
+ // execution path that runs after routing, so save it before the router
6031
+ // call and restore it afterward to keep the turn's cache setting intact.
6032
+ const cacheDisabledForCurrentRequest = this._disableToolCacheForCurrentRequest;
5959
6033
  let routedExcludeTools;
5960
- let selectedServerIds = [];
5961
6034
  let resolvedDecision;
5962
- let fromCache = false;
5963
- const cached = cacheEnabled && cache && cacheKey !== undefined
5964
- ? cache.get(cacheKey)
5965
- : undefined;
5966
- if (cached) {
5967
- // Cache HIT — use stored raw exclusions (pre-stickiness).
5968
- fromCache = true;
5969
- routedExcludeTools = [...cached.excludedToolNames];
5970
- selectedServerIds = [...cached.selectedServerIds];
5971
- logger.debug("[ToolRouting] Cache hit, skipping router LLM", {
5972
- cacheKey,
5973
- });
5974
- // Emit cache-hit telemetry with full parity to the miss path.
5975
- try {
5976
- const activeSpan = trace.getActiveSpan();
5977
- if (activeSpan) {
5978
- const routableCount = catalog.filter((s) => !(routingConfig.alwaysIncludeServerIds ?? []).includes(s.id)).length;
5979
- activeSpan.setAttribute("tool_routing.outcome", "cache-hit");
5980
- activeSpan.setAttribute("tool_routing.routable_server_count", routableCount);
5981
- activeSpan.setAttribute("tool_routing.cache_hit", true);
5982
- activeSpan.setAttribute("tool_routing.selected_server_ids", spanJsonAttribute(selectedServerIds));
5983
- activeSpan.setAttribute("tool_routing.excluded_tool_count", routedExcludeTools.length);
6035
+ try {
6036
+ // Intercept the decision so we can store it in the cache.
6037
+ const captureDecision = (decision) => {
6038
+ resolvedDecision = decision;
6039
+ emitDecision(decision);
6040
+ };
6041
+ // --- ITEM B: build the embedFn for the L2 embedding fast-path ---
6042
+ // The vector cache is persisted at the NeuroLink instance level so
6043
+ // tool embedding vectors are computed once and reused across turns
6044
+ // (Finding 1 fix). It is cleared by setToolRoutingServers() when the
6045
+ // catalog changes so stale vectors are never used after an update.
6046
+ let routingEmbedFn;
6047
+ const embeddingCfg = routingConfig.embedding;
6048
+ if (embeddingCfg?.enabled === true) {
6049
+ try {
6050
+ // Resolve the embedding provider: use the explicitly configured one
6051
+ // if present, otherwise fall back to the stream/generate call's
6052
+ // provider. The factory call is wrapped in try/catch so a provider
6053
+ // that doesn't support embedMany (it throws at call time, not
6054
+ // construction time) fails open when routingEmbedFn is invoked.
6055
+ const embProviderName = embeddingCfg.provider ??
6056
+ (options.provider && options.provider !== "auto"
6057
+ ? options.provider
6058
+ : undefined) ??
6059
+ routingConfig.routerModel?.provider;
6060
+ if (embProviderName) {
6061
+ const embProvider = await AIProviderFactory.createProvider(embProviderName, embeddingCfg.model, true, this, undefined, this.resolveCredentials(options.credentials));
6062
+ // Bind embedMany with the configured model (may be undefined —
6063
+ // the provider uses its default embedding model in that case).
6064
+ routingEmbedFn = (texts) => withTimeout(embProvider.embedMany(texts, embeddingCfg.model), embeddingCfg.timeoutMs ?? 10000);
6065
+ // Lazy-init the persistent vector cache for this instance.
6066
+ // Subsequent turns reuse the same Map so text→vector lookups
6067
+ // already populated from earlier turns are served from memory.
6068
+ if (!this.toolRoutingVectorCache) {
6069
+ this.toolRoutingVectorCache = new Map();
6070
+ }
6071
+ }
6072
+ }
6073
+ catch (embSetupError) {
6074
+ logger.debug("[ToolRouting] Embedding provider setup failed, L2 path disabled for this turn", {
6075
+ error: embSetupError instanceof Error
6076
+ ? embSetupError.message
6077
+ : String(embSetupError),
6078
+ });
6079
+ // routingEmbedFn remains undefined — fast-path is skipped.
5984
6080
  }
5985
6081
  }
5986
- catch {
5987
- // Telemetry must never affect routing behaviour.
5988
- }
6082
+ routedExcludeTools = await resolveToolRoutingExclusions({
6083
+ catalog,
6084
+ alwaysIncludeServerIds: routingConfig.alwaysIncludeServerIds ?? [],
6085
+ userQuery: routingQuery,
6086
+ routerPromptPrefix: routingConfig.routerPromptPrefix,
6087
+ routerModel: {
6088
+ provider: routingConfig.routerModel?.provider ??
6089
+ options.provider,
6090
+ model: routingConfig.routerModel?.model ?? options.model,
6091
+ region: routingConfig.routerModel?.region ?? options.region,
6092
+ temperature: routingConfig.routerModel?.temperature,
6093
+ },
6094
+ timeoutMs: routingConfig.timeoutMs ?? DEFAULT_TOOL_ROUTING_TIMEOUT_MS,
6095
+ // Forward the abort signal so a cancelled turn aborts the router
6096
+ // call promptly instead of waiting out the routing timeout.
6097
+ generateFn: (generateOptions) => this.generate({
6098
+ ...generateOptions,
6099
+ abortSignal: options.abortSignal,
6100
+ }),
6101
+ emitDecision: captureDecision,
6102
+ // L2 / ITEM D — only populated when embedding is configured.
6103
+ embedFn: routingEmbedFn,
6104
+ embeddingConfig: embeddingCfg,
6105
+ granularity: routingConfig.granularity ?? "server",
6106
+ // Pass the persistent vector cache so tool embeddings are reused
6107
+ // across turns (Finding 1).
6108
+ embeddingVectorCache: routingEmbedFn !== undefined
6109
+ ? this.toolRoutingVectorCache
6110
+ : undefined,
6111
+ });
5989
6112
  }
5990
- else {
5991
- // Cache MISS — run the router LLM.
5992
- // The router call re-enters the public generate(), whose finally block
5993
- // resets _disableToolCacheForCurrentRequest to false. That flag is
5994
- // turn-scoped and read by the main tool execution path after routing, so
5995
- // save it before the router call and restore it afterward.
5996
- const cacheDisabledForCurrentRequest = this._disableToolCacheForCurrentRequest;
5997
- try {
5998
- // Intercept the decision so we can store it in the cache.
5999
- const captureDecision = (decision) => {
6000
- resolvedDecision = decision;
6001
- emitDecision(decision);
6002
- };
6003
- routedExcludeTools = await resolveToolRoutingExclusions({
6004
- catalog,
6005
- alwaysIncludeServerIds: routingConfig.alwaysIncludeServerIds ?? [],
6006
- userQuery: routingQuery,
6007
- routerPromptPrefix: routingConfig.routerPromptPrefix,
6008
- routerModel: {
6009
- provider: routingConfig.routerModel?.provider ??
6010
- options.provider,
6011
- model: routingConfig.routerModel?.model ?? options.model,
6012
- region: routingConfig.routerModel?.region ?? options.region,
6013
- temperature: routingConfig.routerModel?.temperature,
6014
- },
6015
- timeoutMs: routingConfig.timeoutMs ?? DEFAULT_TOOL_ROUTING_TIMEOUT_MS,
6016
- // Forward the abort signal so a cancelled turn aborts the router
6017
- // call promptly instead of waiting out the routing timeout.
6018
- generateFn: (generateOptions) => this.generate({
6019
- ...generateOptions,
6020
- abortSignal: options.abortSignal,
6021
- }),
6022
- emitDecision: captureDecision,
6023
- });
6024
- }
6025
- finally {
6026
- this._disableToolCacheForCurrentRequest =
6027
- cacheDisabledForCurrentRequest;
6028
- }
6029
- if (resolvedDecision?.outcome === "applied") {
6030
- selectedServerIds = resolvedDecision.selectedServerIds;
6031
- }
6113
+ finally {
6114
+ this._disableToolCacheForCurrentRequest =
6115
+ cacheDisabledForCurrentRequest;
6032
6116
  }
6033
6117
  // Aborted during the router call — skip applying now-stale exclusions;
6034
6118
  // the main generation path enforces the abort itself.
6035
6119
  if (options.abortSignal?.aborted) {
6036
6120
  return;
6037
6121
  }
6038
- // Capture raw (pre-stickiness) exclusions for cache storage on MISS path.
6039
- const rawExclusions = [...routedExcludeTools];
6040
- // Apply stickiness on BOTH hit and miss paths: the sticky ids were
6041
- // recorded on a prior turn and represent servers that should stay warm for
6042
- // the current turn. Consuming (decrementing) them before recordSelection
6043
- // ensures the window covers the correct set of future turns rather than
6044
- // burning one count on the same turn the selection is recorded
6045
- // (off-by-one fix).
6122
+ // Snapshot the raw (pre-stickiness) exclusion list before applying
6123
+ // stickiness overrides. The cache stores this snapshot so future cache
6124
+ // hits can re-apply the then-current stickiness state (Finding 2).
6125
+ const preStickinessExcludeTools = routedExcludeTools;
6126
+ // Apply stickiness FIRST: the sticky ids were recorded on a prior turn and
6127
+ // represent servers that should stay warm for the current turn. Consuming
6128
+ // (decrementing) them before recordSelection ensures the window covers the
6129
+ // correct set of future turns rather than burning one count on the same
6130
+ // turn the selection is recorded (off-by-one fix).
6046
6131
  if (stickinessEnabled && cache && sessionId) {
6047
6132
  try {
6048
6133
  const stickyIds = cache.getStickyServerIds(sessionId);
@@ -6057,15 +6142,16 @@ Current user's request: ${currentInput}`;
6057
6142
  // Stickiness failure is non-fatal.
6058
6143
  }
6059
6144
  }
6060
- // --- ITEM C: store result for future turns (MISS path only) ---
6061
- // Cache stores RAW (pre-stickiness) exclusions so the cached value is
6062
- // correct regardless of which sessions hit it on subsequent turns.
6063
- if (!fromCache && resolvedDecision?.outcome === "applied" && cache) {
6145
+ // --- ITEM C: store result for future turns ---
6146
+ if (resolvedDecision?.outcome === "applied" && cache) {
6147
+ // Store the PRE-stickiness exclusion list in the cache so future hits
6148
+ // re-apply the live stickiness window rather than the one from this
6149
+ // turn (Finding 2).
6064
6150
  if (cacheEnabled && cacheKey !== undefined) {
6065
6151
  try {
6066
6152
  cache.set(cacheKey, {
6067
- excludedToolNames: rawExclusions,
6068
- selectedServerIds,
6153
+ excludedToolNames: preStickinessExcludeTools,
6154
+ selectedServerIds: resolvedDecision.selectedServerIds,
6069
6155
  });
6070
6156
  }
6071
6157
  catch {
@@ -6077,7 +6163,7 @@ Current user's request: ${currentInput}`;
6077
6163
  // the same turn it is set (the off-by-one fix above).
6078
6164
  if (stickinessEnabled && sessionId) {
6079
6165
  try {
6080
- cache.recordSelection(sessionId, selectedServerIds);
6166
+ cache.recordSelection(sessionId, resolvedDecision.selectedServerIds);
6081
6167
  }
6082
6168
  catch {
6083
6169
  // Stickiness failure is non-fatal.
@@ -6174,6 +6260,12 @@ Current user's request: ${currentInput}`;
6174
6260
  return;
6175
6261
  }
6176
6262
  this.toolRoutingConfig.servers = servers;
6263
+ // Clear the persisted vector cache so tool vectors are recomputed against
6264
+ // the new catalog on the next turn (stale vectors must never be reused).
6265
+ this.toolRoutingVectorCache = undefined;
6266
+ // Cached routing decisions are catalog-dependent too; force the next turn
6267
+ // to recompute exclusions against the new server/tool set.
6268
+ this.toolRoutingCacheInstance = undefined;
6177
6269
  }
6178
6270
  async validateStreamRequestOptions(options, startTime) {
6179
6271
  await this.validateStreamInput(options);
@@ -38,6 +38,71 @@ export type ToolRoutingModelConfig = {
38
38
  /** Router sampling temperature. Default: 0. */
39
39
  temperature?: number;
40
40
  };
41
+ /**
42
+ * Weights for the hybrid scoring formula used by `ToolEmbeddingIndex.rank()`.
43
+ * Scores are computed as: `cosine * cosine + bm25 * bm25Score` then
44
+ * normalized before sorting.
45
+ * Default: `{ cosine: 0.8, bm25: 0.2 }`.
46
+ */
47
+ export type ToolRetrievalWeights = {
48
+ /** Weight applied to the cosine-similarity (dense) component. */
49
+ cosine: number;
50
+ /** Weight applied to the BM25 (sparse/lexical) component. */
51
+ bm25: number;
52
+ };
53
+ /**
54
+ * Configuration for the L2 embedding fast-path (ITEM B).
55
+ *
56
+ * When enabled and the catalog's total tool count reaches `minToolsToActivate`,
57
+ * a hybrid cosine + BM25 retriever ranks all tools by relevance to the query
58
+ * and takes the top-`topK` candidates. This is far cheaper than an LLM call
59
+ * (sub-10 ms warm) and fires BEFORE or INSTEAD of the LLM router.
60
+ *
61
+ * Fail-open: any embedding error (missing provider, network failure, wrong
62
+ * model) silently falls back to the existing LLM-router / server-granularity
63
+ * path — the turn is never broken.
64
+ */
65
+ export type ToolRoutingEmbeddingConfig = {
66
+ /**
67
+ * Activate the embedding fast-path. Default: false (backward-compatible).
68
+ * Setting this to true without supplying `provider`/`model` causes the SDK
69
+ * to try the stream call's configured provider; if that provider does not
70
+ * support embeddings the layer fails open.
71
+ */
72
+ enabled?: boolean;
73
+ /**
74
+ * Maximum number of top-ranked tool candidates passed to the post-embedding
75
+ * decision stage. Default: 20.
76
+ */
77
+ topK?: number;
78
+ /**
79
+ * Minimum total tool count in the catalog before the embedding path
80
+ * activates. Below this threshold the catalog is small enough that the LLM
81
+ * router alone is cheap and fast. Default: 20.
82
+ */
83
+ minToolsToActivate?: number;
84
+ /**
85
+ * Weights for the hybrid scoring formula:
86
+ * score = cosine * cosineSim + bm25 * bm25Score (both normalized to [0,1])
87
+ * Default: `{ cosine: 0.8, bm25: 0.2 }`.
88
+ */
89
+ weights?: ToolRetrievalWeights;
90
+ /**
91
+ * Provider name to use for the embedding call (e.g. "openai", "vertex").
92
+ * Defaults to the stream/generate call's configured provider. The provider
93
+ * must support `embedMany()`.
94
+ */
95
+ provider?: string;
96
+ /**
97
+ * Embedding model name (provider-specific). When omitted the provider's
98
+ * default embedding model is used (e.g. text-embedding-3-small for OpenAI).
99
+ */
100
+ model?: string;
101
+ /**
102
+ * Timeout for embedding calls in milliseconds. Default: 10000.
103
+ */
104
+ timeoutMs?: number;
105
+ };
41
106
  /** Constructor-level configuration for pre-call tool routing. */
42
107
  export type ToolRoutingConfig = {
43
108
  /** Master switch. Routing runs only when true AND the server catalog is non-empty. */
@@ -88,6 +153,24 @@ export type ToolRoutingConfig = {
88
153
  /** Number of turns for which a previously-selected server stays warm. Default: 3. */
89
154
  turns?: number;
90
155
  };
156
+ /**
157
+ * L2 embedding fast-path (ITEM B). When enabled the SDK ranks tools by
158
+ * semantic + lexical relevance using a hybrid cosine/BM25 score and narrows
159
+ * the candidate set BEFORE (or instead of) the LLM router. Disabled by
160
+ * default for backward compatibility.
161
+ */
162
+ embedding?: ToolRoutingEmbeddingConfig;
163
+ /**
164
+ * Routing granularity (ITEM D).
165
+ *
166
+ * - `"server"` (default) — routing excludes the tools of entire unpicked
167
+ * servers. This is the original behavior.
168
+ * - `"tool"` — routing excludes individual tools that are not in the
169
+ * embedding top-K candidate set, regardless of which server they belong
170
+ * to. Requires `embedding.enabled: true`; if the embedding fast-path is
171
+ * off (or fails) the granularity falls back to `"server"` automatically.
172
+ */
173
+ granularity?: "server" | "tool";
91
174
  };
92
175
  /** Catalog entry pairing a server descriptor with its registered tool names. */
93
176
  export type ToolRoutingCatalogEntry = {
@@ -152,6 +235,19 @@ export type ToolRoutingDecision = {
152
235
  cacheHit: boolean;
153
236
  /** Wall-clock time spent in the routing resolution in milliseconds. */
154
237
  durationMs: number;
238
+ /** True when the L2 embedding fast-path ran and produced candidate results. */
239
+ embeddingActivated?: boolean;
240
+ /**
241
+ * Number of tool candidates produced by the embedding retriever before the
242
+ * post-embedding server or tool filtering step.
243
+ */
244
+ candidateToolCount?: number;
245
+ /**
246
+ * Granularity at which exclusions were applied ("server" or "tool").
247
+ * Matches `ToolRoutingConfig.granularity`; present only when routing was
248
+ * applied (outcome === "applied").
249
+ */
250
+ granularity?: "server" | "tool";
155
251
  };
156
252
  /** Parameters for `resolveToolRoutingExclusions()`. */
157
253
  export type ToolRoutingResolutionParams = {
@@ -176,4 +272,79 @@ export type ToolRoutingResolutionParams = {
176
272
  * the resolver.
177
273
  */
178
274
  emitDecision?: (decision: ToolRoutingDecision) => void;
275
+ /**
276
+ * Injected async function that converts an array of texts into embedding
277
+ * vectors. Built by the caller (NeuroLink) from the configured embedding
278
+ * provider so the resolver stays pure and free of provider imports.
279
+ * When undefined the embedding fast-path is skipped entirely.
280
+ */
281
+ embedFn?: (texts: string[]) => Promise<number[][]>;
282
+ /**
283
+ * Embedding fast-path configuration forwarded from `ToolRoutingConfig`.
284
+ * Only consulted when `embedFn` is provided.
285
+ */
286
+ embeddingConfig?: ToolRoutingEmbeddingConfig;
287
+ /**
288
+ * Routing granularity forwarded from `ToolRoutingConfig`. Default: "server".
289
+ */
290
+ granularity?: "server" | "tool";
291
+ /**
292
+ * Optional shared vector cache for the L2 embedding fast-path. When
293
+ * supplied, tool embedding vectors computed on prior turns are reused rather
294
+ * than re-fetched from the embedding provider on every call.
295
+ *
296
+ * The NeuroLink instance manages the lifecycle: it creates the Map once and
297
+ * passes the same reference across turns. It clears the reference when the
298
+ * tool catalog changes (via `setToolRoutingServers`) so stale vectors are
299
+ * never used after a catalog update.
300
+ */
301
+ embeddingVectorCache?: Map<string, number[]>;
302
+ };
303
+ /**
304
+ * A single item in the tool retrieval catalog, pairing a tool name with the
305
+ * text (tool description + server context) used to build its embedding vector.
306
+ */
307
+ export type ToolRetrievalItem = {
308
+ /** Fully-qualified tool name (e.g. `${serverId}_${toolName}`). */
309
+ name: string;
310
+ /** Descriptive text used as the embedding document for this tool. */
311
+ text: string;
312
+ };
313
+ /**
314
+ * One ranked result from `ToolEmbeddingIndex.rank()` or
315
+ * `selectRelevantToolNames()`.
316
+ */
317
+ export type ToolRetrievalRankedResult = {
318
+ /** Tool name (mirrors `ToolRetrievalItem.name`). */
319
+ name: string;
320
+ /** Combined hybrid score (higher = more relevant). */
321
+ score: number;
322
+ };
323
+ /**
324
+ * Options passed to `selectRelevantToolNames()` — the high-level convenience
325
+ * wrapper around `ToolEmbeddingIndex`.
326
+ */
327
+ export type ToolRetrievalSelectOptions = {
328
+ /** Maximum number of tool names to return. */
329
+ topK: number;
330
+ /** Optional weight override (defaults to `{ cosine: 0.8, bm25: 0.2 }`). */
331
+ weights?: ToolRetrievalWeights;
332
+ /**
333
+ * Async function that converts an array of text strings into embedding
334
+ * vectors. Must return one vector per input text in the same order.
335
+ * Errors thrown here propagate to the caller (so it can fail open).
336
+ */
337
+ embedFn: (texts: string[]) => Promise<number[][]>;
338
+ /**
339
+ * Optional shared vector cache (keyed by text string). When supplied the
340
+ * underlying `ToolEmbeddingIndex` reads from and writes to this Map so that
341
+ * tool vectors computed on a prior call are reused on subsequent calls for
342
+ * the same item text. Callers that want warm-cache behavior across turns
343
+ * should pass the same Map instance each time.
344
+ */
345
+ vectorCache?: Map<string, number[]>;
346
+ /**
347
+ * Timeout for each embedding provider call in milliseconds. Default: 10000.
348
+ */
349
+ timeoutMs?: number;
179
350
  };
@@ -102,6 +102,7 @@ export declare class NeuroLink {
102
102
  private conversationMemoryConfig?;
103
103
  private toolRoutingConfig?;
104
104
  private toolRoutingCacheInstance?;
105
+ private toolRoutingVectorCache?;
105
106
  private toolDedupConfig?;
106
107
  private enableOrchestration;
107
108
  private authProvider?;