@juspay/neurolink 9.73.0 → 9.75.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 +16 -0
- package/dist/browser/neurolink.min.js +349 -349
- package/dist/cli/factories/commandFactory.js +62 -0
- package/dist/cli/utils/toolRoutingFlags.d.ts +20 -0
- package/dist/cli/utils/toolRoutingFlags.js +115 -0
- package/dist/core/toolRouting.js +90 -4
- package/dist/core/toolRoutingCache.d.ts +64 -0
- package/dist/core/toolRoutingCache.js +154 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +4 -0
- package/dist/lib/core/toolRouting.js +90 -4
- package/dist/lib/core/toolRoutingCache.d.ts +64 -0
- package/dist/lib/core/toolRoutingCache.js +155 -0
- package/dist/lib/index.d.ts +2 -0
- package/dist/lib/index.js +4 -0
- package/dist/lib/neurolink.d.ts +8 -6
- package/dist/lib/neurolink.js +187 -39
- package/dist/lib/session/globalSessionState.d.ts +10 -1
- package/dist/lib/session/globalSessionState.js +18 -0
- package/dist/lib/types/cli.d.ts +29 -3
- package/dist/lib/types/toolRouting.d.ts +88 -0
- package/dist/neurolink.d.ts +8 -6
- package/dist/neurolink.js +187 -39
- package/dist/session/globalSessionState.d.ts +10 -1
- package/dist/session/globalSessionState.js +18 -0
- package/dist/types/cli.d.ts +29 -3
- package/dist/types/toolRouting.d.ts +88 -0
- package/package.json +6 -2
package/dist/lib/neurolink.js
CHANGED
|
@@ -31,6 +31,7 @@ import { repairToolPairs } from "./context/toolPairRepair.js";
|
|
|
31
31
|
import { SYSTEM_LIMITS, DEFAULT_TOOL_ROUTING_TIMEOUT_MS, } from "./core/constants.js";
|
|
32
32
|
import { ConversationMemoryManager } from "./core/conversationMemoryManager.js";
|
|
33
33
|
import { buildToolRoutingCatalog, buildRoutingQueryFromHistory, resolveToolRoutingExclusions, } from "./core/toolRouting.js";
|
|
34
|
+
import { ToolRoutingCache } from "./core/toolRoutingCache.js";
|
|
34
35
|
import { AIProviderFactory } from "./core/factory.js";
|
|
35
36
|
import { createToolEventPayload } from "./core/toolEvents.js";
|
|
36
37
|
import { ProviderRegistry } from "./factories/providerRegistry.js";
|
|
@@ -59,7 +60,7 @@ import { SpanSerializer } from "./observability/utils/spanSerializer.js";
|
|
|
59
60
|
import { flushOpenTelemetry, getLangfuseContext, getLangfuseHealthStatus, initializeOpenTelemetry, isOpenTelemetryInitialized, runWithCurrentLangfuseContext, setLangfuseContext, shutdownOpenTelemetry, stampGuestRescueIdentity, } from "./services/server/ai/observability/instrumentation.js";
|
|
60
61
|
import { TaskManager } from "./tasks/taskManager.js";
|
|
61
62
|
import { createTaskTools } from "./tasks/tools/taskTools.js";
|
|
62
|
-
import { ATTR } from "./telemetry/attributes.js";
|
|
63
|
+
import { ATTR, spanJsonAttribute } from "./telemetry/attributes.js";
|
|
63
64
|
import { tracers } from "./telemetry/tracers.js";
|
|
64
65
|
import { getConversationMessages, storeConversationTurn, } from "./utils/conversationMemory.js";
|
|
65
66
|
// Enhanced error handling imports
|
|
@@ -441,6 +442,9 @@ export class NeuroLink {
|
|
|
441
442
|
// The server catalog inside it can be supplied/replaced later via
|
|
442
443
|
// setToolRoutingServers() for hosts that register tools after construction.
|
|
443
444
|
toolRoutingConfig;
|
|
445
|
+
// Lazy-initialized routing decision cache (ITEM C). Created on first use so
|
|
446
|
+
// instances that don't use routing pay no overhead.
|
|
447
|
+
toolRoutingCacheInstance;
|
|
444
448
|
// Add orchestration property
|
|
445
449
|
enableOrchestration;
|
|
446
450
|
// Authentication provider for secure access control
|
|
@@ -3019,7 +3023,14 @@ Current user's request: ${currentInput}`;
|
|
|
3019
3023
|
generateSpan.setStatus({ code: SpanStatusCode.OK });
|
|
3020
3024
|
return earlyResult;
|
|
3021
3025
|
}
|
|
3022
|
-
|
|
3026
|
+
// Pre-call tool routing for generate(): mirrors the stream() routing path.
|
|
3027
|
+
// Runs inside the generate's Langfuse context (setLangfuseContextFromOptions)
|
|
3028
|
+
// so the router's own generation span nests under this turn's trace.
|
|
3029
|
+
// After the early-result short-circuit so workflow/media turns skip it.
|
|
3030
|
+
const result = await this.setLangfuseContextFromOptions(options, async () => {
|
|
3031
|
+
await this.applyToolRoutingExclusions(options, options.input?.text ?? "");
|
|
3032
|
+
return this.runStandardGenerateRequest(options, originalPrompt, generateSpan);
|
|
3033
|
+
});
|
|
3023
3034
|
generateSpan.setStatus({ code: SpanStatusCode.OK });
|
|
3024
3035
|
return result;
|
|
3025
3036
|
}
|
|
@@ -5688,12 +5699,13 @@ Current user's request: ${currentInput}`;
|
|
|
5688
5699
|
}
|
|
5689
5700
|
}
|
|
5690
5701
|
/**
|
|
5691
|
-
* Pre-call tool routing for stream(): runs the
|
|
5692
|
-
* and appends the unpicked servers' registered tool
|
|
5693
|
-
* `options.excludeTools` — the per-call denylist enforced by
|
|
5694
|
-
* `baseProvider.applyToolFiltering`. No-op unless `toolRouting.enabled`
|
|
5695
|
-
*
|
|
5696
|
-
*
|
|
5702
|
+
* Pre-call tool routing for both stream() and generate() turns: runs the
|
|
5703
|
+
* router LLM once per turn and appends the unpicked servers' registered tool
|
|
5704
|
+
* names to `options.excludeTools` — the per-call denylist enforced by
|
|
5705
|
+
* `baseProvider.applyToolFiltering`. No-op unless `toolRouting.enabled` is
|
|
5706
|
+
* true and a non-empty server catalog has been supplied. Never throws (the
|
|
5707
|
+
* resolver fails open to an empty exclusion list). Accepts both StreamOptions
|
|
5708
|
+
* and GenerateOptions since both share the required fields.
|
|
5697
5709
|
*/
|
|
5698
5710
|
async applyToolRoutingExclusions(options, userQuery) {
|
|
5699
5711
|
const routingConfig = this.toolRoutingConfig;
|
|
@@ -5706,8 +5718,8 @@ Current user's request: ${currentInput}`;
|
|
|
5706
5718
|
}
|
|
5707
5719
|
// Whole setup is fail-open: catalog building (getCustomTools /
|
|
5708
5720
|
// buildToolRoutingCatalog) and the router call degrade to no exclusions
|
|
5709
|
-
// rather than killing the stream, honoring this method's
|
|
5710
|
-
// contract. Genuine
|
|
5721
|
+
// rather than killing the stream/generate turn, honoring this method's
|
|
5722
|
+
// "never throws" contract. Genuine cancellations still propagate.
|
|
5711
5723
|
try {
|
|
5712
5724
|
const registeredToolNames = Array.from(this.getCustomTools().keys());
|
|
5713
5725
|
const catalog = buildToolRoutingCatalog(servers, registeredToolNames);
|
|
@@ -5724,44 +5736,180 @@ Current user's request: ${currentInput}`;
|
|
|
5724
5736
|
const routingQuery = recentMessages.length > 0
|
|
5725
5737
|
? buildRoutingQueryFromHistory(recentMessages, userQuery)
|
|
5726
5738
|
: userQuery;
|
|
5727
|
-
//
|
|
5728
|
-
|
|
5729
|
-
|
|
5730
|
-
|
|
5731
|
-
//
|
|
5732
|
-
|
|
5739
|
+
// --- ITEM C: routing decision cache ---
|
|
5740
|
+
const cacheConfig = routingConfig.cache;
|
|
5741
|
+
const cacheEnabled = cacheConfig?.enabled === true;
|
|
5742
|
+
const stickinessEnabled = routingConfig.stickiness?.enabled === true;
|
|
5743
|
+
// Lazy-init the cache instance once per NeuroLink instance.
|
|
5744
|
+
if ((cacheEnabled || stickinessEnabled) &&
|
|
5745
|
+
!this.toolRoutingCacheInstance) {
|
|
5746
|
+
this.toolRoutingCacheInstance = new ToolRoutingCache({
|
|
5747
|
+
ttlMs: cacheConfig?.ttlMs,
|
|
5748
|
+
maxEntries: cacheConfig?.maxEntries,
|
|
5749
|
+
stickyTurns: routingConfig.stickiness?.turns,
|
|
5750
|
+
});
|
|
5751
|
+
}
|
|
5752
|
+
const cache = this.toolRoutingCacheInstance;
|
|
5753
|
+
// Derive sessionId for stickiness — same extraction path as
|
|
5754
|
+
// fetchRecentRoutingHistory.
|
|
5755
|
+
const requestContext = options.context;
|
|
5756
|
+
const sessionId = typeof requestContext?.sessionId === "string"
|
|
5757
|
+
? requestContext.sessionId
|
|
5758
|
+
: "";
|
|
5759
|
+
// Cache key: session + normalized routing query.
|
|
5760
|
+
// Require a non-empty sessionId to avoid anonymous sessions sharing a
|
|
5761
|
+
// ":query" namespace (cross-session cache leak).
|
|
5762
|
+
const cacheKey = cacheEnabled && sessionId ? `${sessionId}:${routingQuery}` : undefined;
|
|
5763
|
+
// --- ITEM E: build the emitDecision callback ---
|
|
5764
|
+
const emitDecision = (decision) => {
|
|
5765
|
+
try {
|
|
5766
|
+
const activeSpan = trace.getActiveSpan();
|
|
5767
|
+
if (!activeSpan) {
|
|
5768
|
+
return;
|
|
5769
|
+
}
|
|
5770
|
+
activeSpan.setAttribute("tool_routing.outcome", decision.outcome);
|
|
5771
|
+
activeSpan.setAttribute("tool_routing.routable_server_count", decision.routableServerCount);
|
|
5772
|
+
activeSpan.setAttribute("tool_routing.selected_server_ids", spanJsonAttribute(decision.selectedServerIds));
|
|
5773
|
+
activeSpan.setAttribute("tool_routing.excluded_server_ids", spanJsonAttribute(decision.excludedServerIds));
|
|
5774
|
+
activeSpan.setAttribute("tool_routing.hallucinated_ids", spanJsonAttribute(decision.hallucinatedIds));
|
|
5775
|
+
activeSpan.setAttribute("tool_routing.excluded_tool_count", decision.excludedToolCount);
|
|
5776
|
+
activeSpan.setAttribute("tool_routing.cache_hit", decision.cacheHit);
|
|
5777
|
+
activeSpan.setAttribute("tool_routing.duration_ms", decision.durationMs);
|
|
5778
|
+
}
|
|
5779
|
+
catch {
|
|
5780
|
+
// Telemetry must never affect routing behaviour.
|
|
5781
|
+
}
|
|
5782
|
+
};
|
|
5783
|
+
// Unified hit/miss block: always produces raw (pre-stickiness) exclusions
|
|
5784
|
+
// so stickiness can be applied identically on both paths below.
|
|
5733
5785
|
let routedExcludeTools;
|
|
5734
|
-
|
|
5735
|
-
|
|
5736
|
-
|
|
5737
|
-
|
|
5738
|
-
|
|
5739
|
-
|
|
5740
|
-
|
|
5741
|
-
|
|
5742
|
-
|
|
5743
|
-
|
|
5744
|
-
|
|
5745
|
-
|
|
5746
|
-
|
|
5747
|
-
timeoutMs: routingConfig.timeoutMs ?? DEFAULT_TOOL_ROUTING_TIMEOUT_MS,
|
|
5748
|
-
// Forward the stream's abort signal so a cancelled stream aborts the
|
|
5749
|
-
// router call promptly instead of waiting out the routing timeout.
|
|
5750
|
-
generateFn: (generateOptions) => this.generate({
|
|
5751
|
-
...generateOptions,
|
|
5752
|
-
abortSignal: options.abortSignal,
|
|
5753
|
-
}),
|
|
5786
|
+
let selectedServerIds = [];
|
|
5787
|
+
let resolvedDecision;
|
|
5788
|
+
let fromCache = false;
|
|
5789
|
+
const cached = cacheEnabled && cache && cacheKey !== undefined
|
|
5790
|
+
? cache.get(cacheKey)
|
|
5791
|
+
: undefined;
|
|
5792
|
+
if (cached) {
|
|
5793
|
+
// Cache HIT — use stored raw exclusions (pre-stickiness).
|
|
5794
|
+
fromCache = true;
|
|
5795
|
+
routedExcludeTools = [...cached.excludedToolNames];
|
|
5796
|
+
selectedServerIds = [...cached.selectedServerIds];
|
|
5797
|
+
logger.debug("[ToolRouting] Cache hit, skipping router LLM", {
|
|
5798
|
+
cacheKey,
|
|
5754
5799
|
});
|
|
5800
|
+
// Emit cache-hit telemetry with full parity to the miss path.
|
|
5801
|
+
try {
|
|
5802
|
+
const activeSpan = trace.getActiveSpan();
|
|
5803
|
+
if (activeSpan) {
|
|
5804
|
+
const routableCount = catalog.filter((s) => !(routingConfig.alwaysIncludeServerIds ?? []).includes(s.id)).length;
|
|
5805
|
+
activeSpan.setAttribute("tool_routing.outcome", "cache-hit");
|
|
5806
|
+
activeSpan.setAttribute("tool_routing.routable_server_count", routableCount);
|
|
5807
|
+
activeSpan.setAttribute("tool_routing.cache_hit", true);
|
|
5808
|
+
activeSpan.setAttribute("tool_routing.selected_server_ids", spanJsonAttribute(selectedServerIds));
|
|
5809
|
+
activeSpan.setAttribute("tool_routing.excluded_tool_count", routedExcludeTools.length);
|
|
5810
|
+
}
|
|
5811
|
+
}
|
|
5812
|
+
catch {
|
|
5813
|
+
// Telemetry must never affect routing behaviour.
|
|
5814
|
+
}
|
|
5755
5815
|
}
|
|
5756
|
-
|
|
5757
|
-
|
|
5758
|
-
|
|
5816
|
+
else {
|
|
5817
|
+
// Cache MISS — run the router LLM.
|
|
5818
|
+
// The router call re-enters the public generate(), whose finally block
|
|
5819
|
+
// resets _disableToolCacheForCurrentRequest to false. That flag is
|
|
5820
|
+
// turn-scoped and read by the main tool execution path after routing, so
|
|
5821
|
+
// save it before the router call and restore it afterward.
|
|
5822
|
+
const cacheDisabledForCurrentRequest = this._disableToolCacheForCurrentRequest;
|
|
5823
|
+
try {
|
|
5824
|
+
// Intercept the decision so we can store it in the cache.
|
|
5825
|
+
const captureDecision = (decision) => {
|
|
5826
|
+
resolvedDecision = decision;
|
|
5827
|
+
emitDecision(decision);
|
|
5828
|
+
};
|
|
5829
|
+
routedExcludeTools = await resolveToolRoutingExclusions({
|
|
5830
|
+
catalog,
|
|
5831
|
+
alwaysIncludeServerIds: routingConfig.alwaysIncludeServerIds ?? [],
|
|
5832
|
+
userQuery: routingQuery,
|
|
5833
|
+
routerPromptPrefix: routingConfig.routerPromptPrefix,
|
|
5834
|
+
routerModel: {
|
|
5835
|
+
provider: routingConfig.routerModel?.provider ??
|
|
5836
|
+
options.provider,
|
|
5837
|
+
model: routingConfig.routerModel?.model ?? options.model,
|
|
5838
|
+
region: routingConfig.routerModel?.region ?? options.region,
|
|
5839
|
+
temperature: routingConfig.routerModel?.temperature,
|
|
5840
|
+
},
|
|
5841
|
+
timeoutMs: routingConfig.timeoutMs ?? DEFAULT_TOOL_ROUTING_TIMEOUT_MS,
|
|
5842
|
+
// Forward the abort signal so a cancelled turn aborts the router
|
|
5843
|
+
// call promptly instead of waiting out the routing timeout.
|
|
5844
|
+
generateFn: (generateOptions) => this.generate({
|
|
5845
|
+
...generateOptions,
|
|
5846
|
+
abortSignal: options.abortSignal,
|
|
5847
|
+
}),
|
|
5848
|
+
emitDecision: captureDecision,
|
|
5849
|
+
});
|
|
5850
|
+
}
|
|
5851
|
+
finally {
|
|
5852
|
+
this._disableToolCacheForCurrentRequest =
|
|
5853
|
+
cacheDisabledForCurrentRequest;
|
|
5854
|
+
}
|
|
5855
|
+
if (resolvedDecision?.outcome === "applied") {
|
|
5856
|
+
selectedServerIds = resolvedDecision.selectedServerIds;
|
|
5857
|
+
}
|
|
5759
5858
|
}
|
|
5760
5859
|
// Aborted during the router call — skip applying now-stale exclusions;
|
|
5761
5860
|
// the main generation path enforces the abort itself.
|
|
5762
5861
|
if (options.abortSignal?.aborted) {
|
|
5763
5862
|
return;
|
|
5764
5863
|
}
|
|
5864
|
+
// Capture raw (pre-stickiness) exclusions for cache storage on MISS path.
|
|
5865
|
+
const rawExclusions = [...routedExcludeTools];
|
|
5866
|
+
// Apply stickiness on BOTH hit and miss paths: the sticky ids were
|
|
5867
|
+
// recorded on a prior turn and represent servers that should stay warm for
|
|
5868
|
+
// the current turn. Consuming (decrementing) them before recordSelection
|
|
5869
|
+
// ensures the window covers the correct set of future turns rather than
|
|
5870
|
+
// burning one count on the same turn the selection is recorded
|
|
5871
|
+
// (off-by-one fix).
|
|
5872
|
+
if (stickinessEnabled && cache && sessionId) {
|
|
5873
|
+
try {
|
|
5874
|
+
const stickyIds = cache.getStickyServerIds(sessionId);
|
|
5875
|
+
if (stickyIds.length > 0) {
|
|
5876
|
+
// Remove from routedExcludeTools any tool belonging to a sticky server.
|
|
5877
|
+
// Precompute prefixes to avoid rebuilding the set inside the predicate.
|
|
5878
|
+
const stickyPrefixes = stickyIds.map((id) => `${id}_`);
|
|
5879
|
+
routedExcludeTools = routedExcludeTools.filter((toolName) => !stickyPrefixes.some((prefix) => toolName.startsWith(prefix)));
|
|
5880
|
+
}
|
|
5881
|
+
}
|
|
5882
|
+
catch {
|
|
5883
|
+
// Stickiness failure is non-fatal.
|
|
5884
|
+
}
|
|
5885
|
+
}
|
|
5886
|
+
// --- ITEM C: store result for future turns (MISS path only) ---
|
|
5887
|
+
// Cache stores RAW (pre-stickiness) exclusions so the cached value is
|
|
5888
|
+
// correct regardless of which sessions hit it on subsequent turns.
|
|
5889
|
+
if (!fromCache && resolvedDecision?.outcome === "applied" && cache) {
|
|
5890
|
+
if (cacheEnabled && cacheKey !== undefined) {
|
|
5891
|
+
try {
|
|
5892
|
+
cache.set(cacheKey, {
|
|
5893
|
+
excludedToolNames: rawExclusions,
|
|
5894
|
+
selectedServerIds,
|
|
5895
|
+
});
|
|
5896
|
+
}
|
|
5897
|
+
catch {
|
|
5898
|
+
// Cache write failure is non-fatal.
|
|
5899
|
+
}
|
|
5900
|
+
}
|
|
5901
|
+
// Record selected servers for stickiness on subsequent turns. This must
|
|
5902
|
+
// come after getStickyServerIds so the window count is not consumed on
|
|
5903
|
+
// the same turn it is set (the off-by-one fix above).
|
|
5904
|
+
if (stickinessEnabled && sessionId) {
|
|
5905
|
+
try {
|
|
5906
|
+
cache.recordSelection(sessionId, selectedServerIds);
|
|
5907
|
+
}
|
|
5908
|
+
catch {
|
|
5909
|
+
// Stickiness failure is non-fatal.
|
|
5910
|
+
}
|
|
5911
|
+
}
|
|
5912
|
+
}
|
|
5765
5913
|
if (routedExcludeTools.length > 0) {
|
|
5766
5914
|
options.excludeTools = [
|
|
5767
5915
|
...(options.excludeTools ?? []),
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { NeuroLink } from "../neurolink.js";
|
|
2
|
-
import type { ConversationMemoryConfig, LoopSessionState, SessionVariableValue } from "../types/index.js";
|
|
2
|
+
import type { ConversationMemoryConfig, LoopSessionState, SessionVariableValue, ToolRoutingConfig } from "../types/index.js";
|
|
3
3
|
export declare class GlobalSessionManager {
|
|
4
4
|
private static instance;
|
|
5
5
|
private loopSession;
|
|
6
|
+
/** Optional tool-routing config set by CLI handlers before SDK construction. */
|
|
7
|
+
private _toolRoutingConfig;
|
|
6
8
|
static getInstance(): GlobalSessionManager;
|
|
7
9
|
setLoopSession(config?: ConversationMemoryConfig): string;
|
|
8
10
|
/**
|
|
@@ -33,6 +35,13 @@ export declare class GlobalSessionManager {
|
|
|
33
35
|
updateSessionId(newSessionId: string): void;
|
|
34
36
|
getLoopSession(): LoopSessionState | null;
|
|
35
37
|
clearLoopSession(): void;
|
|
38
|
+
/**
|
|
39
|
+
* Store a tool-routing config to be injected at SDK construction time.
|
|
40
|
+
* Call this BEFORE `getOrCreateNeuroLink()` inside a command handler.
|
|
41
|
+
* When a loop session is already active the config is ignored (the instance
|
|
42
|
+
* already exists).
|
|
43
|
+
*/
|
|
44
|
+
setToolRoutingConfig(config: ToolRoutingConfig): void;
|
|
36
45
|
getOrCreateNeuroLink(): NeuroLink;
|
|
37
46
|
getCurrentSessionId(): string | undefined;
|
|
38
47
|
setSessionVariable(key: string, value: SessionVariableValue): void;
|
|
@@ -31,6 +31,8 @@ function buildMcpOutputLimitsFromEnv() {
|
|
|
31
31
|
export class GlobalSessionManager {
|
|
32
32
|
static instance;
|
|
33
33
|
loopSession = null;
|
|
34
|
+
/** Optional tool-routing config set by CLI handlers before SDK construction. */
|
|
35
|
+
_toolRoutingConfig = undefined;
|
|
34
36
|
static getInstance() {
|
|
35
37
|
if (!GlobalSessionManager.instance) {
|
|
36
38
|
GlobalSessionManager.instance = new GlobalSessionManager();
|
|
@@ -127,6 +129,18 @@ export class GlobalSessionManager {
|
|
|
127
129
|
this.loopSession = null;
|
|
128
130
|
}
|
|
129
131
|
}
|
|
132
|
+
/**
|
|
133
|
+
* Store a tool-routing config to be injected at SDK construction time.
|
|
134
|
+
* Call this BEFORE `getOrCreateNeuroLink()` inside a command handler.
|
|
135
|
+
* When a loop session is already active the config is ignored (the instance
|
|
136
|
+
* already exists).
|
|
137
|
+
*/
|
|
138
|
+
setToolRoutingConfig(config) {
|
|
139
|
+
if (this.hasActiveSession()) {
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
this._toolRoutingConfig = config;
|
|
143
|
+
}
|
|
130
144
|
getOrCreateNeuroLink() {
|
|
131
145
|
const session = this.getLoopSession();
|
|
132
146
|
if (session) {
|
|
@@ -142,6 +156,10 @@ export class GlobalSessionManager {
|
|
|
142
156
|
if (mcpOutputLimits) {
|
|
143
157
|
options.mcp = { outputLimits: mcpOutputLimits };
|
|
144
158
|
}
|
|
159
|
+
if (this._toolRoutingConfig) {
|
|
160
|
+
options.toolRouting = this._toolRoutingConfig;
|
|
161
|
+
this._toolRoutingConfig = undefined;
|
|
162
|
+
}
|
|
145
163
|
return new NeuroLink(Object.keys(options).length ? options : undefined);
|
|
146
164
|
}
|
|
147
165
|
getCurrentSessionId() {
|
package/dist/lib/types/cli.d.ts
CHANGED
|
@@ -45,7 +45,7 @@ export type BaseCommandArgs = {
|
|
|
45
45
|
/**
|
|
46
46
|
* Generate command arguments
|
|
47
47
|
*/
|
|
48
|
-
export type GenerateCommandArgs = BaseCommandArgs & {
|
|
48
|
+
export type GenerateCommandArgs = BaseCommandArgs & CliToolRoutingFlags & {
|
|
49
49
|
/** Input text or prompt */
|
|
50
50
|
input?: string;
|
|
51
51
|
/** AI provider to use */
|
|
@@ -110,7 +110,7 @@ export type GenerateCommandArgs = BaseCommandArgs & {
|
|
|
110
110
|
/**
|
|
111
111
|
* Stream command arguments
|
|
112
112
|
*/
|
|
113
|
-
export type StreamCommandArgs = BaseCommandArgs & {
|
|
113
|
+
export type StreamCommandArgs = BaseCommandArgs & CliToolRoutingFlags & {
|
|
114
114
|
/** Input text or prompt */
|
|
115
115
|
input?: string;
|
|
116
116
|
/** AI provider to use */
|
|
@@ -137,7 +137,7 @@ export type StreamCommandArgs = BaseCommandArgs & {
|
|
|
137
137
|
/**
|
|
138
138
|
* Batch command arguments
|
|
139
139
|
*/
|
|
140
|
-
export type BatchCommandArgs = BaseCommandArgs & {
|
|
140
|
+
export type BatchCommandArgs = BaseCommandArgs & CliToolRoutingFlags & {
|
|
141
141
|
/** Input file path */
|
|
142
142
|
file?: string;
|
|
143
143
|
/** AI provider to use */
|
|
@@ -1500,3 +1500,29 @@ export type CliServeFlatRoute = {
|
|
|
1500
1500
|
description?: string;
|
|
1501
1501
|
group: string;
|
|
1502
1502
|
};
|
|
1503
|
+
/**
|
|
1504
|
+
* Raw CLI flag shape for the tool-routing family of options.
|
|
1505
|
+
* Keys are camelCase as yargs delivers them after parsing kebab-case aliases.
|
|
1506
|
+
*/
|
|
1507
|
+
export type CliToolRoutingFlags = {
|
|
1508
|
+
/** Master enable switch (--tool-routing). */
|
|
1509
|
+
toolRouting?: boolean;
|
|
1510
|
+
/** Router LLM hard timeout in ms (--tool-routing-timeout). */
|
|
1511
|
+
toolRoutingTimeout?: number;
|
|
1512
|
+
/** Router LLM provider override (--tool-routing-router-provider). */
|
|
1513
|
+
toolRoutingRouterProvider?: string;
|
|
1514
|
+
/** Router LLM model override (--tool-routing-router-model). */
|
|
1515
|
+
toolRoutingRouterModel?: string;
|
|
1516
|
+
/** Router LLM region override (--tool-routing-router-region). */
|
|
1517
|
+
toolRoutingRouterRegion?: string;
|
|
1518
|
+
/**
|
|
1519
|
+
* Server ids that are always kept and never offered to the router
|
|
1520
|
+
* (--tool-routing-always-include, repeatable).
|
|
1521
|
+
*/
|
|
1522
|
+
toolRoutingAlwaysInclude?: string[];
|
|
1523
|
+
/**
|
|
1524
|
+
* Path to a JSON file OR inline JSON array of server descriptors
|
|
1525
|
+
* (--tool-routing-servers).
|
|
1526
|
+
*/
|
|
1527
|
+
toolRoutingServers?: string;
|
|
1528
|
+
};
|
|
@@ -64,6 +64,30 @@ export type ToolRoutingConfig = {
|
|
|
64
64
|
* always appended by the SDK regardless of this value.
|
|
65
65
|
*/
|
|
66
66
|
routerPromptPrefix?: string;
|
|
67
|
+
/**
|
|
68
|
+
* LRU+TTL cache for routing decisions. When enabled, identical routing
|
|
69
|
+
* queries within the TTL window skip the router LLM entirely and reuse
|
|
70
|
+
* the cached exclusion list.
|
|
71
|
+
*/
|
|
72
|
+
cache?: {
|
|
73
|
+
/** Whether the cache is active. Default: false. */
|
|
74
|
+
enabled?: boolean;
|
|
75
|
+
/** Time-to-live in milliseconds for each cached entry. Default: 60000. */
|
|
76
|
+
ttlMs?: number;
|
|
77
|
+
/** Maximum number of entries in the LRU cache. Default: 256. */
|
|
78
|
+
maxEntries?: number;
|
|
79
|
+
};
|
|
80
|
+
/**
|
|
81
|
+
* Session stickiness: once the router picks a set of servers for a session,
|
|
82
|
+
* those servers are kept warm (not excluded) for the next N turns to prevent
|
|
83
|
+
* flapping.
|
|
84
|
+
*/
|
|
85
|
+
stickiness?: {
|
|
86
|
+
/** Whether session stickiness is active. Default: false. */
|
|
87
|
+
enabled?: boolean;
|
|
88
|
+
/** Number of turns for which a previously-selected server stays warm. Default: 3. */
|
|
89
|
+
turns?: number;
|
|
90
|
+
};
|
|
67
91
|
};
|
|
68
92
|
/** Catalog entry pairing a server descriptor with its registered tool names. */
|
|
69
93
|
export type ToolRoutingCatalogEntry = {
|
|
@@ -72,6 +96,63 @@ export type ToolRoutingCatalogEntry = {
|
|
|
72
96
|
/** Registered tool names for this server, i.e. `${serverId}_${toolName}`. */
|
|
73
97
|
toolNames: string[];
|
|
74
98
|
};
|
|
99
|
+
/** Internal cache entry for `ToolRoutingCache`. */
|
|
100
|
+
export type ToolRoutingCacheEntry = {
|
|
101
|
+
excludedToolNames: string[];
|
|
102
|
+
selectedServerIds: string[];
|
|
103
|
+
/** Absolute expiry timestamp (from the injected `now()` clock). */
|
|
104
|
+
expiresAt: number;
|
|
105
|
+
/** LRU eviction order — lower = older. Bumped on each get/set. */
|
|
106
|
+
accessOrder: number;
|
|
107
|
+
};
|
|
108
|
+
/** Internal stickiness entry for `ToolRoutingCache`. */
|
|
109
|
+
export type ToolRoutingStickiness = {
|
|
110
|
+
serverIds: string[];
|
|
111
|
+
/** Turn counter — decremented on each routing turn, removed when it hits 0. */
|
|
112
|
+
turnsRemaining: number;
|
|
113
|
+
};
|
|
114
|
+
/** Constructor options for `ToolRoutingCache`. */
|
|
115
|
+
export type ToolRoutingCacheOptions = {
|
|
116
|
+
/** Time-to-live in milliseconds for each cached entry. Default: 60_000. */
|
|
117
|
+
ttlMs?: number;
|
|
118
|
+
/** Maximum number of entries kept in the LRU before eviction. Default: 256. */
|
|
119
|
+
maxEntries?: number;
|
|
120
|
+
/** Number of turns a selected server remains sticky per session. Default: 3. */
|
|
121
|
+
stickyTurns?: number;
|
|
122
|
+
/**
|
|
123
|
+
* Clock function for TTL calculations. Defaults to `Date.now`.
|
|
124
|
+
* Inject a deterministic function in tests to control time.
|
|
125
|
+
*/
|
|
126
|
+
now?: () => number;
|
|
127
|
+
};
|
|
128
|
+
/**
|
|
129
|
+
* Outcome classifier for a single routing resolution. Used in
|
|
130
|
+
* `ToolRoutingDecision` and emitted as an OTel span attribute.
|
|
131
|
+
*/
|
|
132
|
+
export type ToolRoutingOutcome = "applied" | "skipped-no-query" | "skipped-single-server" | "empty-pick" | "failed-open-parse" | "failed-open-timeout" | "failed-open-error" | "cache-hit";
|
|
133
|
+
/**
|
|
134
|
+
* Machine-readable summary of one routing resolution. Emitted via the
|
|
135
|
+
* `emitDecision` callback so the caller can attach it as OTel span attributes
|
|
136
|
+
* or record it in any other telemetry sink.
|
|
137
|
+
*/
|
|
138
|
+
export type ToolRoutingDecision = {
|
|
139
|
+
/** How the routing turn concluded. */
|
|
140
|
+
outcome: ToolRoutingOutcome;
|
|
141
|
+
/** Server ids the router kept (selected as relevant). */
|
|
142
|
+
selectedServerIds: string[];
|
|
143
|
+
/** Server ids whose tools were excluded (router considered them irrelevant). */
|
|
144
|
+
excludedServerIds: string[];
|
|
145
|
+
/** Server ids the router returned that did not exist in the catalog. */
|
|
146
|
+
hallucinatedIds: string[];
|
|
147
|
+
/** Total number of individual tool names added to the exclusion list. */
|
|
148
|
+
excludedToolCount: number;
|
|
149
|
+
/** Number of servers that were offered to the router (always-include excluded). */
|
|
150
|
+
routableServerCount: number;
|
|
151
|
+
/** True when the result was served from cache, skipping the router LLM. */
|
|
152
|
+
cacheHit: boolean;
|
|
153
|
+
/** Wall-clock time spent in the routing resolution in milliseconds. */
|
|
154
|
+
durationMs: number;
|
|
155
|
+
};
|
|
75
156
|
/** Parameters for `resolveToolRoutingExclusions()`. */
|
|
76
157
|
export type ToolRoutingResolutionParams = {
|
|
77
158
|
/** Full catalog; always-include servers are filtered out internally. */
|
|
@@ -88,4 +169,11 @@ export type ToolRoutingResolutionParams = {
|
|
|
88
169
|
timeoutMs: number;
|
|
89
170
|
/** Invokes the router LLM — `NeuroLink.generate` bound by the caller. */
|
|
90
171
|
generateFn: (options: GenerateOptions) => Promise<GenerateResult>;
|
|
172
|
+
/**
|
|
173
|
+
* Optional callback invoked once per resolution with a structured summary of
|
|
174
|
+
* the routing decision. Called on every return path (applied, skipped,
|
|
175
|
+
* failed-open). Must never throw — any error inside is swallowed by
|
|
176
|
+
* the resolver.
|
|
177
|
+
*/
|
|
178
|
+
emitDecision?: (decision: ToolRoutingDecision) => void;
|
|
91
179
|
};
|
package/dist/neurolink.d.ts
CHANGED
|
@@ -101,6 +101,7 @@ export declare class NeuroLink {
|
|
|
101
101
|
private conversationMemoryNeedsInit;
|
|
102
102
|
private conversationMemoryConfig?;
|
|
103
103
|
private toolRoutingConfig?;
|
|
104
|
+
private toolRoutingCacheInstance?;
|
|
104
105
|
private enableOrchestration;
|
|
105
106
|
private authProvider?;
|
|
106
107
|
private pendingAuthConfig?;
|
|
@@ -805,12 +806,13 @@ export declare class NeuroLink {
|
|
|
805
806
|
private streamWithIterationFallback;
|
|
806
807
|
private executeStreamRequest;
|
|
807
808
|
/**
|
|
808
|
-
* Pre-call tool routing for stream(): runs the
|
|
809
|
-
* and appends the unpicked servers' registered tool
|
|
810
|
-
* `options.excludeTools` — the per-call denylist enforced by
|
|
811
|
-
* `baseProvider.applyToolFiltering`. No-op unless `toolRouting.enabled`
|
|
812
|
-
*
|
|
813
|
-
*
|
|
809
|
+
* Pre-call tool routing for both stream() and generate() turns: runs the
|
|
810
|
+
* router LLM once per turn and appends the unpicked servers' registered tool
|
|
811
|
+
* names to `options.excludeTools` — the per-call denylist enforced by
|
|
812
|
+
* `baseProvider.applyToolFiltering`. No-op unless `toolRouting.enabled` is
|
|
813
|
+
* true and a non-empty server catalog has been supplied. Never throws (the
|
|
814
|
+
* resolver fails open to an empty exclusion list). Accepts both StreamOptions
|
|
815
|
+
* and GenerateOptions since both share the required fields.
|
|
814
816
|
*/
|
|
815
817
|
private applyToolRoutingExclusions;
|
|
816
818
|
/**
|