@juspay/neurolink 9.69.3 → 9.70.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 (39) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/dist/browser/neurolink.min.js +295 -295
  3. package/dist/lib/types/index.d.ts +1 -0
  4. package/dist/lib/types/index.js +1 -0
  5. package/dist/lib/types/livekit.d.ts +369 -0
  6. package/dist/lib/types/livekit.js +13 -0
  7. package/dist/lib/voice/livekit/brain.d.ts +21 -0
  8. package/dist/lib/voice/livekit/brain.js +75 -0
  9. package/dist/lib/voice/livekit/config.d.ts +41 -0
  10. package/dist/lib/voice/livekit/config.js +80 -0
  11. package/dist/lib/voice/livekit/eventBridge.d.ts +27 -0
  12. package/dist/lib/voice/livekit/eventBridge.js +360 -0
  13. package/dist/lib/voice/livekit/index.d.ts +15 -0
  14. package/dist/lib/voice/livekit/index.js +16 -0
  15. package/dist/lib/voice/livekit/tokens.d.ts +19 -0
  16. package/dist/lib/voice/livekit/tokens.js +51 -0
  17. package/dist/lib/voice/livekit/voiceAgent.d.ts +32 -0
  18. package/dist/lib/voice/livekit/voiceAgent.js +415 -0
  19. package/dist/lib/voice/livekit/voiceAgentWorker.d.ts +27 -0
  20. package/dist/lib/voice/livekit/voiceAgentWorker.js +58 -0
  21. package/dist/types/index.d.ts +1 -0
  22. package/dist/types/index.js +1 -0
  23. package/dist/types/livekit.d.ts +369 -0
  24. package/dist/types/livekit.js +12 -0
  25. package/dist/voice/livekit/brain.d.ts +21 -0
  26. package/dist/voice/livekit/brain.js +74 -0
  27. package/dist/voice/livekit/config.d.ts +41 -0
  28. package/dist/voice/livekit/config.js +79 -0
  29. package/dist/voice/livekit/eventBridge.d.ts +27 -0
  30. package/dist/voice/livekit/eventBridge.js +359 -0
  31. package/dist/voice/livekit/index.d.ts +15 -0
  32. package/dist/voice/livekit/index.js +15 -0
  33. package/dist/voice/livekit/tokens.d.ts +19 -0
  34. package/dist/voice/livekit/tokens.js +50 -0
  35. package/dist/voice/livekit/voiceAgent.d.ts +32 -0
  36. package/dist/voice/livekit/voiceAgent.js +414 -0
  37. package/dist/voice/livekit/voiceAgentWorker.d.ts +27 -0
  38. package/dist/voice/livekit/voiceAgentWorker.js +57 -0
  39. package/package.json +20 -6
