@absolutejs/voice 0.0.22-beta.23 → 0.0.22-beta.231

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 (207) hide show
  1. package/README.md +3051 -239
  2. package/dist/agent.d.ts +62 -0
  3. package/dist/agentSquadContract.d.ts +69 -0
  4. package/dist/angular/index.d.ts +13 -0
  5. package/dist/angular/index.js +3154 -1110
  6. package/dist/angular/voice-agent-squad-status.service.d.ts +12 -0
  7. package/dist/angular/voice-campaign-dialer-proof.service.d.ts +14 -0
  8. package/dist/angular/voice-controller.service.d.ts +1 -0
  9. package/dist/angular/voice-delivery-runtime.component.d.ts +17 -0
  10. package/dist/angular/voice-delivery-runtime.service.d.ts +16 -0
  11. package/dist/angular/voice-live-ops.service.d.ts +11 -0
  12. package/dist/angular/voice-ops-action-center.service.d.ts +13 -0
  13. package/dist/angular/voice-ops-status.component.d.ts +15 -0
  14. package/dist/angular/voice-ops-status.service.d.ts +12 -0
  15. package/dist/angular/voice-provider-capabilities.service.d.ts +12 -0
  16. package/dist/angular/voice-provider-contracts.service.d.ts +12 -0
  17. package/dist/angular/voice-routing-status.service.d.ts +11 -0
  18. package/dist/angular/voice-stream.service.d.ts +3 -0
  19. package/dist/angular/voice-trace-timeline.service.d.ts +12 -0
  20. package/dist/angular/voice-turn-latency.service.d.ts +13 -0
  21. package/dist/angular/voice-turn-quality.service.d.ts +12 -0
  22. package/dist/angular/voice-workflow-status.service.d.ts +12 -0
  23. package/dist/audit.d.ts +128 -0
  24. package/dist/auditDeliveryRoutes.d.ts +85 -0
  25. package/dist/auditExport.d.ts +34 -0
  26. package/dist/auditRoutes.d.ts +66 -0
  27. package/dist/auditSinks.d.ts +151 -0
  28. package/dist/bargeInRoutes.d.ts +56 -0
  29. package/dist/campaign.d.ts +746 -0
  30. package/dist/campaignDialers.d.ts +90 -0
  31. package/dist/client/actions.d.ts +105 -0
  32. package/dist/client/agentSquadStatus.d.ts +37 -0
  33. package/dist/client/agentSquadStatusWidget.d.ts +24 -0
  34. package/dist/client/bargeInMonitor.d.ts +7 -0
  35. package/dist/client/campaignDialerProof.d.ts +23 -0
  36. package/dist/client/connection.d.ts +3 -0
  37. package/dist/client/deliveryRuntime.d.ts +34 -0
  38. package/dist/client/deliveryRuntimeWidget.d.ts +37 -0
  39. package/dist/client/duplex.d.ts +1 -1
  40. package/dist/client/htmxBootstrap.js +747 -15
  41. package/dist/client/index.d.ts +62 -0
  42. package/dist/client/index.js +4407 -20
  43. package/dist/client/liveOps.d.ts +22 -0
  44. package/dist/client/liveOpsWidget.d.ts +23 -0
  45. package/dist/client/liveTurnLatency.d.ts +41 -0
  46. package/dist/client/opsActionCenter.d.ts +54 -0
  47. package/dist/client/opsActionCenterWidget.d.ts +29 -0
  48. package/dist/client/opsActionHistory.d.ts +19 -0
  49. package/dist/client/opsActionHistoryWidget.d.ts +11 -0
  50. package/dist/client/opsStatus.d.ts +19 -0
  51. package/dist/client/opsStatusWidget.d.ts +40 -0
  52. package/dist/client/providerCapabilities.d.ts +19 -0
  53. package/dist/client/providerCapabilitiesWidget.d.ts +32 -0
  54. package/dist/client/providerContracts.d.ts +19 -0
  55. package/dist/client/providerContractsWidget.d.ts +37 -0
  56. package/dist/client/providerSimulationControls.d.ts +33 -0
  57. package/dist/client/providerSimulationControlsWidget.d.ts +20 -0
  58. package/dist/client/providerStatusWidget.d.ts +32 -0
  59. package/dist/client/routingStatus.d.ts +19 -0
  60. package/dist/client/routingStatusWidget.d.ts +28 -0
  61. package/dist/client/traceTimeline.d.ts +19 -0
  62. package/dist/client/traceTimelineWidget.d.ts +36 -0
  63. package/dist/client/turnLatency.d.ts +22 -0
  64. package/dist/client/turnLatencyWidget.d.ts +33 -0
  65. package/dist/client/turnQuality.d.ts +19 -0
  66. package/dist/client/turnQualityWidget.d.ts +32 -0
  67. package/dist/client/workflowStatus.d.ts +19 -0
  68. package/dist/dataControl.d.ts +140 -0
  69. package/dist/deliveryRuntime.d.ts +158 -0
  70. package/dist/deliverySinkRoutes.d.ts +117 -0
  71. package/dist/demoReadyRoutes.d.ts +98 -0
  72. package/dist/diagnosticsRoutes.d.ts +44 -0
  73. package/dist/evalRoutes.d.ts +219 -0
  74. package/dist/fileStore.d.ts +14 -2
  75. package/dist/handoff.d.ts +54 -0
  76. package/dist/handoffHealth.d.ts +94 -0
  77. package/dist/incidentBundle.d.ts +116 -0
  78. package/dist/index.d.ts +126 -13
  79. package/dist/index.js +22717 -4796
  80. package/dist/latencySlo.d.ts +56 -0
  81. package/dist/liveLatency.d.ts +78 -0
  82. package/dist/liveOps.d.ts +122 -0
  83. package/dist/modelAdapters.d.ts +23 -2
  84. package/dist/observabilityExport.d.ts +297 -0
  85. package/dist/openaiRealtime.d.ts +27 -0
  86. package/dist/openaiTTS.d.ts +18 -0
  87. package/dist/operationsRecord.d.ts +158 -0
  88. package/dist/opsActionAuditRoutes.d.ts +99 -0
  89. package/dist/opsConsoleRoutes.d.ts +80 -0
  90. package/dist/opsRecovery.d.ts +137 -0
  91. package/dist/opsStatus.d.ts +76 -0
  92. package/dist/opsStatusRoutes.d.ts +33 -0
  93. package/dist/opsWebhook.d.ts +126 -0
  94. package/dist/outcomeContract.d.ts +115 -0
  95. package/dist/phoneAgent.d.ts +76 -0
  96. package/dist/phoneAgentProductionSmoke.d.ts +115 -0
  97. package/dist/postgresStore.d.ts +13 -2
  98. package/dist/productionReadiness.d.ts +453 -0
  99. package/dist/providerAdapters.d.ts +48 -0
  100. package/dist/providerCapabilities.d.ts +92 -0
  101. package/dist/providerHealth.d.ts +1 -0
  102. package/dist/providerRoutingContract.d.ts +38 -0
  103. package/dist/providerSlo.d.ts +114 -0
  104. package/dist/providerStackRecommendations.d.ts +145 -0
  105. package/dist/qualityRoutes.d.ts +76 -0
  106. package/dist/queue.d.ts +61 -0
  107. package/dist/react/VoiceAgentSquadStatus.d.ts +5 -0
  108. package/dist/react/VoiceDeliveryRuntime.d.ts +7 -0
  109. package/dist/react/VoiceOpsActionCenter.d.ts +5 -0
  110. package/dist/react/VoiceOpsStatus.d.ts +6 -0
  111. package/dist/react/VoiceProviderCapabilities.d.ts +6 -0
  112. package/dist/react/VoiceProviderContracts.d.ts +6 -0
  113. package/dist/react/VoiceProviderSimulationControls.d.ts +5 -0
  114. package/dist/react/VoiceProviderStatus.d.ts +6 -0
  115. package/dist/react/VoiceRoutingStatus.d.ts +6 -0
  116. package/dist/react/VoiceTraceTimeline.d.ts +6 -0
  117. package/dist/react/VoiceTurnLatency.d.ts +6 -0
  118. package/dist/react/VoiceTurnQuality.d.ts +6 -0
  119. package/dist/react/index.d.ts +26 -0
  120. package/dist/react/index.js +4074 -33
  121. package/dist/react/useVoiceAgentSquadStatus.d.ts +8 -0
  122. package/dist/react/useVoiceCampaignDialerProof.d.ts +10 -0
  123. package/dist/react/useVoiceController.d.ts +3 -0
  124. package/dist/react/useVoiceDeliveryRuntime.d.ts +13 -0
  125. package/dist/react/useVoiceLiveOps.d.ts +9 -0
  126. package/dist/react/useVoiceOpsActionCenter.d.ts +11 -0
  127. package/dist/react/useVoiceOpsStatus.d.ts +8 -0
  128. package/dist/react/useVoiceProviderCapabilities.d.ts +8 -0
  129. package/dist/react/useVoiceProviderContracts.d.ts +8 -0
  130. package/dist/react/useVoiceProviderSimulationControls.d.ts +10 -0
  131. package/dist/react/useVoiceRoutingStatus.d.ts +8 -0
  132. package/dist/react/useVoiceStream.d.ts +3 -0
  133. package/dist/react/useVoiceTraceTimeline.d.ts +8 -0
  134. package/dist/react/useVoiceTurnLatency.d.ts +9 -0
  135. package/dist/react/useVoiceTurnQuality.d.ts +8 -0
  136. package/dist/react/useVoiceWorkflowStatus.d.ts +8 -0
  137. package/dist/readinessProfiles.d.ts +37 -0
  138. package/dist/reconnectContract.d.ts +87 -0
  139. package/dist/resilienceRoutes.d.ts +143 -0
  140. package/dist/sessionReplay.d.ts +12 -0
  141. package/dist/simulationSuite.d.ts +121 -0
  142. package/dist/sqliteStore.d.ts +13 -2
  143. package/dist/svelte/createVoiceAgentSquadStatus.d.ts +9 -0
  144. package/dist/svelte/createVoiceCampaignDialerProof.d.ts +9 -0
  145. package/dist/svelte/createVoiceDeliveryRuntime.d.ts +11 -0
  146. package/dist/svelte/createVoiceLiveOps.d.ts +13 -0
  147. package/dist/svelte/createVoiceOpsActionCenter.d.ts +10 -0
  148. package/dist/svelte/createVoiceOpsStatus.d.ts +9 -0
  149. package/dist/svelte/createVoiceProviderCapabilities.d.ts +10 -0
  150. package/dist/svelte/createVoiceProviderContracts.d.ts +10 -0
  151. package/dist/svelte/createVoiceProviderSimulationControls.d.ts +11 -0
  152. package/dist/svelte/createVoiceProviderStatus.d.ts +4 -2
  153. package/dist/svelte/createVoiceRoutingStatus.d.ts +10 -0
  154. package/dist/svelte/createVoiceTraceTimeline.d.ts +10 -0
  155. package/dist/svelte/createVoiceTurnLatency.d.ts +11 -0
  156. package/dist/svelte/createVoiceTurnQuality.d.ts +10 -0
  157. package/dist/svelte/createVoiceWorkflowStatus.d.ts +8 -0
  158. package/dist/svelte/index.d.ts +14 -0
  159. package/dist/svelte/index.js +4574 -439
  160. package/dist/telephony/contract.d.ts +61 -0
  161. package/dist/telephony/matrix.d.ts +97 -0
  162. package/dist/telephony/plivo.d.ts +254 -0
  163. package/dist/telephony/telnyx.d.ts +247 -0
  164. package/dist/telephony/twilio.d.ts +135 -2
  165. package/dist/telephonyOutcome.d.ts +201 -0
  166. package/dist/testing/index.d.ts +1 -0
  167. package/dist/testing/index.js +2024 -69
  168. package/dist/testing/ioProviderSimulator.d.ts +41 -0
  169. package/dist/toolContract.d.ts +133 -0
  170. package/dist/toolRuntime.d.ts +50 -0
  171. package/dist/trace.d.ts +19 -1
  172. package/dist/traceDeliveryRoutes.d.ts +86 -0
  173. package/dist/traceTimeline.d.ts +97 -0
  174. package/dist/turnLatency.d.ts +95 -0
  175. package/dist/turnQuality.d.ts +94 -0
  176. package/dist/types.d.ts +180 -4
  177. package/dist/vue/VoiceDeliveryRuntime.d.ts +30 -0
  178. package/dist/vue/VoiceOpsActionCenter.d.ts +13 -0
  179. package/dist/vue/VoiceOpsStatus.d.ts +30 -0
  180. package/dist/vue/VoiceProviderCapabilities.d.ts +51 -0
  181. package/dist/vue/VoiceProviderContracts.d.ts +21 -0
  182. package/dist/vue/VoiceProviderSimulationControls.d.ts +88 -0
  183. package/dist/vue/VoiceProviderStatus.d.ts +51 -0
  184. package/dist/vue/VoiceRoutingStatus.d.ts +51 -0
  185. package/dist/vue/VoiceTurnLatency.d.ts +69 -0
  186. package/dist/vue/VoiceTurnQuality.d.ts +51 -0
  187. package/dist/vue/index.d.ts +24 -0
  188. package/dist/vue/index.js +3852 -57
  189. package/dist/vue/useVoiceAgentSquadStatus.d.ts +9 -0
  190. package/dist/vue/useVoiceCampaignDialerProof.d.ts +11 -0
  191. package/dist/vue/useVoiceController.d.ts +2 -1
  192. package/dist/vue/useVoiceDeliveryRuntime.d.ts +13 -0
  193. package/dist/vue/useVoiceLiveOps.d.ts +9 -0
  194. package/dist/vue/useVoiceOpsActionCenter.d.ts +11 -0
  195. package/dist/vue/useVoiceOpsStatus.d.ts +9 -0
  196. package/dist/vue/useVoiceProviderCapabilities.d.ts +9 -0
  197. package/dist/vue/useVoiceProviderContracts.d.ts +9 -0
  198. package/dist/vue/useVoiceProviderSimulationControls.d.ts +24 -0
  199. package/dist/vue/useVoiceProviderStatus.d.ts +1 -1
  200. package/dist/vue/useVoiceRoutingStatus.d.ts +8 -0
  201. package/dist/vue/useVoiceStream.d.ts +4 -1
  202. package/dist/vue/useVoiceTraceTimeline.d.ts +9 -0
  203. package/dist/vue/useVoiceTurnLatency.d.ts +10 -0
  204. package/dist/vue/useVoiceTurnQuality.d.ts +9 -0
  205. package/dist/vue/useVoiceWorkflowStatus.d.ts +9 -0
  206. package/dist/workflowContract.d.ts +91 -0
  207. package/package.json +1 -1
