@juspay/neurolink 9.77.0 → 9.79.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.
- package/CHANGELOG.md +12 -0
- package/dist/browser/neurolink.min.js +323 -323
- package/dist/core/toolRouting.d.ts +9 -0
- package/dist/core/toolRouting.js +178 -1
- package/dist/core/toolRoutingEmbedding.d.ts +112 -0
- package/dist/core/toolRoutingEmbedding.js +322 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +2 -0
- package/dist/lib/core/toolRouting.d.ts +9 -0
- package/dist/lib/core/toolRouting.js +178 -1
- package/dist/lib/core/toolRoutingEmbedding.d.ts +112 -0
- package/dist/lib/core/toolRoutingEmbedding.js +323 -0
- package/dist/lib/index.d.ts +1 -0
- package/dist/lib/index.js +2 -0
- package/dist/lib/neurolink.d.ts +1 -0
- package/dist/lib/neurolink.js +191 -90
- package/dist/lib/tasks/autoresearchTaskExecutor.js +1 -1
- package/dist/lib/types/toolRouting.d.ts +171 -0
- package/dist/lib/workflow/core/ensembleExecutor.js +0 -2
- package/dist/neurolink.d.ts +1 -0
- package/dist/neurolink.js +191 -90
- package/dist/tasks/autoresearchTaskExecutor.js +1 -1
- package/dist/types/toolRouting.d.ts +171 -0
- package/dist/workflow/core/ensembleExecutor.js +0 -2
- package/package.json +4 -2
package/dist/neurolink.js
CHANGED
|
@@ -340,7 +340,7 @@ export class NeuroLink {
|
|
|
340
340
|
* @param result - The result of the tool execution (optional)
|
|
341
341
|
* @param error - The error if execution failed (optional)
|
|
342
342
|
*/
|
|
343
|
-
emitToolEndEvent(toolName, startTime, success, result, error) {
|
|
343
|
+
emitToolEndEvent(toolName, startTime, success, result, error, executionId) {
|
|
344
344
|
// Emit tool end event (NeuroLink format - enhanced with result/error)
|
|
345
345
|
// Serialize error to string for consumer compatibility (event listeners
|
|
346
346
|
// commonly check `typeof event.error === "string"`).
|
|
@@ -350,6 +350,7 @@ export class NeuroLink {
|
|
|
350
350
|
timestamp: Date.now(),
|
|
351
351
|
result,
|
|
352
352
|
error: error ? error.message : undefined,
|
|
353
|
+
executionId,
|
|
353
354
|
}));
|
|
354
355
|
}
|
|
355
356
|
// Conversation memory support
|
|
@@ -363,6 +364,11 @@ export class NeuroLink {
|
|
|
363
364
|
// Lazy-initialized routing decision cache (ITEM C). Created on first use so
|
|
364
365
|
// instances that don't use routing pay no overhead.
|
|
365
366
|
toolRoutingCacheInstance;
|
|
367
|
+
// Persisted vector cache for the L2 embedding fast-path (ITEM B). Populated
|
|
368
|
+
// on the first turn and reused across subsequent turns so tool embedding
|
|
369
|
+
// vectors are computed once. Cleared when the catalog changes via
|
|
370
|
+
// setToolRoutingServers() so stale vectors are never reused.
|
|
371
|
+
toolRoutingVectorCache;
|
|
366
372
|
// Opt-in tool-signature deduplication config.
|
|
367
373
|
toolDedupConfig;
|
|
368
374
|
// Add orchestration property
|
|
@@ -5934,7 +5940,11 @@ Current user's request: ${currentInput}`;
|
|
|
5934
5940
|
// Require a non-empty sessionId to avoid anonymous sessions sharing a
|
|
5935
5941
|
// ":query" namespace (cross-session cache leak).
|
|
5936
5942
|
const cacheKey = cacheEnabled && sessionId ? `${sessionId}:${routingQuery}` : undefined;
|
|
5943
|
+
const routableServerCount = catalog.filter((s) => !(routingConfig.alwaysIncludeServerIds ?? []).includes(s.id)).length;
|
|
5937
5944
|
// --- ITEM E: build the emitDecision callback ---
|
|
5945
|
+
// Constructed BEFORE the cache-hit check so it is available for all
|
|
5946
|
+
// outcome paths, including cache-hit turns (Finding 4).
|
|
5947
|
+
const routingStartTime = Date.now();
|
|
5938
5948
|
const emitDecision = (decision) => {
|
|
5939
5949
|
try {
|
|
5940
5950
|
const activeSpan = trace.getActiveSpan();
|
|
@@ -5949,100 +5959,176 @@ Current user's request: ${currentInput}`;
|
|
|
5949
5959
|
activeSpan.setAttribute("tool_routing.excluded_tool_count", decision.excludedToolCount);
|
|
5950
5960
|
activeSpan.setAttribute("tool_routing.cache_hit", decision.cacheHit);
|
|
5951
5961
|
activeSpan.setAttribute("tool_routing.duration_ms", decision.durationMs);
|
|
5962
|
+
// Optional L2 / tool-granularity fields (present only when embedding ran).
|
|
5963
|
+
if (decision.embeddingActivated !== undefined) {
|
|
5964
|
+
activeSpan.setAttribute("tool_routing.embedding_activated", decision.embeddingActivated);
|
|
5965
|
+
}
|
|
5966
|
+
if (decision.candidateToolCount !== undefined) {
|
|
5967
|
+
activeSpan.setAttribute("tool_routing.candidate_tool_count", decision.candidateToolCount);
|
|
5968
|
+
}
|
|
5969
|
+
if (decision.granularity !== undefined) {
|
|
5970
|
+
activeSpan.setAttribute("tool_routing.granularity", decision.granularity);
|
|
5971
|
+
}
|
|
5952
5972
|
}
|
|
5953
5973
|
catch {
|
|
5954
5974
|
// Telemetry must never affect routing behaviour.
|
|
5955
5975
|
}
|
|
5956
5976
|
};
|
|
5957
|
-
//
|
|
5958
|
-
|
|
5977
|
+
// Check the cache first.
|
|
5978
|
+
if (cacheEnabled && cache && cacheKey !== undefined) {
|
|
5979
|
+
const cached = cache.get(cacheKey);
|
|
5980
|
+
if (cached) {
|
|
5981
|
+
// Decrement stickiness turn counter even on a cache hit so the window
|
|
5982
|
+
// advances correctly regardless of whether the LLM router ran
|
|
5983
|
+
// (Finding 3). Re-apply stickiness to the cached exclusion list so
|
|
5984
|
+
// the live stickiness window, not the one at write-time, is honoured
|
|
5985
|
+
// (Finding 2 complement: we stored the pre-stickiness list, so
|
|
5986
|
+
// re-applying here gives the correct per-turn view).
|
|
5987
|
+
let cachedExcluded = cached.excludedToolNames;
|
|
5988
|
+
if (stickinessEnabled && sessionId) {
|
|
5989
|
+
try {
|
|
5990
|
+
const stickyIds = cache.getStickyServerIds(sessionId);
|
|
5991
|
+
if (stickyIds.length > 0) {
|
|
5992
|
+
cachedExcluded = cachedExcluded.filter((toolName) => !stickyIds.some((id) => toolName.startsWith(`${id}_`)));
|
|
5993
|
+
}
|
|
5994
|
+
}
|
|
5995
|
+
catch {
|
|
5996
|
+
// Stickiness failure is non-fatal.
|
|
5997
|
+
}
|
|
5998
|
+
}
|
|
5999
|
+
// Notify the emitDecision callback so custom telemetry sinks observe
|
|
6000
|
+
// every routing outcome, not just non-cached turns (Finding 4).
|
|
6001
|
+
const cachedSelectedSet = new Set(cached.selectedServerIds);
|
|
6002
|
+
const cachedExcludedServerIds = catalog
|
|
6003
|
+
.map((e) => e.id)
|
|
6004
|
+
.filter((id) => !cachedSelectedSet.has(id));
|
|
6005
|
+
emitDecision({
|
|
6006
|
+
outcome: "cache-hit",
|
|
6007
|
+
selectedServerIds: cached.selectedServerIds,
|
|
6008
|
+
excludedServerIds: cachedExcludedServerIds,
|
|
6009
|
+
hallucinatedIds: [],
|
|
6010
|
+
excludedToolCount: cachedExcluded.length,
|
|
6011
|
+
routableServerCount,
|
|
6012
|
+
cacheHit: true,
|
|
6013
|
+
durationMs: Date.now() - routingStartTime,
|
|
6014
|
+
});
|
|
6015
|
+
logger.debug("[ToolRouting] Cache hit, skipping router LLM", {
|
|
6016
|
+
hasSessionId: !!sessionId,
|
|
6017
|
+
routingQueryLength: routingQuery.length,
|
|
6018
|
+
});
|
|
6019
|
+
if (cachedExcluded.length > 0) {
|
|
6020
|
+
options.excludeTools = [
|
|
6021
|
+
...(options.excludeTools ?? []),
|
|
6022
|
+
...cachedExcluded,
|
|
6023
|
+
];
|
|
6024
|
+
}
|
|
6025
|
+
return;
|
|
6026
|
+
}
|
|
6027
|
+
}
|
|
6028
|
+
// The router call below re-enters the public generate(), whose finally
|
|
6029
|
+
// block resets _disableToolCacheForCurrentRequest to false. That flag is
|
|
6030
|
+
// turn-scoped (set at the top of this turn) and read by the main tool
|
|
6031
|
+
// execution path that runs after routing, so save it before the router
|
|
6032
|
+
// call and restore it afterward to keep the turn's cache setting intact.
|
|
6033
|
+
const cacheDisabledForCurrentRequest = this._disableToolCacheForCurrentRequest;
|
|
5959
6034
|
let routedExcludeTools;
|
|
5960
|
-
let selectedServerIds = [];
|
|
5961
6035
|
let resolvedDecision;
|
|
5962
|
-
|
|
5963
|
-
|
|
5964
|
-
|
|
5965
|
-
|
|
5966
|
-
|
|
5967
|
-
|
|
5968
|
-
|
|
5969
|
-
|
|
5970
|
-
|
|
5971
|
-
|
|
5972
|
-
|
|
5973
|
-
|
|
5974
|
-
|
|
5975
|
-
|
|
5976
|
-
|
|
5977
|
-
|
|
5978
|
-
|
|
5979
|
-
|
|
5980
|
-
|
|
5981
|
-
|
|
5982
|
-
|
|
5983
|
-
|
|
6036
|
+
try {
|
|
6037
|
+
// Intercept the decision so we can store it in the cache.
|
|
6038
|
+
const captureDecision = (decision) => {
|
|
6039
|
+
resolvedDecision = decision;
|
|
6040
|
+
emitDecision(decision);
|
|
6041
|
+
};
|
|
6042
|
+
// --- ITEM B: build the embedFn for the L2 embedding fast-path ---
|
|
6043
|
+
// The vector cache is persisted at the NeuroLink instance level so
|
|
6044
|
+
// tool embedding vectors are computed once and reused across turns
|
|
6045
|
+
// (Finding 1 fix). It is cleared by setToolRoutingServers() when the
|
|
6046
|
+
// catalog changes so stale vectors are never used after an update.
|
|
6047
|
+
let routingEmbedFn;
|
|
6048
|
+
const embeddingCfg = routingConfig.embedding;
|
|
6049
|
+
if (embeddingCfg?.enabled === true) {
|
|
6050
|
+
try {
|
|
6051
|
+
// Resolve the embedding provider: use the explicitly configured one
|
|
6052
|
+
// if present, otherwise fall back to the stream/generate call's
|
|
6053
|
+
// provider. The factory call is wrapped in try/catch so a provider
|
|
6054
|
+
// that doesn't support embedMany (it throws at call time, not
|
|
6055
|
+
// construction time) fails open when routingEmbedFn is invoked.
|
|
6056
|
+
const embProviderName = embeddingCfg.provider ??
|
|
6057
|
+
(options.provider && options.provider !== "auto"
|
|
6058
|
+
? options.provider
|
|
6059
|
+
: undefined) ??
|
|
6060
|
+
routingConfig.routerModel?.provider;
|
|
6061
|
+
if (embProviderName) {
|
|
6062
|
+
const embProvider = await AIProviderFactory.createProvider(embProviderName, embeddingCfg.model, true, this, undefined, this.resolveCredentials(options.credentials));
|
|
6063
|
+
// Bind embedMany with the configured model (may be undefined —
|
|
6064
|
+
// the provider uses its default embedding model in that case).
|
|
6065
|
+
routingEmbedFn = (texts) => withTimeout(embProvider.embedMany(texts, embeddingCfg.model), embeddingCfg.timeoutMs ?? 10000);
|
|
6066
|
+
// Lazy-init the persistent vector cache for this instance.
|
|
6067
|
+
// Subsequent turns reuse the same Map so text→vector lookups
|
|
6068
|
+
// already populated from earlier turns are served from memory.
|
|
6069
|
+
if (!this.toolRoutingVectorCache) {
|
|
6070
|
+
this.toolRoutingVectorCache = new Map();
|
|
6071
|
+
}
|
|
6072
|
+
}
|
|
6073
|
+
}
|
|
6074
|
+
catch (embSetupError) {
|
|
6075
|
+
logger.debug("[ToolRouting] Embedding provider setup failed, L2 path disabled for this turn", {
|
|
6076
|
+
error: embSetupError instanceof Error
|
|
6077
|
+
? embSetupError.message
|
|
6078
|
+
: String(embSetupError),
|
|
6079
|
+
});
|
|
6080
|
+
// routingEmbedFn remains undefined — fast-path is skipped.
|
|
5984
6081
|
}
|
|
5985
6082
|
}
|
|
5986
|
-
|
|
5987
|
-
|
|
5988
|
-
|
|
6083
|
+
routedExcludeTools = await resolveToolRoutingExclusions({
|
|
6084
|
+
catalog,
|
|
6085
|
+
alwaysIncludeServerIds: routingConfig.alwaysIncludeServerIds ?? [],
|
|
6086
|
+
userQuery: routingQuery,
|
|
6087
|
+
routerPromptPrefix: routingConfig.routerPromptPrefix,
|
|
6088
|
+
routerModel: {
|
|
6089
|
+
provider: routingConfig.routerModel?.provider ??
|
|
6090
|
+
options.provider,
|
|
6091
|
+
model: routingConfig.routerModel?.model ?? options.model,
|
|
6092
|
+
region: routingConfig.routerModel?.region ?? options.region,
|
|
6093
|
+
temperature: routingConfig.routerModel?.temperature,
|
|
6094
|
+
},
|
|
6095
|
+
timeoutMs: routingConfig.timeoutMs ?? DEFAULT_TOOL_ROUTING_TIMEOUT_MS,
|
|
6096
|
+
// Forward the abort signal so a cancelled turn aborts the router
|
|
6097
|
+
// call promptly instead of waiting out the routing timeout.
|
|
6098
|
+
generateFn: (generateOptions) => this.generate({
|
|
6099
|
+
...generateOptions,
|
|
6100
|
+
abortSignal: options.abortSignal,
|
|
6101
|
+
}),
|
|
6102
|
+
emitDecision: captureDecision,
|
|
6103
|
+
// L2 / ITEM D — only populated when embedding is configured.
|
|
6104
|
+
embedFn: routingEmbedFn,
|
|
6105
|
+
embeddingConfig: embeddingCfg,
|
|
6106
|
+
granularity: routingConfig.granularity ?? "server",
|
|
6107
|
+
// Pass the persistent vector cache so tool embeddings are reused
|
|
6108
|
+
// across turns (Finding 1).
|
|
6109
|
+
embeddingVectorCache: routingEmbedFn !== undefined
|
|
6110
|
+
? this.toolRoutingVectorCache
|
|
6111
|
+
: undefined,
|
|
6112
|
+
});
|
|
5989
6113
|
}
|
|
5990
|
-
|
|
5991
|
-
|
|
5992
|
-
|
|
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
|
-
}
|
|
6114
|
+
finally {
|
|
6115
|
+
this._disableToolCacheForCurrentRequest =
|
|
6116
|
+
cacheDisabledForCurrentRequest;
|
|
6032
6117
|
}
|
|
6033
6118
|
// Aborted during the router call — skip applying now-stale exclusions;
|
|
6034
6119
|
// the main generation path enforces the abort itself.
|
|
6035
6120
|
if (options.abortSignal?.aborted) {
|
|
6036
6121
|
return;
|
|
6037
6122
|
}
|
|
6038
|
-
//
|
|
6039
|
-
|
|
6040
|
-
//
|
|
6041
|
-
|
|
6042
|
-
// the
|
|
6043
|
-
//
|
|
6044
|
-
//
|
|
6045
|
-
//
|
|
6123
|
+
// Snapshot the raw (pre-stickiness) exclusion list before applying
|
|
6124
|
+
// stickiness overrides. The cache stores this snapshot so future cache
|
|
6125
|
+
// hits can re-apply the then-current stickiness state (Finding 2).
|
|
6126
|
+
const preStickinessExcludeTools = routedExcludeTools;
|
|
6127
|
+
// Apply stickiness FIRST: the sticky ids were recorded on a prior turn and
|
|
6128
|
+
// represent servers that should stay warm for the current turn. Consuming
|
|
6129
|
+
// (decrementing) them before recordSelection ensures the window covers the
|
|
6130
|
+
// correct set of future turns rather than burning one count on the same
|
|
6131
|
+
// turn the selection is recorded (off-by-one fix).
|
|
6046
6132
|
if (stickinessEnabled && cache && sessionId) {
|
|
6047
6133
|
try {
|
|
6048
6134
|
const stickyIds = cache.getStickyServerIds(sessionId);
|
|
@@ -6057,15 +6143,16 @@ Current user's request: ${currentInput}`;
|
|
|
6057
6143
|
// Stickiness failure is non-fatal.
|
|
6058
6144
|
}
|
|
6059
6145
|
}
|
|
6060
|
-
// --- ITEM C: store result for future turns
|
|
6061
|
-
|
|
6062
|
-
|
|
6063
|
-
|
|
6146
|
+
// --- ITEM C: store result for future turns ---
|
|
6147
|
+
if (resolvedDecision?.outcome === "applied" && cache) {
|
|
6148
|
+
// Store the PRE-stickiness exclusion list in the cache so future hits
|
|
6149
|
+
// re-apply the live stickiness window rather than the one from this
|
|
6150
|
+
// turn (Finding 2).
|
|
6064
6151
|
if (cacheEnabled && cacheKey !== undefined) {
|
|
6065
6152
|
try {
|
|
6066
6153
|
cache.set(cacheKey, {
|
|
6067
|
-
excludedToolNames:
|
|
6068
|
-
selectedServerIds,
|
|
6154
|
+
excludedToolNames: preStickinessExcludeTools,
|
|
6155
|
+
selectedServerIds: resolvedDecision.selectedServerIds,
|
|
6069
6156
|
});
|
|
6070
6157
|
}
|
|
6071
6158
|
catch {
|
|
@@ -6077,7 +6164,7 @@ Current user's request: ${currentInput}`;
|
|
|
6077
6164
|
// the same turn it is set (the off-by-one fix above).
|
|
6078
6165
|
if (stickinessEnabled && sessionId) {
|
|
6079
6166
|
try {
|
|
6080
|
-
cache.recordSelection(sessionId, selectedServerIds);
|
|
6167
|
+
cache.recordSelection(sessionId, resolvedDecision.selectedServerIds);
|
|
6081
6168
|
}
|
|
6082
6169
|
catch {
|
|
6083
6170
|
// Stickiness failure is non-fatal.
|
|
@@ -6174,6 +6261,12 @@ Current user's request: ${currentInput}`;
|
|
|
6174
6261
|
return;
|
|
6175
6262
|
}
|
|
6176
6263
|
this.toolRoutingConfig.servers = servers;
|
|
6264
|
+
// Clear the persisted vector cache so tool vectors are recomputed against
|
|
6265
|
+
// the new catalog on the next turn (stale vectors must never be reused).
|
|
6266
|
+
this.toolRoutingVectorCache = undefined;
|
|
6267
|
+
// Cached routing decisions are catalog-dependent too; force the next turn
|
|
6268
|
+
// to recompute exclusions against the new server/tool set.
|
|
6269
|
+
this.toolRoutingCacheInstance = undefined;
|
|
6177
6270
|
}
|
|
6178
6271
|
async validateStreamRequestOptions(options, startTime) {
|
|
6179
6272
|
await this.validateStreamInput(options);
|
|
@@ -8484,9 +8577,16 @@ Current user's request: ${currentInput}`;
|
|
|
8484
8577
|
: params
|
|
8485
8578
|
? JSON.stringify(params)
|
|
8486
8579
|
: "";
|
|
8580
|
+
const executionStartTime = Date.now();
|
|
8581
|
+
// Per-invocation id so consumers can correlate a tool:start with its matching
|
|
8582
|
+
// tool:end even when the same tool runs multiple times concurrently.
|
|
8583
|
+
const executionId = `${toolName}-${executionStartTime}-${Math.random()
|
|
8584
|
+
.toString(36)
|
|
8585
|
+
.slice(2, 11)}`;
|
|
8487
8586
|
return {
|
|
8488
8587
|
functionTag: "NeuroLink.executeTool",
|
|
8489
|
-
executionStartTime
|
|
8588
|
+
executionStartTime,
|
|
8589
|
+
executionId,
|
|
8490
8590
|
externalTool,
|
|
8491
8591
|
toolType,
|
|
8492
8592
|
inputSize: inputStr.length,
|
|
@@ -8544,6 +8644,7 @@ Current user's request: ${currentInput}`;
|
|
|
8544
8644
|
this.emitter.emit("tool:start", createToolEventPayload(toolName, {
|
|
8545
8645
|
timestamp: executionContext.executionStartTime,
|
|
8546
8646
|
input: params,
|
|
8647
|
+
executionId: executionContext.executionId,
|
|
8547
8648
|
}));
|
|
8548
8649
|
const toolInfo = this.toolRegistry.getToolInfo(toolName);
|
|
8549
8650
|
const finalOptions = {
|
|
@@ -8709,7 +8810,7 @@ Current user's request: ${currentInput}`;
|
|
|
8709
8810
|
prepared.metrics.errorCategories[mappedCategory] =
|
|
8710
8811
|
(prepared.metrics.errorCategories[mappedCategory] || 0) + 1;
|
|
8711
8812
|
}
|
|
8712
|
-
this.emitToolEndEvent(toolName, executionContext.executionStartTime, !isToolError, result, isToolError && errorText ? new Error(errorText) : undefined);
|
|
8813
|
+
this.emitToolEndEvent(toolName, executionContext.executionStartTime, !isToolError, result, isToolError && errorText ? new Error(errorText) : undefined, executionContext.executionId);
|
|
8713
8814
|
toolSpan.setAttribute("tool.result.status", isToolError ? "error" : "success");
|
|
8714
8815
|
toolSpan.setAttribute("tool.duration_ms", executionTime);
|
|
8715
8816
|
return result;
|
|
@@ -8728,7 +8829,7 @@ Current user's request: ${currentInput}`;
|
|
|
8728
8829
|
});
|
|
8729
8830
|
prepared.metrics.errorCategories[ErrorCategory.EXECUTION] =
|
|
8730
8831
|
(prepared.metrics.errorCategories[ErrorCategory.EXECUTION] || 0) + 1;
|
|
8731
|
-
this.emitToolEndEvent(toolName, executionContext.executionStartTime, false, undefined, new Error(`Circuit breaker open for ${toolName} (state=${error.breakerState}, failures=${error.failureCount})`));
|
|
8832
|
+
this.emitToolEndEvent(toolName, executionContext.executionStartTime, false, undefined, new Error(`Circuit breaker open for ${toolName} (state=${error.breakerState}, failures=${error.failureCount})`), executionContext.executionId);
|
|
8732
8833
|
toolSpan.setAttribute("tool.result.status", "circuit_breaker_open");
|
|
8733
8834
|
toolSpan.setAttribute("tool.duration_ms", executionTime);
|
|
8734
8835
|
toolSpan.setAttribute("tool.circuit_breaker.state", error.breakerState);
|
|
@@ -8783,7 +8884,7 @@ Current user's request: ${currentInput}`;
|
|
|
8783
8884
|
const category = structuredError.category || ErrorCategory.EXECUTION;
|
|
8784
8885
|
prepared.metrics.errorCategories[category] =
|
|
8785
8886
|
(prepared.metrics.errorCategories[category] || 0) + 1;
|
|
8786
|
-
this.emitToolEndEvent(toolName, executionContext.executionStartTime, false, undefined, structuredError);
|
|
8887
|
+
this.emitToolEndEvent(toolName, executionContext.executionStartTime, false, undefined, structuredError, executionContext.executionId);
|
|
8787
8888
|
// Gate on listenerCount: Node EventEmitter rethrows the original error
|
|
8788
8889
|
// from emit("error", e) when no listener is registered, which would
|
|
8789
8890
|
// short-circuit the surrounding flow and surface as an unhandled
|
|
@@ -220,7 +220,7 @@ export async function executeAutoresearchTick(task, neurolink, emitter) {
|
|
|
220
220
|
...(phasePolicy.forcedTool
|
|
221
221
|
? {
|
|
222
222
|
prepareStep: ({ stepNumber }) => {
|
|
223
|
-
if (stepNumber === 0) {
|
|
223
|
+
if (stepNumber === 0 && phasePolicy.forcedTool) {
|
|
224
224
|
return {
|
|
225
225
|
toolChoice: {
|
|
226
226
|
type: "tool",
|
|
@@ -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
|
};
|
|
@@ -128,7 +128,6 @@ async function executeModel(options, workflowDefaultSystemPrompt) {
|
|
|
128
128
|
let result;
|
|
129
129
|
let attempts = 0;
|
|
130
130
|
const MAX_ATTEMPTS = 2;
|
|
131
|
-
/* eslint-disable no-constant-condition */
|
|
132
131
|
while (true) {
|
|
133
132
|
attempts++;
|
|
134
133
|
result = await executeWithTimeout(async () => {
|
|
@@ -145,7 +144,6 @@ async function executeModel(options, workflowDefaultSystemPrompt) {
|
|
|
145
144
|
}
|
|
146
145
|
logger.warn(`[${functionTag}] Model returned empty content — retrying once`, { provider: model.provider, model: model.model, attempt: attempts });
|
|
147
146
|
}
|
|
148
|
-
/* eslint-enable no-constant-condition */
|
|
149
147
|
const responseTime = Date.now() - startTime;
|
|
150
148
|
logger.debug(`[${functionTag}] Model execution successful`, {
|
|
151
149
|
provider: model.provider,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@juspay/neurolink",
|
|
3
|
-
"version": "9.
|
|
3
|
+
"version": "9.79.0",
|
|
4
4
|
"packageManager": "pnpm@10.15.1",
|
|
5
5
|
"description": "Universal AI Development Platform with working MCP integration, multi-provider support, voice (TTS/STT/realtime), and professional CLI. 58+ external MCP servers discoverable, multimodal file processing, RAG pipelines. Build, test, and deploy AI applications with 21+ providers: OpenAI, Anthropic, Google AI Studio, Google Vertex, AWS Bedrock, Azure OpenAI, Mistral, LiteLLM, SageMaker, Hugging Face, Ollama, OpenAI-compatible, OpenRouter, DeepSeek, NVIDIA NIM, LM Studio, llama.cpp, plus voice (OpenAI TTS, ElevenLabs, Deepgram, Azure Speech).",
|
|
6
6
|
"author": {
|
|
@@ -128,7 +128,9 @@
|
|
|
128
128
|
"// CI tier — fast, no live AI calls, safe for every commit": "",
|
|
129
129
|
"test:unit:vitest": "pnpm exec vitest run test/toolRouting.test.ts",
|
|
130
130
|
"test:tool-routing": "pnpm run test:unit:vitest && npx tsx test/continuous-test-suite-tool-routing.ts",
|
|
131
|
-
"test:
|
|
131
|
+
"test:tool-routing-semantic:vitest": "pnpm exec vitest run test/toolRoutingSemantic.test.ts",
|
|
132
|
+
"test:tool-routing-semantic": "pnpm run test:tool-routing-semantic:vitest && npx tsx test/continuous-test-suite-tool-routing-semantic.ts",
|
|
133
|
+
"test:unit": "pnpm run test:envguard && pnpm run test:bugfixes && pnpm run test:mcp:infra && pnpm run test:mcp:bash && pnpm run test:mcp:limits && pnpm run test:mcp:spans && pnpm run test:autoresearch:redis && pnpm run test:unit:vitest && pnpm run test:tool-routing-cli:vitest && pnpm run test:tool-dedup:vitest && pnpm run test:model-pool:vitest && pnpm run test:tool-routing-semantic:vitest",
|
|
132
134
|
"// CI tier — live providers, runs only when API keys are present (test:credentials and test:dynamic make real provider calls when keys are set, so they live here, not in test:unit)": "",
|
|
133
135
|
"test:live": "pnpm run test:providers && pnpm run test:mcp:http && pnpm run test:mcp:sdk && pnpm run test:mcp:cli && pnpm run test:observability && pnpm run test:context && pnpm run test:memory && pnpm run test:tool-reliability && pnpm run test:evaluation && pnpm run test:autoresearch && pnpm run test:credentials && pnpm run test:dynamic",
|
|
134
136
|
"// CI tier — product output (image/video/TTS/PPT) — costs $$ per run": "",
|