@juspay/neurolink 9.72.0 → 9.74.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/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/types/livekit.d.ts +134 -0
- package/dist/lib/types/toolRouting.d.ts +88 -0
- package/dist/lib/voice/livekit/brain.js +1 -1
- package/dist/lib/voice/livekit/config.d.ts +12 -1
- package/dist/lib/voice/livekit/config.js +54 -0
- package/dist/lib/voice/livekit/eventBridge.js +4 -4
- package/dist/lib/voice/livekit/index.d.ts +9 -2
- package/dist/lib/voice/livekit/index.js +9 -2
- package/dist/lib/voice/livekit/realtimeEventBridge.d.ts +14 -0
- package/dist/lib/voice/livekit/realtimeEventBridge.js +161 -0
- package/dist/lib/voice/livekit/realtimeMcpTools.d.ts +31 -0
- package/dist/lib/voice/livekit/realtimeMcpTools.js +194 -0
- package/dist/lib/voice/livekit/realtimeVoiceAgent.d.ts +26 -0
- package/dist/lib/voice/livekit/realtimeVoiceAgent.js +362 -0
- package/dist/lib/voice/livekit/roomContext.d.ts +23 -0
- package/dist/lib/voice/livekit/roomContext.js +57 -0
- package/dist/lib/voice/livekit/roomDispatch.d.ts +24 -0
- package/dist/lib/voice/livekit/roomDispatch.js +31 -0
- package/dist/lib/voice/livekit/schemaSanitizer.d.ts +26 -0
- package/dist/lib/voice/livekit/schemaSanitizer.js +144 -0
- package/dist/lib/voice/livekit/vertexAuth.d.ts +30 -0
- package/dist/lib/voice/livekit/vertexAuth.js +73 -0
- package/dist/lib/voice/livekit/voiceAgent.js +47 -37
- package/dist/lib/voice/livekit/voiceAgentWorker.d.ts +2 -0
- package/dist/lib/voice/livekit/voiceAgentWorker.js +64 -0
- package/dist/neurolink.d.ts +8 -6
- package/dist/neurolink.js +187 -39
- package/dist/types/livekit.d.ts +134 -0
- package/dist/types/toolRouting.d.ts +88 -0
- package/dist/voice/livekit/brain.js +1 -1
- package/dist/voice/livekit/config.d.ts +12 -1
- package/dist/voice/livekit/config.js +54 -0
- package/dist/voice/livekit/eventBridge.js +4 -4
- package/dist/voice/livekit/index.d.ts +9 -2
- package/dist/voice/livekit/index.js +9 -2
- package/dist/voice/livekit/realtimeEventBridge.d.ts +14 -0
- package/dist/voice/livekit/realtimeEventBridge.js +160 -0
- package/dist/voice/livekit/realtimeMcpTools.d.ts +31 -0
- package/dist/voice/livekit/realtimeMcpTools.js +193 -0
- package/dist/voice/livekit/realtimeVoiceAgent.d.ts +26 -0
- package/dist/voice/livekit/realtimeVoiceAgent.js +361 -0
- package/dist/voice/livekit/roomContext.d.ts +23 -0
- package/dist/voice/livekit/roomContext.js +56 -0
- package/dist/voice/livekit/roomDispatch.d.ts +24 -0
- package/dist/voice/livekit/roomDispatch.js +30 -0
- package/dist/voice/livekit/schemaSanitizer.d.ts +26 -0
- package/dist/voice/livekit/schemaSanitizer.js +143 -0
- package/dist/voice/livekit/vertexAuth.d.ts +30 -0
- package/dist/voice/livekit/vertexAuth.js +72 -0
- package/dist/voice/livekit/voiceAgent.js +47 -37
- package/dist/voice/livekit/voiceAgentWorker.d.ts +2 -0
- package/dist/voice/livekit/voiceAgentWorker.js +64 -0
- package/package.json +5 -2
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LRU+TTL cache for pre-call tool routing decisions, plus session stickiness
|
|
3
|
+
* tracking to reduce turn-to-turn server flapping.
|
|
4
|
+
*
|
|
5
|
+
* The cache is keyed by a caller-supplied string (typically a normalized hash
|
|
6
|
+
* of sessionId + routing query). On a hit the cached exclusion list is
|
|
7
|
+
* returned, skipping the router LLM entirely. On a miss the caller resolves
|
|
8
|
+
* normally, stores the result, and `recordSelection` is called with the
|
|
9
|
+
* selected server ids so stickiness can suppress their exclusion for the next
|
|
10
|
+
* N turns.
|
|
11
|
+
*
|
|
12
|
+
* All operations are synchronous and pure (no I/O). The clock is injected via
|
|
13
|
+
* `now` so tests can control time without `Date.now` side effects.
|
|
14
|
+
*/
|
|
15
|
+
const DEFAULT_TTL_MS = 60_000;
|
|
16
|
+
const DEFAULT_MAX_ENTRIES = 256;
|
|
17
|
+
const DEFAULT_STICKY_TURNS = 3;
|
|
18
|
+
/**
|
|
19
|
+
* Internal cap on the number of sessions tracked in the sticky map.
|
|
20
|
+
* Not configurable via public API — avoids unbounded memory growth when many
|
|
21
|
+
* ephemeral session ids are generated. Oldest entry (Map insertion order) is
|
|
22
|
+
* evicted when the cap is reached.
|
|
23
|
+
*/
|
|
24
|
+
const MAX_STICKY_ENTRIES = 1024;
|
|
25
|
+
export class ToolRoutingCache {
|
|
26
|
+
ttlMs;
|
|
27
|
+
maxEntries;
|
|
28
|
+
stickyTurns;
|
|
29
|
+
now;
|
|
30
|
+
/** Main LRU+TTL store keyed by routing key. */
|
|
31
|
+
store = new Map();
|
|
32
|
+
/** Per-session stickiness tracking keyed by session id. */
|
|
33
|
+
sticky = new Map();
|
|
34
|
+
/** Monotonic counter for LRU ordering — incremented on each access. */
|
|
35
|
+
accessCounter = 0;
|
|
36
|
+
constructor(opts = {}) {
|
|
37
|
+
// Fall back to defaults for invalid (<=0 or NaN) values so callers cannot
|
|
38
|
+
// accidentally disable the cache by passing bad config.
|
|
39
|
+
const validOrDefault = (v, def) => v !== undefined && Number.isFinite(v) && v > 0 ? v : def;
|
|
40
|
+
this.ttlMs = validOrDefault(opts.ttlMs, DEFAULT_TTL_MS);
|
|
41
|
+
this.maxEntries = validOrDefault(opts.maxEntries, DEFAULT_MAX_ENTRIES);
|
|
42
|
+
this.stickyTurns = validOrDefault(opts.stickyTurns, DEFAULT_STICKY_TURNS);
|
|
43
|
+
this.now = opts.now ?? Date.now;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Returns the cached routing result for the given key, or `undefined` on a
|
|
47
|
+
* miss or expiry. Expired entries are deleted on access (lazy eviction).
|
|
48
|
+
*/
|
|
49
|
+
get(key) {
|
|
50
|
+
const entry = this.store.get(key);
|
|
51
|
+
if (!entry) {
|
|
52
|
+
return undefined;
|
|
53
|
+
}
|
|
54
|
+
if (this.now() >= entry.expiresAt) {
|
|
55
|
+
this.store.delete(key);
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
// Bump access order for LRU.
|
|
59
|
+
entry.accessOrder = ++this.accessCounter;
|
|
60
|
+
// Return shallow copies so callers cannot mutate cached state.
|
|
61
|
+
return {
|
|
62
|
+
excludedToolNames: [...entry.excludedToolNames],
|
|
63
|
+
selectedServerIds: [...entry.selectedServerIds],
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Stores a routing result under the given key. Evicts the least-recently-
|
|
68
|
+
* used entry when the store is at capacity. Silently no-ops on any error
|
|
69
|
+
* (caller is responsible for fail-open behaviour).
|
|
70
|
+
*/
|
|
71
|
+
set(key, value) {
|
|
72
|
+
try {
|
|
73
|
+
if (this.store.size >= this.maxEntries && !this.store.has(key)) {
|
|
74
|
+
this.evictLRU();
|
|
75
|
+
}
|
|
76
|
+
// Defensive copies: guard against callers mutating arrays after storing.
|
|
77
|
+
this.store.set(key, {
|
|
78
|
+
excludedToolNames: [...value.excludedToolNames],
|
|
79
|
+
selectedServerIds: [...value.selectedServerIds],
|
|
80
|
+
expiresAt: this.now() + this.ttlMs,
|
|
81
|
+
accessOrder: ++this.accessCounter,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
// Silently no-ops on any error — cache writes are non-fatal.
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Records the server ids selected by the router for a session so they stay
|
|
90
|
+
* warm for the next `stickyTurns` turns. Called after a successful
|
|
91
|
+
* (non-cached) routing resolution.
|
|
92
|
+
*/
|
|
93
|
+
recordSelection(sessionId, serverIds) {
|
|
94
|
+
if (!sessionId || serverIds.length === 0) {
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
// Evict the oldest session when at the internal cap (Map preserves insertion
|
|
98
|
+
// order, so the first key is always the oldest).
|
|
99
|
+
if (this.sticky.size >= MAX_STICKY_ENTRIES && !this.sticky.has(sessionId)) {
|
|
100
|
+
const oldestKey = this.sticky.keys().next().value;
|
|
101
|
+
if (oldestKey !== undefined) {
|
|
102
|
+
this.sticky.delete(oldestKey);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
this.sticky.set(sessionId, {
|
|
106
|
+
serverIds: [...serverIds],
|
|
107
|
+
turnsRemaining: this.stickyTurns,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Returns the server ids that should be kept warm (not excluded) for the
|
|
112
|
+
* given session due to stickiness. Decrements the turn counter; when it
|
|
113
|
+
* reaches zero the entry is removed. Returns an empty array when there is no
|
|
114
|
+
* active sticky state for the session.
|
|
115
|
+
*/
|
|
116
|
+
getStickyServerIds(sessionId) {
|
|
117
|
+
if (!sessionId) {
|
|
118
|
+
return [];
|
|
119
|
+
}
|
|
120
|
+
const entry = this.sticky.get(sessionId);
|
|
121
|
+
if (!entry) {
|
|
122
|
+
return [];
|
|
123
|
+
}
|
|
124
|
+
const ids = [...entry.serverIds];
|
|
125
|
+
if (entry.turnsRemaining <= 1) {
|
|
126
|
+
this.sticky.delete(sessionId);
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
entry.turnsRemaining -= 1;
|
|
130
|
+
}
|
|
131
|
+
return ids;
|
|
132
|
+
}
|
|
133
|
+
// ---------------------------------------------------------------------------
|
|
134
|
+
// Private helpers
|
|
135
|
+
// ---------------------------------------------------------------------------
|
|
136
|
+
/**
|
|
137
|
+
* Scans the store for the entry with the lowest accessOrder and removes it.
|
|
138
|
+
* O(n) scan is acceptable at the default maxEntries (256); very large caches
|
|
139
|
+
* should consider an O(1) doubly-linked-list approach instead.
|
|
140
|
+
*/
|
|
141
|
+
evictLRU() {
|
|
142
|
+
let lruKey;
|
|
143
|
+
let lruOrder = Infinity;
|
|
144
|
+
for (const [key, entry] of this.store) {
|
|
145
|
+
if (entry.accessOrder < lruOrder) {
|
|
146
|
+
lruOrder = entry.accessOrder;
|
|
147
|
+
lruKey = key;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
if (lruKey !== undefined) {
|
|
151
|
+
this.store.delete(lruKey);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
//# sourceMappingURL=toolRoutingCache.js.map
|
package/dist/lib/index.d.ts
CHANGED
|
@@ -216,6 +216,8 @@ export declare function createBestAIProvider(requestedProvider?: string, modelNa
|
|
|
216
216
|
export { CircuitBreakerManager, calculateExpiresAt, createMCPServer, createOAuthProviderFromConfig, DEFAULT_HTTP_RETRY_CONFIG, DEFAULT_RATE_LIMIT_CONFIG, executeMCP, FileTokenStorage, getMCPStats, getServerInfo, globalCircuitBreakerManager, globalRateLimiterManager, HTTPRateLimiter, InMemoryTokenStorage, initializeMCPEcosystem, isRetryableHTTPError, isRetryableStatusCode, isTokenExpired, listMCPs, CircuitBreakerOpenError, MCPCircuitBreaker, mcpLogger, NeuroLinkOAuthProvider, RateLimiterManager, validateServerTools, validateTool as validateMCPTool, withHTTPRetry, MCPToolRegistry, ExternalServerManager, MCPClientFactory, ToolRouter, createToolRouter, DEFAULT_ROUTER_CONFIG, ToolCache, createToolCache, DEFAULT_CACHE_CONFIG, ToolResultCache, createToolResultCache, RequestBatcher, ToolCallBatcher, createRequestBatcher, createToolCallBatcher, DEFAULT_BATCH_CONFIG, inferAnnotations, createAnnotatedTool, validateAnnotations, filterToolsByAnnotations, mergeAnnotations, getAnnotationSummary, requiresConfirmation, isSafeToRetry, getToolSafetyLevel, ElicitationManager, globalElicitationManager, EnhancedToolDiscovery, MCPRegistryClient, globalMCPRegistryClient, getWellKnownServer, getAllWellKnownServers, MCPServerBase, MultiServerManager, globalMultiServerManager, AgentExposureManager, exposeAgentAsTool, exposeAgentsAsTools, exposeWorkflowAsTool, exposeWorkflowsAsTools, globalAgentExposureManager, ServerCapabilitiesManager, createTextResource, createJsonResource, createPrompt, neuroLinkToolToMCP, mcpToolToNeuroLink, batchConvertToMCP, batchConvertToNeuroLink, sanitizeToolName, validateToolName, createToolFromFunction, mcpProtocolToolToServerTool, serverToolToMCPProtocol, TOOL_COMPATIBILITY, ToolIntegrationManager, globalToolIntegrationManager, wrapToolWithElicitation, wrapToolsWithElicitation, createElicitationContext, confirmationMiddleware, validationMiddleware, loggingMiddleware, createRetryMiddleware, createTimeoutMiddleware, createToolMiddlewareChain, ElicitationProtocolAdapter, globalElicitationProtocol, isElicitationProtocolMessage, protocolMessageToElicitation, elicitationResponseToProtocol, createElicitationRequest, createElicitationResponse, createElicitationCancel, createTextInputRequest, createSelectRequest, createConfirmationRequest, createFormRequest, } from "./mcp/index.js";
|
|
217
217
|
export { logger } from "./utils/logger.js";
|
|
218
218
|
export { getPoolStats } from "./utils/redis.js";
|
|
219
|
+
export { ToolRoutingCache } from "./core/toolRoutingCache.js";
|
|
220
|
+
export { resolveToolRoutingExclusions, buildToolRoutingCatalog, buildRoutingQueryFromHistory, DEFAULT_ROUTER_PROMPT_PREFIX, } from "./core/toolRouting.js";
|
|
219
221
|
export declare function initializeTelemetry(): Promise<boolean>;
|
|
220
222
|
export declare function getTelemetryStatus(): Promise<{
|
|
221
223
|
enabled: boolean;
|
package/dist/lib/index.js
CHANGED
|
@@ -356,6 +356,10 @@ ToolIntegrationManager, globalToolIntegrationManager, wrapToolWithElicitation, w
|
|
|
356
356
|
ElicitationProtocolAdapter, globalElicitationProtocol, isElicitationProtocolMessage, protocolMessageToElicitation, elicitationResponseToProtocol, createElicitationRequest, createElicitationResponse, createElicitationCancel, createTextInputRequest, createSelectRequest, createConfirmationRequest, createFormRequest, } from "./mcp/index.js";
|
|
357
357
|
export { logger } from "./utils/logger.js";
|
|
358
358
|
export { getPoolStats } from "./utils/redis.js";
|
|
359
|
+
// Pre-call tool routing cache (ITEM C: LRU+TTL cache + session stickiness)
|
|
360
|
+
export { ToolRoutingCache } from "./core/toolRoutingCache.js";
|
|
361
|
+
// Pre-call tool routing resolver — exported for host integrations and testing
|
|
362
|
+
export { resolveToolRoutingExclusions, buildToolRoutingCatalog, buildRoutingQueryFromHistory, DEFAULT_ROUTER_PROMPT_PREFIX, } from "./core/toolRouting.js";
|
|
359
363
|
// ============================================================================
|
|
360
364
|
// REAL-TIME SERVICES & TELEMETRY - Enterprise Platform Features
|
|
361
365
|
// ============================================================================
|
package/dist/lib/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
|
/**
|
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 ?? []),
|
|
@@ -155,6 +155,7 @@ export type LiveKitVoiceAgentConfig = {
|
|
|
155
155
|
conversationIdPrefix?: string;
|
|
156
156
|
/** Optional user id recorded alongside memory. */
|
|
157
157
|
userId?: string;
|
|
158
|
+
greeting?: string;
|
|
158
159
|
/** Silero VAD tuning (stricter = ignores background noise). */
|
|
159
160
|
vad?: LiveKitVadConfig;
|
|
160
161
|
/** Turn-detection tuning (VAD vs STT endpointing, delays). */
|
|
@@ -367,3 +368,136 @@ export type LiveKitEventBridgeHandle = {
|
|
|
367
368
|
/** Remove all listeners and stop publishing. Idempotent. */
|
|
368
369
|
dispose: () => void;
|
|
369
370
|
};
|
|
371
|
+
/**
|
|
372
|
+
* Credentials for LiveKit server-side REST calls (room create, agent dispatch).
|
|
373
|
+
* `url` accepts `ws(s)://` or `http(s)://`; helpers convert it to https.
|
|
374
|
+
*/
|
|
375
|
+
export type LiveKitServerCredentials = {
|
|
376
|
+
url: string;
|
|
377
|
+
apiKey: string;
|
|
378
|
+
apiSecret: string;
|
|
379
|
+
};
|
|
380
|
+
/**
|
|
381
|
+
* Realtime voice configuration resolved from the environment.
|
|
382
|
+
*
|
|
383
|
+
* In speech-to-speech mode one realtime model (Gemini Live on Vertex) does STT,
|
|
384
|
+
* reasoning, TTS, and turn detection — so there is no separate STT/TTS/VAD/EOU
|
|
385
|
+
* config. `resolveRealtimeVoiceConfig` fills every field from `process.env`
|
|
386
|
+
* (with defaults); `RealtimeVoiceAgentConfig` lets a caller override any of them.
|
|
387
|
+
*/
|
|
388
|
+
export type RealtimeVoiceConfig = {
|
|
389
|
+
/** Vertex project id (from VERTEX_PROJECT / GOOGLE_AUTH_* / GOOGLE_CLOUD_PROJECT_ID). */
|
|
390
|
+
project: string | undefined;
|
|
391
|
+
/** Vertex location; native-audio Live is served on `global`, not regionally. */
|
|
392
|
+
location: string;
|
|
393
|
+
/** Realtime model id (e.g. "gemini-live-2.5-flash"). */
|
|
394
|
+
model: string;
|
|
395
|
+
/** Optional Gemini voice name; omit for the plugin default. */
|
|
396
|
+
voice: string | undefined;
|
|
397
|
+
/** Response modality: "AUDIO" (native S2S) or "TEXT" (half-cascade). */
|
|
398
|
+
responseModality: string;
|
|
399
|
+
/** System prompt / instructions for the agent. */
|
|
400
|
+
systemPrompt: string;
|
|
401
|
+
/** Opening line the agent speaks on connect ("" disables). */
|
|
402
|
+
greeting: string;
|
|
403
|
+
/** Whether to bridge Lighthouse MCP tools as Gemini function tools. */
|
|
404
|
+
toolsEnabled: boolean;
|
|
405
|
+
/** Full URL of the MCP server the tools are bridged from. */
|
|
406
|
+
mcpUrl: string;
|
|
407
|
+
/** Grace period after the caller leaves before the job shuts down (ms). */
|
|
408
|
+
emptyRoomGraceMs: number;
|
|
409
|
+
/** Deadline for a participant to join before the job shuts down (ms). */
|
|
410
|
+
joinDeadlineMs: number;
|
|
411
|
+
/** How long a HITL confirmation waits before being treated as a decline (ms). */
|
|
412
|
+
hitlTimeoutMs: number;
|
|
413
|
+
/** Interval for the RSS/heap metrics log (ms). */
|
|
414
|
+
metricsIntervalMs: number;
|
|
415
|
+
};
|
|
416
|
+
/** A single log record handed to a `RealtimeVoiceAgentConfig.onLog` sink. */
|
|
417
|
+
export type RealtimeVoiceLogEntry = {
|
|
418
|
+
level: "debug" | "info" | "warn" | "error";
|
|
419
|
+
message: string;
|
|
420
|
+
timestamp: number;
|
|
421
|
+
data?: unknown;
|
|
422
|
+
};
|
|
423
|
+
export type RealtimeVoiceLogContext = {
|
|
424
|
+
room: string;
|
|
425
|
+
};
|
|
426
|
+
/**
|
|
427
|
+
* Options for `defineRealtimeVoiceAgent`. Every field is optional: omitted
|
|
428
|
+
* values fall back to `resolveRealtimeVoiceConfig()` (i.e. the environment), so
|
|
429
|
+
* a caller can use `defineRealtimeVoiceAgent()` with no arguments and configure
|
|
430
|
+
* everything via env.
|
|
431
|
+
*/
|
|
432
|
+
export type RealtimeVoiceAgentConfig = {
|
|
433
|
+
project?: string;
|
|
434
|
+
location?: string;
|
|
435
|
+
model?: string;
|
|
436
|
+
voice?: string;
|
|
437
|
+
responseModality?: string;
|
|
438
|
+
systemPrompt?: string;
|
|
439
|
+
greeting?: string;
|
|
440
|
+
/** MCP tool bridging overrides. */
|
|
441
|
+
tools?: {
|
|
442
|
+
enabled?: boolean;
|
|
443
|
+
mcpUrl?: string;
|
|
444
|
+
};
|
|
445
|
+
/** Data-channel topic for outbound events (default "ai-events"). */
|
|
446
|
+
eventsTopic?: string;
|
|
447
|
+
/** Data-channel topic for inbound control messages (default "ai-control"). */
|
|
448
|
+
controlTopic?: string;
|
|
449
|
+
/**
|
|
450
|
+
* Optional sink for the agent's own logs. When set, the realtime agent wires
|
|
451
|
+
* NeuroLink's logger to this callback for the duration of the call, so a host
|
|
452
|
+
* can forward worker logs into its logging pipeline. Each record is tagged
|
|
453
|
+
* with per-call context (the room name). Subject to the logger's level gate:
|
|
454
|
+
* without debug mode only `error` records are emitted (set `NEUROLINK_DEBUG`).
|
|
455
|
+
*/
|
|
456
|
+
onLog?: (entry: RealtimeVoiceLogEntry, ctx: RealtimeVoiceLogContext) => void;
|
|
457
|
+
};
|
|
458
|
+
/** Auth token + base64 MCP execution context decoded from a room's metadata. */
|
|
459
|
+
export type LiveKitRoomCallContext = {
|
|
460
|
+
/** Lighthouse access JWT used as `x-auth-token` to the MCP server. */
|
|
461
|
+
authToken: string;
|
|
462
|
+
/** base64(JSON) MCP execution context used as `x-context` (or "" if absent). */
|
|
463
|
+
xContext: string;
|
|
464
|
+
};
|
|
465
|
+
/** Publishes a single voice event envelope onto the room data channel. */
|
|
466
|
+
export type RealtimeEventPublisher = (type: LiveKitVoiceEventType, data: Record<string, unknown>) => void;
|
|
467
|
+
/** Requests a HITL confirmation and resolves to the user's decision. */
|
|
468
|
+
export type RealtimeConfirmationRequester = (toolName: string, args: Record<string, unknown>) => Promise<boolean>;
|
|
469
|
+
/** Handle returned by `attachRealtimeEventBridge`. */
|
|
470
|
+
export type RealtimeEventBridgeHandle = {
|
|
471
|
+
/** Publish an outbound event to the browser (data packet or text stream). */
|
|
472
|
+
publishEvent: RealtimeEventPublisher;
|
|
473
|
+
/** Open a HITL prompt and await the browser's decision (timeout = decline). */
|
|
474
|
+
requestConfirmation: RealtimeConfirmationRequester;
|
|
475
|
+
/** Remove the control-channel listener and clear pending confirmations. */
|
|
476
|
+
dispose: () => void;
|
|
477
|
+
};
|
|
478
|
+
/** Inputs to `attachRealtimeEventBridge`. */
|
|
479
|
+
export type RealtimeEventBridgeParams = {
|
|
480
|
+
/** The LiveKit room for this call (from the job context). */
|
|
481
|
+
room: LiveKitBridgeRoom;
|
|
482
|
+
/** HITL confirmation timeout in ms before a request is auto-declined. */
|
|
483
|
+
hitlTimeoutMs?: number;
|
|
484
|
+
/** Outbound events topic (default "ai-events"). */
|
|
485
|
+
eventsTopic?: string;
|
|
486
|
+
/** Inbound control topic (default "ai-control"). */
|
|
487
|
+
controlTopic?: string;
|
|
488
|
+
/** Payloads larger than this are sent via the chunked text stream (default 12000). */
|
|
489
|
+
maxInlineBytes?: number;
|
|
490
|
+
};
|
|
491
|
+
/** Inputs to `buildRealtimeMcpTools`. */
|
|
492
|
+
export type BuildRealtimeMcpToolsParams = {
|
|
493
|
+
/** Full URL of the MCP server (e.g. ".../ai/mcp/v2"). */
|
|
494
|
+
mcpUrl: string;
|
|
495
|
+
/** Lighthouse access JWT forwarded as `x-auth-token`. */
|
|
496
|
+
authToken: string;
|
|
497
|
+
/** base64(JSON) execution context forwarded as `x-context`. */
|
|
498
|
+
xContext: string;
|
|
499
|
+
/** Publishes tool start/result events to the browser. */
|
|
500
|
+
publishEvent: RealtimeEventPublisher;
|
|
501
|
+
/** Opens a HITL confirmation for destructive tools and awaits the decision. */
|
|
502
|
+
requestConfirmation: RealtimeConfirmationRequester;
|
|
503
|
+
};
|