@@ -188,6 +188,17 @@ var serverMessageToAction = (message) => {
188
188
  sessionId: message.sessionId,
189
189
  type: "complete"
190
190
  };
191
+ case "connection":
192
+ return {
193
+ reconnect: message.reconnect,
194
+ type: "connection"
195
+ };
196
+ case "call_lifecycle":
197
+ return {
198
+ event: message.event,
199
+ sessionId: message.sessionId,
200
+ type: "call_lifecycle"
201
+ };
191
202
  case "error":
192
203
  return {
193
204
  message: normalizeErrorMessage(message.message),
@@ -203,6 +214,17 @@ var serverMessageToAction = (message) => {
203
214
  transcript: message.transcript,
204
215
  type: "partial"
205
216
  };
217
+ case "replay":
218
+ return {
219
+ assistantTexts: message.assistantTexts,
220
+ call: message.call,
221
+ partial: message.partial,
222
+ scenarioId: message.scenarioId,
223
+ sessionId: message.sessionId,
224
+ status: message.status,
225
+ turns: message.turns,
226
+ type: "replay"
227
+ };
206
228
  case "session":
207
229
  return {
208
230
  sessionId: message.sessionId,
@@ -231,7 +253,7 @@ var DEFAULT_SCENARIO_QUERY_PARAM = "scenarioId";
231
253
  var noop = () => {};
232
254
  var noopUnsubscribe = () => noop;
233
255
  var NOOP_CONNECTION = {
234
- start: () => {},
256
+ callControl: noop,
235
257
  close: noop,
236
258
  endTurn: noop,
237
259
  getReadyState: () => WS_CLOSED,
@@ -239,6 +261,7 @@ var NOOP_CONNECTION = {
239
261
  getSessionId: () => "",
240
262
  send: noop,
241
263
  sendAudio: noop,
264
+ start: () => {},
242
265
  subscribe: noopUnsubscribe
243
266
  };
244
267
  var createSessionId = () => crypto.randomUUID();
@@ -260,11 +283,14 @@ var isVoiceServerMessage = (value) => {
260
283
  switch (value.type) {
261
284
  case "audio":
262
285
  case "assistant":
286
+ case "call_lifecycle":
263
287
  case "complete":
288
+ case "connection":
264
289
  case "error":
265
290
  case "final":
266
291
  case "partial":
267
292
  case "pong":
293
+ case "replay":
268
294
  case "session":
269
295
  case "turn":
270
296
  return true;
@@ -301,6 +327,9 @@ var createVoiceConnection = (path, options = {}) => {
301
327
  sessionId: options.sessionId ?? createSessionId(),
302
328
  ws: null
303
329
  };
330
+ const emitConnection = (reconnect) => {
331
+ listeners.forEach((listener) => listener(reconnect));
332
+ };
304
333
  const clearTimers = () => {
305
334
  if (state.pingInterval) {
306
335
  clearInterval(state.pingInterval);
@@ -323,9 +352,28 @@ var createVoiceConnection = (path, options = {}) => {
323
352
  }
324
353
  };
325
354
  const scheduleReconnect = () => {
355
+ const nextAttemptAt = Date.now() + RECONNECT_DELAY_MS;
326
356
  state.reconnectAttempts += 1;
357
+ emitConnection({
358
+ reconnect: {
359
+ attempts: state.reconnectAttempts,
360
+ lastDisconnectAt: Date.now(),
361
+ maxAttempts: maxReconnectAttempts,
362
+ nextAttemptAt,
363
+ status: "reconnecting"
364
+ },
365
+ type: "connection"
366
+ });
327
367
  state.reconnectTimeout = setTimeout(() => {
328
368
  if (state.reconnectAttempts > maxReconnectAttempts) {
369
+ emitConnection({
370
+ reconnect: {
371
+ attempts: state.reconnectAttempts,
372
+ maxAttempts: maxReconnectAttempts,
373
+ status: "exhausted"
374
+ },
375
+ type: "connection"
376
+ });
329
377
  return;
330
378
  }
331
379
  connect();
@@ -335,9 +383,21 @@ var createVoiceConnection = (path, options = {}) => {
335
383
  const ws = new WebSocket(buildWsUrl(path, state.sessionId, state.scenarioId));
336
384
  ws.binaryType = "arraybuffer";
337
385
  ws.onopen = () => {
386
+ const wasReconnecting = state.reconnectAttempts > 0;
338
387
  state.isConnected = true;
339
- state.reconnectAttempts = 0;
340
388
  flushPendingMessages();
389
+ if (wasReconnecting) {
390
+ emitConnection({
391
+ reconnect: {
392
+ attempts: state.reconnectAttempts,
393
+ lastResumedAt: Date.now(),
394
+ maxAttempts: maxReconnectAttempts,
395
+ status: "resumed"
396
+ },
397
+ type: "connection"
398
+ });
399
+ state.reconnectAttempts = 0;
400
+ }
341
401
  listeners.forEach((listener) => listener({
342
402
  scenarioId: state.scenarioId ?? undefined,
343
403
  sessionId: state.sessionId,
@@ -367,6 +427,16 @@ var createVoiceConnection = (path, options = {}) => {
367
427
  const reconnectable = shouldReconnect && event.code !== WS_NORMAL_CLOSURE && state.reconnectAttempts < maxReconnectAttempts;
368
428
  if (reconnectable) {
369
429
  scheduleReconnect();
430
+ } else if (shouldReconnect && event.code !== WS_NORMAL_CLOSURE) {
431
+ emitConnection({
432
+ reconnect: {
433
+ attempts: state.reconnectAttempts,
434
+ lastDisconnectAt: Date.now(),
435
+ maxAttempts: maxReconnectAttempts,
436
+ status: "exhausted"
437
+ },
438
+ type: "connection"
439
+ });
370
440
  }
371
441
  };
372
442
  state.ws = ws;
@@ -400,6 +470,12 @@ var createVoiceConnection = (path, options = {}) => {
400
470
  const endTurn = () => {
401
471
  send({ type: "end_turn" });
402
472
  };
473
+ const callControl = (message) => {
474
+ send({
475
+ ...message,
476
+ type: "call_control"
477
+ });
478
+ };
403
479
  const close = () => {
404
480
  clearTimers();
405
481
  if (state.ws) {
@@ -417,7 +493,7 @@ var createVoiceConnection = (path, options = {}) => {
417
493
  };
418
494
  connect();
419
495
  return {
420
- start,
496
+ callControl,
421
497
  close,
422
498
  endTurn,
423
499
  getReadyState: () => state.ws?.readyState ?? WS_CLOSED,
@@ -425,18 +501,26 @@ var createVoiceConnection = (path, options = {}) => {
425
501
  getSessionId: () => state.sessionId,
426
502
  send,
427
503
  sendAudio,
504
+ start,
428
505
  subscribe
429
506
  };
430
507
  };
431
508
 
432
509
  // src/client/store.ts
510
+ var createInitialReconnectState = () => ({
511
+ attempts: 0,
512
+ maxAttempts: 0,
513
+ status: "idle"
514
+ });
433
515
  var createInitialState = () => ({
434
516
  assistantAudio: [],
435
517
  assistantTexts: [],
518
+ call: null,
436
519
  error: null,
437
520
  isConnected: false,
438
521
  scenarioId: null,
439
522
  partial: "",
523
+ reconnect: createInitialReconnectState(),
440
524
  sessionId: null,
441
525
  status: "idle",
442
526
  turns: []
@@ -476,10 +560,36 @@ var createVoiceStreamStore = () => {
476
560
  status: "completed"
477
561
  };
478
562
  break;
563
+ case "call_lifecycle":
564
+ state = {
565
+ ...state,
566
+ call: {
567
+ ...state.call,
568
+ disposition: action.event.type === "end" ? action.event.disposition : state.call?.disposition,
569
+ endedAt: action.event.type === "end" ? action.event.at : state.call?.endedAt,
570
+ events: [...state.call?.events ?? [], action.event],
571
+ lastEventAt: action.event.at,
572
+ startedAt: state.call?.startedAt ?? action.event.at
573
+ },
574
+ sessionId: action.sessionId
575
+ };
576
+ break;
479
577
  case "connected":
480
578
  state = {
481
579
  ...state,
482
- isConnected: true
580
+ isConnected: true,
581
+ reconnect: state.reconnect.status === "reconnecting" ? {
582
+ ...state.reconnect,
583
+ lastResumedAt: Date.now(),
584
+ nextAttemptAt: undefined,
585
+ status: "resumed"
586
+ } : state.reconnect
587
+ };
588
+ break;
589
+ case "connection":
590
+ state = {
591
+ ...state,
592
+ reconnect: action.reconnect
483
593
  };
484
594
  break;
485
595
  case "disconnected":
@@ -507,6 +617,26 @@ var createVoiceStreamStore = () => {
507
617
  partial: action.transcript.text
508
618
  };
509
619
  break;
620
+ case "replay":
621
+ state = {
622
+ ...state,
623
+ assistantTexts: [...action.assistantTexts],
624
+ call: action.call ?? null,
625
+ error: null,
626
+ isConnected: action.status === "active",
627
+ partial: action.partial,
628
+ reconnect: state.reconnect.status === "reconnecting" ? {
629
+ ...state.reconnect,
630
+ lastResumedAt: Date.now(),
631
+ nextAttemptAt: undefined,
632
+ status: "resumed"
633
+ } : state.reconnect,
634
+ scenarioId: action.scenarioId ?? state.scenarioId,
635
+ sessionId: action.sessionId,
636
+ status: action.status,
637
+ turns: [...action.turns]
638
+ };
639
+ break;
510
640
  case "session":
511
641
  state = {
512
642
  ...state,
@@ -554,14 +684,41 @@ var createVoiceStream = (path, options = {}) => {
554
684
  const notify = () => {
555
685
  subscribers.forEach((subscriber) => subscriber());
556
686
  };
687
+ const reportReconnect = () => {
688
+ if (!options.reconnectReportPath || typeof fetch === "undefined") {
689
+ return;
690
+ }
691
+ const snapshot = store.getSnapshot();
692
+ const body = JSON.stringify({
693
+ at: Date.now(),
694
+ reconnect: snapshot.reconnect,
695
+ scenarioId: snapshot.scenarioId,
696
+ sessionId: connection.getSessionId(),
697
+ turnIds: snapshot.turns.map((turn) => turn.id)
698
+ });
699
+ fetch(options.reconnectReportPath, {
700
+ body,
701
+ headers: {
702
+ "Content-Type": "application/json"
703
+ },
704
+ keepalive: true,
705
+ method: "POST"
706
+ }).catch(() => {});
707
+ };
557
708
  const unsubscribeConnection = connection.subscribe((message) => {
558
709
  const action = serverMessageToAction(message);
559
710
  if (action) {
560
711
  store.dispatch(action);
712
+ if (message.type === "connection") {
713
+ reportReconnect();
714
+ }
561
715
  notify();
562
716
  }
563
717
  });
564
718
  return {
719
+ callControl(message) {
720
+ connection.callControl(message);
721
+ },
565
722
  close() {
566
723
  unsubscribeConnection();
567
724
  connection.close();
@@ -590,6 +747,9 @@ var createVoiceStream = (path, options = {}) => {
590
747
  get partial() {
591
748
  return store.getSnapshot().partial;
592
749
  },
750
+ get reconnect() {
751
+ return store.getSnapshot().reconnect;
752
+ },
593
753
  get sessionId() {
594
754
  return connection.getSessionId();
595
755
  },
@@ -605,6 +765,9 @@ var createVoiceStream = (path, options = {}) => {
605
765
  get assistantAudio() {
606
766
  return store.getSnapshot().assistantAudio;
607
767
  },
768
+ get call() {
769
+ return store.getSnapshot().call;
770
+ },
608
771
  sendAudio(audio) {
609
772
  connection.sendAudio(audio);
610
773
  },
@@ -900,10 +1063,12 @@ var resolveVoiceRuntimePreset = (name = "default") => {
900
1063
  var createInitialState2 = (stream) => ({
901
1064
  assistantAudio: [...stream.assistantAudio],
902
1065
  assistantTexts: [...stream.assistantTexts],
1066
+ call: stream.call,
903
1067
  error: stream.error,
904
1068
  isConnected: stream.isConnected,
905
1069
  isRecording: false,
906
1070
  partial: stream.partial,
1071
+ reconnect: stream.reconnect,
907
1072
  recordingError: null,
908
1073
  sessionId: stream.sessionId,
909
1074
  scenarioId: stream.scenarioId,
@@ -929,9 +1094,11 @@ var createVoiceController = (path, options = {}) => {
929
1094
  ...state,
930
1095
  assistantAudio: [...stream.assistantAudio],
931
1096
  assistantTexts: [...stream.assistantTexts],
1097
+ call: stream.call,
932
1098
  error: stream.error,
933
1099
  isConnected: stream.isConnected,
934
1100
  partial: stream.partial,
1101
+ reconnect: stream.reconnect,
935
1102
  sessionId: stream.sessionId,
936
1103
  scenarioId: stream.scenarioId,
937
1104
  status: stream.status,
@@ -956,7 +1123,13 @@ var createVoiceController = (path, options = {}) => {
956
1123
  capture = createMicrophoneCapture({
957
1124
  channelCount: options.capture?.channelCount ?? preset.capture.channelCount,
958
1125
  onLevel: options.capture?.onLevel,
959
- onAudio: (audio) => stream.sendAudio(audio),
1126
+ onAudio: (audio) => {
1127
+ if (options.capture?.onAudio) {
1128
+ options.capture.onAudio(audio, stream.sendAudio);
1129
+ return;
1130
+ }
1131
+ stream.sendAudio(audio);
1132
+ },
960
1133
  sampleRateHz: options.capture?.sampleRateHz ?? preset.capture.sampleRateHz
961
1134
  });
962
1135
  return capture;
@@ -1006,6 +1179,7 @@ var createVoiceController = (path, options = {}) => {
1006
1179
  bindHTMX(bindingOptions) {
1007
1180
  return bindVoiceHTMX(stream, bindingOptions);
1008
1181
  },
1182
+ callControl: (message) => stream.callControl(message),
1009
1183
  close,
1010
1184
  endTurn: () => stream.endTurn(),
1011
1185
  get error() {
@@ -1025,6 +1199,9 @@ var createVoiceController = (path, options = {}) => {
1025
1199
  get recordingError() {
1026
1200
  return state.recordingError;
1027
1201
  },
1202
+ get reconnect() {
1203
+ return state.reconnect;
1204
+ },
1028
1205
  sendAudio: (audio) => stream.sendAudio(audio),
1029
1206
  get sessionId() {
1030
1207
  return state.sessionId;
@@ -1058,6 +1235,478 @@ var createVoiceController = (path, options = {}) => {
1058
1235
  },
1059
1236
  get assistantAudio() {
1060
1237
  return state.assistantAudio;
1238
+ },
1239
+ get call() {
1240
+ return state.call;
1241
+ }
1242
+ };
1243
+ };
1244
+
1245
+ // src/client/audioPlayer.ts
1246
+ var DEFAULT_LOOKAHEAD_MS = 15;
1247
+ var createInitialState3 = () => ({
1248
+ activeSourceCount: 0,
1249
+ error: null,
1250
+ isActive: false,
1251
+ isPlaying: false,
1252
+ lastInterruptLatencyMs: undefined,
1253
+ lastPlaybackStopLatencyMs: undefined,
1254
+ processedChunkCount: 0,
1255
+ queuedChunkCount: 0
1256
+ });
1257
+ var getAudioContextCtor = () => {
1258
+ if (typeof window === "undefined") {
1259
+ return typeof AudioContext === "undefined" ? undefined : AudioContext;
1260
+ }
1261
+ return window.AudioContext ?? window.webkitAudioContext;
1262
+ };
1263
+ var decodePCM16LEChunk = (audioContext, chunk) => {
1264
+ const format = chunk.format;
1265
+ if (format.container !== "raw" || format.encoding !== "pcm_s16le") {
1266
+ throw new Error(`Unsupported assistant audio format: ${format.container}/${format.encoding}`);
1267
+ }
1268
+ const bytes = chunk.chunk;
1269
+ const channels = Math.max(1, format.channels);
1270
+ const sampleCount = Math.floor(bytes.byteLength / 2);
1271
+ const frameCount = Math.max(1, Math.floor(sampleCount / channels));
1272
+ const audioBuffer = audioContext.createBuffer(channels, frameCount, format.sampleRateHz);
1273
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
1274
+ for (let channelIndex = 0;channelIndex < channels; channelIndex += 1) {
1275
+ const channelData = audioBuffer.getChannelData(channelIndex);
1276
+ for (let frameIndex = 0;frameIndex < frameCount; frameIndex += 1) {
1277
+ const sampleIndex = frameIndex * channels + channelIndex;
1278
+ const sampleOffset = sampleIndex * 2;
1279
+ if (sampleOffset + 1 >= bytes.byteLength) {
1280
+ channelData[frameIndex] = 0;
1281
+ continue;
1282
+ }
1283
+ channelData[frameIndex] = view.getInt16(sampleOffset, true) / 32768;
1284
+ }
1285
+ }
1286
+ return audioBuffer;
1287
+ };
1288
+ var createVoiceAudioPlayer = (source, options = {}) => {
1289
+ const subscribers = new Set;
1290
+ const sourceNodes = new Set;
1291
+ const lookaheadSeconds = (options.lookaheadMs ?? DEFAULT_LOOKAHEAD_MS) / 1000;
1292
+ let state = createInitialState3();
1293
+ let audioContext = null;
1294
+ let outputNode = null;
1295
+ let queueEndTime = 0;
1296
+ let syncPromise = Promise.resolve();
1297
+ let interruptStartedAt = null;
1298
+ let interruptPromise = null;
1299
+ let resolveInterruptPromise = null;
1300
+ let interruptFallbackTimer = null;
1301
+ const notify = () => {
1302
+ for (const subscriber of subscribers) {
1303
+ subscriber();
1304
+ }
1305
+ };
1306
+ const setState = (next) => {
1307
+ state = {
1308
+ ...state,
1309
+ ...next
1310
+ };
1311
+ notify();
1312
+ };
1313
+ const clearError = () => {
1314
+ if (state.error !== null) {
1315
+ setState({ error: null });
1316
+ }
1317
+ };
1318
+ const clearInterruptTimer = () => {
1319
+ if (interruptFallbackTimer !== null) {
1320
+ clearTimeout(interruptFallbackTimer);
1321
+ interruptFallbackTimer = null;
1322
+ }
1323
+ };
1324
+ const resolveInterrupt = (latencyMs) => {
1325
+ clearInterruptTimer();
1326
+ interruptStartedAt = null;
1327
+ setState({
1328
+ activeSourceCount: sourceNodes.size,
1329
+ isPlaying: false,
1330
+ lastInterruptLatencyMs: latencyMs,
1331
+ lastPlaybackStopLatencyMs: state.lastPlaybackStopLatencyMs ?? latencyMs
1332
+ });
1333
+ resolveInterruptPromise?.();
1334
+ resolveInterruptPromise = null;
1335
+ interruptPromise = null;
1336
+ };
1337
+ const estimateOutputStopLatencyMs = (context) => {
1338
+ if (!context) {
1339
+ return 0;
1340
+ }
1341
+ return Math.max(0, ((context.baseLatency ?? 0) + (context.outputLatency ?? 0)) * 1000);
1342
+ };
1343
+ const restoreOutputGain = (context) => {
1344
+ if (!outputNode) {
1345
+ return;
1346
+ }
1347
+ const gainValue = 1;
1348
+ if (outputNode.gain.setValueAtTime) {
1349
+ outputNode.gain.setValueAtTime(gainValue, context?.currentTime ?? 0);
1350
+ return;
1351
+ }
1352
+ outputNode.gain.value = gainValue;
1353
+ };
1354
+ const muteOutputGain = (context) => {
1355
+ if (!outputNode) {
1356
+ return;
1357
+ }
1358
+ const gainValue = 0;
1359
+ if (outputNode.gain.setValueAtTime) {
1360
+ outputNode.gain.setValueAtTime(gainValue, context?.currentTime ?? 0);
1361
+ return;
1362
+ }
1363
+ outputNode.gain.value = gainValue;
1364
+ };
1365
+ const maybeResolveInterrupt = () => {
1366
+ if (interruptStartedAt === null || sourceNodes.size > 0) {
1367
+ return;
1368
+ }
1369
+ resolveInterrupt(Date.now() - interruptStartedAt);
1370
+ };
1371
+ const ensureAudioContext = async () => {
1372
+ if (audioContext) {
1373
+ return audioContext;
1374
+ }
1375
+ if (options.createAudioContext) {
1376
+ audioContext = options.createAudioContext();
1377
+ } else {
1378
+ const AudioContextCtor = getAudioContextCtor();
1379
+ if (!AudioContextCtor) {
1380
+ throw new Error("Assistant audio playback requires AudioContext support.");
1381
+ }
1382
+ audioContext = new AudioContextCtor;
1383
+ }
1384
+ if (audioContext.createGain) {
1385
+ outputNode = audioContext.createGain();
1386
+ outputNode.connect?.(audioContext.destination);
1387
+ }
1388
+ queueEndTime = audioContext.currentTime;
1389
+ return audioContext;
1390
+ };
1391
+ const scheduleChunk = async (chunk) => {
1392
+ const context = await ensureAudioContext();
1393
+ const buffer = decodePCM16LEChunk(context, chunk);
1394
+ const node = context.createBufferSource();
1395
+ node.buffer = buffer;
1396
+ node.connect(outputNode ?? context.destination);
1397
+ node.onended = () => {
1398
+ sourceNodes.delete(node);
1399
+ node.disconnect?.();
1400
+ setState({
1401
+ activeSourceCount: sourceNodes.size,
1402
+ isPlaying: sourceNodes.size > 0 && state.isActive
1403
+ });
1404
+ maybeResolveInterrupt();
1405
+ };
1406
+ const startAt = Math.max(context.currentTime + lookaheadSeconds, queueEndTime);
1407
+ queueEndTime = startAt + buffer.duration;
1408
+ sourceNodes.add(node);
1409
+ setState({
1410
+ activeSourceCount: sourceNodes.size,
1411
+ isPlaying: true
1412
+ });
1413
+ node.start(startAt);
1414
+ };
1415
+ const stopQueuedPlayback = (options2) => {
1416
+ for (const node of [...sourceNodes]) {
1417
+ node.stop?.();
1418
+ }
1419
+ queueEndTime = audioContext ? audioContext.currentTime : 0;
1420
+ if (options2?.forceClear) {
1421
+ for (const node of sourceNodes) {
1422
+ node.disconnect?.();
1423
+ }
1424
+ sourceNodes.clear();
1425
+ maybeResolveInterrupt();
1426
+ }
1427
+ };
1428
+ const sync = async () => {
1429
+ if (!state.isActive) {
1430
+ return;
1431
+ }
1432
+ const nextChunks = source.assistantAudio.slice(state.processedChunkCount);
1433
+ if (nextChunks.length === 0) {
1434
+ return;
1435
+ }
1436
+ try {
1437
+ clearError();
1438
+ for (const chunk of nextChunks) {
1439
+ await scheduleChunk(chunk);
1440
+ }
1441
+ setState({
1442
+ processedChunkCount: source.assistantAudio.length,
1443
+ queuedChunkCount: state.queuedChunkCount + nextChunks.length
1444
+ });
1445
+ } catch (error) {
1446
+ setState({
1447
+ error: error instanceof Error ? error.message : String(error)
1448
+ });
1449
+ }
1450
+ };
1451
+ const queueSync = () => {
1452
+ syncPromise = syncPromise.then(() => sync(), () => sync());
1453
+ return syncPromise;
1454
+ };
1455
+ const unsubscribeSource = source.subscribe(() => {
1456
+ if (options.autoStart && !state.isActive && source.assistantAudio.length > 0) {
1457
+ player.start();
1458
+ return;
1459
+ }
1460
+ if (state.isActive) {
1461
+ queueSync();
1462
+ }
1463
+ });
1464
+ const player = {
1465
+ close: async () => {
1466
+ unsubscribeSource();
1467
+ stopQueuedPlayback({ forceClear: true });
1468
+ clearInterruptTimer();
1469
+ resolveInterruptPromise?.();
1470
+ resolveInterruptPromise = null;
1471
+ interruptPromise = null;
1472
+ interruptStartedAt = null;
1473
+ if (audioContext && audioContext.state !== "closed") {
1474
+ await audioContext.close();
1475
+ }
1476
+ audioContext = null;
1477
+ outputNode?.disconnect?.();
1478
+ outputNode = null;
1479
+ queueEndTime = 0;
1480
+ setState({
1481
+ activeSourceCount: 0,
1482
+ isActive: false,
1483
+ isPlaying: false
1484
+ });
1485
+ },
1486
+ get activeSourceCount() {
1487
+ return state.activeSourceCount;
1488
+ },
1489
+ get error() {
1490
+ return state.error;
1491
+ },
1492
+ getSnapshot: () => state,
1493
+ get isActive() {
1494
+ return state.isActive;
1495
+ },
1496
+ get isPlaying() {
1497
+ return state.isPlaying;
1498
+ },
1499
+ interrupt: async () => {
1500
+ const startedAt = Date.now();
1501
+ const context = await ensureAudioContext();
1502
+ interruptStartedAt = startedAt;
1503
+ muteOutputGain(context);
1504
+ const playbackStopLatencyMs = Date.now() - startedAt + estimateOutputStopLatencyMs(context);
1505
+ setState({
1506
+ isActive: false,
1507
+ isPlaying: sourceNodes.size > 0,
1508
+ lastPlaybackStopLatencyMs: playbackStopLatencyMs
1509
+ });
1510
+ if (sourceNodes.size === 0) {
1511
+ resolveInterrupt(playbackStopLatencyMs);
1512
+ return;
1513
+ }
1514
+ if (!interruptPromise) {
1515
+ interruptPromise = new Promise((resolve) => {
1516
+ resolveInterruptPromise = resolve;
1517
+ });
1518
+ }
1519
+ clearInterruptTimer();
1520
+ interruptFallbackTimer = setTimeout(() => {
1521
+ for (const node of sourceNodes) {
1522
+ node.disconnect?.();
1523
+ }
1524
+ sourceNodes.clear();
1525
+ resolveInterrupt(Date.now() - startedAt);
1526
+ }, 250);
1527
+ stopQueuedPlayback();
1528
+ await interruptPromise;
1529
+ },
1530
+ get lastInterruptLatencyMs() {
1531
+ return state.lastInterruptLatencyMs;
1532
+ },
1533
+ get lastPlaybackStopLatencyMs() {
1534
+ return state.lastPlaybackStopLatencyMs;
1535
+ },
1536
+ pause: async () => {
1537
+ if (!audioContext) {
1538
+ setState({
1539
+ activeSourceCount: 0,
1540
+ isActive: false,
1541
+ isPlaying: false
1542
+ });
1543
+ return;
1544
+ }
1545
+ await audioContext.suspend();
1546
+ setState({
1547
+ activeSourceCount: sourceNodes.size,
1548
+ isActive: false,
1549
+ isPlaying: false
1550
+ });
1551
+ },
1552
+ get processedChunkCount() {
1553
+ return state.processedChunkCount;
1554
+ },
1555
+ get queuedChunkCount() {
1556
+ return state.queuedChunkCount;
1557
+ },
1558
+ start: async () => {
1559
+ try {
1560
+ clearError();
1561
+ const context = await ensureAudioContext();
1562
+ restoreOutputGain(context);
1563
+ if (context.state === "suspended") {
1564
+ await context.resume();
1565
+ }
1566
+ setState({
1567
+ activeSourceCount: sourceNodes.size,
1568
+ isActive: true,
1569
+ isPlaying: context.state === "running"
1570
+ });
1571
+ await queueSync();
1572
+ } catch (error) {
1573
+ setState({
1574
+ error: error instanceof Error ? error.message : String(error),
1575
+ isActive: false,
1576
+ isPlaying: false
1577
+ });
1578
+ throw error;
1579
+ }
1580
+ },
1581
+ subscribe: (subscriber) => {
1582
+ subscribers.add(subscriber);
1583
+ return () => {
1584
+ subscribers.delete(subscriber);
1585
+ };
1586
+ }
1587
+ };
1588
+ return player;
1589
+ };
1590
+
1591
+ // src/client/bargeInMonitor.ts
1592
+ var DEFAULT_THRESHOLD_MS = 250;
1593
+ var createEventId = () => `barge-in:${Date.now()}:${crypto.randomUUID?.() ?? Math.random().toString(36).slice(2)}`;
1594
+ var summarize = (events, thresholdMs) => {
1595
+ const stopped = events.filter((event) => event.status === "stopped");
1596
+ const latencies = stopped.map((event) => event.latencyMs).filter((value) => typeof value === "number");
1597
+ const failed = stopped.filter((event) => typeof event.latencyMs === "number" && event.latencyMs > thresholdMs).length;
1598
+ const passed = stopped.length - failed;
1599
+ return {
1600
+ averageLatencyMs: latencies.length > 0 ? Math.round(latencies.reduce((total, value) => total + value, 0) / latencies.length) : undefined,
1601
+ events: [...events],
1602
+ failed,
1603
+ lastEvent: events.at(-1),
1604
+ passed,
1605
+ status: events.length === 0 ? "empty" : failed > 0 ? "fail" : stopped.length === 0 ? "warn" : "pass",
1606
+ thresholdMs,
1607
+ total: stopped.length
1608
+ };
1609
+ };
1610
+ var createVoiceBargeInMonitor = (options = {}) => {
1611
+ const listeners = new Set;
1612
+ const thresholdMs = options.thresholdMs ?? DEFAULT_THRESHOLD_MS;
1613
+ const fetchImpl = options.fetch ?? globalThis.fetch;
1614
+ const events = [];
1615
+ const emit = () => {
1616
+ for (const listener of listeners) {
1617
+ listener();
1618
+ }
1619
+ };
1620
+ const postEvent = (event) => {
1621
+ if (!options.path || typeof fetchImpl !== "function") {
1622
+ return;
1623
+ }
1624
+ fetchImpl(options.path, {
1625
+ body: JSON.stringify(event),
1626
+ headers: {
1627
+ "Content-Type": "application/json"
1628
+ },
1629
+ method: "POST"
1630
+ }).catch(() => {});
1631
+ };
1632
+ const record = (status, input) => {
1633
+ const event = {
1634
+ at: Date.now(),
1635
+ id: createEventId(),
1636
+ latencyMs: input.latencyMs,
1637
+ playbackStopLatencyMs: input.playbackStopLatencyMs,
1638
+ reason: input.reason,
1639
+ sessionId: input.sessionId,
1640
+ status,
1641
+ thresholdMs
1642
+ };
1643
+ events.push(event);
1644
+ postEvent(event);
1645
+ emit();
1646
+ return event;
1647
+ };
1648
+ return {
1649
+ getSnapshot: () => summarize(events, thresholdMs),
1650
+ recordRequested: (input) => record("requested", input),
1651
+ recordSkipped: (input) => record("skipped", input),
1652
+ recordStopped: (input) => record("stopped", input),
1653
+ subscribe: (subscriber) => {
1654
+ listeners.add(subscriber);
1655
+ return () => {
1656
+ listeners.delete(subscriber);
1657
+ };
1658
+ }
1659
+ };
1660
+ };
1661
+
1662
+ // src/client/duplex.ts
1663
+ var DEFAULT_INTERRUPT_THRESHOLD = 0.08;
1664
+ var shouldInterruptForLevel = (level, options = {}) => (options.enabled ?? true) && level >= (options.interruptThreshold ?? DEFAULT_INTERRUPT_THRESHOLD);
1665
+ var bindVoiceBargeIn = (controller, player, options = {}) => {
1666
+ let lastPartial = controller.partial;
1667
+ const interruptIfPlaying = (reason) => {
1668
+ if (!player.isPlaying || options.enabled === false) {
1669
+ options.monitor?.recordSkipped({
1670
+ reason,
1671
+ sessionId: controller.sessionId
1672
+ });
1673
+ return;
1674
+ }
1675
+ options.monitor?.recordRequested({
1676
+ reason,
1677
+ sessionId: controller.sessionId
1678
+ });
1679
+ player.interrupt().then(() => {
1680
+ options.monitor?.recordStopped({
1681
+ latencyMs: player.lastInterruptLatencyMs,
1682
+ playbackStopLatencyMs: player.lastPlaybackStopLatencyMs,
1683
+ reason,
1684
+ sessionId: controller.sessionId
1685
+ });
1686
+ });
1687
+ };
1688
+ const unsubscribe = controller.subscribe(() => {
1689
+ if (options.interruptOnPartial === false) {
1690
+ lastPartial = controller.partial;
1691
+ return;
1692
+ }
1693
+ if (!lastPartial && controller.partial) {
1694
+ interruptIfPlaying("partial-transcript");
1695
+ }
1696
+ lastPartial = controller.partial;
1697
+ });
1698
+ return {
1699
+ close: () => {
1700
+ unsubscribe();
1701
+ },
1702
+ handleLevel: (level) => {
1703
+ if (shouldInterruptForLevel(level, options)) {
1704
+ interruptIfPlaying("input-level");
1705
+ }
1706
+ },
1707
+ sendAudio: (audio) => {
1708
+ interruptIfPlaying("manual-audio");
1709
+ controller.sendAudio(audio);
1061
1710
  }
1062
1711
  };
1063
1712
  };
@@ -1118,6 +1767,17 @@ var formatErrorMessage = (error) => {
1118
1767
  }
1119
1768
  return "Unexpected error";
1120
1769
  };
1770
+ var formatReconnectState = (reconnect) => {
1771
+ const pieces = [reconnect.status];
1772
+ if (reconnect.attempts > 0 || reconnect.maxAttempts > 0) {
1773
+ pieces.push(`${reconnect.attempts}/${reconnect.maxAttempts} attempts`);
1774
+ }
1775
+ if (reconnect.nextAttemptAt) {
1776
+ const waitMs = Math.max(0, reconnect.nextAttemptAt - Date.now());
1777
+ pieces.push(`retry in ${Math.ceil(waitMs / 100) / 10}s`);
1778
+ }
1779
+ return pieces.join(" · ");
1780
+ };
1121
1781
  var createInitialVoiceWaveLevels = (count = VOICE_WAVE_POINTS) => Array.from({ length: count }, () => 0);
1122
1782
  var pushVoiceWaveLevel = (levels, nextLevel, count = VOICE_WAVE_POINTS) => {
1123
1783
  const next = levels.slice(-(count - 1));
@@ -1174,6 +1834,17 @@ var parsePromptList = (value) => {
1174
1834
  } catch {}
1175
1835
  return DEFAULT_GUIDED_PROMPTS;
1176
1836
  };
1837
+ var parseOptionalNumber = (value) => {
1838
+ if (!value) {
1839
+ return;
1840
+ }
1841
+ const parsed = Number(value);
1842
+ return Number.isFinite(parsed) ? parsed : undefined;
1843
+ };
1844
+ var resolveElement2 = (root, selector, ctor) => {
1845
+ const value = selector ? document.querySelector(selector) : root.querySelector(selector ?? "");
1846
+ return value instanceof ctor ? value : null;
1847
+ };
1177
1848
  var requireElement = (root, selector, ctor, name) => {
1178
1849
  const value = selector ? document.querySelector(selector) : null;
1179
1850
  if (value instanceof ctor) {
@@ -1224,11 +1895,20 @@ var initVoiceHTMXRoot = (root) => {
1224
1895
  const guidedPrompts = parsePromptList(root.dataset.voiceGuidedPrompts);
1225
1896
  const guidedLabel = root.dataset.voiceGuidedLabel ?? DEFAULT_GUIDED_LABEL;
1226
1897
  const generalLabel = root.dataset.voiceGeneralLabel ?? DEFAULT_GENERAL_LABEL;
1898
+ const reconnectReportPath = root.dataset.voiceReconnectReportPath;
1899
+ const bargeInPath = root.dataset.voiceBargeInPath;
1900
+ const bargeInMonitor = bargeInPath ? createVoiceBargeInMonitor({
1901
+ path: bargeInPath,
1902
+ thresholdMs: parseOptionalNumber(root.dataset.voiceBargeInThresholdMs)
1903
+ }) : null;
1904
+ const bargeInRecentWindowMs = parseOptionalNumber(root.dataset.voiceBargeInRecentWindowMs) ?? 4000;
1905
+ const bargeInSpeechThreshold = parseOptionalNumber(root.dataset.voiceBargeInSpeechThreshold) ?? 0.04;
1227
1906
  const syncElement = requireElement(document, root.dataset.voiceSync, HTMLElement, "voice-htmx-sync");
1228
1907
  const connectionMetric = requireElement(root, root.dataset.voiceConnection, HTMLElement, "metric-connection");
1229
1908
  const errorStatus = requireElement(root, root.dataset.voiceError, HTMLElement, "status-error");
1230
1909
  const microphoneStatus = requireElement(root, root.dataset.voiceMicrophone, HTMLElement, "status-mic");
1231
1910
  const promptStatus = requireElement(root, root.dataset.voicePrompt, HTMLElement, "status-prompt");
1911
+ const reconnectStatus = resolveElement2(root, root.dataset.voiceReconnect, HTMLElement);
1232
1912
  const chatList = requireElement(root, root.dataset.voiceChat, HTMLElement, "chat-list");
1233
1913
  const startGuidedButton = requireElement(root, root.dataset.voiceStartGuided, HTMLButtonElement, "start-guided");
1234
1914
  const startGeneralButton = requireElement(root, root.dataset.voiceStartGeneral, HTMLButtonElement, "start-general");
@@ -1237,35 +1917,70 @@ var initVoiceHTMXRoot = (root) => {
1237
1917
  const voiceMonitorCopy = requireElement(root, root.dataset.voiceMonitorCopy, HTMLElement, "voice-monitor-copy");
1238
1918
  const voiceWaveGlow = requireElement(root, root.dataset.voiceWaveGlow, SVGPathElement, "voice-wave-glow");
1239
1919
  const voiceWavePath = requireElement(root, root.dataset.voiceWavePath, SVGPathElement, "voice-wave-path");
1920
+ let activeMode = null;
1921
+ let hasStartedModes = {
1922
+ general: false,
1923
+ guided: false
1924
+ };
1925
+ let isCapturing = false;
1926
+ let micError = null;
1927
+ let waveLevels = createInitialVoiceWaveLevels();
1928
+ let guidedBargeInBinding = null;
1929
+ let generalBargeInBinding = null;
1240
1930
  const guidedVoice = createVoiceController(guidedPath, {
1241
1931
  capture: {
1932
+ onAudio: (audio, sendAudio) => {
1933
+ if (guidedBargeInBinding) {
1934
+ guidedBargeInBinding.sendAudio(audio);
1935
+ return;
1936
+ }
1937
+ sendAudio(audio);
1938
+ },
1242
1939
  onLevel: (level) => {
1940
+ guidedBargeInBinding?.handleLevel(level);
1243
1941
  waveLevels = pushVoiceWaveLevel(waveLevels, level);
1244
1942
  renderWave();
1245
1943
  }
1246
1944
  },
1945
+ connection: {
1946
+ reconnectReportPath
1947
+ },
1247
1948
  preset: "guided-intake"
1248
1949
  });
1249
1950
  const generalVoice = createVoiceController(generalPath, {
1250
1951
  capture: {
1952
+ onAudio: (audio, sendAudio) => {
1953
+ if (generalBargeInBinding) {
1954
+ generalBargeInBinding.sendAudio(audio);
1955
+ return;
1956
+ }
1957
+ sendAudio(audio);
1958
+ },
1251
1959
  onLevel: (level) => {
1960
+ generalBargeInBinding?.handleLevel(level);
1252
1961
  waveLevels = pushVoiceWaveLevel(waveLevels, level);
1253
1962
  renderWave();
1254
1963
  }
1255
1964
  },
1965
+ connection: {
1966
+ reconnectReportPath
1967
+ },
1256
1968
  preset: "dictation"
1257
1969
  });
1258
1970
  const stopGuidedBinding = guidedVoice.bindHTMX({ element: syncElement });
1259
1971
  const stopGeneralBinding = generalVoice.bindHTMX({ element: syncElement });
1260
- let activeMode = null;
1261
- let hasStartedModes = {
1262
- general: false,
1263
- guided: false
1264
- };
1265
- let isCapturing = false;
1266
- let micError = null;
1267
- let waveLevels = createInitialVoiceWaveLevels();
1972
+ const guidedAudioPlayer = createVoiceAudioPlayer(guidedVoice);
1973
+ const generalAudioPlayer = createVoiceAudioPlayer(generalVoice);
1974
+ guidedBargeInBinding = bindVoiceBargeIn(guidedVoice, guidedAudioPlayer, {
1975
+ interruptThreshold: bargeInSpeechThreshold,
1976
+ monitor: bargeInMonitor ?? undefined
1977
+ });
1978
+ generalBargeInBinding = bindVoiceBargeIn(generalVoice, generalAudioPlayer, {
1979
+ interruptThreshold: bargeInSpeechThreshold,
1980
+ monitor: bargeInMonitor ?? undefined
1981
+ });
1268
1982
  const currentVoice = () => activeMode === "general" ? generalVoice : guidedVoice;
1983
+ const currentAudioPlayer = () => activeMode === "general" ? generalAudioPlayer : guidedAudioPlayer;
1269
1984
  const renderWave = () => {
1270
1985
  const path = createVoiceWavePath(waveLevels);
1271
1986
  voiceWaveGlow.setAttribute("d", path);
@@ -1280,6 +1995,9 @@ var initVoiceHTMXRoot = (root) => {
1280
1995
  const status = voice.status;
1281
1996
  connectionMetric.textContent = voice.isConnected ? "Connected" : "Waiting";
1282
1997
  errorStatus.textContent = micError || voice.error || "None";
1998
+ if (reconnectStatus) {
1999
+ reconnectStatus.textContent = formatReconnectState(voice.reconnect);
2000
+ }
1283
2001
  microphoneStatus.textContent = isCapturing ? DEFAULT_MIC_LIVE : DEFAULT_MIC_IDLE;
1284
2002
  promptStatus.textContent = resolvePromptMessage({
1285
2003
  guidedPrompts,
@@ -1343,8 +2061,18 @@ var initVoiceHTMXRoot = (root) => {
1343
2061
  render();
1344
2062
  }
1345
2063
  };
1346
- guidedVoice.subscribe(render);
1347
- generalVoice.subscribe(render);
2064
+ guidedVoice.subscribe(() => {
2065
+ if (guidedVoice.assistantAudio.length > 0) {
2066
+ guidedAudioPlayer.start().catch(() => {});
2067
+ }
2068
+ render();
2069
+ });
2070
+ generalVoice.subscribe(() => {
2071
+ if (generalVoice.assistantAudio.length > 0) {
2072
+ generalAudioPlayer.start().catch(() => {});
2073
+ }
2074
+ render();
2075
+ });
1348
2076
  startGuidedButton.addEventListener("click", () => {
1349
2077
  startMode("guided");
1350
2078
  });
@@ -1357,6 +2085,10 @@ var initVoiceHTMXRoot = (root) => {
1357
2085
  window.addEventListener("beforeunload", () => {
1358
2086
  guidedVoice.stopRecording();
1359
2087
  generalVoice.stopRecording();
2088
+ guidedBargeInBinding?.close();
2089
+ generalBargeInBinding?.close();
2090
+ guidedAudioPlayer.close();
2091
+ generalAudioPlayer.close();
1360
2092
  stopGuidedBinding();
1361
2093
  stopGeneralBinding();
1362
2094
  guidedVoice.close();