@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.
Files changed (67) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/dist/browser/neurolink.min.js +349 -349
  3. package/dist/core/toolRouting.js +90 -4
  4. package/dist/core/toolRoutingCache.d.ts +64 -0
  5. package/dist/core/toolRoutingCache.js +154 -0
  6. package/dist/index.d.ts +2 -0
  7. package/dist/index.js +4 -0
  8. package/dist/lib/core/toolRouting.js +90 -4
  9. package/dist/lib/core/toolRoutingCache.d.ts +64 -0
  10. package/dist/lib/core/toolRoutingCache.js +155 -0
  11. package/dist/lib/index.d.ts +2 -0
  12. package/dist/lib/index.js +4 -0
  13. package/dist/lib/neurolink.d.ts +8 -6
  14. package/dist/lib/neurolink.js +187 -39
  15. package/dist/lib/types/livekit.d.ts +134 -0
  16. package/dist/lib/types/toolRouting.d.ts +88 -0
  17. package/dist/lib/voice/livekit/brain.js +1 -1
  18. package/dist/lib/voice/livekit/config.d.ts +12 -1
  19. package/dist/lib/voice/livekit/config.js +54 -0
  20. package/dist/lib/voice/livekit/eventBridge.js +4 -4
  21. package/dist/lib/voice/livekit/index.d.ts +9 -2
  22. package/dist/lib/voice/livekit/index.js +9 -2
  23. package/dist/lib/voice/livekit/realtimeEventBridge.d.ts +14 -0
  24. package/dist/lib/voice/livekit/realtimeEventBridge.js +161 -0
  25. package/dist/lib/voice/livekit/realtimeMcpTools.d.ts +31 -0
  26. package/dist/lib/voice/livekit/realtimeMcpTools.js +194 -0
  27. package/dist/lib/voice/livekit/realtimeVoiceAgent.d.ts +26 -0
  28. package/dist/lib/voice/livekit/realtimeVoiceAgent.js +362 -0
  29. package/dist/lib/voice/livekit/roomContext.d.ts +23 -0
  30. package/dist/lib/voice/livekit/roomContext.js +57 -0
  31. package/dist/lib/voice/livekit/roomDispatch.d.ts +24 -0
  32. package/dist/lib/voice/livekit/roomDispatch.js +31 -0
  33. package/dist/lib/voice/livekit/schemaSanitizer.d.ts +26 -0
  34. package/dist/lib/voice/livekit/schemaSanitizer.js +144 -0
  35. package/dist/lib/voice/livekit/vertexAuth.d.ts +30 -0
  36. package/dist/lib/voice/livekit/vertexAuth.js +73 -0
  37. package/dist/lib/voice/livekit/voiceAgent.js +47 -37
  38. package/dist/lib/voice/livekit/voiceAgentWorker.d.ts +2 -0
  39. package/dist/lib/voice/livekit/voiceAgentWorker.js +64 -0
  40. package/dist/neurolink.d.ts +8 -6
  41. package/dist/neurolink.js +187 -39
  42. package/dist/types/livekit.d.ts +134 -0
  43. package/dist/types/toolRouting.d.ts +88 -0
  44. package/dist/voice/livekit/brain.js +1 -1
  45. package/dist/voice/livekit/config.d.ts +12 -1
  46. package/dist/voice/livekit/config.js +54 -0
  47. package/dist/voice/livekit/eventBridge.js +4 -4
  48. package/dist/voice/livekit/index.d.ts +9 -2
  49. package/dist/voice/livekit/index.js +9 -2
  50. package/dist/voice/livekit/realtimeEventBridge.d.ts +14 -0
  51. package/dist/voice/livekit/realtimeEventBridge.js +160 -0
  52. package/dist/voice/livekit/realtimeMcpTools.d.ts +31 -0
  53. package/dist/voice/livekit/realtimeMcpTools.js +193 -0
  54. package/dist/voice/livekit/realtimeVoiceAgent.d.ts +26 -0
  55. package/dist/voice/livekit/realtimeVoiceAgent.js +361 -0
  56. package/dist/voice/livekit/roomContext.d.ts +23 -0
  57. package/dist/voice/livekit/roomContext.js +56 -0
  58. package/dist/voice/livekit/roomDispatch.d.ts +24 -0
  59. package/dist/voice/livekit/roomDispatch.js +30 -0
  60. package/dist/voice/livekit/schemaSanitizer.d.ts +26 -0
  61. package/dist/voice/livekit/schemaSanitizer.js +143 -0
  62. package/dist/voice/livekit/vertexAuth.d.ts +30 -0
  63. package/dist/voice/livekit/vertexAuth.js +72 -0
  64. package/dist/voice/livekit/voiceAgent.js +47 -37
  65. package/dist/voice/livekit/voiceAgentWorker.d.ts +2 -0
  66. package/dist/voice/livekit/voiceAgentWorker.js +64 -0
  67. package/package.json +5 -2