@@ -0,0 +1,360 @@
1
+ /**
2
+ * Data-channel event bridge.
3
+ *
4
+ * Connects NeuroLink's event emitter to the LiveKit room's data channel: it
5
+ * forwards NeuroLink events (text deltas, tool start/result, HITL prompts,
6
+ * stream lifecycle) to the browser as small versioned envelopes, and accepts
7
+ * control messages (HITL responses) back, re-emitting them onto the emitter so
8
+ * NeuroLink's HITL manager resolves.
9
+ *
10
+ * This is the WebRTC counterpart of the chat-mode SSE controller: same event
11
+ * source, different transport. The browser renders the envelopes (transcript,
12
+ * tool status, charts, confirmation prompts).
13
+ *
14
+ * `@livekit/rtc-node` is an optional dependency, imported dynamically only for
15
+ * the `RoomEvent` enum value. All payloads arrive typed as `unknown` and are
16
+ * narrowed with runtime guards — no type assertions.
17
+ *
18
+ * See docs/features/livekit-voice-agent.md.
19
+ */
20
+ import { logger } from "../../utils/logger.js";
21
+ const DEFAULT_EVENTS_TOPIC = "ai-events";
22
+ const DEFAULT_CONTROL_TOPIC = "ai-control";
23
+ const DEFAULT_MAX_INLINE_BYTES = 12_000;
24
+ /** NeuroLink emitter event names this bridge forwards. */
25
+ const USER_TEXT_EVENT = "voice:user-transcript";
26
+ const TEXT_EVENT = "response:chunk";
27
+ const TOOL_START_EVENT = "tool:start";
28
+ const TOOL_END_EVENT = "tool:end";
29
+ const HITL_REQUEST_EVENT = "hitl:confirmation-request";
30
+ const STREAM_START_EVENT = "stream:start";
31
+ const STREAM_COMPLETE_EVENT = "stream:complete";
32
+ const STREAM_END_EVENT = "stream:end";
33
+ const STREAM_ERROR_EVENT = "stream:error";
34
+ /** Read a property off a non-null object as `unknown` without a type assertion. */
35
+ function readProp(source, key) {
36
+ return Reflect.get(source, key);
37
+ }
38
+ /** Narrow an unknown value to a non-null object, or `undefined`. */
39
+ function asObject(value) {
40
+ return typeof value === "object" && value !== null ? value : undefined;
41
+ }
42
+ /** Extract the text delta from a `response:chunk` payload. */
43
+ function readTextDelta(payload) {
44
+ return typeof payload === "string" && payload.length > 0
45
+ ? payload
46
+ : undefined;
47
+ }
48
+ /** Extract tool fields from a `tool:start` / `tool:end` payload. */
49
+ function readToolFields(payload) {
50
+ const obj = asObject(payload);
51
+ if (!obj) {
52
+ return undefined;
53
+ }
54
+ const toolName = readProp(obj, "toolName");
55
+ const tool = readProp(obj, "tool");
56
+ const name = typeof toolName === "string"
57
+ ? toolName
58
+ : typeof tool === "string"
59
+ ? tool
60
+ : undefined;
61
+ if (name === undefined) {
62
+ return undefined;
63
+ }
64
+ const fields = { name };
65
+ const executionId = readProp(obj, "executionId");
66
+ if (typeof executionId === "string") {
67
+ fields.id = executionId;
68
+ }
69
+ const input = readProp(obj, "input");
70
+ if (input !== undefined) {
71
+ fields.input = input;
72
+ }
73
+ const result = readProp(obj, "result");
74
+ if (result !== undefined) {
75
+ fields.result = result;
76
+ }
77
+ const success = readProp(obj, "success");
78
+ if (typeof success === "boolean") {
79
+ fields.success = success;
80
+ }
81
+ const error = readProp(obj, "error");
82
+ if (typeof error === "string") {
83
+ fields.error = error;
84
+ }
85
+ return fields;
86
+ }
87
+ /** Extract the HITL prompt fields from a `hitl:confirmation-request` payload. */
88
+ function readHitlPrompt(payload) {
89
+ const event = asObject(payload);
90
+ if (!event) {
91
+ return undefined;
92
+ }
93
+ const inner = asObject(readProp(event, "payload"));
94
+ if (!inner) {
95
+ return undefined;
96
+ }
97
+ const confirmationId = readProp(inner, "confirmationId");
98
+ const toolName = readProp(inner, "toolName");
99
+ if (typeof confirmationId !== "string" || typeof toolName !== "string") {
100
+ return undefined;
101
+ }
102
+ const data = {
103
+ confirmationId,
104
+ toolName,
105
+ };
106
+ const actionType = readProp(inner, "actionType");
107
+ if (typeof actionType === "string") {
108
+ data.actionType = actionType;
109
+ }
110
+ const args = readProp(inner, "arguments");
111
+ if (args !== undefined) {
112
+ data.arguments = args;
113
+ }
114
+ const timeoutMs = readProp(inner, "timeoutMs");
115
+ if (typeof timeoutMs === "number") {
116
+ data.timeoutMs = timeoutMs;
117
+ }
118
+ const allowModification = readProp(inner, "allowModification");
119
+ if (typeof allowModification === "boolean") {
120
+ data.allowModification = allowModification;
121
+ }
122
+ return { type: "hitl-prompt", data };
123
+ }
124
+ /** Narrow a decoded inbound message to a control message, or `undefined`. */
125
+ function readControlMessage(value) {
126
+ const obj = asObject(value);
127
+ if (!obj) {
128
+ return undefined;
129
+ }
130
+ const action = readProp(obj, "action");
131
+ const confirmationId = readProp(obj, "confirmationId");
132
+ if (typeof confirmationId !== "string") {
133
+ return undefined;
134
+ }
135
+ if (action === "hitl:accept") {
136
+ const message = {
137
+ action: "hitl:accept",
138
+ confirmationId,
139
+ };
140
+ const modifiedArguments = readProp(obj, "modifiedArguments");
141
+ if (modifiedArguments !== undefined) {
142
+ message.modifiedArguments = modifiedArguments;
143
+ }
144
+ return message;
145
+ }
146
+ if (action === "hitl:reject") {
147
+ const message = {
148
+ action: "hitl:reject",
149
+ confirmationId,
150
+ };
151
+ const reason = readProp(obj, "reason");
152
+ if (typeof reason === "string") {
153
+ message.reason = reason;
154
+ }
155
+ return message;
156
+ }
157
+ return undefined;
158
+ }
159
+ /**
160
+ * Attach the data-channel event bridge to a room.
161
+ *
162
+ * Returns a handle whose `dispose()` removes every listener and stops
163
+ * publishing; it is safe to call more than once.
164
+ */
165
+ export async function attachEventBridge(params) {
166
+ const { room, emitter, options } = params;
167
+ const eventsTopic = options?.eventsTopic ?? DEFAULT_EVENTS_TOPIC;
168
+ const controlTopic = options?.controlTopic ?? DEFAULT_CONTROL_TOPIC;
169
+ const maxInlineBytes = options?.maxInlineBytes ?? DEFAULT_MAX_INLINE_BYTES;
170
+ const include = options?.include && options.include.length > 0
171
+ ? new Set(options.include)
172
+ : undefined;
173
+ const { RoomEvent } = await import("@livekit/rtc-node");
174
+ const encoder = new TextEncoder();
175
+ const decoder = new TextDecoder();
176
+ let seq = 0;
177
+ let disposed = false;
178
+ function publish(event) {
179
+ if (disposed) {
180
+ return;
181
+ }
182
+ if (include && !include.has(event.type)) {
183
+ return;
184
+ }
185
+ const localParticipant = room.localParticipant;
186
+ if (!localParticipant) {
187
+ return;
188
+ }
189
+ seq += 1;
190
+ const envelope = Object.assign({ seq, ts: Date.now() }, event);
191
+ const json = JSON.stringify(envelope);
192
+ const bytes = encoder.encode(json);
193
+ const onError = (transport) => (error) => {
194
+ logger.warn("[LiveKitEventBridge] Failed to publish event", {
195
+ transport,
196
+ type: event.type,
197
+ error: error instanceof Error ? error.message : String(error),
198
+ });
199
+ };
200
+ if (bytes.byteLength <= maxInlineBytes) {
201
+ void localParticipant
202
+ .publishData(bytes, { reliable: true, topic: eventsTopic })
203
+ .catch(onError("publishData"));
204
+ }
205
+ else {
206
+ // Large payloads (e.g. chart data) exceed a single reliable data packet;
207
+ // the chunked text stream handles arbitrary sizes.
208
+ void localParticipant
209
+ .sendText(json, { topic: eventsTopic })
210
+ .catch(onError("sendText"));
211
+ }
212
+ }
213
+ // --- Outbound: NeuroLink emitter → data channel ---------------------------
214
+ const onUserText = (...args) => {
215
+ const payload = asObject(args[0]);
216
+ if (!payload) {
217
+ return;
218
+ }
219
+ const text = readProp(payload, "text");
220
+ const final = readProp(payload, "final");
221
+ const replacesPrevious = readProp(payload, "replacesPrevious");
222
+ if (typeof text === "string" && text.trim().length > 0) {
223
+ const data = {
224
+ text,
225
+ final: final === true,
226
+ };
227
+ if (replacesPrevious === true) {
228
+ data.replacesPrevious = true;
229
+ }
230
+ publish({ type: "user-text", data });
231
+ }
232
+ };
233
+ const onText = (...args) => {
234
+ const delta = readTextDelta(args[0]);
235
+ if (delta !== undefined) {
236
+ publish({ type: "text", data: { delta } });
237
+ }
238
+ };
239
+ const onToolStart = (...args) => {
240
+ const fields = readToolFields(args[0]);
241
+ if (fields) {
242
+ publish({
243
+ type: "tool-start",
244
+ data: { id: fields.id, name: fields.name, input: fields.input },
245
+ });
246
+ }
247
+ };
248
+ const onToolEnd = (...args) => {
249
+ const fields = readToolFields(args[0]);
250
+ if (fields) {
251
+ publish({
252
+ type: "tool-result",
253
+ data: {
254
+ id: fields.id,
255
+ name: fields.name,
256
+ result: fields.result,
257
+ success: fields.success,
258
+ error: fields.error,
259
+ },
260
+ });
261
+ }
262
+ };
263
+ const onHitlRequest = (...args) => {
264
+ const event = readHitlPrompt(args[0]);
265
+ if (event) {
266
+ publish(event);
267
+ }
268
+ };
269
+ const onStreamStart = () => {
270
+ publish({ type: "status", data: { state: "thinking" } });
271
+ };
272
+ const onStreamComplete = () => {
273
+ publish({ type: "done", data: {} });
274
+ };
275
+ const onStreamError = (...args) => {
276
+ const obj = asObject(args[0]);
277
+ const message = obj ? readProp(obj, "message") : undefined;
278
+ publish({
279
+ type: "status",
280
+ data: {
281
+ state: "error",
282
+ detail: typeof message === "string" ? message : undefined,
283
+ },
284
+ });
285
+ };
286
+ emitter.on(USER_TEXT_EVENT, onUserText);
287
+ emitter.on(TEXT_EVENT, onText);
288
+ emitter.on(TOOL_START_EVENT, onToolStart);
289
+ emitter.on(TOOL_END_EVENT, onToolEnd);
290
+ emitter.on(HITL_REQUEST_EVENT, onHitlRequest);
291
+ emitter.on(STREAM_START_EVENT, onStreamStart);
292
+ emitter.on(STREAM_COMPLETE_EVENT, onStreamComplete);
293
+ emitter.on(STREAM_END_EVENT, onStreamComplete);
294
+ emitter.on(STREAM_ERROR_EVENT, onStreamError);
295
+ // --- Inbound: data channel → NeuroLink emitter (HITL responses) -----------
296
+ const onData = (...args) => {
297
+ const payload = args[0];
298
+ const topic = args[3];
299
+ if (topic !== controlTopic || !(payload instanceof Uint8Array)) {
300
+ return;
301
+ }
302
+ let parsed;
303
+ try {
304
+ parsed = JSON.parse(decoder.decode(payload));
305
+ }
306
+ catch (error) {
307
+ logger.warn("[LiveKitEventBridge] Dropping malformed control message", {
308
+ error: error instanceof Error ? error.message : String(error),
309
+ });
310
+ return;
311
+ }
312
+ const message = readControlMessage(parsed);
313
+ if (!message) {
314
+ return;
315
+ }
316
+ const responsePayload = {
317
+ confirmationId: message.confirmationId,
318
+ approved: message.action === "hitl:accept",
319
+ metadata: { timestamp: new Date().toISOString(), responseTime: 0 },
320
+ };
321
+ if (message.action === "hitl:reject" && message.reason !== undefined) {
322
+ responsePayload.reason = message.reason;
323
+ }
324
+ if (message.action === "hitl:accept" &&
325
+ message.modifiedArguments !== undefined) {
326
+ responsePayload.modifiedArguments = message.modifiedArguments;
327
+ }
328
+ emitter.emit("hitl:confirmation-response", {
329
+ type: "hitl:confirmation-response",
330
+ payload: responsePayload,
331
+ });
332
+ };
333
+ room.on(RoomEvent.DataReceived, onData);
334
+ publish({ type: "status", data: { state: "listening" } });
335
+ logger.info("[LiveKitEventBridge] Attached", {
336
+ eventsTopic,
337
+ controlTopic,
338
+ filtered: include !== undefined,
339
+ });
340
+ return {
341
+ dispose() {
342
+ if (disposed) {
343
+ return;
344
+ }
345
+ disposed = true;
346
+ emitter.off(USER_TEXT_EVENT, onUserText);
347
+ emitter.off(TEXT_EVENT, onText);
348
+ emitter.off(TOOL_START_EVENT, onToolStart);
349
+ emitter.off(TOOL_END_EVENT, onToolEnd);
350
+ emitter.off(HITL_REQUEST_EVENT, onHitlRequest);
351
+ emitter.off(STREAM_START_EVENT, onStreamStart);
352
+ emitter.off(STREAM_COMPLETE_EVENT, onStreamComplete);
353
+ emitter.off(STREAM_END_EVENT, onStreamComplete);
354
+ emitter.off(STREAM_ERROR_EVENT, onStreamError);
355
+ room.off(RoomEvent.DataReceived, onData);
356
+ logger.info("[LiveKitEventBridge] Disposed");
357
+ },
358
+ };
359
+ }
360
+ //# sourceMappingURL=eventBridge.js.map
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Public entry point for the LiveKit voice agent integration.
3
+ *
4
+ * Exposed to consumers as `@juspay/neurolink/livekit`. Re-exports runtime
5
+ * values only; type definitions live in `src/lib/types/livekit.ts` and are
6
+ * available from the main type exports.
7
+ *
8
+ * See docs/features/livekit-voice-agent.md.
9
+ */
10
+ export { createVoiceBrain } from "./brain.js";
11
+ export { resolveLiveKitServerConfig, resolveBrainDefaults } from "./config.js";
12
+ export { attachEventBridge } from "./eventBridge.js";
13
+ export { mintJoinToken } from "./tokens.js";
14
+ export { defineVoiceAgent } from "./voiceAgent.js";
15
+ export { startVoiceAgentWorker } from "./voiceAgentWorker.js";
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Public entry point for the LiveKit voice agent integration.
3
+ *
4
+ * Exposed to consumers as `@juspay/neurolink/livekit`. Re-exports runtime
5
+ * values only; type definitions live in `src/lib/types/livekit.ts` and are
6
+ * available from the main type exports.
7
+ *
8
+ * See docs/features/livekit-voice-agent.md.
9
+ */
10
+ export { createVoiceBrain } from "./brain.js";
11
+ export { resolveLiveKitServerConfig, resolveBrainDefaults } from "./config.js";
12
+ export { attachEventBridge } from "./eventBridge.js";
13
+ export { mintJoinToken } from "./tokens.js";
14
+ export { defineVoiceAgent } from "./voiceAgent.js";
15
+ export { startVoiceAgentWorker } from "./voiceAgentWorker.js";
16
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,19 @@
1
+ /**
2
+ * LiveKit join-token minting.
3
+ *
4
+ * Mints a short-lived JWT a browser uses to join a room. The token is signed
5
+ * locally with the LiveKit API key/secret — no network call and no room
6
+ * pre-creation (rooms are auto-created on first join).
7
+ *
8
+ * `livekit-server-sdk` is an optional dependency and is imported dynamically so
9
+ * the core package does not require it unless the LiveKit voice agent is used.
10
+ *
11
+ */
12
+ import type { LiveKitTokenRequest } from "../../types/index.js";
13
+ /**
14
+ * Mint a LiveKit join token for an authenticated participant.
15
+ *
16
+ * Grants `roomJoin` plus publish/subscribe for the named room. The room is
17
+ * created automatically by LiveKit when the first participant joins.
18
+ */
19
+ export declare function mintJoinToken(req: LiveKitTokenRequest): Promise<string>;
@@ -0,0 +1,51 @@
1
+ /**
2
+ * LiveKit join-token minting.
3
+ *
4
+ * Mints a short-lived JWT a browser uses to join a room. The token is signed
5
+ * locally with the LiveKit API key/secret — no network call and no room
6
+ * pre-creation (rooms are auto-created on first join).
7
+ *
8
+ * `livekit-server-sdk` is an optional dependency and is imported dynamically so
9
+ * the core package does not require it unless the LiveKit voice agent is used.
10
+ *
11
+ */
12
+ const DEFAULT_TTL_SECONDS = 600;
13
+ /**
14
+ * Upper bound on join-token lifetime (1 hour). A join token only needs to live
15
+ * long enough for the participant to connect, so capping it keeps token-expiry
16
+ * controls meaningful even if a caller requests a very large `ttlSeconds`.
17
+ */
18
+ const MAX_TTL_SECONDS = 3600;
19
+ /**
20
+ * Resolve a safe token lifetime. Non-finite or non-positive requests fall back
21
+ * to the default; anything above the ceiling is clamped to `MAX_TTL_SECONDS`.
22
+ */
23
+ function resolveTtlSeconds(ttlSeconds) {
24
+ if (ttlSeconds === undefined ||
25
+ !Number.isFinite(ttlSeconds) ||
26
+ ttlSeconds <= 0) {
27
+ return DEFAULT_TTL_SECONDS;
28
+ }
29
+ return Math.min(Math.floor(ttlSeconds), MAX_TTL_SECONDS);
30
+ }
31
+ /**
32
+ * Mint a LiveKit join token for an authenticated participant.
33
+ *
34
+ * Grants `roomJoin` plus publish/subscribe for the named room. The room is
35
+ * created automatically by LiveKit when the first participant joins.
36
+ */
37
+ export async function mintJoinToken(req) {
38
+ const { AccessToken } = await import("livekit-server-sdk");
39
+ const token = new AccessToken(req.apiKey, req.apiSecret, {
40
+ identity: req.identity,
41
+ ttl: resolveTtlSeconds(req.ttlSeconds),
42
+ });
43
+ token.addGrant({
44
+ roomJoin: true,
45
+ room: req.room,
46
+ canPublish: true,
47
+ canSubscribe: true,
48
+ });
49
+ return token.toJwt();
50
+ }
51
+ //# sourceMappingURL=tokens.js.map
@@ -0,0 +1,32 @@
1
+ /**
2
+ * LiveKit Agents agent definition.
3
+ *
4
+ * `defineVoiceAgent` returns the agent object placed as the default export of a
5
+ * worker entry file. The framework runs it as a Job (one per call, in its own
6
+ * process): it connects to the room, builds the NeuroLink brain via the
7
+ * supplied factory, wires Silero VAD + STT/TTS plugins, and overrides `llmNode`
8
+ * so every turn is generated by `neurolink.stream()`.
9
+ *
10
+ * `@livekit/agents` and the plugins are optional dependencies, imported
11
+ * dynamically so the core package does not require them unless the LiveKit
12
+ * voice agent is used. Type-only imports are erased at build time and add no
13
+ * runtime dependency.
14
+ *
15
+ * See docs/features/livekit-voice-agent.md.
16
+ */
17
+ import type { Agent as JobAgent } from "@livekit/agents";
18
+ import type { LiveKitVoiceAgentConfig } from "../../types/index.js";
19
+ /**
20
+ * Define a LiveKit voice agent backed by NeuroLink.
21
+ *
22
+ * Place the result as the default export of the worker entry file:
23
+ *
24
+ * ```ts
25
+ * export default defineVoiceAgent({
26
+ * createNeuroLink: async () => buildConfiguredNeuroLink(),
27
+ * stt: { provider: "deepgram" },
28
+ * tts: { provider: "elevenlabs" },
29
+ * });
30
+ * ```
31
+ */
32
+ export declare function defineVoiceAgent(config: LiveKitVoiceAgentConfig): JobAgent;