@voicethere/agent 0.8.3 → 0.9.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.
@@ -0,0 +1,1050 @@
1
+ import { createRequire } from "node:module";
2
+ const require = createRequire(import.meta.url);
3
+
4
+
5
+ // dist/runtime.js
6
+ import { randomUUID } from "node:crypto";
7
+ import { AsyncLocalStorage } from "node:async_hooks";
8
+
9
+ // dist/session-serial-queue.js
10
+ var SessionSerialQueueCancellationError = class extends Error {
11
+ sessionId;
12
+ generation;
13
+ code = "SESSION_SERIAL_QUEUE_CANCELLED";
14
+ constructor(sessionId, generation) {
15
+ super(`session serial queue cancelled (sessionId=${sessionId}, generation=${generation})`);
16
+ this.sessionId = sessionId;
17
+ this.generation = generation;
18
+ this.name = "SessionSerialQueueCancellationError";
19
+ }
20
+ };
21
+ var SessionSerialQueue = class {
22
+ sessions = /* @__PURE__ */ new Map();
23
+ /** Monotonic token source — never reused across clear/enqueue cycles. */
24
+ nextGeneration = 1;
25
+ /**
26
+ * Enqueue work for `sessionId`. Returns a promise that settles when this
27
+ * task finishes, is skipped as stale, or is cancelled by {@link clear}.
28
+ * Fire-and-forget callers may ignore the return value (compat).
29
+ */
30
+ enqueue(sessionId, task) {
31
+ const state = this.ensureState(sessionId);
32
+ const captured = state;
33
+ const generation = captured.generation;
34
+ captured.pending += 1;
35
+ const previous = captured.tail;
36
+ let taskAbort = null;
37
+ const settled = previous.catch(() => void 0).then(async () => {
38
+ if (this.sessions.get(sessionId) !== captured) {
39
+ return "cancelled";
40
+ }
41
+ const abort = new AbortController();
42
+ taskAbort = abort;
43
+ captured.abort = abort;
44
+ const context = {
45
+ sessionId,
46
+ generation,
47
+ signal: abort.signal
48
+ };
49
+ try {
50
+ await Promise.resolve(task(abort.signal, context));
51
+ if (this.sessions.get(sessionId) !== captured) {
52
+ return "cancelled";
53
+ }
54
+ return "completed";
55
+ } catch (error) {
56
+ if (abort.signal.aborted || this.sessions.get(sessionId) !== captured || error instanceof SessionSerialQueueCancellationError) {
57
+ return "cancelled";
58
+ }
59
+ return "failed";
60
+ } finally {
61
+ if (captured.abort === abort) {
62
+ captured.abort = null;
63
+ }
64
+ }
65
+ }).finally(() => {
66
+ captured.pending = Math.max(0, captured.pending - 1);
67
+ if (taskAbort && captured.abort === taskAbort) {
68
+ captured.abort = null;
69
+ }
70
+ });
71
+ captured.tail = settled;
72
+ return settled;
73
+ }
74
+ /**
75
+ * Invalidate queued + running work for `sessionId`. Aborts the live AbortSignal,
76
+ * drops the map entry, and leaves a fresh identity for the next enqueue.
77
+ * Old in-flight `finally` blocks only touch their captured state object.
78
+ */
79
+ clear(sessionId) {
80
+ const old = this.sessions.get(sessionId);
81
+ if (!old)
82
+ return;
83
+ old.abort?.abort();
84
+ old.abort = null;
85
+ this.sessions.delete(sessionId);
86
+ }
87
+ clearAll() {
88
+ for (const sessionId of [...this.sessions.keys()]) {
89
+ this.clear(sessionId);
90
+ }
91
+ }
92
+ hasPending(sessionId) {
93
+ return (this.sessions.get(sessionId)?.pending ?? 0) > 0;
94
+ }
95
+ /** Live generation token for a session, or `undefined` when cleared. */
96
+ generationOf(sessionId) {
97
+ return this.sessions.get(sessionId)?.generation;
98
+ }
99
+ /** True when `generation` is the live map entry for `sessionId`. */
100
+ isCurrentGeneration(sessionId, generation) {
101
+ return this.sessions.get(sessionId)?.generation === generation;
102
+ }
103
+ /**
104
+ * True when a queue row exists for the session (registered since first
105
+ * enqueue, until {@link clear}). Not the same as {@link hasPending}.
106
+ */
107
+ isLive(sessionId) {
108
+ return this.sessions.has(sessionId);
109
+ }
110
+ /** Registered session rows (must not grow without matching `clear` calls). */
111
+ get activeSessionCount() {
112
+ return this.sessions.size;
113
+ }
114
+ ensureState(sessionId) {
115
+ const existing = this.sessions.get(sessionId);
116
+ if (existing)
117
+ return existing;
118
+ const created = {
119
+ generation: this.nextGeneration++,
120
+ tail: Promise.resolve(),
121
+ abort: null,
122
+ pending: 0
123
+ };
124
+ this.sessions.set(sessionId, created);
125
+ return created;
126
+ }
127
+ };
128
+
129
+ // dist/runtime.js
130
+ var SESSION_START_INIT_DELAY_ENABLED_ENV = "AGENT_SESSION_START_INIT_DELAY_ENABLED";
131
+ var SESSION_START_INIT_DELAY_MS_ENV = "AGENT_SESSION_START_INIT_DELAY_MS";
132
+ var DEFAULT_SESSION_START_INIT_DELAY_MS = 500;
133
+ function isPlayAckMessage(value) {
134
+ if (!value || typeof value !== "object")
135
+ return false;
136
+ const msg = value;
137
+ return msg.type === "play_ack" && typeof msg.requestId === "string";
138
+ }
139
+ function isPlayStatusAckMessage(value) {
140
+ if (!value || typeof value !== "object")
141
+ return false;
142
+ const msg = value;
143
+ return msg.type === "play_status_ack" && typeof msg.requestId === "string";
144
+ }
145
+ function isPlayStopAckMessage(value) {
146
+ if (!value || typeof value !== "object")
147
+ return false;
148
+ const msg = value;
149
+ return msg.type === "play_stop_ack" && typeof msg.requestId === "string";
150
+ }
151
+ function isPlayPoseAckMessage(value) {
152
+ if (!value || typeof value !== "object")
153
+ return false;
154
+ const msg = value;
155
+ return msg.type === "play_pose_ack" && typeof msg.requestId === "string";
156
+ }
157
+ function isRecordingControlAckMessage(value) {
158
+ if (!value || typeof value !== "object")
159
+ return false;
160
+ const msg = value;
161
+ return msg.type === "recording_control_ack" && typeof msg.requestId === "string";
162
+ }
163
+ function isConversationHistoryControlAckMessage(value) {
164
+ if (!value || typeof value !== "object")
165
+ return false;
166
+ const msg = value;
167
+ return msg.type === "conversation_history_control_ack" && typeof msg.requestId === "string";
168
+ }
169
+ function isMixControlAckMessage(value) {
170
+ if (!value || typeof value !== "object")
171
+ return false;
172
+ const msg = value;
173
+ return msg.type === "mix_control_ack" && typeof msg.requestId === "string";
174
+ }
175
+ function isSttControlAckMessage(value) {
176
+ if (!value || typeof value !== "object")
177
+ return false;
178
+ const msg = value;
179
+ return msg.type === "stt_control_ack" && typeof msg.requestId === "string";
180
+ }
181
+ function isVoiceLanguageControlAckMessage(value) {
182
+ if (!value || typeof value !== "object")
183
+ return false;
184
+ const msg = value;
185
+ return msg.type === "voice_language_control_ack" && typeof msg.requestId === "string";
186
+ }
187
+ function isWebhookMessage(value) {
188
+ if (!value || typeof value !== "object")
189
+ return false;
190
+ const msg = value;
191
+ return msg.type === "webhook" && typeof msg.eventId === "string" && typeof msg.projectId === "string";
192
+ }
193
+ function coerceInboundBinary(value) {
194
+ if (!value)
195
+ return null;
196
+ if (Buffer.isBuffer(value))
197
+ return value;
198
+ if (value instanceof Uint8Array) {
199
+ return Buffer.from(value.buffer, value.byteOffset, value.byteLength);
200
+ }
201
+ if (value instanceof ArrayBuffer)
202
+ return Buffer.from(value);
203
+ if (ArrayBuffer.isView(value)) {
204
+ return Buffer.from(value.buffer, value.byteOffset, value.byteLength);
205
+ }
206
+ if (typeof value === "object") {
207
+ const maybeBufferLike = value;
208
+ if (maybeBufferLike.type === "Buffer" && Array.isArray(maybeBufferLike.data)) {
209
+ return Buffer.from(maybeBufferLike.data);
210
+ }
211
+ }
212
+ return null;
213
+ }
214
+ function normalizeWebhookHeaders(value) {
215
+ if (!value || typeof value !== "object")
216
+ return {};
217
+ const out = {};
218
+ for (const [key, raw] of Object.entries(value)) {
219
+ if (typeof raw === "string") {
220
+ out[key] = raw;
221
+ }
222
+ }
223
+ return out;
224
+ }
225
+ function isParentMessage(value) {
226
+ if (!value || typeof value !== "object")
227
+ return false;
228
+ const msg = value;
229
+ return msg.type === "session_start" || msg.type === "speech_event" || msg.type === "session_end" || msg.type === "data_channel_message" || msg.type === "data_channel_binary" || msg.type === "idle_timeout" || msg.type === "recording_control_ack" || msg.type === "conversation_history_control_ack" || msg.type === "play_ack" || msg.type === "play_status_ack" || msg.type === "play_stop_ack" || msg.type === "play_pose_ack" || msg.type === "mix_control_ack" || msg.type === "stt_control_ack" || msg.type === "voice_language_control_ack" || msg.type === "webhook";
230
+ }
231
+ function isSessionScopedParentMessage(value) {
232
+ return isParentMessage(value) && !isWebhookMessage(value);
233
+ }
234
+ function parseDataChannelPayload(raw) {
235
+ try {
236
+ return JSON.parse(raw);
237
+ } catch {
238
+ return raw;
239
+ }
240
+ }
241
+ var peerEnvBySessionId = /* @__PURE__ */ new Map();
242
+ var recordingAvailableBySessionId = /* @__PURE__ */ new Map();
243
+ var conversationHistoryAvailableBySessionId = /* @__PURE__ */ new Map();
244
+ var mixAvailableBySessionId = /* @__PURE__ */ new Map();
245
+ var ttsPoseAvailableBySessionId = /* @__PURE__ */ new Map();
246
+ var endedSessionIds = /* @__PURE__ */ new Set();
247
+ var PLAY_BYTES_MAX_DECODED_LENGTH = 64 * 1024;
248
+ var pendingRecordingAcks = /* @__PURE__ */ new Map();
249
+ var pendingConversationHistoryAcks = /* @__PURE__ */ new Map();
250
+ var pendingPlayAcks = /* @__PURE__ */ new Map();
251
+ var pendingPlayStatusAcks = /* @__PURE__ */ new Map();
252
+ var pendingPlayStopAcks = /* @__PURE__ */ new Map();
253
+ var pendingPlayPoseAcks = /* @__PURE__ */ new Map();
254
+ var pendingMixAcks = /* @__PURE__ */ new Map();
255
+ var pendingSttAcks = /* @__PURE__ */ new Map();
256
+ var VOICE_LANGUAGE_CONTROL_ACK_TIMEOUT_MS = 66e4;
257
+ var pendingVoiceLanguageAcks = /* @__PURE__ */ new Map();
258
+ function handleRecordingControlAck(message) {
259
+ const pending = pendingRecordingAcks.get(message.requestId);
260
+ if (!pending)
261
+ return;
262
+ clearTimeout(pending.timer);
263
+ pendingRecordingAcks.delete(message.requestId);
264
+ pending.resolve({
265
+ ok: message.ok,
266
+ reason: message.reason,
267
+ requestId: message.requestId
268
+ });
269
+ }
270
+ function handleConversationHistoryControlAck(message) {
271
+ const pending = pendingConversationHistoryAcks.get(message.requestId);
272
+ if (!pending)
273
+ return;
274
+ clearTimeout(pending.timer);
275
+ pendingConversationHistoryAcks.delete(message.requestId);
276
+ pending.resolve({
277
+ ok: message.ok,
278
+ reason: message.reason,
279
+ requestId: message.requestId
280
+ });
281
+ }
282
+ function handlePlayAck(message) {
283
+ const pending = pendingPlayAcks.get(message.requestId);
284
+ if (!pending)
285
+ return;
286
+ clearTimeout(pending.timer);
287
+ pendingPlayAcks.delete(message.requestId);
288
+ pending.resolve({
289
+ ok: message.ok,
290
+ ...message.playId ? { playId: message.playId } : {},
291
+ reason: message.reason,
292
+ requestId: message.requestId
293
+ });
294
+ }
295
+ function handlePlayStatusAck(message) {
296
+ const pending = pendingPlayStatusAcks.get(message.requestId);
297
+ if (!pending)
298
+ return;
299
+ clearTimeout(pending.timer);
300
+ pendingPlayStatusAcks.delete(message.requestId);
301
+ pending.resolve({
302
+ ok: message.ok,
303
+ playId: message.playId,
304
+ ...message.status ? { status: message.status } : {},
305
+ reason: message.reason,
306
+ requestId: message.requestId
307
+ });
308
+ }
309
+ function handlePlayStopAck(message) {
310
+ const pending = pendingPlayStopAcks.get(message.requestId);
311
+ if (!pending)
312
+ return;
313
+ clearTimeout(pending.timer);
314
+ pendingPlayStopAcks.delete(message.requestId);
315
+ pending.resolve({
316
+ ok: message.ok,
317
+ playId: message.playId,
318
+ reason: message.reason,
319
+ requestId: message.requestId
320
+ });
321
+ }
322
+ function handlePlayPoseAck(message) {
323
+ const pending = pendingPlayPoseAcks.get(message.requestId);
324
+ if (!pending)
325
+ return;
326
+ clearTimeout(pending.timer);
327
+ pendingPlayPoseAcks.delete(message.requestId);
328
+ pending.resolve({
329
+ ok: message.ok,
330
+ playId: message.playId,
331
+ reason: message.reason,
332
+ requestId: message.requestId
333
+ });
334
+ }
335
+ function handleMixControlAck(message) {
336
+ const pending = pendingMixAcks.get(message.requestId);
337
+ if (!pending)
338
+ return;
339
+ clearTimeout(pending.timer);
340
+ pendingMixAcks.delete(message.requestId);
341
+ pending.resolve({
342
+ ok: message.ok,
343
+ reason: message.reason,
344
+ requestId: message.requestId,
345
+ ...message.statuses ? { statuses: message.statuses } : {}
346
+ });
347
+ }
348
+ function handleSttControlAck(message) {
349
+ const pending = pendingSttAcks.get(message.requestId);
350
+ if (!pending)
351
+ return;
352
+ clearTimeout(pending.timer);
353
+ pendingSttAcks.delete(message.requestId);
354
+ pending.resolve({
355
+ ok: message.ok,
356
+ reason: message.reason,
357
+ requestId: message.requestId
358
+ });
359
+ }
360
+ function handleVoiceLanguageControlAck(message) {
361
+ const pending = pendingVoiceLanguageAcks.get(message.requestId);
362
+ if (!pending)
363
+ return;
364
+ clearTimeout(pending.timer);
365
+ pendingVoiceLanguageAcks.delete(message.requestId);
366
+ pending.resolve({
367
+ ok: message.ok,
368
+ reason: message.reason,
369
+ requestId: message.requestId,
370
+ ...message.language ? { language: message.language } : {},
371
+ ...message.voice ? { voice: message.voice } : {},
372
+ ...message.stt ? { stt: message.stt } : {},
373
+ ...message.scope ? { scope: message.scope } : {},
374
+ ...message.sttProvider ? { sttProvider: message.sttProvider } : {},
375
+ ...message.ttsProvider ? { ttsProvider: message.ttsProvider } : {}
376
+ });
377
+ }
378
+ function clearPendingRecordingAcksForSession(sessionId, reason) {
379
+ for (const [requestId, pending] of pendingRecordingAcks) {
380
+ if (pending.sessionId !== sessionId)
381
+ continue;
382
+ clearTimeout(pending.timer);
383
+ pendingRecordingAcks.delete(requestId);
384
+ pending.resolve({ ok: false, reason, requestId });
385
+ }
386
+ }
387
+ function clearPendingConversationHistoryAcksForSession(sessionId, reason) {
388
+ for (const [requestId, pending] of pendingConversationHistoryAcks) {
389
+ if (pending.sessionId !== sessionId)
390
+ continue;
391
+ clearTimeout(pending.timer);
392
+ pendingConversationHistoryAcks.delete(requestId);
393
+ pending.resolve({ ok: false, reason, requestId });
394
+ }
395
+ }
396
+ function isVoicethereAgentChild() {
397
+ return typeof process.send === "function" && process.connected !== false && typeof process.env.__CHILD_BUNDLE_PATH__ === "string" && process.env.__CHILD_BUNDLE_PATH__.trim().length > 0;
398
+ }
399
+ var sessionExecutionContext = new AsyncLocalStorage();
400
+ var agentLogSessionContext = new AsyncLocalStorage();
401
+ var inboundQueueAuthority = null;
402
+ var childUnhandledRejectionGuardInstalled = false;
403
+ function normalizeRejectionReason(reason) {
404
+ return reason instanceof Error ? reason : new Error(String(reason));
405
+ }
406
+ function installChildUnhandledRejectionGuard() {
407
+ if (childUnhandledRejectionGuardInstalled) {
408
+ return;
409
+ }
410
+ childUnhandledRejectionGuardInstalled = true;
411
+ process.on("unhandledRejection", (reason) => {
412
+ const err = normalizeRejectionReason(reason);
413
+ const store = sessionExecutionContext.getStore();
414
+ const sessionId = store?.sessionId ?? agentLogSessionContext.getStore() ?? "";
415
+ if (!allowOutboundForSession(sessionId || void 0)) {
416
+ return;
417
+ }
418
+ agentLog("error", `unhandledRejection: ${err.message}`, sessionId || void 0);
419
+ sendParentMessage({
420
+ type: "agent_error",
421
+ sessionId,
422
+ message: err.message,
423
+ stack: err.stack
424
+ });
425
+ });
426
+ }
427
+ function allowOutboundForSession(sessionId) {
428
+ if (!sessionId) {
429
+ return true;
430
+ }
431
+ const store = sessionExecutionContext.getStore();
432
+ const queue = inboundQueueAuthority;
433
+ if (store && store.sessionId === sessionId) {
434
+ if (!queue)
435
+ return !endedSessionIds.has(sessionId);
436
+ return queue.isCurrentGeneration(sessionId, store.generation);
437
+ }
438
+ if (endedSessionIds.has(sessionId))
439
+ return false;
440
+ if (!queue)
441
+ return true;
442
+ return queue.isLive(sessionId);
443
+ }
444
+ function sendParentMessage(message) {
445
+ const msgType = message && typeof message === "object" && "type" in message ? message.type : void 0;
446
+ if (msgType === "mix_control" || msgType === "stt_control" || msgType === "voice_language_control") {
447
+ process.send?.(message);
448
+ return;
449
+ }
450
+ if (msgType === "play" || msgType === "play_status" || msgType === "play_stop" || msgType === "play_pose") {
451
+ process.send?.(message);
452
+ return;
453
+ }
454
+ const sessionId = message && typeof message === "object" && "sessionId" in message && typeof message.sessionId === "string" ? message.sessionId : void 0;
455
+ if (!allowOutboundForSession(sessionId))
456
+ return;
457
+ process.send?.(message);
458
+ }
459
+ function parseBooleanEnv(value, defaultValue) {
460
+ if (value === void 0)
461
+ return defaultValue;
462
+ const normalized = value.trim().toLowerCase();
463
+ if (normalized === "")
464
+ return defaultValue;
465
+ if (["0", "false", "off", "no"].includes(normalized))
466
+ return false;
467
+ if (["1", "true", "on", "yes"].includes(normalized))
468
+ return true;
469
+ return defaultValue;
470
+ }
471
+ function parseNonNegativeIntegerEnv(value, defaultValue) {
472
+ if (value === void 0)
473
+ return defaultValue;
474
+ const parsed = Number(value);
475
+ if (!Number.isFinite(parsed) || parsed < 0)
476
+ return defaultValue;
477
+ return Math.floor(parsed);
478
+ }
479
+ function resolveSessionStartInitDelayMs() {
480
+ const enabled = parseBooleanEnv(process.env[SESSION_START_INIT_DELAY_ENABLED_ENV], true);
481
+ if (!enabled)
482
+ return 0;
483
+ return parseNonNegativeIntegerEnv(process.env[SESSION_START_INIT_DELAY_MS_ENV], DEFAULT_SESSION_START_INIT_DELAY_MS);
484
+ }
485
+ async function handleWebhookMessage(message, handlers) {
486
+ if (!handlers.onWebhook)
487
+ return;
488
+ const body = coerceInboundBinary(message.body);
489
+ if (!body) {
490
+ agentLog("warn", "webhook ipc dropped: body is not binary");
491
+ return;
492
+ }
493
+ const ctx = {
494
+ eventId: message.eventId,
495
+ projectId: message.projectId,
496
+ method: typeof message.method === "string" ? message.method : "POST",
497
+ path: typeof message.path === "string" ? message.path : "",
498
+ headers: normalizeWebhookHeaders(message.headers),
499
+ body,
500
+ contentType: typeof message.contentType === "string" ? message.contentType : null,
501
+ receivedAt: typeof message.receivedAt === "string" ? message.receivedAt : "",
502
+ sessionIds: Array.isArray(message.sessionIds) ? message.sessionIds.filter((id) => typeof id === "string" && id.length > 0) : []
503
+ };
504
+ try {
505
+ const started = Date.now();
506
+ await handlers.onWebhook(ctx);
507
+ sendParentMessage({
508
+ type: "webhook_handled",
509
+ projectId: message.projectId,
510
+ eventId: message.eventId,
511
+ durationMs: Date.now() - started
512
+ });
513
+ } catch (error) {
514
+ const err = error instanceof Error ? error : new Error(String(error));
515
+ await runErrorHook(handlers, {
516
+ sessionId: "",
517
+ projectId: message.projectId,
518
+ env: process.env,
519
+ error: err
520
+ });
521
+ sendParentMessage({
522
+ type: "agent_error",
523
+ sessionId: "",
524
+ message: err.message,
525
+ stack: err.stack
526
+ });
527
+ }
528
+ }
529
+ function resolveUserLanguageCode(event) {
530
+ const withLanguage = event;
531
+ const candidates = [withLanguage.language, event.text];
532
+ for (const value of candidates) {
533
+ if (typeof value === "string" && value.trim()) {
534
+ return value.trim();
535
+ }
536
+ }
537
+ return void 0;
538
+ }
539
+ async function handleParentMessage(message, handlers) {
540
+ switch (message.type) {
541
+ case "session_start":
542
+ endedSessionIds.delete(message.sessionId);
543
+ peerEnvBySessionId.set(message.sessionId, message.env);
544
+ recordingAvailableBySessionId.set(message.sessionId, message.recordingAvailable ?? false);
545
+ conversationHistoryAvailableBySessionId.set(message.sessionId, message.conversationHistoryAvailable ?? false);
546
+ mixAvailableBySessionId.set(message.sessionId, message.mixAvailable ?? false);
547
+ ttsPoseAvailableBySessionId.set(message.sessionId, message.ttsPoseAvailable ?? false);
548
+ const sessionStartInitDelayMs = resolveSessionStartInitDelayMs();
549
+ if (sessionStartInitDelayMs > 0) {
550
+ await new Promise((resolve) => setTimeout(resolve, sessionStartInitDelayMs));
551
+ }
552
+ await (handlers.onClientJoin ?? handlers.onSessionStart)?.({
553
+ sessionId: message.sessionId,
554
+ env: message.env,
555
+ recordingAvailable: message.recordingAvailable ?? false,
556
+ conversationHistoryAvailable: message.conversationHistoryAvailable ?? false,
557
+ mixAvailable: message.mixAvailable ?? false,
558
+ ttsPoseAvailable: message.ttsPoseAvailable ?? false
559
+ });
560
+ sendParentMessage({
561
+ type: "session_start_ack",
562
+ sessionId: message.sessionId
563
+ });
564
+ break;
565
+ case "speech_event":
566
+ await handlers.onSpeechEvent?.({ sessionId: message.sessionId }, message.event);
567
+ if (message.event.type === "user_speech_final" && typeof message.event.text === "string" && message.event.text.trim()) {
568
+ await handlers.onUserSpeechFinal?.({
569
+ sessionId: message.sessionId,
570
+ text: message.event.text.trim()
571
+ });
572
+ }
573
+ if (message.event.type === "user_language") {
574
+ const language = resolveUserLanguageCode(message.event);
575
+ if (language) {
576
+ await handlers.onUserLanguage?.({
577
+ sessionId: message.sessionId,
578
+ language
579
+ });
580
+ }
581
+ }
582
+ break;
583
+ case "data_channel_message":
584
+ await handlers.onDataChannelMessage?.({
585
+ sessionId: message.sessionId,
586
+ message: parseDataChannelPayload(message.payload),
587
+ raw: message.payload,
588
+ rawBinary: null,
589
+ channel: "control"
590
+ });
591
+ break;
592
+ case "data_channel_binary":
593
+ await handlers.onDataChannelBinary?.({
594
+ sessionId: message.sessionId,
595
+ message: null,
596
+ raw: null,
597
+ rawBinary: message.data,
598
+ channel: message.channel ?? "sync"
599
+ });
600
+ break;
601
+ case "session_end":
602
+ clearPendingRecordingAcksForSession(message.sessionId, "session_ended");
603
+ clearPendingConversationHistoryAcksForSession(message.sessionId, "session_ended");
604
+ peerEnvBySessionId.delete(message.sessionId);
605
+ recordingAvailableBySessionId.delete(message.sessionId);
606
+ conversationHistoryAvailableBySessionId.delete(message.sessionId);
607
+ mixAvailableBySessionId.delete(message.sessionId);
608
+ ttsPoseAvailableBySessionId.delete(message.sessionId);
609
+ await (handlers.onClientLeave ?? handlers.onSessionEnd)?.({
610
+ sessionId: message.sessionId
611
+ });
612
+ break;
613
+ case "idle_timeout":
614
+ await runIdleTimeoutHook(handlers, message);
615
+ break;
616
+ }
617
+ }
618
+ function defineAgent(handlers) {
619
+ installChildUnhandledRejectionGuard();
620
+ const inboundBySession = new SessionSerialQueue();
621
+ inboundQueueAuthority = inboundBySession;
622
+ const agentStartReady = runAgentStartHook(handlers);
623
+ process.on("message", (message) => {
624
+ if (isRecordingControlAckMessage(message)) {
625
+ handleRecordingControlAck(message);
626
+ return;
627
+ }
628
+ if (isConversationHistoryControlAckMessage(message)) {
629
+ handleConversationHistoryControlAck(message);
630
+ return;
631
+ }
632
+ if (isPlayAckMessage(message)) {
633
+ handlePlayAck(message);
634
+ return;
635
+ }
636
+ if (isPlayStatusAckMessage(message)) {
637
+ handlePlayStatusAck(message);
638
+ return;
639
+ }
640
+ if (isPlayStopAckMessage(message)) {
641
+ handlePlayStopAck(message);
642
+ return;
643
+ }
644
+ if (isPlayPoseAckMessage(message)) {
645
+ handlePlayPoseAck(message);
646
+ return;
647
+ }
648
+ if (isMixControlAckMessage(message)) {
649
+ handleMixControlAck(message);
650
+ return;
651
+ }
652
+ if (isSttControlAckMessage(message)) {
653
+ handleSttControlAck(message);
654
+ return;
655
+ }
656
+ if (isVoiceLanguageControlAckMessage(message)) {
657
+ handleVoiceLanguageControlAck(message);
658
+ return;
659
+ }
660
+ if (isWebhookMessage(message)) {
661
+ void agentStartReady.then(() => handleWebhookMessage(message, handlers));
662
+ return;
663
+ }
664
+ if (!isSessionScopedParentMessage(message))
665
+ return;
666
+ if (message.type === "session_end") {
667
+ endedSessionIds.add(message.sessionId);
668
+ clearPendingRecordingAcksForSession(message.sessionId, "session_ended");
669
+ clearPendingConversationHistoryAcksForSession(message.sessionId, "session_ended");
670
+ inboundBySession.clear(message.sessionId);
671
+ }
672
+ if (message.type === "session_start") {
673
+ endedSessionIds.delete(message.sessionId);
674
+ inboundBySession.clear(message.sessionId);
675
+ }
676
+ inboundBySession.enqueue(message.sessionId, async (_signal, context) => {
677
+ await agentStartReady;
678
+ try {
679
+ await sessionExecutionContext.run({
680
+ sessionId: message.sessionId,
681
+ generation: context.generation
682
+ }, async () => agentLogSessionContext.run(message.sessionId, async () => {
683
+ try {
684
+ await handleParentMessage(message, handlers);
685
+ } catch (error) {
686
+ const err = error instanceof Error ? error : new Error(String(error));
687
+ const env = peerEnvBySessionId.get(message.sessionId) ?? buildIdleEnv(message.sessionId);
688
+ await runErrorHook(handlers, {
689
+ sessionId: message.sessionId,
690
+ projectId: env.PROJECT_ID,
691
+ buildId: env.BUILD_ID,
692
+ env,
693
+ error: err,
694
+ customerContext: parseCustomerContext(env.AGENT_CUSTOMER_CONTEXT)
695
+ });
696
+ sendParentMessage({
697
+ type: "agent_error",
698
+ sessionId: message.sessionId,
699
+ message: err.message,
700
+ stack: err.stack
701
+ });
702
+ }
703
+ }));
704
+ } finally {
705
+ if (message.type === "session_end" && inboundBySession.isCurrentGeneration(message.sessionId, context.generation)) {
706
+ inboundBySession.clear(message.sessionId);
707
+ }
708
+ }
709
+ });
710
+ });
711
+ }
712
+ async function runAgentStartHook(handlers) {
713
+ if (!handlers.onAgentStart)
714
+ return;
715
+ try {
716
+ await handlers.onAgentStart({
717
+ env: process.env
718
+ });
719
+ } catch (error) {
720
+ const err = error instanceof Error ? error : new Error(String(error));
721
+ agentLog("error", `onAgentStart failed: ${err.message}`);
722
+ sendParentMessage({
723
+ type: "agent_error",
724
+ sessionId: "",
725
+ message: err.message,
726
+ stack: err.stack
727
+ });
728
+ }
729
+ }
730
+ function parseCustomerContext(raw) {
731
+ if (!raw?.trim())
732
+ return void 0;
733
+ try {
734
+ const parsed = JSON.parse(raw);
735
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
736
+ return parsed;
737
+ }
738
+ } catch {
739
+ }
740
+ return void 0;
741
+ }
742
+ async function runErrorHook(handlers, ctx) {
743
+ if (!handlers.errorHook)
744
+ return;
745
+ try {
746
+ await handlers.errorHook(ctx);
747
+ } catch (hookError) {
748
+ const message = hookError instanceof Error ? hookError.message : String(hookError);
749
+ agentLog("error", `errorHook failed: ${message}`, ctx.sessionId);
750
+ }
751
+ }
752
+ async function runIdleTimeoutHook(handlers, message) {
753
+ const onIdleTimeout = handlers.onIdleTimeout;
754
+ agentLog("info", `idle_timeout ipc received (maxGraceMs=${message.maxGraceMs}, onIdleTimeout=${typeof onIdleTimeout === "function"})`, message.sessionId);
755
+ if (!onIdleTimeout) {
756
+ sendParentMessage({
757
+ type: "idle_timeout_done",
758
+ sessionId: message.sessionId
759
+ });
760
+ agentLog("info", "idle_timeout_done ipc sent (no onIdleTimeout handler)", message.sessionId);
761
+ return;
762
+ }
763
+ const env = peerEnvBySessionId.get(message.sessionId) ?? buildIdleEnv(message.sessionId);
764
+ const idleTimeoutSeconds = Number(env.IDLE_TIMEOUT_SEC) || 0;
765
+ const ctx = {
766
+ sessionId: message.sessionId,
767
+ projectId: env.PROJECT_ID,
768
+ buildId: env.BUILD_ID,
769
+ env,
770
+ idleTimeoutSeconds
771
+ };
772
+ let error;
773
+ try {
774
+ await onIdleTimeout(ctx);
775
+ } catch (hookError) {
776
+ error = hookError instanceof Error ? hookError.message : String(hookError);
777
+ agentLog("error", `onIdleTimeout failed: ${error}`, message.sessionId);
778
+ }
779
+ sendParentMessage({
780
+ type: "idle_timeout_done",
781
+ sessionId: message.sessionId,
782
+ error
783
+ });
784
+ agentLog("info", error ? `idle_timeout_done ipc sent (onIdleTimeout error: ${error})` : "idle_timeout_done ipc sent (onIdleTimeout completed)", message.sessionId);
785
+ }
786
+ function buildIdleEnv(sessionId) {
787
+ return {
788
+ SESSION_ID: sessionId,
789
+ ...process.env.PROJECT_ID ? { PROJECT_ID: process.env.PROJECT_ID } : {},
790
+ ...process.env.BUILD_ID ? { BUILD_ID: process.env.BUILD_ID } : {},
791
+ ...process.env.IDLE_TIMEOUT_SEC ? { IDLE_TIMEOUT_SEC: process.env.IDLE_TIMEOUT_SEC } : {}
792
+ };
793
+ }
794
+ function speak(sessionId, text) {
795
+ sendParentMessage({ type: "speak", sessionId, text });
796
+ }
797
+ function cleanVendor(selection) {
798
+ const provider = selection?.provider?.trim();
799
+ if (!provider)
800
+ return void 0;
801
+ return {
802
+ provider,
803
+ ...selection?.model?.trim() ? { model: selection.model.trim() } : {},
804
+ ...selection?.voice?.trim() ? { voice: selection.voice.trim() } : {},
805
+ ...selection?.language?.trim() ? { language: selection.language.trim() } : {}
806
+ };
807
+ }
808
+ function setVoiceLanguage(sessionId, options) {
809
+ const requestId = randomUUID();
810
+ const language = options.language?.trim() ?? "";
811
+ const scope = options.scope;
812
+ if (scope && scope !== "stt" && scope !== "tts" && scope !== "both") {
813
+ return Promise.resolve({
814
+ ok: false,
815
+ reason: "invalid_scope",
816
+ requestId
817
+ });
818
+ }
819
+ const sttVendor = cleanVendor(options.sttVendor);
820
+ const ttsVendor = cleanVendor(options.ttsVendor);
821
+ const hasWork = Boolean(language || options.voice?.trim() || options.stt?.trim() || sttVendor || ttsVendor);
822
+ if (!hasWork) {
823
+ return Promise.resolve({
824
+ ok: false,
825
+ reason: "nothing_to_apply",
826
+ requestId
827
+ });
828
+ }
829
+ if (!isVoicethereAgentChild()) {
830
+ return Promise.resolve({ ok: true, reason: "local_mock", requestId });
831
+ }
832
+ return new Promise((resolve) => {
833
+ const timer = setTimeout(() => {
834
+ pendingVoiceLanguageAcks.delete(requestId);
835
+ resolve({ ok: false, reason: "timeout", requestId });
836
+ }, VOICE_LANGUAGE_CONTROL_ACK_TIMEOUT_MS);
837
+ pendingVoiceLanguageAcks.set(requestId, { resolve, timer });
838
+ sendParentMessage({
839
+ type: "voice_language_control",
840
+ requestId,
841
+ sessionId,
842
+ ...language ? { language } : {},
843
+ ...scope ? { scope } : {},
844
+ ...options.voice?.trim() ? { voice: options.voice.trim() } : {},
845
+ ...options.stt?.trim() ? { stt: options.stt.trim() } : {},
846
+ ...sttVendor ? { sttVendor } : {},
847
+ ...ttsVendor ? { ttsVendor } : {}
848
+ });
849
+ });
850
+ }
851
+ var AGENT_LOG_MESSAGE_MAX_CHARS = 2048;
852
+ var AGENT_LOG_FIELDS_MAX_CHARS = 8192;
853
+ function truncateAgentLogMessage(message) {
854
+ if (message.length <= AGENT_LOG_MESSAGE_MAX_CHARS) {
855
+ return message;
856
+ }
857
+ const suffix = "\u2026[truncated]";
858
+ return message.slice(0, AGENT_LOG_MESSAGE_MAX_CHARS - suffix.length) + suffix;
859
+ }
860
+ function isPlainObject(value) {
861
+ return typeof value === "object" && value !== null && !Array.isArray(value);
862
+ }
863
+ function sanitizeAgentLogFields(fields) {
864
+ if (Object.keys(fields).length === 0) {
865
+ return void 0;
866
+ }
867
+ try {
868
+ const serialized = JSON.stringify(fields);
869
+ if (serialized.length <= AGENT_LOG_FIELDS_MAX_CHARS) {
870
+ return fields;
871
+ }
872
+ return {
873
+ _agentLogFieldsTruncated: true,
874
+ _originalBytes: serialized.length,
875
+ _preview: serialized.slice(0, AGENT_LOG_FIELDS_MAX_CHARS - 80) + "\u2026[truncated]"
876
+ };
877
+ } catch {
878
+ return { _agentLogFieldsError: "not_serializable" };
879
+ }
880
+ }
881
+ function buildAgentLogPayload(level, message, fields, sessionId) {
882
+ const resolvedSessionId = sessionId ?? sessionExecutionContext.getStore()?.sessionId ?? agentLogSessionContext.getStore();
883
+ const sanitizedFields = fields ? sanitizeAgentLogFields(fields) : void 0;
884
+ return {
885
+ type: "log",
886
+ level,
887
+ message: truncateAgentLogMessage(message),
888
+ ts: Date.now(),
889
+ ...resolvedSessionId ? { sessionId: resolvedSessionId } : {},
890
+ ...sanitizedFields ? { fields: sanitizedFields } : {}
891
+ };
892
+ }
893
+ function agentLog(level, message, fieldsOrSessionId, sessionId) {
894
+ let fields;
895
+ let resolvedSessionId;
896
+ if (typeof fieldsOrSessionId === "string") {
897
+ resolvedSessionId = fieldsOrSessionId;
898
+ } else if (isPlainObject(fieldsOrSessionId)) {
899
+ fields = fieldsOrSessionId;
900
+ resolvedSessionId = sessionId;
901
+ } else {
902
+ resolvedSessionId = sessionId;
903
+ }
904
+ sendParentMessage(buildAgentLogPayload(level, message, fields, resolvedSessionId));
905
+ }
906
+ function parseChatText(message) {
907
+ if (!message || typeof message !== "object")
908
+ return null;
909
+ const record = message;
910
+ if (record.type !== "chat" || typeof record.text !== "string")
911
+ return null;
912
+ const trimmed = record.text.trim();
913
+ return trimmed.length > 0 ? trimmed : null;
914
+ }
915
+
916
+ // templates/language-switch/agent.ts
917
+ var ECHO_PREFIX = "you said:";
918
+ var ECHO_WAIT_MS = 4e3;
919
+ var REPLIES = {
920
+ de: "Guten Tag. Ich antworte jetzt auf Deutsch.",
921
+ es: "Hola. Ahora respondo en espa\xF1ol.",
922
+ fr: "Bonjour. Je r\xE9ponds maintenant en fran\xE7ais.",
923
+ it: "Ciao. Adesso rispondo in italiano.",
924
+ pt: "Ol\xE1. Agora respondo em portugu\xEAs.",
925
+ nl: "Hallo. Ik antwoord nu in het Nederlands.",
926
+ pl: "Dzie\u0144 dobry. Teraz odpowiadam po polsku.",
927
+ ru: "\u0417\u0434\u0440\u0430\u0432\u0441\u0442\u0432\u0443\u0439\u0442\u0435. \u0422\u0435\u043F\u0435\u0440\u044C \u044F \u043E\u0442\u0432\u0435\u0447\u0430\u044E \u043F\u043E-\u0440\u0443\u0441\u0441\u043A\u0438."
928
+ };
929
+ var sessions = /* @__PURE__ */ new Map();
930
+ function stateFor(sessionId) {
931
+ const existing = sessions.get(sessionId);
932
+ if (existing) return existing;
933
+ const created = {
934
+ language: "en",
935
+ echoTimer: void 0,
936
+ suppressNextFinal: false
937
+ };
938
+ sessions.set(sessionId, created);
939
+ return created;
940
+ }
941
+ function replyFor(language) {
942
+ return REPLIES[language] ?? `Continuing in ${language}.`;
943
+ }
944
+ function logSwitch(sessionId, side, result) {
945
+ if (!result.ok) {
946
+ agentLog("warn", `setVoiceLanguage ${side} failed: ${result.reason ?? "unknown"}`);
947
+ return;
948
+ }
949
+ const provider = side === "tts" ? result.ttsProvider : result.sttProvider;
950
+ const model = side === "tts" ? result.voice : result.stt;
951
+ agentLog(
952
+ "info",
953
+ `voice ${side} ${sessionId} ${result.language ?? ""} provider=${provider ?? "unchanged"} model=${model ?? "unchanged"}`
954
+ );
955
+ }
956
+ defineAgent({
957
+ onSessionStart({ sessionId }) {
958
+ stateFor(sessionId);
959
+ agentLog("info", `language-switch session_start ${sessionId}`);
960
+ },
961
+ async onUserLanguage({ sessionId, language }) {
962
+ const state = stateFor(sessionId);
963
+ if (!language || language === state.language) return;
964
+ if (state.echoTimer) {
965
+ clearTimeout(state.echoTimer);
966
+ state.echoTimer = void 0;
967
+ } else {
968
+ state.suppressNextFinal = true;
969
+ }
970
+ const tts = await setVoiceLanguage(sessionId, {
971
+ scope: "tts",
972
+ language,
973
+ voice: language
974
+ });
975
+ logSwitch(sessionId, "tts", tts);
976
+ const stt = await setVoiceLanguage(sessionId, {
977
+ scope: "stt",
978
+ language,
979
+ stt: language
980
+ });
981
+ logSwitch(sessionId, "stt", stt);
982
+ if (!tts.ok) {
983
+ state.suppressNextFinal = false;
984
+ return;
985
+ }
986
+ state.language = language;
987
+ speak(sessionId, replyFor(language));
988
+ },
989
+ async onDataChannelMessage(ctx) {
990
+ const text = parseChatText(ctx.message);
991
+ if (!text) return;
992
+ const [command, vendor, language] = text.trim().split(/\s+/);
993
+ if (command === "/tts" && vendor === "sherpa" && language) {
994
+ const result = await setVoiceLanguage(ctx.sessionId, {
995
+ scope: "tts",
996
+ language,
997
+ ttsVendor: { provider: "local-sherpa", voice: language }
998
+ });
999
+ logSwitch(ctx.sessionId, "tts", result);
1000
+ if (result.ok) speak(ctx.sessionId, replyFor(language));
1001
+ return;
1002
+ }
1003
+ if (command === "/stt" && vendor === "sherpa" && language) {
1004
+ const result = await setVoiceLanguage(ctx.sessionId, {
1005
+ scope: "stt",
1006
+ language,
1007
+ sttVendor: { provider: "local-sherpa", model: language }
1008
+ });
1009
+ logSwitch(ctx.sessionId, "stt", result);
1010
+ return;
1011
+ }
1012
+ if (command === "/tts" && vendor === "elevenlabs") {
1013
+ const result = await setVoiceLanguage(ctx.sessionId, {
1014
+ scope: "tts",
1015
+ ttsVendor: { provider: "elevenlabs", model: "eleven_multilingual_v2" }
1016
+ });
1017
+ logSwitch(ctx.sessionId, "tts", result);
1018
+ if (result.ok) speak(ctx.sessionId, "Speaking with ElevenLabs.");
1019
+ return;
1020
+ }
1021
+ if (command === "/stt" && vendor === "deepgram") {
1022
+ const result = await setVoiceLanguage(ctx.sessionId, {
1023
+ scope: "stt",
1024
+ sttVendor: {
1025
+ provider: "deepgram",
1026
+ model: "nova-3",
1027
+ language: language || "en"
1028
+ }
1029
+ });
1030
+ logSwitch(ctx.sessionId, "stt", result);
1031
+ }
1032
+ },
1033
+ onUserSpeechFinal({ sessionId, text }) {
1034
+ const state = stateFor(sessionId);
1035
+ if (state.suppressNextFinal) {
1036
+ state.suppressNextFinal = false;
1037
+ return;
1038
+ }
1039
+ if (state.echoTimer) clearTimeout(state.echoTimer);
1040
+ state.echoTimer = setTimeout(() => {
1041
+ state.echoTimer = void 0;
1042
+ speak(sessionId, `${ECHO_PREFIX} ${text}`.trim());
1043
+ }, ECHO_WAIT_MS);
1044
+ },
1045
+ onSessionEnd({ sessionId }) {
1046
+ const state = sessions.get(sessionId);
1047
+ if (state?.echoTimer) clearTimeout(state.echoTimer);
1048
+ sessions.delete(sessionId);
1049
+ }
1050
+ });