@@ -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
  };
@@ -61,7 +61,7 @@ export function createVoiceBrain(config) {
61
61
  });
62
62
  for await (const chunk of result.stream) {
63
63
  if (signal?.aborted) {
64
- logger.debug("[LiveKitBrain] Turn aborted mid-stream; stopping");
64
+ logger.debug("voice.brain.turnAborted", { reason: "signal-aborted" });
65
65
  return;
66
66
  }
67
67
  const delta = extractTextDelta(chunk);
@@ -7,7 +7,7 @@
7
7
  *
8
8
  * See docs/features/livekit-voice-agent.md.
9
9
  */
10
- import type { LiveKitServerConfig, LiveKitBrainDefaults } from "../../types/index.js";
10
+ import type { LiveKitServerConfig, LiveKitBrainDefaults, RealtimeVoiceConfig } from "../../types/index.js";
11
11
  /**
12
12
  * Resolve LiveKit server connection settings from the environment.
13
13
  *
@@ -23,6 +23,17 @@ export declare function resolveLiveKitServerConfig(): LiveKitServerConfig;
23
23
  * `VOICE_LLM_MODEL`.
24
24
  */
25
25
  export declare function resolveBrainDefaults(): LiveKitBrainDefaults;
26
+ /**
27
+ * Resolve the realtime (Gemini Live speech-to-speech) voice configuration from
28
+ * the environment.
29
+ *
30
+ * Every value has a safe default so a worker can run with no realtime-specific
31
+ * env at all. Vertex `project` is resolved from `VERTEX_PROJECT`, then the
32
+ * service-account project (`GOOGLE_AUTH_BREEZE_PROJECT_ID`), then
33
+ * `GOOGLE_CLOUD_PROJECT_ID`. The MCP URL is `VOICE_MCP_URL` if set, otherwise
34
+ * `${LIGHTHOUSE_URL}/ai/mcp/v2`.
35
+ */
36
+ export declare function resolveRealtimeVoiceConfig(): RealtimeVoiceConfig;
26
37
  /**
27
38
  * Resolve the semantic end-of-utterance (EOU) turn-detection settings.
28
39
  *
@@ -25,6 +25,22 @@ function readEnv(name, fallback) {
25
25
  }
26
26
  return fallback;
27
27
  }
28
+ function readOptionalEnv(name) {
29
+ const value = process.env[name];
30
+ return typeof value === "string" && value.trim().length > 0
31
+ ? value.trim()
32
+ : undefined;
33
+ }
34
+ function readNumberEnv(name, fallback) {
35
+ const raw = process.env[name];
36
+ if (typeof raw === "string" && raw.trim().length > 0) {
37
+ const parsed = Number(raw.trim());
38
+ if (Number.isFinite(parsed) && parsed > 0) {
39
+ return parsed;
40
+ }
41
+ }
42
+ return fallback;
43
+ }
28
44
  /**
29
45
  * Resolve LiveKit server connection settings from the environment.
30
46
  *
@@ -51,6 +67,44 @@ export function resolveBrainDefaults() {
51
67
  model: readEnv("VOICE_LLM_MODEL", DEFAULT_LLM_MODEL),
52
68
  };
53
69
  }
70
+ const DEFAULT_REALTIME_MODEL = "gemini-live-2.5-flash";
71
+ const DEFAULT_REALTIME_LOCATION = "global";
72
+ const DEFAULT_RESPONSE_MODALITY = "AUDIO";
73
+ const DEFAULT_SYSTEM_PROMPT = "You are a concise voice assistant for a merchant dashboard. Keep replies short and spoken.";
74
+ const DEFAULT_GREETING = "Briefly greet the merchant and ask how you can help with their store today.";
75
+ const DEFAULT_LIGHTHOUSE_URL = "http://localhost:5173";
76
+ const DEFAULT_MCP_PATH = "/ai/mcp/v2";
77
+ /**
78
+ * Resolve the realtime (Gemini Live speech-to-speech) voice configuration from
79
+ * the environment.
80
+ *
81
+ * Every value has a safe default so a worker can run with no realtime-specific
82
+ * env at all. Vertex `project` is resolved from `VERTEX_PROJECT`, then the
83
+ * service-account project (`GOOGLE_AUTH_BREEZE_PROJECT_ID`), then
84
+ * `GOOGLE_CLOUD_PROJECT_ID`. The MCP URL is `VOICE_MCP_URL` if set, otherwise
85
+ * `${LIGHTHOUSE_URL}/ai/mcp/v2`.
86
+ */
87
+ export function resolveRealtimeVoiceConfig() {
88
+ const lighthouseUrl = readEnv("LIGHTHOUSE_URL", DEFAULT_LIGHTHOUSE_URL);
89
+ const mcpUrl = readEnv("VOICE_MCP_URL", `${lighthouseUrl}${DEFAULT_MCP_PATH}`);
90
+ return {
91
+ project: readOptionalEnv("VERTEX_PROJECT") ??
92
+ readOptionalEnv("GOOGLE_AUTH_BREEZE_PROJECT_ID") ??
93
+ readOptionalEnv("GOOGLE_CLOUD_PROJECT_ID"),
94
+ location: readEnv("VERTEX_LOCATION", DEFAULT_REALTIME_LOCATION),
95
+ model: readEnv("VOICE_LIVE_MODEL", DEFAULT_REALTIME_MODEL),
96
+ voice: readOptionalEnv("VOICE_S2S_VOICE"),
97
+ responseModality: readEnv("VOICE_RESPONSE_MODALITY", DEFAULT_RESPONSE_MODALITY).toUpperCase(),
98
+ systemPrompt: readEnv("VOICE_SYSTEM_PROMPT", DEFAULT_SYSTEM_PROMPT),
99
+ greeting: readEnv("VOICE_GREETING", DEFAULT_GREETING),
100
+ toolsEnabled: readEnv("VOICE_S2S_TOOLS", "").toLowerCase() === "true",
101
+ mcpUrl,
102
+ emptyRoomGraceMs: readNumberEnv("VOICE_EMPTY_ROOM_GRACE_MS", 30_000),
103
+ joinDeadlineMs: readNumberEnv("VOICE_JOIN_DEADLINE_MS", 60_000),
104
+ hitlTimeoutMs: readNumberEnv("VOICE_HITL_TIMEOUT_MS", 45_000),
105
+ metricsIntervalMs: readNumberEnv("VOICE_METRICS_INTERVAL_MS", 10_000),
106
+ };
107
+ }
54
108
  const EOU_TRUTHY = new Set(["1", "true", "english", "en", "on", "yes"]);
55
109
  /**
56
110
  * Resolve the semantic end-of-utterance (EOU) turn-detection settings.
@@ -191,7 +191,7 @@ export async function attachEventBridge(params) {
191
191
  const json = JSON.stringify(envelope);
192
192
  const bytes = encoder.encode(json);
193
193
  const onError = (transport) => (error) => {
194
- logger.warn("[LiveKitEventBridge] Failed to publish event", {
194
+ logger.warn("voice.bridge.publishFailed", {
195
195
  transport,
196
196
  type: event.type,
197
197
  error: error instanceof Error ? error.message : String(error),
@@ -304,7 +304,7 @@ export async function attachEventBridge(params) {
304
304
  parsed = JSON.parse(decoder.decode(payload));
305
305
  }
306
306
  catch (error) {
307
- logger.warn("[LiveKitEventBridge] Dropping malformed control message", {
307
+ logger.warn("voice.bridge.controlMalformed", {
308
308
  error: error instanceof Error ? error.message : String(error),
309
309
  });
310
310
  return;
@@ -332,7 +332,7 @@ export async function attachEventBridge(params) {
332
332
  };
333
333
  room.on(RoomEvent.DataReceived, onData);
334
334
  publish({ type: "status", data: { state: "listening" } });
335
- logger.info("[LiveKitEventBridge] Attached", {
335
+ logger.info("voice.bridge.attached", {
336
336
  eventsTopic,
337
337
  controlTopic,
338
338
  filtered: include !== undefined,
@@ -353,7 +353,7 @@ export async function attachEventBridge(params) {
353
353
  emitter.off(STREAM_END_EVENT, onStreamComplete);
354
354
  emitter.off(STREAM_ERROR_EVENT, onStreamError);
355
355
  room.off(RoomEvent.DataReceived, onData);
356
- logger.info("[LiveKitEventBridge] Disposed");
356
+ logger.info("voice.bridge.disposed", {});
357
357
  },
358
358
  };
359
359
  }
@@ -8,8 +8,15 @@
8
8
  * See docs/features/livekit-voice-agent.md.
9
9
  */
10
10
  export { createVoiceBrain } from "./brain.js";
11
- export { resolveLiveKitServerConfig, resolveBrainDefaults } from "./config.js";
11
+ export { resolveLiveKitServerConfig, resolveBrainDefaults, resolveRealtimeVoiceConfig, } from "./config.js";
12
12
  export { attachEventBridge } from "./eventBridge.js";
13
+ export { attachRealtimeEventBridge } from "./realtimeEventBridge.js";
13
14
  export { mintJoinToken } from "./tokens.js";
15
+ export { createVoiceRoom, dispatchVoiceAgent } from "./roomDispatch.js";
14
16
  export { defineVoiceAgent } from "./voiceAgent.js";
15
- export { startVoiceAgentWorker } from "./voiceAgentWorker.js";
17
+ export { defineRealtimeVoiceAgent } from "./realtimeVoiceAgent.js";
18
+ export { startVoiceAgentWorker, startRealtimeVoiceAgentWorker, installVoiceWorkerProcessGuards, } from "./voiceAgentWorker.js";
19
+ export { ensureVertexAdc, clearGeminiApiKeyEnv } from "./vertexAuth.js";
20
+ export { readCallContextFromRoom } from "./roomContext.js";
21
+ export { sanitizeSchema, sanitizeToolParameters, findSchemaIssue, } from "./schemaSanitizer.js";
22
+ export { buildRealtimeMcpTools, mcpResultToText } from "./realtimeMcpTools.js";
@@ -8,9 +8,16 @@
8
8
  * See docs/features/livekit-voice-agent.md.
9
9
  */
10
10
  export { createVoiceBrain } from "./brain.js";
11
- export { resolveLiveKitServerConfig, resolveBrainDefaults } from "./config.js";
11
+ export { resolveLiveKitServerConfig, resolveBrainDefaults, resolveRealtimeVoiceConfig, } from "./config.js";
12
12
  export { attachEventBridge } from "./eventBridge.js";
13
+ export { attachRealtimeEventBridge } from "./realtimeEventBridge.js";
13
14
  export { mintJoinToken } from "./tokens.js";
15
+ export { createVoiceRoom, dispatchVoiceAgent } from "./roomDispatch.js";
14
16
  export { defineVoiceAgent } from "./voiceAgent.js";
15
- export { startVoiceAgentWorker } from "./voiceAgentWorker.js";
17
+ export { defineRealtimeVoiceAgent } from "./realtimeVoiceAgent.js";
18
+ export { startVoiceAgentWorker, startRealtimeVoiceAgentWorker, installVoiceWorkerProcessGuards, } from "./voiceAgentWorker.js";
19
+ export { ensureVertexAdc, clearGeminiApiKeyEnv } from "./vertexAuth.js";
20
+ export { readCallContextFromRoom } from "./roomContext.js";
21
+ export { sanitizeSchema, sanitizeToolParameters, findSchemaIssue, } from "./schemaSanitizer.js";
22
+ export { buildRealtimeMcpTools, mcpResultToText } from "./realtimeMcpTools.js";
16
23
  //# sourceMappingURL=index.js.map
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Realtime data-channel event bridge.
3
+ *
4
+ * The cascaded bridge (`eventBridge.ts`) is driven by NeuroLink's event emitter.
5
+ */
6
+ import type { RealtimeEventBridgeHandle, RealtimeEventBridgeParams } from "../../types/index.js";
7
+ /**
8
+ * Attach the realtime event bridge to a room.
9
+ *
10
+ * Returns `publishEvent` (worker → browser), `requestConfirmation` (HITL
11
+ * round-trip), and `dispose` (remove the control listener, decline anything
12
+ * still pending).
13
+ */
14
+ export declare function attachRealtimeEventBridge(params: RealtimeEventBridgeParams): Promise<RealtimeEventBridgeHandle>;
@@ -0,0 +1,161 @@
1
+ /**
2
+ * Realtime data-channel event bridge.
3
+ *
4
+ * The cascaded bridge (`eventBridge.ts`) is driven by NeuroLink's event emitter.
5
+ */
6
+ import { z } from "zod";
7
+ import { logger } from "../../utils/logger.js";
8
+ const DEFAULT_EVENTS_TOPIC = "ai-events";
9
+ const DEFAULT_CONTROL_TOPIC = "ai-control";
10
+ const DEFAULT_MAX_INLINE_BYTES = 12_000;
11
+ const DEFAULT_HITL_TIMEOUT_MS = 45_000;
12
+ const hitlControlMessageSchema = z.object({
13
+ action: z.enum(["hitl:accept", "hitl:reject"]),
14
+ confirmationId: z.string(),
15
+ });
16
+ /**
17
+ * Attach the realtime event bridge to a room.
18
+ *
19
+ * Returns `publishEvent` (worker → browser), `requestConfirmation` (HITL
20
+ * round-trip), and `dispose` (remove the control listener, decline anything
21
+ * still pending).
22
+ */
23
+ export async function attachRealtimeEventBridge(params) {
24
+ const { room } = params;
25
+ const eventsTopic = params.eventsTopic ?? DEFAULT_EVENTS_TOPIC;
26
+ const controlTopic = params.controlTopic ?? DEFAULT_CONTROL_TOPIC;
27
+ const maxInlineBytes = params.maxInlineBytes ?? DEFAULT_MAX_INLINE_BYTES;
28
+ const hitlTimeoutMs = params.hitlTimeoutMs ?? DEFAULT_HITL_TIMEOUT_MS;
29
+ const { RoomEvent } = await import("@livekit/rtc-node");
30
+ const encoder = new TextEncoder();
31
+ const decoder = new TextDecoder();
32
+ let seq = 0;
33
+ let disposed = false;
34
+ const startedAt = Date.now();
35
+ const publishEvent = (type, data) => {
36
+ if (disposed) {
37
+ return;
38
+ }
39
+ const participant = room.localParticipant;
40
+ if (!participant) {
41
+ return;
42
+ }
43
+ try {
44
+ seq += 1;
45
+ const json = JSON.stringify({ seq, ts: Date.now(), type, data });
46
+ const bytes = encoder.encode(json);
47
+ const via = bytes.byteLength <= maxInlineBytes ? "data" : "stream";
48
+ logger.debug("realtime.bridge.publish", {
49
+ ms: Date.now() - startedAt,
50
+ seq,
51
+ type,
52
+ via,
53
+ bytes: bytes.byteLength,
54
+ });
55
+ if (via === "data") {
56
+ void participant.publishData(bytes, {
57
+ reliable: true,
58
+ topic: eventsTopic,
59
+ });
60
+ }
61
+ else {
62
+ void participant.sendText(json, { topic: eventsTopic });
63
+ }
64
+ }
65
+ catch {
66
+ /* non-fatal — the UI bridge is best-effort */
67
+ }
68
+ };
69
+ // HITL: WRITE-labeled tools pause for user confirmation. We publish a
70
+ // `hitl-prompt`, the UI replies on the control topic, and we resolve the
71
+ // pending request by confirmationId. A timeout is treated as a decline so a
72
+ // turn can never hang waiting on the user.
73
+ const pendingHitl = new Map();
74
+ let hitlSeq = 0;
75
+ const onData = (...args) => {
76
+ const payload = args[0];
77
+ const topic = args[3];
78
+ if (topic !== controlTopic || !(payload instanceof Uint8Array)) {
79
+ return;
80
+ }
81
+ let raw;
82
+ try {
83
+ raw = JSON.parse(decoder.decode(payload));
84
+ }
85
+ catch {
86
+ return;
87
+ }
88
+ const message = hitlControlMessageSchema.safeParse(raw);
89
+ if (!message.success) {
90
+ return;
91
+ }
92
+ const { action, confirmationId } = message.data;
93
+ const resolver = pendingHitl.get(confirmationId);
94
+ if (resolver) {
95
+ logger.debug("realtime.bridge.controlReceived", {
96
+ action,
97
+ confirmationId,
98
+ });
99
+ pendingHitl.delete(confirmationId);
100
+ resolver(action === "hitl:accept");
101
+ }
102
+ else {
103
+ logger.warn("realtime.bridge.controlUnmatched", {
104
+ action,
105
+ confirmationId,
106
+ });
107
+ }
108
+ };
109
+ room.on(RoomEvent.DataReceived, onData);
110
+ const requestConfirmation = (toolName, args) => {
111
+ if (disposed) {
112
+ logger.warn("realtime.bridge.hitlDisposed", { toolName });
113
+ return Promise.resolve(false);
114
+ }
115
+ hitlSeq += 1;
116
+ const confirmationId = `hitl_${seq}_${hitlSeq}`;
117
+ publishEvent("hitl-prompt", {
118
+ confirmationId,
119
+ toolName,
120
+ actionType: `Run ${toolName}`,
121
+ arguments: args ?? {},
122
+ });
123
+ logger.info("realtime.bridge.hitlPrompt", { toolName, confirmationId });
124
+ return new Promise((resolve) => {
125
+ const timer = setTimeout(() => {
126
+ pendingHitl.delete(confirmationId);
127
+ logger.warn("realtime.bridge.hitlTimeout", {
128
+ toolName,
129
+ confirmationId,
130
+ timeoutMs: hitlTimeoutMs,
131
+ });
132
+ resolve(false);
133
+ }, hitlTimeoutMs);
134
+ pendingHitl.set(confirmationId, (approved) => {
135
+ clearTimeout(timer);
136
+ logger.info("realtime.bridge.hitlResolved", {
137
+ toolName,
138
+ confirmationId,
139
+ approved,
140
+ });
141
+ resolve(approved);
142
+ });
143
+ });
144
+ };
145
+ return {
146
+ publishEvent,
147
+ requestConfirmation,
148
+ dispose() {
149
+ if (disposed) {
150
+ return;
151
+ }
152
+ disposed = true;
153
+ room.off(RoomEvent.DataReceived, onData);
154
+ for (const [, resolver] of pendingHitl) {
155
+ resolver(false);
156
+ }
157
+ pendingHitl.clear();
158
+ },
159
+ };
160
+ }
161
+ //# sourceMappingURL=realtimeEventBridge.js.map
@@ -0,0 +1,31 @@
1
+ /**
2
+ * MCP tools → Gemini function tools bridge (realtime mode).
3
+ *
4
+ * In speech-to-speech mode Gemini owns the tool loop, so an external MCP
5
+ * server's tools are registered as LiveKit/Gemini function tools rather than
6
+ * NeuroLink tools. This connects to the MCP server as the calling user
7
+ * (`x-auth-token` + base64 `x-context`), sanitizes each tool's schema into the
8
+ * Gemini-safe subset, wires WRITE-labeled (destructive) tools through a HITL
9
+ * confirmation, and forwards tool start/result events to the browser bridge.
10
+ *
11
+ * `@livekit/agents` and `@modelcontextprotocol/sdk` are imported dynamically so
12
+ * the core package does not require them unless the realtime agent is used.
13
+ *
14
+ * See docs/features/livekit-voice-agent.md.
15
+ */
16
+ import type { llm as llmNs } from "@livekit/agents";
17
+ import type { BuildRealtimeMcpToolsParams } from "../../types/index.js";
18
+ /** Flatten an MCP tool result's text content into a single string for the model. */
19
+ export declare function mcpResultToText(result: unknown): string;
20
+ /**
21
+ * Connect to the MCP server as the calling user and bridge every tool into a
22
+ * LiveKit `ToolContext`. Returns the tool map + the client (close on shutdown).
23
+ * The server already scopes the tool list by `x-context`, so no client-side
24
+ * filtering is needed.
25
+ */
26
+ export declare function buildRealtimeMcpTools(params: BuildRealtimeMcpToolsParams): Promise<{
27
+ tools: llmNs.ToolContext;
28
+ client: {
29
+ close: () => Promise<void>;
30
+ };
31
+ }>;