@absolutejs/voice 0.0.22-beta.29 → 0.0.22-beta.291

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 (236) hide show
  1. package/README.md +3234 -228
  2. package/dist/agent.d.ts +62 -0
  3. package/dist/agentSquadContract.d.ts +98 -0
  4. package/dist/angular/index.d.ts +16 -0
  5. package/dist/angular/index.js +3498 -1128
  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-platform-coverage.service.d.ts +12 -0
  16. package/dist/angular/voice-proof-trends.service.d.ts +12 -0
  17. package/dist/angular/voice-provider-capabilities.service.d.ts +12 -0
  18. package/dist/angular/voice-provider-contracts.service.d.ts +12 -0
  19. package/dist/angular/voice-readiness-failures.service.d.ts +13 -0
  20. package/dist/angular/voice-routing-status.service.d.ts +11 -0
  21. package/dist/angular/voice-stream.service.d.ts +1 -0
  22. package/dist/angular/voice-trace-timeline.service.d.ts +12 -0
  23. package/dist/angular/voice-turn-latency.service.d.ts +13 -0
  24. package/dist/angular/voice-turn-quality.service.d.ts +12 -0
  25. package/dist/angular/voice-workflow-status.service.d.ts +12 -0
  26. package/dist/audit.d.ts +128 -0
  27. package/dist/auditDeliveryRoutes.d.ts +85 -0
  28. package/dist/auditExport.d.ts +34 -0
  29. package/dist/auditRoutes.d.ts +66 -0
  30. package/dist/auditSinks.d.ts +151 -0
  31. package/dist/bargeInRoutes.d.ts +56 -0
  32. package/dist/campaign.d.ts +768 -0
  33. package/dist/campaignDialers.d.ts +111 -0
  34. package/dist/client/actions.d.ts +83 -0
  35. package/dist/client/agentSquadStatus.d.ts +37 -0
  36. package/dist/client/agentSquadStatusWidget.d.ts +24 -0
  37. package/dist/client/bargeInMonitor.d.ts +7 -0
  38. package/dist/client/campaignDialerProof.d.ts +23 -0
  39. package/dist/client/deliveryRuntime.d.ts +34 -0
  40. package/dist/client/deliveryRuntimeWidget.d.ts +37 -0
  41. package/dist/client/duplex.d.ts +1 -1
  42. package/dist/client/htmxBootstrap.js +703 -13
  43. package/dist/client/index.d.ts +70 -0
  44. package/dist/client/index.js +5257 -19
  45. package/dist/client/liveOps.d.ts +22 -0
  46. package/dist/client/liveOpsWidget.d.ts +23 -0
  47. package/dist/client/liveTurnLatency.d.ts +41 -0
  48. package/dist/client/opsActionCenter.d.ts +54 -0
  49. package/dist/client/opsActionCenterWidget.d.ts +29 -0
  50. package/dist/client/opsActionHistory.d.ts +19 -0
  51. package/dist/client/opsActionHistoryWidget.d.ts +11 -0
  52. package/dist/client/opsStatus.d.ts +19 -0
  53. package/dist/client/opsStatusWidget.d.ts +40 -0
  54. package/dist/client/platformCoverage.d.ts +19 -0
  55. package/dist/client/platformCoverageWidget.d.ts +37 -0
  56. package/dist/client/proofTrends.d.ts +19 -0
  57. package/dist/client/proofTrendsWidget.d.ts +37 -0
  58. package/dist/client/providerCapabilities.d.ts +19 -0
  59. package/dist/client/providerCapabilitiesWidget.d.ts +32 -0
  60. package/dist/client/providerContracts.d.ts +19 -0
  61. package/dist/client/providerContractsWidget.d.ts +37 -0
  62. package/dist/client/providerSimulationControls.d.ts +33 -0
  63. package/dist/client/providerSimulationControlsWidget.d.ts +20 -0
  64. package/dist/client/providerStatusWidget.d.ts +32 -0
  65. package/dist/client/readinessFailures.d.ts +19 -0
  66. package/dist/client/readinessFailuresWidget.d.ts +42 -0
  67. package/dist/client/routingStatus.d.ts +19 -0
  68. package/dist/client/routingStatusWidget.d.ts +28 -0
  69. package/dist/client/traceTimeline.d.ts +19 -0
  70. package/dist/client/traceTimelineWidget.d.ts +36 -0
  71. package/dist/client/turnLatency.d.ts +22 -0
  72. package/dist/client/turnLatencyWidget.d.ts +33 -0
  73. package/dist/client/turnQuality.d.ts +19 -0
  74. package/dist/client/turnQualityWidget.d.ts +32 -0
  75. package/dist/client/workflowStatus.d.ts +19 -0
  76. package/dist/dataControl.d.ts +180 -0
  77. package/dist/deliveryRuntime.d.ts +158 -0
  78. package/dist/deliverySinkRoutes.d.ts +117 -0
  79. package/dist/demoReadyRoutes.d.ts +98 -0
  80. package/dist/diagnosticsRoutes.d.ts +44 -0
  81. package/dist/evalRoutes.d.ts +219 -0
  82. package/dist/fileStore.d.ts +14 -2
  83. package/dist/guardrails.d.ts +128 -0
  84. package/dist/handoff.d.ts +15 -1
  85. package/dist/handoffHealth.d.ts +94 -0
  86. package/dist/incidentBundle.d.ts +116 -0
  87. package/dist/index.d.ts +138 -15
  88. package/dist/index.js +26946 -4957
  89. package/dist/latencySlo.d.ts +56 -0
  90. package/dist/liveLatency.d.ts +78 -0
  91. package/dist/liveOps.d.ts +190 -0
  92. package/dist/modelAdapters.d.ts +23 -2
  93. package/dist/observabilityExport.d.ts +481 -0
  94. package/dist/openaiRealtime.d.ts +27 -0
  95. package/dist/openaiTTS.d.ts +18 -0
  96. package/dist/operationsRecord.d.ts +210 -0
  97. package/dist/opsActionAuditRoutes.d.ts +99 -0
  98. package/dist/opsConsoleRoutes.d.ts +80 -0
  99. package/dist/opsRecovery.d.ts +137 -0
  100. package/dist/opsStatus.d.ts +76 -0
  101. package/dist/opsStatusRoutes.d.ts +33 -0
  102. package/dist/outcomeContract.d.ts +146 -0
  103. package/dist/phoneAgent.d.ts +139 -0
  104. package/dist/phoneAgentProductionSmoke.d.ts +115 -0
  105. package/dist/platformCoverage.d.ts +91 -0
  106. package/dist/postCallAnalysis.d.ts +98 -0
  107. package/dist/postgresStore.d.ts +13 -2
  108. package/dist/productionReadiness.d.ts +544 -0
  109. package/dist/proofTrends.d.ts +133 -0
  110. package/dist/providerAdapters.d.ts +48 -0
  111. package/dist/providerCapabilities.d.ts +92 -0
  112. package/dist/providerHealth.d.ts +1 -0
  113. package/dist/providerRoutingContract.d.ts +71 -0
  114. package/dist/providerSlo.d.ts +142 -0
  115. package/dist/providerStackRecommendations.d.ts +187 -0
  116. package/dist/qualityRoutes.d.ts +76 -0
  117. package/dist/queue.d.ts +61 -0
  118. package/dist/react/VoiceAgentSquadStatus.d.ts +5 -0
  119. package/dist/react/VoiceDeliveryRuntime.d.ts +7 -0
  120. package/dist/react/VoiceOpsActionCenter.d.ts +5 -0
  121. package/dist/react/VoiceOpsStatus.d.ts +6 -0
  122. package/dist/react/VoicePlatformCoverage.d.ts +6 -0
  123. package/dist/react/VoiceProofTrends.d.ts +6 -0
  124. package/dist/react/VoiceProviderCapabilities.d.ts +6 -0
  125. package/dist/react/VoiceProviderContracts.d.ts +6 -0
  126. package/dist/react/VoiceProviderSimulationControls.d.ts +5 -0
  127. package/dist/react/VoiceProviderStatus.d.ts +6 -0
  128. package/dist/react/VoiceReadinessFailures.d.ts +6 -0
  129. package/dist/react/VoiceRoutingStatus.d.ts +6 -0
  130. package/dist/react/VoiceTraceTimeline.d.ts +6 -0
  131. package/dist/react/VoiceTurnLatency.d.ts +6 -0
  132. package/dist/react/VoiceTurnQuality.d.ts +6 -0
  133. package/dist/react/index.d.ts +32 -0
  134. package/dist/react/index.js +5059 -31
  135. package/dist/react/useVoiceAgentSquadStatus.d.ts +8 -0
  136. package/dist/react/useVoiceCampaignDialerProof.d.ts +10 -0
  137. package/dist/react/useVoiceController.d.ts +1 -0
  138. package/dist/react/useVoiceDeliveryRuntime.d.ts +13 -0
  139. package/dist/react/useVoiceLiveOps.d.ts +9 -0
  140. package/dist/react/useVoiceOpsActionCenter.d.ts +11 -0
  141. package/dist/react/useVoiceOpsStatus.d.ts +8 -0
  142. package/dist/react/useVoicePlatformCoverage.d.ts +8 -0
  143. package/dist/react/useVoiceProofTrends.d.ts +8 -0
  144. package/dist/react/useVoiceProviderCapabilities.d.ts +8 -0
  145. package/dist/react/useVoiceProviderContracts.d.ts +8 -0
  146. package/dist/react/useVoiceProviderSimulationControls.d.ts +10 -0
  147. package/dist/react/useVoiceReadinessFailures.d.ts +8 -0
  148. package/dist/react/useVoiceRoutingStatus.d.ts +8 -0
  149. package/dist/react/useVoiceStream.d.ts +1 -0
  150. package/dist/react/useVoiceTraceTimeline.d.ts +8 -0
  151. package/dist/react/useVoiceTurnLatency.d.ts +9 -0
  152. package/dist/react/useVoiceTurnQuality.d.ts +8 -0
  153. package/dist/react/useVoiceWorkflowStatus.d.ts +8 -0
  154. package/dist/readinessProfiles.d.ts +38 -0
  155. package/dist/reconnectContract.d.ts +88 -0
  156. package/dist/resilienceRoutes.d.ts +143 -0
  157. package/dist/sessionReplay.d.ts +12 -0
  158. package/dist/simulationSuite.d.ts +143 -0
  159. package/dist/sloCalibration.d.ts +185 -0
  160. package/dist/sqliteStore.d.ts +13 -2
  161. package/dist/svelte/createVoiceAgentSquadStatus.d.ts +9 -0
  162. package/dist/svelte/createVoiceCampaignDialerProof.d.ts +9 -0
  163. package/dist/svelte/createVoiceDeliveryRuntime.d.ts +11 -0
  164. package/dist/svelte/createVoiceLiveOps.d.ts +13 -0
  165. package/dist/svelte/createVoiceOpsActionCenter.d.ts +10 -0
  166. package/dist/svelte/createVoiceOpsStatus.d.ts +9 -0
  167. package/dist/svelte/createVoicePlatformCoverage.d.ts +7 -0
  168. package/dist/svelte/createVoiceProofTrends.d.ts +7 -0
  169. package/dist/svelte/createVoiceProviderCapabilities.d.ts +10 -0
  170. package/dist/svelte/createVoiceProviderContracts.d.ts +10 -0
  171. package/dist/svelte/createVoiceProviderSimulationControls.d.ts +11 -0
  172. package/dist/svelte/createVoiceProviderStatus.d.ts +4 -2
  173. package/dist/svelte/createVoiceReadinessFailures.d.ts +7 -0
  174. package/dist/svelte/createVoiceRoutingStatus.d.ts +10 -0
  175. package/dist/svelte/createVoiceTraceTimeline.d.ts +10 -0
  176. package/dist/svelte/createVoiceTurnLatency.d.ts +11 -0
  177. package/dist/svelte/createVoiceTurnQuality.d.ts +10 -0
  178. package/dist/svelte/createVoiceWorkflowStatus.d.ts +8 -0
  179. package/dist/svelte/index.d.ts +17 -0
  180. package/dist/svelte/index.js +4924 -420
  181. package/dist/telephony/contract.d.ts +61 -0
  182. package/dist/telephony/matrix.d.ts +97 -0
  183. package/dist/telephony/plivo.d.ts +303 -0
  184. package/dist/telephony/security.d.ts +182 -0
  185. package/dist/telephony/telnyx.d.ts +291 -0
  186. package/dist/telephony/twilio.d.ts +135 -2
  187. package/dist/telephonyOutcome.d.ts +273 -0
  188. package/dist/testing/index.d.ts +1 -0
  189. package/dist/testing/index.js +1778 -36
  190. package/dist/testing/ioProviderSimulator.d.ts +41 -0
  191. package/dist/toolContract.d.ts +161 -0
  192. package/dist/toolRuntime.d.ts +50 -0
  193. package/dist/trace.d.ts +19 -1
  194. package/dist/traceDeliveryRoutes.d.ts +86 -0
  195. package/dist/traceTimeline.d.ts +97 -0
  196. package/dist/turnLatency.d.ts +95 -0
  197. package/dist/turnQuality.d.ts +94 -0
  198. package/dist/types.d.ts +127 -3
  199. package/dist/voiceMonitoring.d.ts +444 -0
  200. package/dist/vue/VoiceDeliveryRuntime.d.ts +30 -0
  201. package/dist/vue/VoiceOpsActionCenter.d.ts +13 -0
  202. package/dist/vue/VoiceOpsStatus.d.ts +30 -0
  203. package/dist/vue/VoicePlatformCoverage.d.ts +23 -0
  204. package/dist/vue/VoiceProofTrends.d.ts +21 -0
  205. package/dist/vue/VoiceProviderCapabilities.d.ts +51 -0
  206. package/dist/vue/VoiceProviderContracts.d.ts +21 -0
  207. package/dist/vue/VoiceProviderSimulationControls.d.ts +88 -0
  208. package/dist/vue/VoiceProviderStatus.d.ts +51 -0
  209. package/dist/vue/VoiceReadinessFailures.d.ts +21 -0
  210. package/dist/vue/VoiceRoutingStatus.d.ts +51 -0
  211. package/dist/vue/VoiceTurnLatency.d.ts +69 -0
  212. package/dist/vue/VoiceTurnQuality.d.ts +51 -0
  213. package/dist/vue/index.d.ts +30 -0
  214. package/dist/vue/index.js +4828 -56
  215. package/dist/vue/useVoiceAgentSquadStatus.d.ts +9 -0
  216. package/dist/vue/useVoiceCampaignDialerProof.d.ts +11 -0
  217. package/dist/vue/useVoiceController.d.ts +2 -1
  218. package/dist/vue/useVoiceDeliveryRuntime.d.ts +13 -0
  219. package/dist/vue/useVoiceLiveOps.d.ts +9 -0
  220. package/dist/vue/useVoiceOpsActionCenter.d.ts +11 -0
  221. package/dist/vue/useVoiceOpsStatus.d.ts +9 -0
  222. package/dist/vue/useVoicePlatformCoverage.d.ts +9 -0
  223. package/dist/vue/useVoiceProofTrends.d.ts +9 -0
  224. package/dist/vue/useVoiceProviderCapabilities.d.ts +9 -0
  225. package/dist/vue/useVoiceProviderContracts.d.ts +9 -0
  226. package/dist/vue/useVoiceProviderSimulationControls.d.ts +24 -0
  227. package/dist/vue/useVoiceProviderStatus.d.ts +1 -1
  228. package/dist/vue/useVoiceReadinessFailures.d.ts +755 -0
  229. package/dist/vue/useVoiceRoutingStatus.d.ts +8 -0
  230. package/dist/vue/useVoiceStream.d.ts +2 -1
  231. package/dist/vue/useVoiceTraceTimeline.d.ts +9 -0
  232. package/dist/vue/useVoiceTurnLatency.d.ts +10 -0
  233. package/dist/vue/useVoiceTurnQuality.d.ts +9 -0
  234. package/dist/vue/useVoiceWorkflowStatus.d.ts +9 -0
  235. package/dist/workflowContract.d.ts +91 -0
  236. package/package.json +1 -1
@@ -2105,6 +2105,11 @@ var serverMessageToAction = (message) => {
2105
2105
  sessionId: message.sessionId,
2106
2106
  type: "complete"
2107
2107
  };
2108
+ case "connection":
2109
+ return {
2110
+ reconnect: message.reconnect,
2111
+ type: "connection"
2112
+ };
2108
2113
  case "call_lifecycle":
2109
2114
  return {
2110
2115
  event: message.event,
@@ -2126,6 +2131,17 @@ var serverMessageToAction = (message) => {
2126
2131
  transcript: message.transcript,
2127
2132
  type: "partial"
2128
2133
  };
2134
+ case "replay":
2135
+ return {
2136
+ assistantTexts: message.assistantTexts,
2137
+ call: message.call,
2138
+ partial: message.partial,
2139
+ scenarioId: message.scenarioId,
2140
+ sessionId: message.sessionId,
2141
+ status: message.status,
2142
+ turns: message.turns,
2143
+ type: "replay"
2144
+ };
2129
2145
  case "session":
2130
2146
  return {
2131
2147
  sessionId: message.sessionId,
@@ -2186,10 +2202,12 @@ var isVoiceServerMessage = (value) => {
2186
2202
  case "assistant":
2187
2203
  case "call_lifecycle":
2188
2204
  case "complete":
2205
+ case "connection":
2189
2206
  case "error":
2190
2207
  case "final":
2191
2208
  case "partial":
2192
2209
  case "pong":
2210
+ case "replay":
2193
2211
  case "session":
2194
2212
  case "turn":
2195
2213
  return true;
@@ -2226,6 +2244,9 @@ var createVoiceConnection = (path, options = {}) => {
2226
2244
  sessionId: options.sessionId ?? createSessionId(),
2227
2245
  ws: null
2228
2246
  };
2247
+ const emitConnection = (reconnect) => {
2248
+ listeners.forEach((listener) => listener(reconnect));
2249
+ };
2229
2250
  const clearTimers = () => {
2230
2251
  if (state.pingInterval) {
2231
2252
  clearInterval(state.pingInterval);
@@ -2248,9 +2269,28 @@ var createVoiceConnection = (path, options = {}) => {
2248
2269
  }
2249
2270
  };
2250
2271
  const scheduleReconnect = () => {
2272
+ const nextAttemptAt = Date.now() + RECONNECT_DELAY_MS;
2251
2273
  state.reconnectAttempts += 1;
2274
+ emitConnection({
2275
+ reconnect: {
2276
+ attempts: state.reconnectAttempts,
2277
+ lastDisconnectAt: Date.now(),
2278
+ maxAttempts: maxReconnectAttempts,
2279
+ nextAttemptAt,
2280
+ status: "reconnecting"
2281
+ },
2282
+ type: "connection"
2283
+ });
2252
2284
  state.reconnectTimeout = setTimeout(() => {
2253
2285
  if (state.reconnectAttempts > maxReconnectAttempts) {
2286
+ emitConnection({
2287
+ reconnect: {
2288
+ attempts: state.reconnectAttempts,
2289
+ maxAttempts: maxReconnectAttempts,
2290
+ status: "exhausted"
2291
+ },
2292
+ type: "connection"
2293
+ });
2254
2294
  return;
2255
2295
  }
2256
2296
  connect();
@@ -2260,9 +2300,21 @@ var createVoiceConnection = (path, options = {}) => {
2260
2300
  const ws = new WebSocket(buildWsUrl(path, state.sessionId, state.scenarioId));
2261
2301
  ws.binaryType = "arraybuffer";
2262
2302
  ws.onopen = () => {
2303
+ const wasReconnecting = state.reconnectAttempts > 0;
2263
2304
  state.isConnected = true;
2264
- state.reconnectAttempts = 0;
2265
2305
  flushPendingMessages();
2306
+ if (wasReconnecting) {
2307
+ emitConnection({
2308
+ reconnect: {
2309
+ attempts: state.reconnectAttempts,
2310
+ lastResumedAt: Date.now(),
2311
+ maxAttempts: maxReconnectAttempts,
2312
+ status: "resumed"
2313
+ },
2314
+ type: "connection"
2315
+ });
2316
+ state.reconnectAttempts = 0;
2317
+ }
2266
2318
  listeners.forEach((listener) => listener({
2267
2319
  scenarioId: state.scenarioId ?? undefined,
2268
2320
  sessionId: state.sessionId,
@@ -2292,6 +2344,16 @@ var createVoiceConnection = (path, options = {}) => {
2292
2344
  const reconnectable = shouldReconnect && event.code !== WS_NORMAL_CLOSURE && state.reconnectAttempts < maxReconnectAttempts;
2293
2345
  if (reconnectable) {
2294
2346
  scheduleReconnect();
2347
+ } else if (shouldReconnect && event.code !== WS_NORMAL_CLOSURE) {
2348
+ emitConnection({
2349
+ reconnect: {
2350
+ attempts: state.reconnectAttempts,
2351
+ lastDisconnectAt: Date.now(),
2352
+ maxAttempts: maxReconnectAttempts,
2353
+ status: "exhausted"
2354
+ },
2355
+ type: "connection"
2356
+ });
2295
2357
  }
2296
2358
  };
2297
2359
  state.ws = ws;
@@ -2362,6 +2424,11 @@ var createVoiceConnection = (path, options = {}) => {
2362
2424
  };
2363
2425
 
2364
2426
  // src/client/store.ts
2427
+ var createInitialReconnectState = () => ({
2428
+ attempts: 0,
2429
+ maxAttempts: 0,
2430
+ status: "idle"
2431
+ });
2365
2432
  var createInitialState2 = () => ({
2366
2433
  assistantAudio: [],
2367
2434
  assistantTexts: [],
@@ -2370,6 +2437,7 @@ var createInitialState2 = () => ({
2370
2437
  isConnected: false,
2371
2438
  scenarioId: null,
2372
2439
  partial: "",
2440
+ reconnect: createInitialReconnectState(),
2373
2441
  sessionId: null,
2374
2442
  status: "idle",
2375
2443
  turns: []
@@ -2426,7 +2494,19 @@ var createVoiceStreamStore = () => {
2426
2494
  case "connected":
2427
2495
  state = {
2428
2496
  ...state,
2429
- isConnected: true
2497
+ isConnected: true,
2498
+ reconnect: state.reconnect.status === "reconnecting" ? {
2499
+ ...state.reconnect,
2500
+ lastResumedAt: Date.now(),
2501
+ nextAttemptAt: undefined,
2502
+ status: "resumed"
2503
+ } : state.reconnect
2504
+ };
2505
+ break;
2506
+ case "connection":
2507
+ state = {
2508
+ ...state,
2509
+ reconnect: action.reconnect
2430
2510
  };
2431
2511
  break;
2432
2512
  case "disconnected":
@@ -2454,6 +2534,26 @@ var createVoiceStreamStore = () => {
2454
2534
  partial: action.transcript.text
2455
2535
  };
2456
2536
  break;
2537
+ case "replay":
2538
+ state = {
2539
+ ...state,
2540
+ assistantTexts: [...action.assistantTexts],
2541
+ call: action.call ?? null,
2542
+ error: null,
2543
+ isConnected: action.status === "active",
2544
+ partial: action.partial,
2545
+ reconnect: state.reconnect.status === "reconnecting" ? {
2546
+ ...state.reconnect,
2547
+ lastResumedAt: Date.now(),
2548
+ nextAttemptAt: undefined,
2549
+ status: "resumed"
2550
+ } : state.reconnect,
2551
+ scenarioId: action.scenarioId ?? state.scenarioId,
2552
+ sessionId: action.sessionId,
2553
+ status: action.status,
2554
+ turns: [...action.turns]
2555
+ };
2556
+ break;
2457
2557
  case "session":
2458
2558
  state = {
2459
2559
  ...state,
@@ -2501,10 +2601,34 @@ var createVoiceStream = (path, options = {}) => {
2501
2601
  const notify = () => {
2502
2602
  subscribers.forEach((subscriber) => subscriber());
2503
2603
  };
2604
+ const reportReconnect = () => {
2605
+ if (!options.reconnectReportPath || typeof fetch === "undefined") {
2606
+ return;
2607
+ }
2608
+ const snapshot = store.getSnapshot();
2609
+ const body = JSON.stringify({
2610
+ at: Date.now(),
2611
+ reconnect: snapshot.reconnect,
2612
+ scenarioId: snapshot.scenarioId,
2613
+ sessionId: connection.getSessionId(),
2614
+ turnIds: snapshot.turns.map((turn) => turn.id)
2615
+ });
2616
+ fetch(options.reconnectReportPath, {
2617
+ body,
2618
+ headers: {
2619
+ "Content-Type": "application/json"
2620
+ },
2621
+ keepalive: true,
2622
+ method: "POST"
2623
+ }).catch(() => {});
2624
+ };
2504
2625
  const unsubscribeConnection = connection.subscribe((message) => {
2505
2626
  const action = serverMessageToAction(message);
2506
2627
  if (action) {
2507
2628
  store.dispatch(action);
2629
+ if (message.type === "connection") {
2630
+ reportReconnect();
2631
+ }
2508
2632
  notify();
2509
2633
  }
2510
2634
  });
@@ -2540,6 +2664,9 @@ var createVoiceStream = (path, options = {}) => {
2540
2664
  get partial() {
2541
2665
  return store.getSnapshot().partial;
2542
2666
  },
2667
+ get reconnect() {
2668
+ return store.getSnapshot().reconnect;
2669
+ },
2543
2670
  get sessionId() {
2544
2671
  return connection.getSessionId();
2545
2672
  },
@@ -2895,6 +3022,7 @@ var createInitialState3 = (stream) => ({
2895
3022
  isConnected: stream.isConnected,
2896
3023
  isRecording: false,
2897
3024
  partial: stream.partial,
3025
+ reconnect: stream.reconnect,
2898
3026
  recordingError: null,
2899
3027
  sessionId: stream.sessionId,
2900
3028
  scenarioId: stream.scenarioId,
@@ -2924,6 +3052,7 @@ var createVoiceController = (path, options = {}) => {
2924
3052
  error: stream.error,
2925
3053
  isConnected: stream.isConnected,
2926
3054
  partial: stream.partial,
3055
+ reconnect: stream.reconnect,
2927
3056
  sessionId: stream.sessionId,
2928
3057
  scenarioId: stream.scenarioId,
2929
3058
  status: stream.status,
@@ -2948,7 +3077,13 @@ var createVoiceController = (path, options = {}) => {
2948
3077
  capture = createMicrophoneCapture({
2949
3078
  channelCount: options.capture?.channelCount ?? preset.capture.channelCount,
2950
3079
  onLevel: options.capture?.onLevel,
2951
- onAudio: (audio) => stream.sendAudio(audio),
3080
+ onAudio: (audio) => {
3081
+ if (options.capture?.onAudio) {
3082
+ options.capture.onAudio(audio, stream.sendAudio);
3083
+ return;
3084
+ }
3085
+ stream.sendAudio(audio);
3086
+ },
2952
3087
  sampleRateHz: options.capture?.sampleRateHz ?? preset.capture.sampleRateHz
2953
3088
  });
2954
3089
  return capture;
@@ -3018,6 +3153,9 @@ var createVoiceController = (path, options = {}) => {
3018
3153
  get recordingError() {
3019
3154
  return state.recordingError;
3020
3155
  },
3156
+ get reconnect() {
3157
+ return state.reconnect;
3158
+ },
3021
3159
  sendAudio: (audio) => stream.sendAudio(audio),
3022
3160
  get sessionId() {
3023
3161
  return state.sessionId;
@@ -3063,11 +3201,26 @@ var DEFAULT_INTERRUPT_THRESHOLD = 0.08;
3063
3201
  var shouldInterruptForLevel = (level, options = {}) => (options.enabled ?? true) && level >= (options.interruptThreshold ?? DEFAULT_INTERRUPT_THRESHOLD);
3064
3202
  var bindVoiceBargeIn = (controller, player, options = {}) => {
3065
3203
  let lastPartial = controller.partial;
3066
- const interruptIfPlaying = () => {
3204
+ const interruptIfPlaying = (reason) => {
3067
3205
  if (!player.isPlaying || options.enabled === false) {
3206
+ options.monitor?.recordSkipped({
3207
+ reason,
3208
+ sessionId: controller.sessionId
3209
+ });
3068
3210
  return;
3069
3211
  }
3070
- player.interrupt();
3212
+ options.monitor?.recordRequested({
3213
+ reason,
3214
+ sessionId: controller.sessionId
3215
+ });
3216
+ player.interrupt().then(() => {
3217
+ options.monitor?.recordStopped({
3218
+ latencyMs: player.lastInterruptLatencyMs,
3219
+ playbackStopLatencyMs: player.lastPlaybackStopLatencyMs,
3220
+ reason,
3221
+ sessionId: controller.sessionId
3222
+ });
3223
+ });
3071
3224
  };
3072
3225
  const unsubscribe = controller.subscribe(() => {
3073
3226
  if (options.interruptOnPartial === false) {
@@ -3075,7 +3228,7 @@ var bindVoiceBargeIn = (controller, player, options = {}) => {
3075
3228
  return;
3076
3229
  }
3077
3230
  if (!lastPartial && controller.partial) {
3078
- interruptIfPlaying();
3231
+ interruptIfPlaying("partial-transcript");
3079
3232
  }
3080
3233
  lastPartial = controller.partial;
3081
3234
  });
@@ -3085,11 +3238,11 @@ var bindVoiceBargeIn = (controller, player, options = {}) => {
3085
3238
  },
3086
3239
  handleLevel: (level) => {
3087
3240
  if (shouldInterruptForLevel(level, options)) {
3088
- interruptIfPlaying();
3241
+ interruptIfPlaying("input-level");
3089
3242
  }
3090
3243
  },
3091
3244
  sendAudio: (audio) => {
3092
- interruptIfPlaying();
3245
+ interruptIfPlaying("manual-audio");
3093
3246
  controller.sendAudio(audio);
3094
3247
  }
3095
3248
  };
@@ -3119,7 +3272,17 @@ var createVoiceDuplexController = (path, options = {}) => {
3119
3272
  audioPlayer,
3120
3273
  close,
3121
3274
  interruptAssistant: async () => {
3275
+ options.bargeIn?.monitor?.recordRequested({
3276
+ reason: "manual-interrupt",
3277
+ sessionId: controller.sessionId
3278
+ });
3122
3279
  await audioPlayer.interrupt();
3280
+ options.bargeIn?.monitor?.recordStopped({
3281
+ latencyMs: audioPlayer.lastInterruptLatencyMs,
3282
+ playbackStopLatencyMs: audioPlayer.lastPlaybackStopLatencyMs,
3283
+ reason: "manual-interrupt",
3284
+ sessionId: controller.sessionId
3285
+ });
3123
3286
  },
3124
3287
  sendAudio: (audio) => {
3125
3288
  bargeInBinding?.sendAudio(audio);
@@ -3510,7 +3673,165 @@ var loadVoiceTestFixtures = async (fixtureDirectory) => {
3510
3673
  }
3511
3674
  return fixtures;
3512
3675
  };
3676
+ // src/testing/ioProviderSimulator.ts
3677
+ var defaultFailureMessage = (input) => `Simulated ${input.provider} ${input.kind.toUpperCase()} ${input.operation} failure.`;
3678
+ var resolveRecoveryElapsedMs = (value, provider) => {
3679
+ if (typeof value === "number") {
3680
+ return value;
3681
+ }
3682
+ return value?.[provider] ?? 25;
3683
+ };
3684
+ var createHealth = (input) => ({
3685
+ consecutiveFailures: input.status === "healthy" ? 0 : 1,
3686
+ lastFailureAt: input.status === "healthy" ? undefined : input.now,
3687
+ provider: input.provider,
3688
+ status: input.status,
3689
+ suppressedUntil: input.suppressedUntil
3690
+ });
3691
+ var resolveFallback = async (options, provider) => {
3692
+ const configured = typeof options.fallback === "function" ? await options.fallback(provider) : options.fallback;
3693
+ return (configured ?? options.providers).find((candidate) => candidate !== provider);
3694
+ };
3695
+ var createVoiceIOProviderFailureSimulator = (options) => {
3696
+ if (options.providers.length === 0) {
3697
+ throw new Error("At least one provider is required.");
3698
+ }
3699
+ const now = options.now ?? Date.now;
3700
+ const operation = options.operation ?? "open";
3701
+ const cooldownMs = Math.max(0, options.cooldownMs ?? 30000);
3702
+ const emit = async (event, input) => {
3703
+ await options.onProviderEvent?.(event, input);
3704
+ };
3705
+ const run = async (provider, mode) => {
3706
+ if (!options.providers.includes(provider)) {
3707
+ throw new Error(`${provider} is not configured for simulation.`);
3708
+ }
3709
+ const startedAt = now();
3710
+ const sessionId = options.sessionId?.({ mode, now: startedAt, provider }) ?? `${options.kind}-provider-sim-${startedAt}`;
3711
+ if (mode === "recovery") {
3712
+ await emit({
3713
+ at: startedAt,
3714
+ attempt: 0,
3715
+ elapsedMs: resolveRecoveryElapsedMs(options.recoveryElapsedMs, provider),
3716
+ kind: options.kind,
3717
+ latencyBudgetMs: options.latencyBudgets?.[provider],
3718
+ operation,
3719
+ provider,
3720
+ providerHealth: createHealth({
3721
+ now: startedAt,
3722
+ provider,
3723
+ status: "healthy"
3724
+ }),
3725
+ selectedProvider: provider,
3726
+ status: "success"
3727
+ }, { mode, provider, sessionId });
3728
+ return {
3729
+ mode,
3730
+ provider,
3731
+ sessionId,
3732
+ status: "simulated"
3733
+ };
3734
+ }
3735
+ const fallbackProvider = await resolveFallback(options, provider);
3736
+ const suppressedUntil = startedAt + cooldownMs;
3737
+ await emit({
3738
+ at: startedAt,
3739
+ attempt: 0,
3740
+ elapsedMs: options.failureElapsedMs ?? 10,
3741
+ error: (options.failureMessage ?? defaultFailureMessage)({
3742
+ kind: options.kind,
3743
+ operation,
3744
+ provider
3745
+ }),
3746
+ fallbackProvider,
3747
+ kind: options.kind,
3748
+ latencyBudgetMs: options.latencyBudgets?.[provider],
3749
+ operation,
3750
+ provider,
3751
+ providerHealth: createHealth({
3752
+ now: startedAt,
3753
+ provider,
3754
+ status: "suppressed",
3755
+ suppressedUntil
3756
+ }),
3757
+ selectedProvider: provider,
3758
+ status: "error",
3759
+ suppressedUntil
3760
+ }, { mode, provider, sessionId });
3761
+ if (fallbackProvider) {
3762
+ await emit({
3763
+ at: startedAt + 1,
3764
+ attempt: 1,
3765
+ elapsedMs: resolveRecoveryElapsedMs(options.recoveryElapsedMs, fallbackProvider),
3766
+ fallbackProvider,
3767
+ kind: options.kind,
3768
+ latencyBudgetMs: options.latencyBudgets?.[fallbackProvider],
3769
+ operation,
3770
+ provider: fallbackProvider,
3771
+ providerHealth: createHealth({
3772
+ now: startedAt + 1,
3773
+ provider: fallbackProvider,
3774
+ status: "healthy"
3775
+ }),
3776
+ selectedProvider: provider,
3777
+ status: "fallback"
3778
+ }, { mode, provider, sessionId });
3779
+ }
3780
+ return {
3781
+ fallbackProvider,
3782
+ mode,
3783
+ provider,
3784
+ sessionId,
3785
+ status: "simulated",
3786
+ suppressedUntil
3787
+ };
3788
+ };
3789
+ return {
3790
+ run
3791
+ };
3792
+ };
3513
3793
  // src/modelAdapters.ts
3794
+ var resolveVoiceProviderRoutingPolicyPreset = (preset, options = {}) => {
3795
+ switch (preset) {
3796
+ case "balanced":
3797
+ return {
3798
+ fallbackMode: "provider-error",
3799
+ strategy: "balanced",
3800
+ weights: {
3801
+ cost: 1,
3802
+ latencyMs: 0.005,
3803
+ priority: 1,
3804
+ quality: 10,
3805
+ ...options.weights
3806
+ },
3807
+ ...options
3808
+ };
3809
+ case "cost-cap":
3810
+ return {
3811
+ fallbackMode: "provider-error",
3812
+ strategy: "prefer-cheapest",
3813
+ ...options
3814
+ };
3815
+ case "cost-first":
3816
+ return {
3817
+ fallbackMode: "provider-error",
3818
+ strategy: "prefer-cheapest",
3819
+ ...options
3820
+ };
3821
+ case "latency-first":
3822
+ return {
3823
+ fallbackMode: "provider-error",
3824
+ strategy: "prefer-fastest",
3825
+ ...options
3826
+ };
3827
+ case "quality-first":
3828
+ return {
3829
+ fallbackMode: "provider-error",
3830
+ strategy: "quality-first",
3831
+ ...options
3832
+ };
3833
+ }
3834
+ };
3514
3835
  var OUTPUT_SCHEMA = {
3515
3836
  additionalProperties: false,
3516
3837
  properties: {
@@ -3601,6 +3922,17 @@ var parseJSONValue = (value) => {
3601
3922
  return value;
3602
3923
  }
3603
3924
  };
3925
+
3926
+ class VoiceProviderTimeoutError extends Error {
3927
+ provider;
3928
+ timeoutMs;
3929
+ constructor(provider, timeoutMs) {
3930
+ super(`Voice provider ${provider} exceeded ${timeoutMs}ms latency budget.`);
3931
+ this.name = "VoiceProviderTimeoutError";
3932
+ this.provider = provider;
3933
+ this.timeoutMs = timeoutMs;
3934
+ }
3935
+ }
3604
3936
  var getMessageToolCalls = (message) => {
3605
3937
  const toolCalls = message.metadata?.toolCalls;
3606
3938
  return Array.isArray(toolCalls) ? toolCalls.filter((toolCall) => toolCall && typeof toolCall === "object" && typeof toolCall.name === "string") : [];
@@ -3667,7 +3999,7 @@ var createJSONVoiceAssistantModel = (options) => ({
3667
3999
  var createVoiceProviderRouter = (options) => {
3668
4000
  const providerIds = Object.keys(options.providers);
3669
4001
  const firstProvider = providerIds[0];
3670
- const policy = typeof options.policy === "string" ? {
4002
+ const policy = typeof options.policy === "string" ? options.policy === "balanced" || options.policy === "cost-cap" || options.policy === "cost-first" || options.policy === "latency-first" || options.policy === "quality-first" ? resolveVoiceProviderRoutingPolicyPreset(options.policy) : {
3671
4003
  strategy: options.policy
3672
4004
  } : options.policy;
3673
4005
  const strategy = policy?.strategy ?? "prefer-selected";
@@ -3678,6 +4010,10 @@ var createVoiceProviderRouter = (options) => {
3678
4010
  const failureThreshold = Math.max(1, healthOptions?.failureThreshold ?? 1);
3679
4011
  const cooldownMs = Math.max(0, healthOptions?.cooldownMs ?? 30000);
3680
4012
  const rateLimitCooldownMs = Math.max(0, healthOptions?.rateLimitCooldownMs ?? 60000);
4013
+ const getProviderTimeoutMs = (provider) => {
4014
+ const timeoutMs = options.providerProfiles?.[provider]?.timeoutMs ?? options.timeoutMs;
4015
+ return typeof timeoutMs === "number" && Number.isFinite(timeoutMs) && timeoutMs > 0 ? timeoutMs : undefined;
4016
+ };
3681
4017
  const getHealth = (provider) => {
3682
4018
  const existing = healthState.get(provider);
3683
4019
  if (existing) {
@@ -3745,13 +4081,40 @@ var createVoiceProviderRouter = (options) => {
3745
4081
  const allowed = typeof allowProviders === "function" ? await allowProviders(input) : allowProviders;
3746
4082
  return new Set(allowed ?? providerIds);
3747
4083
  };
4084
+ const passesBudgetFilters = (provider) => {
4085
+ const profile = options.providerProfiles?.[provider];
4086
+ if (typeof policy?.maxCost === "number" && typeof profile?.cost === "number" && profile.cost > policy.maxCost) {
4087
+ return false;
4088
+ }
4089
+ if (typeof policy?.maxLatencyMs === "number" && typeof profile?.latencyMs === "number" && profile.latencyMs > policy.maxLatencyMs) {
4090
+ return false;
4091
+ }
4092
+ if (typeof policy?.minQuality === "number" && typeof profile?.quality === "number" && profile.quality < policy.minQuality) {
4093
+ return false;
4094
+ }
4095
+ return true;
4096
+ };
4097
+ const getBalancedScore = (provider) => {
4098
+ const profile = options.providerProfiles?.[provider];
4099
+ if (policy?.scoreProvider) {
4100
+ return policy.scoreProvider(provider, profile);
4101
+ }
4102
+ const weights = policy?.weights ?? {};
4103
+ return (profile?.cost ?? Number.MAX_SAFE_INTEGER) * (weights.cost ?? 1) + (profile?.latencyMs ?? Number.MAX_SAFE_INTEGER) * (weights.latencyMs ?? 0.005) + (profile?.priority ?? 0) * (weights.priority ?? 1) - (profile?.quality ?? 0) * (weights.quality ?? 10);
4104
+ };
3748
4105
  const sortProviders = (providers) => {
3749
- if (strategy !== "prefer-cheapest" && strategy !== "prefer-fastest") {
4106
+ if (strategy !== "prefer-cheapest" && strategy !== "prefer-fastest" && strategy !== "quality-first" && strategy !== "balanced") {
3750
4107
  return providers;
3751
4108
  }
3752
4109
  return [...providers].sort((left, right) => {
3753
4110
  const leftProfile = options.providerProfiles?.[left];
3754
4111
  const rightProfile = options.providerProfiles?.[right];
4112
+ if (strategy === "quality-first") {
4113
+ return (rightProfile?.quality ?? Number.MIN_SAFE_INTEGER) - (leftProfile?.quality ?? Number.MIN_SAFE_INTEGER) || (leftProfile?.priority ?? Number.MAX_SAFE_INTEGER) - (rightProfile?.priority ?? Number.MAX_SAFE_INTEGER) || (leftProfile?.latencyMs ?? Number.MAX_SAFE_INTEGER) - (rightProfile?.latencyMs ?? Number.MAX_SAFE_INTEGER) || (leftProfile?.cost ?? Number.MAX_SAFE_INTEGER) - (rightProfile?.cost ?? Number.MAX_SAFE_INTEGER);
4114
+ }
4115
+ if (strategy === "balanced") {
4116
+ return getBalancedScore(left) - getBalancedScore(right);
4117
+ }
3755
4118
  const leftValue = strategy === "prefer-cheapest" ? leftProfile?.cost ?? Number.MAX_SAFE_INTEGER : leftProfile?.latencyMs ?? Number.MAX_SAFE_INTEGER;
3756
4119
  const rightValue = strategy === "prefer-cheapest" ? rightProfile?.cost ?? Number.MAX_SAFE_INTEGER : rightProfile?.latencyMs ?? Number.MAX_SAFE_INTEGER;
3757
4120
  return leftValue - rightValue || (leftProfile?.priority ?? Number.MAX_SAFE_INTEGER) - (rightProfile?.priority ?? Number.MAX_SAFE_INTEGER);
@@ -3761,12 +4124,13 @@ var createVoiceProviderRouter = (options) => {
3761
4124
  const selectedProvider = await options.selectProvider?.(input);
3762
4125
  const allowedProviders = await resolveAllowedProviders(input);
3763
4126
  const fallbackOrder = typeof options.fallback === "function" ? await options.fallback(input) : options.fallback;
3764
- const rankedProviders = sortProviders([
4127
+ const allowedRankedProviders = sortProviders([
3765
4128
  ...fallbackOrder ?? providerIds
3766
4129
  ]).filter((provider) => allowedProviders.has(provider));
4130
+ const rankedProviders = allowedRankedProviders.filter(passesBudgetFilters);
3767
4131
  const healthyRankedProviders = healthOptions ? rankedProviders.filter((provider) => !isSuppressed(provider)) : rankedProviders;
3768
4132
  const candidateRankedProviders = healthyRankedProviders.length ? healthyRankedProviders : rankedProviders;
3769
- const preferred = selectedProvider && allowedProviders.has(selectedProvider) && (!healthOptions || !isSuppressed(selectedProvider)) ? selectedProvider : candidateRankedProviders[0] ?? firstProvider;
4133
+ const preferred = selectedProvider && allowedProviders.has(selectedProvider) && passesBudgetFilters(selectedProvider) && (!healthOptions || !isSuppressed(selectedProvider)) ? selectedProvider : candidateRankedProviders[0] ?? firstProvider;
3770
4134
  const seen = new Set;
3771
4135
  const order = [];
3772
4136
  const candidates = strategy === "ordered" ? candidateRankedProviders : [
@@ -3789,6 +4153,25 @@ var createVoiceProviderRouter = (options) => {
3789
4153
  const emit = async (event, input) => {
3790
4154
  await options.onProviderEvent?.(event, input);
3791
4155
  };
4156
+ const runProvider = async (provider, model, input) => {
4157
+ const timeoutMs = getProviderTimeoutMs(provider);
4158
+ if (!timeoutMs) {
4159
+ return model.generate(input);
4160
+ }
4161
+ let timeout;
4162
+ try {
4163
+ return await Promise.race([
4164
+ model.generate(input),
4165
+ new Promise((_, reject) => {
4166
+ timeout = setTimeout(() => reject(new VoiceProviderTimeoutError(provider, timeoutMs)), timeoutMs);
4167
+ })
4168
+ ]);
4169
+ } finally {
4170
+ if (timeout) {
4171
+ clearTimeout(timeout);
4172
+ }
4173
+ }
4174
+ };
3792
4175
  return {
3793
4176
  generate: async (input) => {
3794
4177
  const { order, selectedProvider } = await resolveOrder(input);
@@ -3803,12 +4186,14 @@ var createVoiceProviderRouter = (options) => {
3803
4186
  }
3804
4187
  const startedAt = Date.now();
3805
4188
  try {
3806
- const output = await model.generate(input);
4189
+ const output = await runProvider(provider, model, input);
3807
4190
  const providerHealth = recordProviderSuccess(provider);
3808
4191
  await emit({
3809
4192
  at: Date.now(),
4193
+ attempt: index + 1,
3810
4194
  elapsedMs: Date.now() - startedAt,
3811
4195
  fallbackProvider: provider === selectedProvider ? undefined : provider,
4196
+ latencyBudgetMs: getProviderTimeoutMs(provider),
3812
4197
  provider,
3813
4198
  providerHealth,
3814
4199
  recovered: provider !== selectedProvider,
@@ -3820,22 +4205,26 @@ var createVoiceProviderRouter = (options) => {
3820
4205
  lastError = error;
3821
4206
  const hasNextProvider = index < order.length - 1;
3822
4207
  const isProviderError = options.isProviderError?.(error, provider) ?? true;
4208
+ const timedOut = options.isTimeoutError?.(error, provider) ?? error instanceof VoiceProviderTimeoutError;
3823
4209
  const rateLimited = options.isRateLimitError?.(error, provider) ?? defaultIsRateLimitError(error);
3824
4210
  const shouldFallback = fallbackMode === "provider-error" ? isProviderError : fallbackMode === "rate-limit" ? isProviderError && rateLimited : false;
3825
4211
  const providerHealth = recordProviderError(provider, isProviderError, rateLimited);
3826
4212
  const nextProvider = hasNextProvider ? order[index + 1] : undefined;
3827
4213
  await emit({
3828
4214
  at: Date.now(),
4215
+ attempt: index + 1,
3829
4216
  elapsedMs: Date.now() - startedAt,
3830
4217
  error: errorMessage(error),
3831
4218
  fallbackProvider: shouldFallback ? nextProvider : undefined,
4219
+ latencyBudgetMs: getProviderTimeoutMs(provider),
3832
4220
  provider,
3833
4221
  providerHealth,
3834
4222
  rateLimited,
3835
4223
  selectedProvider,
3836
4224
  suppressionRemainingMs: getSuppressionRemainingMs(provider),
3837
4225
  suppressedUntil: providerHealth?.suppressedUntil,
3838
- status: "error"
4226
+ status: "error",
4227
+ timedOut
3839
4228
  }, input);
3840
4229
  if (!hasNextProvider || !shouldFallback) {
3841
4230
  throw error;
@@ -4457,7 +4846,7 @@ var createVoiceMemoryStore = () => {
4457
4846
  };
4458
4847
 
4459
4848
  // src/session.ts
4460
- import { Buffer } from "buffer";
4849
+ import { Buffer as Buffer2 } from "buffer";
4461
4850
 
4462
4851
  // src/handoff.ts
4463
4852
  var toHex = (bytes) => Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
@@ -4486,6 +4875,14 @@ var aggregateHandoffStatus = (deliveries) => {
4486
4875
  }
4487
4876
  return "skipped";
4488
4877
  };
4878
+ var createHandoffDeliveryId = (input) => [
4879
+ "voice-handoff",
4880
+ input.sessionId,
4881
+ input.action,
4882
+ Date.now(),
4883
+ crypto.randomUUID()
4884
+ ].join(":");
4885
+ var resolveHandoffDeliveryError = (deliveries) => Object.values(deliveries).map((delivery) => delivery.error).find(Boolean);
4489
4886
  var defaultWebhookBody = (input) => ({
4490
4887
  action: input.action,
4491
4888
  metadata: input.metadata,
@@ -4534,6 +4931,73 @@ var deliverVoiceHandoff = async (input) => {
4534
4931
  status: aggregateHandoffStatus(deliveries)
4535
4932
  };
4536
4933
  };
4934
+ var createVoiceHandoffDeliveryRecord = (input) => {
4935
+ const now = Date.now();
4936
+ return {
4937
+ action: input.action,
4938
+ context: input.context,
4939
+ createdAt: now,
4940
+ deliveryAttempts: 0,
4941
+ deliveryStatus: "pending",
4942
+ id: input.id ?? createHandoffDeliveryId({
4943
+ action: input.action,
4944
+ sessionId: input.session.id
4945
+ }),
4946
+ metadata: input.metadata,
4947
+ reason: input.reason,
4948
+ result: input.result,
4949
+ session: input.session,
4950
+ sessionId: input.session.id,
4951
+ target: input.target,
4952
+ updatedAt: now
4953
+ };
4954
+ };
4955
+ var applyVoiceHandoffDeliveryResult = (delivery, result) => ({
4956
+ ...delivery,
4957
+ deliveredAt: result.status === "delivered" || result.status === "skipped" ? Date.now() : delivery.deliveredAt,
4958
+ deliveries: result.deliveries,
4959
+ deliveryAttempts: (delivery.deliveryAttempts ?? 0) + 1,
4960
+ deliveryError: result.status === "failed" ? resolveHandoffDeliveryError(result.deliveries) : undefined,
4961
+ deliveryStatus: result.status,
4962
+ updatedAt: Date.now()
4963
+ });
4964
+ var deliverVoiceHandoffDelivery = async (options) => {
4965
+ const result = await deliverVoiceHandoff({
4966
+ config: {
4967
+ adapters: options.adapters,
4968
+ failMode: options.failMode
4969
+ },
4970
+ handoff: {
4971
+ action: options.delivery.action,
4972
+ api: options.api,
4973
+ context: options.delivery.context,
4974
+ metadata: options.delivery.metadata,
4975
+ reason: options.delivery.reason,
4976
+ result: options.delivery.result,
4977
+ session: options.delivery.session,
4978
+ target: options.delivery.target
4979
+ }
4980
+ });
4981
+ return result ? applyVoiceHandoffDeliveryResult(options.delivery, result) : {
4982
+ ...options.delivery,
4983
+ deliveryAttempts: (options.delivery.deliveryAttempts ?? 0) + 1,
4984
+ deliveryStatus: "skipped",
4985
+ updatedAt: Date.now()
4986
+ };
4987
+ };
4988
+ var createVoiceMemoryHandoffDeliveryStore = () => {
4989
+ const deliveries = new Map;
4990
+ return {
4991
+ get: async (id) => deliveries.get(id),
4992
+ list: async () => [...deliveries.values()].sort((left, right) => left.createdAt - right.createdAt || left.id.localeCompare(right.id)),
4993
+ remove: async (id) => {
4994
+ deliveries.delete(id);
4995
+ },
4996
+ set: async (id, delivery) => {
4997
+ deliveries.set(id, delivery);
4998
+ }
4999
+ };
5000
+ };
4537
5001
  var createVoiceWebhookHandoffAdapter = (options) => ({
4538
5002
  actions: options.actions,
4539
5003
  handoff: async (input) => {
@@ -4701,6 +5165,12 @@ var DEFAULT_FORMAT = {
4701
5165
  encoding: "pcm_s16le",
4702
5166
  sampleRateHz: 16000
4703
5167
  };
5168
+ var DEFAULT_REALTIME_FORMAT = {
5169
+ channels: 1,
5170
+ container: "raw",
5171
+ encoding: "pcm_s16le",
5172
+ sampleRateHz: 24000
5173
+ };
4704
5174
  var toError = (value) => value instanceof Error ? value : new Error(String(value));
4705
5175
  var createEmptyCurrentTurn = () => ({
4706
5176
  finalText: "",
@@ -4713,7 +5183,7 @@ var createEmptyCurrentTurn = () => ({
4713
5183
  transcripts: []
4714
5184
  });
4715
5185
  var cloneTranscript = (transcript) => ({ ...transcript });
4716
- var encodeBase64 = (chunk) => Buffer.from(chunk).toString("base64");
5186
+ var encodeBase64 = (chunk) => Buffer2.from(chunk).toString("base64");
4717
5187
  var countWords2 = (text) => text.trim().split(/\s+/).filter(Boolean).length;
4718
5188
  var normalizeText2 = (text) => text.trim().replace(/\s+/g, " ");
4719
5189
  var getAudioChunkDurationMs = (chunk) => chunk.byteLength / (DEFAULT_FORMAT.sampleRateHz * DEFAULT_FORMAT.channels * 2) * 1000;
@@ -4884,7 +5354,7 @@ var createVoiceSession = (options) => {
4884
5354
  } : undefined;
4885
5355
  const appendTrace = async (input) => {
4886
5356
  await options.trace?.append({
4887
- at: Date.now(),
5357
+ at: input.at ?? Date.now(),
4888
5358
  metadata: input.metadata,
4889
5359
  payload: input.payload,
4890
5360
  scenarioId: input.session?.scenarioId ?? options.scenarioId,
@@ -4893,6 +5363,13 @@ var createVoiceSession = (options) => {
4893
5363
  type: input.type
4894
5364
  });
4895
5365
  };
5366
+ const appendTurnLatencyStage = async (input) => appendTrace({
5367
+ at: input.at,
5368
+ payload: { stage: input.stage },
5369
+ session: input.session,
5370
+ turnId: input.turnId,
5371
+ type: "turn_latency.stage"
5372
+ });
4896
5373
  const phraseHints = options.phraseHints ?? [];
4897
5374
  const lexicon = options.lexicon ?? [];
4898
5375
  let socket = options.socket;
@@ -4971,7 +5448,34 @@ var createVoiceSession = (options) => {
4971
5448
  type: "call_lifecycle"
4972
5449
  });
4973
5450
  };
5451
+ const sendReplay = async (session) => {
5452
+ await send({
5453
+ assistantTexts: session.turns.flatMap((turn) => turn.assistantText ? [turn.assistantText] : []),
5454
+ call: session.call,
5455
+ partial: session.currentTurn.partialText,
5456
+ scenarioId: session.scenarioId,
5457
+ sessionId: options.id,
5458
+ status: session.status,
5459
+ turns: session.turns,
5460
+ type: "replay"
5461
+ });
5462
+ };
4974
5463
  const runHandoff = async (input) => {
5464
+ const queuedDelivery = options.handoff?.deliveryQueue ? createVoiceHandoffDeliveryRecord({
5465
+ action: input.action,
5466
+ context: options.context,
5467
+ metadata: input.metadata,
5468
+ reason: input.reason,
5469
+ result: input.result,
5470
+ session: input.session,
5471
+ target: input.target
5472
+ }) : undefined;
5473
+ if (queuedDelivery) {
5474
+ await options.handoff?.deliveryQueue?.set(queuedDelivery.id, queuedDelivery);
5475
+ }
5476
+ if (options.handoff?.enqueueOnly) {
5477
+ return;
5478
+ }
4975
5479
  const result = await deliverVoiceHandoff({
4976
5480
  config: options.handoff,
4977
5481
  handoff: {
@@ -4988,6 +5492,10 @@ var createVoiceSession = (options) => {
4988
5492
  if (!result) {
4989
5493
  return;
4990
5494
  }
5495
+ if (queuedDelivery) {
5496
+ const updatedDelivery = applyVoiceHandoffDeliveryResult(queuedDelivery, result);
5497
+ await options.handoff?.deliveryQueue?.set(updatedDelivery.id, updatedDelivery);
5498
+ }
4991
5499
  await appendTrace({
4992
5500
  metadata: input.metadata,
4993
5501
  payload: {
@@ -5055,6 +5563,23 @@ var createVoiceSession = (options) => {
5055
5563
  });
5056
5564
  }
5057
5565
  };
5566
+ const sendAssistantAudio = async (chunk, input) => {
5567
+ const normalizedChunk = chunk instanceof Uint8Array ? new Uint8Array(chunk) : chunk instanceof ArrayBuffer ? new Uint8Array(chunk.slice(0)) : new Uint8Array(chunk.buffer.slice(chunk.byteOffset, chunk.byteOffset + chunk.byteLength));
5568
+ await send({
5569
+ chunkBase64: encodeBase64(normalizedChunk),
5570
+ format: input.format,
5571
+ receivedAt: input.receivedAt,
5572
+ turnId: activeTTSTurnId,
5573
+ type: "audio"
5574
+ });
5575
+ if (activeTTSTurnId) {
5576
+ await appendTurnLatencyStage({
5577
+ at: input.receivedAt,
5578
+ stage: "assistant_audio_received",
5579
+ turnId: activeTTSTurnId
5580
+ });
5581
+ }
5582
+ };
5058
5583
  const scheduleTurnCommit = (delayMs, reason, reset = true) => {
5059
5584
  if (!reset && silenceTimer) {
5060
5585
  return;
@@ -5756,8 +6281,12 @@ var createVoiceSession = (options) => {
5756
6281
  if (sttSession) {
5757
6282
  return sttSession;
5758
6283
  }
5759
- const openedSession = await options.stt.open({
5760
- format: DEFAULT_FORMAT,
6284
+ const inputAdapter = options.realtime ?? options.stt;
6285
+ if (!inputAdapter) {
6286
+ throw new Error("Voice session requires either an stt or realtime adapter.");
6287
+ }
6288
+ const openedSession = await inputAdapter.open({
6289
+ format: options.realtime ? options.realtimeInputFormat ?? DEFAULT_REALTIME_FORMAT : DEFAULT_FORMAT,
5761
6290
  languageStrategy: options.languageStrategy,
5762
6291
  lexicon,
5763
6292
  phraseHints,
@@ -5792,6 +6321,16 @@ var createVoiceSession = (options) => {
5792
6321
  openedSession.on("close", (event) => {
5793
6322
  runAdapterEvent("adapter.close", () => handleClose(event));
5794
6323
  });
6324
+ if (options.realtime) {
6325
+ openedSession.on("audio", ({ chunk, format, receivedAt }) => {
6326
+ runAdapterEvent("adapter.audio", async () => {
6327
+ await sendAssistantAudio(chunk, {
6328
+ format,
6329
+ receivedAt
6330
+ });
6331
+ });
6332
+ });
6333
+ }
5795
6334
  return openedSession;
5796
6335
  };
5797
6336
  const ensureTTSSession = async () => {
@@ -5816,13 +6355,9 @@ var createVoiceSession = (options) => {
5816
6355
  if (ttsSession !== openedSession) {
5817
6356
  return;
5818
6357
  }
5819
- const normalizedChunk = chunk instanceof Uint8Array ? new Uint8Array(chunk) : chunk instanceof ArrayBuffer ? new Uint8Array(chunk.slice(0)) : new Uint8Array(chunk.buffer.slice(chunk.byteOffset, chunk.byteOffset + chunk.byteLength));
5820
- await send({
5821
- chunkBase64: encodeBase64(normalizedChunk),
6358
+ await sendAssistantAudio(chunk, {
5822
6359
  format,
5823
- receivedAt,
5824
- turnId: activeTTSTurnId,
5825
- type: "audio"
6360
+ receivedAt
5826
6361
  });
5827
6362
  });
5828
6363
  });
@@ -5866,9 +6401,32 @@ var createVoiceSession = (options) => {
5866
6401
  });
5867
6402
  };
5868
6403
  const completeTurn = async (session, turn) => {
6404
+ const liveOpsControl = await options.liveOps?.getControl(options.id);
6405
+ if (liveOpsControl?.assistantPaused || liveOpsControl?.operatorTakeover) {
6406
+ await appendTrace({
6407
+ metadata: {
6408
+ source: "voice-live-ops"
6409
+ },
6410
+ payload: {
6411
+ action: "turn.skipped",
6412
+ control: liveOpsControl,
6413
+ reason: liveOpsControl.operatorTakeover ? "operator-takeover" : "assistant-paused",
6414
+ status: "skipped"
6415
+ },
6416
+ session,
6417
+ turnId: turn.id,
6418
+ type: "operator.action"
6419
+ });
6420
+ return;
6421
+ }
6422
+ const injectedInstruction = liveOpsControl?.injectedInstruction?.trim();
5869
6423
  const committedOutput = await options.route.onTurn({
5870
6424
  api,
5871
6425
  context: options.context,
6426
+ liveOps: liveOpsControl ? {
6427
+ control: liveOpsControl,
6428
+ injectedInstruction
6429
+ } : undefined,
5872
6430
  session,
5873
6431
  turn
5874
6432
  });
@@ -5882,6 +6440,7 @@ var createVoiceSession = (options) => {
5882
6440
  voicemail: committedOutput?.voicemail
5883
6441
  };
5884
6442
  if (output?.assistantText) {
6443
+ const assistantTextStartedAt = Date.now();
5885
6444
  await writeSession((currentSession) => {
5886
6445
  setTurnResult(currentSession, turn.id, {
5887
6446
  assistantText: output.assistantText
@@ -5892,10 +6451,17 @@ var createVoiceSession = (options) => {
5892
6451
  turnId: turn.id,
5893
6452
  type: "assistant"
5894
6453
  });
6454
+ await appendTurnLatencyStage({
6455
+ at: assistantTextStartedAt,
6456
+ session,
6457
+ stage: "assistant_text_started",
6458
+ turnId: turn.id
6459
+ });
5895
6460
  await appendTrace({
5896
6461
  payload: {
5897
6462
  text: output.assistantText,
5898
- ttsConfigured: Boolean(options.tts)
6463
+ ttsConfigured: Boolean(options.tts),
6464
+ realtimeConfigured: Boolean(options.realtime)
5899
6465
  },
5900
6466
  session,
5901
6467
  turnId: turn.id,
@@ -5906,7 +6472,18 @@ var createVoiceSession = (options) => {
5906
6472
  if (activeTTSSession) {
5907
6473
  const ttsStartedAt = Date.now();
5908
6474
  activeTTSTurnId = turn.id;
6475
+ await appendTurnLatencyStage({
6476
+ at: ttsStartedAt,
6477
+ session,
6478
+ stage: "tts_send_started",
6479
+ turnId: turn.id
6480
+ });
5909
6481
  await activeTTSSession.send(output.assistantText);
6482
+ await appendTurnLatencyStage({
6483
+ session,
6484
+ stage: "tts_send_completed",
6485
+ turnId: turn.id
6486
+ });
5910
6487
  await appendTrace({
5911
6488
  payload: {
5912
6489
  elapsedMs: Date.now() - ttsStartedAt,
@@ -5916,9 +6493,35 @@ var createVoiceSession = (options) => {
5916
6493
  turnId: turn.id,
5917
6494
  type: "turn.assistant"
5918
6495
  });
6496
+ } else if (options.realtime) {
6497
+ const activeRealtimeSession = await ensureAdapter();
6498
+ const realtimeStartedAt = Date.now();
6499
+ activeTTSTurnId = turn.id;
6500
+ await appendTurnLatencyStage({
6501
+ at: realtimeStartedAt,
6502
+ session,
6503
+ stage: "tts_send_started",
6504
+ turnId: turn.id
6505
+ });
6506
+ await activeRealtimeSession.send(output.assistantText);
6507
+ await appendTurnLatencyStage({
6508
+ session,
6509
+ stage: "tts_send_completed",
6510
+ turnId: turn.id
6511
+ });
6512
+ await appendTrace({
6513
+ payload: {
6514
+ elapsedMs: Date.now() - realtimeStartedAt,
6515
+ mode: "realtime",
6516
+ status: "sent"
6517
+ },
6518
+ session,
6519
+ turnId: turn.id,
6520
+ type: "turn.assistant"
6521
+ });
5919
6522
  }
5920
6523
  } catch (error) {
5921
- logger.warn("voice tts send failed", {
6524
+ logger.warn("voice assistant audio send failed", {
5922
6525
  error: toError(error).message,
5923
6526
  sessionId: options.id,
5924
6527
  turnId: turn.id
@@ -5926,7 +6529,7 @@ var createVoiceSession = (options) => {
5926
6529
  await appendTrace({
5927
6530
  payload: {
5928
6531
  error: toError(error).message,
5929
- status: "tts-send-failed"
6532
+ status: options.realtime ? "realtime-send-failed" : "tts-send-failed"
5930
6533
  },
5931
6534
  session,
5932
6535
  turnId: turn.id,
@@ -6103,11 +6706,35 @@ var createVoiceSession = (options) => {
6103
6706
  turnId: turn.id,
6104
6707
  type: "turn.cost"
6105
6708
  });
6709
+ const firstTranscriptAt = turn.transcripts.map((transcript) => transcript.endedAtMs ?? transcript.startedAtMs).filter((value) => typeof value === "number").sort((left, right) => left - right)[0];
6710
+ const finalTranscriptAt = turn.transcripts.filter((transcript) => transcript.isFinal).map((transcript) => transcript.endedAtMs ?? transcript.startedAtMs).filter((value) => typeof value === "number").sort((left, right) => left - right)[0];
6711
+ if (firstTranscriptAt !== undefined) {
6712
+ await appendTurnLatencyStage({
6713
+ at: firstTranscriptAt,
6714
+ session: updatedSession,
6715
+ stage: "speech_detected",
6716
+ turnId: turn.id
6717
+ });
6718
+ }
6719
+ if (finalTranscriptAt !== undefined) {
6720
+ await appendTurnLatencyStage({
6721
+ at: finalTranscriptAt,
6722
+ session: updatedSession,
6723
+ stage: "final_transcript",
6724
+ turnId: turn.id
6725
+ });
6726
+ }
6727
+ await appendTurnLatencyStage({
6728
+ at: turn.committedAt,
6729
+ session: updatedSession,
6730
+ stage: "turn_committed",
6731
+ turnId: turn.id
6732
+ });
6106
6733
  await send({
6107
6734
  turn,
6108
6735
  type: "turn"
6109
6736
  });
6110
- if (options.sttLifecycle === "turn-scoped") {
6737
+ if (options.stt && options.sttLifecycle === "turn-scoped") {
6111
6738
  await closeAdapter("turn-commit");
6112
6739
  }
6113
6740
  await completeTurn(updatedSession, turn);
@@ -6170,6 +6797,7 @@ var createVoiceSession = (options) => {
6170
6797
  scenarioId: session.scenarioId,
6171
6798
  type: "session"
6172
6799
  });
6800
+ await sendReplay(session);
6173
6801
  if (shouldFireOnSession) {
6174
6802
  await options.route.onCallStart?.({
6175
6803
  api,
@@ -7546,10 +8174,981 @@ var runVoiceSessionBenchmarkSeries = async (input) => {
7546
8174
  });
7547
8175
  };
7548
8176
  // src/telephony/twilio.ts
7549
- import { Buffer as Buffer2 } from "buffer";
8177
+ import { Buffer as Buffer3 } from "buffer";
8178
+ import { Elysia as Elysia2 } from "elysia";
8179
+
8180
+ // src/telephonyOutcome.ts
8181
+ import { Elysia } from "elysia";
8182
+ var DEFAULT_COMPLETED_STATUSES = [
8183
+ "answered",
8184
+ "completed",
8185
+ "complete",
8186
+ "connected",
8187
+ "in-progress",
8188
+ "live"
8189
+ ];
8190
+ var DEFAULT_NO_ANSWER_STATUSES = [
8191
+ "busy",
8192
+ "canceled",
8193
+ "cancelled",
8194
+ "failed",
8195
+ "no-answer",
8196
+ "no_answer",
8197
+ "not-answered",
8198
+ "ring-no-answer",
8199
+ "timeout",
8200
+ "unanswered"
8201
+ ];
8202
+ var DEFAULT_VOICEMAIL_STATUSES = [
8203
+ "answering-machine",
8204
+ "machine",
8205
+ "voicemail",
8206
+ "voice-mail"
8207
+ ];
8208
+ var DEFAULT_TRANSFER_STATUSES = ["bridged", "forwarded", "transferred"];
8209
+ var DEFAULT_ESCALATION_STATUSES = ["escalated", "human-required", "operator"];
8210
+ var DEFAULT_FAILED_STATUSES = ["busy", "failed", "no-answer"];
8211
+ var DEFAULT_MACHINE_VOICEMAIL_VALUES = [
8212
+ "answering-machine",
8213
+ "fax",
8214
+ "machine",
8215
+ "machine-end-beep",
8216
+ "machine-end-other",
8217
+ "machine-start",
8218
+ "voicemail"
8219
+ ];
8220
+ var DEFAULT_NO_ANSWER_SIP_CODES = [408, 480, 486, 487, 603];
8221
+ var isRecord = (value) => Boolean(value) && typeof value === "object" && !Array.isArray(value);
8222
+ var uniqueSorted = (values) => Array.from(new Set(values)).sort();
8223
+ var findMissing = (values, required) => {
8224
+ if (!required?.length) {
8225
+ return [];
8226
+ }
8227
+ const valueSet = new Set(values);
8228
+ return required.filter((value) => !valueSet.has(value));
8229
+ };
8230
+
8231
+ class VoiceTelephonyWebhookVerificationError extends Error {
8232
+ result;
8233
+ constructor(result) {
8234
+ super(result.ok ? "telephony webhook verified" : result.reason);
8235
+ this.name = "VoiceTelephonyWebhookVerificationError";
8236
+ this.result = result;
8237
+ }
8238
+ }
8239
+ var createMemoryVoiceTelephonyWebhookIdempotencyStore = () => {
8240
+ const decisions = new Map;
8241
+ return {
8242
+ get: (key) => decisions.get(key),
8243
+ set: (key, decision) => {
8244
+ decisions.set(key, decision);
8245
+ }
8246
+ };
8247
+ };
8248
+ var isTelephonyWebhookProvider = (value) => value === "generic" || value === "plivo" || value === "telnyx" || value === "twilio";
8249
+ var isTelephonyOutcomeAction = (value) => value === "complete" || value === "escalate" || value === "ignore" || value === "no-answer" || value === "transfer" || value === "voicemail";
8250
+ var isCallDisposition = (value) => value === "completed" || value === "escalated" || value === "failed" || value === "no-answer" || value === "transferred" || value === "voicemail";
8251
+ var evaluateVoiceTelephonyWebhookNormalizationEvidence = (input = {}) => {
8252
+ const issues = [];
8253
+ const decisions = input.decisions ?? [];
8254
+ const verificationAttempts = input.verificationAttempts ?? [];
8255
+ const actions = uniqueSorted(decisions.map((decision) => decision.decision?.action ?? decision.action).filter(isTelephonyOutcomeAction));
8256
+ const dispositions = uniqueSorted(decisions.map((decision) => decision.decision?.disposition ?? decision.disposition).filter(isCallDisposition));
8257
+ const providers = uniqueSorted(decisions.map((decision) => decision.provider ?? decision.event?.provider).filter(isTelephonyWebhookProvider));
8258
+ const sources = uniqueSorted(decisions.map((decision) => decision.decision?.source ?? decision.source).filter((source) => typeof source === "string"));
8259
+ const applied = decisions.filter((decision) => decision.applied === true).length;
8260
+ const duplicateDecisions = decisions.filter((decision) => decision.duplicate === true);
8261
+ const duplicateProviders = uniqueSorted(duplicateDecisions.map((decision) => decision.provider ?? decision.event?.provider).filter(isTelephonyWebhookProvider));
8262
+ const duplicateIdempotencyKeys = new Set(duplicateDecisions.map((decision) => decision.idempotencyKey).filter((key) => typeof key === "string" && key.length > 0)).size;
8263
+ const duplicateCampaignOutcomesApplied = duplicateDecisions.filter((decision) => isRecord(decision.campaignOutcome) && decision.campaignOutcome.applied === true).length;
8264
+ const duplicateOutcomeReasons = uniqueSorted(duplicateDecisions.map((decision) => isRecord(decision.campaignOutcome) ? decision.campaignOutcome.reason : undefined).filter((reason) => typeof reason === "string"));
8265
+ const routeResults = decisions.filter((decision) => isRecord(decision.routeResult)).length;
8266
+ const missingSessionIds = decisions.filter((decision) => !decision.sessionId).length;
8267
+ const rejectedVerificationAttempts = verificationAttempts.filter((attempt) => attempt.rejected === true || attempt.status === 401 || attempt.verification?.ok === false && attempt.verification.reason === "invalid-signature");
8268
+ const rejectedVerificationProviders = uniqueSorted(rejectedVerificationAttempts.map((attempt) => attempt.provider).filter(isTelephonyWebhookProvider));
8269
+ const replayRejectedVerificationAttempts = rejectedVerificationAttempts.filter((attempt) => attempt.replayRejected === true);
8270
+ const replayRejectedVerificationProviders = uniqueSorted(replayRejectedVerificationAttempts.map((attempt) => attempt.provider).filter(isTelephonyWebhookProvider));
8271
+ const rejectedVerificationSideEffects = rejectedVerificationAttempts.reduce((total, attempt) => total + Math.max(0, attempt.sideEffects ?? 0), 0);
8272
+ if (input.minDecisions !== undefined && decisions.length < input.minDecisions) {
8273
+ issues.push(`Expected at least ${String(input.minDecisions)} telephony webhook decision(s), found ${String(decisions.length)}.`);
8274
+ }
8275
+ if (input.minApplied !== undefined && applied < input.minApplied) {
8276
+ issues.push(`Expected at least ${String(input.minApplied)} applied telephony webhook decision(s), found ${String(applied)}.`);
8277
+ }
8278
+ if (input.minDuplicates !== undefined && duplicateDecisions.length < input.minDuplicates) {
8279
+ issues.push(`Expected at least ${String(input.minDuplicates)} duplicate telephony webhook decision(s), found ${String(duplicateDecisions.length)}.`);
8280
+ }
8281
+ if (input.minDuplicateIdempotencyKeys !== undefined && duplicateIdempotencyKeys < input.minDuplicateIdempotencyKeys) {
8282
+ issues.push(`Expected at least ${String(input.minDuplicateIdempotencyKeys)} duplicate telephony webhook idempotency key(s), found ${String(duplicateIdempotencyKeys)}.`);
8283
+ }
8284
+ if (input.maxDuplicateCampaignOutcomesApplied !== undefined && duplicateCampaignOutcomesApplied > input.maxDuplicateCampaignOutcomesApplied) {
8285
+ issues.push(`Expected at most ${String(input.maxDuplicateCampaignOutcomesApplied)} duplicate telephony webhook campaign outcome application(s), found ${String(duplicateCampaignOutcomesApplied)}.`);
8286
+ }
8287
+ if (input.minRejectedVerificationAttempts !== undefined && rejectedVerificationAttempts.length < input.minRejectedVerificationAttempts) {
8288
+ issues.push(`Expected at least ${String(input.minRejectedVerificationAttempts)} rejected telephony webhook verification attempt(s), found ${String(rejectedVerificationAttempts.length)}.`);
8289
+ }
8290
+ if (input.maxRejectedVerificationSideEffects !== undefined && rejectedVerificationSideEffects > input.maxRejectedVerificationSideEffects) {
8291
+ issues.push(`Expected at most ${String(input.maxRejectedVerificationSideEffects)} rejected telephony webhook side effect(s), found ${String(rejectedVerificationSideEffects)}.`);
8292
+ }
8293
+ if (input.minReplayRejectedVerificationAttempts !== undefined && replayRejectedVerificationAttempts.length < input.minReplayRejectedVerificationAttempts) {
8294
+ issues.push(`Expected at least ${String(input.minReplayRejectedVerificationAttempts)} replay-rejected telephony webhook verification attempt(s), found ${String(replayRejectedVerificationAttempts.length)}.`);
8295
+ }
8296
+ if (input.maxMissingSessionIds !== undefined && missingSessionIds > input.maxMissingSessionIds) {
8297
+ issues.push(`Expected at most ${String(input.maxMissingSessionIds)} telephony webhook decision(s) without sessionId, found ${String(missingSessionIds)}.`);
8298
+ }
8299
+ if (input.requireRouteResults && routeResults < decisions.length) {
8300
+ issues.push(`Expected every telephony webhook decision to include a route result, found ${String(routeResults)} of ${String(decisions.length)}.`);
8301
+ }
8302
+ for (const provider of findMissing(providers, input.requiredProviders)) {
8303
+ issues.push(`Missing telephony webhook provider: ${provider}.`);
8304
+ }
8305
+ for (const provider of findMissing(duplicateProviders, input.requiredDuplicateProviders)) {
8306
+ issues.push(`Missing duplicate telephony webhook provider: ${provider}.`);
8307
+ }
8308
+ for (const provider of findMissing(rejectedVerificationProviders, input.requiredRejectedVerificationProviders)) {
8309
+ issues.push(`Missing rejected telephony webhook verification provider: ${provider}.`);
8310
+ }
8311
+ for (const provider of findMissing(replayRejectedVerificationProviders, input.requiredReplayRejectedVerificationProviders)) {
8312
+ issues.push(`Missing replay-rejected telephony webhook verification provider: ${provider}.`);
8313
+ }
8314
+ for (const action of findMissing(actions, input.requiredActions)) {
8315
+ issues.push(`Missing telephony webhook action: ${action}.`);
8316
+ }
8317
+ for (const disposition of findMissing(dispositions, input.requiredDispositions)) {
8318
+ issues.push(`Missing telephony webhook disposition: ${disposition}.`);
8319
+ }
8320
+ return {
8321
+ actions,
8322
+ applied,
8323
+ decisions: decisions.length,
8324
+ dispositions,
8325
+ duplicateCampaignOutcomesApplied,
8326
+ duplicateIdempotencyKeys,
8327
+ duplicateOutcomeReasons,
8328
+ duplicateProviders,
8329
+ duplicates: duplicateDecisions.length,
8330
+ issues,
8331
+ missingSessionIds,
8332
+ ok: issues.length === 0,
8333
+ providers,
8334
+ rejectedVerificationAttempts: rejectedVerificationAttempts.length,
8335
+ rejectedVerificationProviders,
8336
+ rejectedVerificationSideEffects,
8337
+ replayRejectedVerificationAttempts: replayRejectedVerificationAttempts.length,
8338
+ replayRejectedVerificationProviders,
8339
+ routeResults,
8340
+ sources,
8341
+ verificationAttempts: verificationAttempts.length
8342
+ };
8343
+ };
8344
+ var assertVoiceTelephonyWebhookNormalizationEvidence = (input = {}) => {
8345
+ const assertion = evaluateVoiceTelephonyWebhookNormalizationEvidence(input);
8346
+ if (!assertion.ok) {
8347
+ throw new Error(`Voice telephony webhook normalization evidence assertion failed: ${assertion.issues.join(" ")}`);
8348
+ }
8349
+ return assertion;
8350
+ };
8351
+ var normalizeToken = (value) => typeof value === "string" ? value.trim().toLowerCase().replace(/\s+/g, "-").replace(/_+/g, "-") : undefined;
8352
+ var firstString = (source, keys) => {
8353
+ for (const key of keys) {
8354
+ const value = source[key];
8355
+ if (typeof value === "string" && value.trim()) {
8356
+ return value.trim();
8357
+ }
8358
+ if (typeof value === "number" && Number.isFinite(value)) {
8359
+ return String(value);
8360
+ }
8361
+ }
8362
+ };
8363
+ var firstNumber = (source, keys) => {
8364
+ for (const key of keys) {
8365
+ const value = source[key];
8366
+ if (typeof value === "number" && Number.isFinite(value)) {
8367
+ return value;
8368
+ }
8369
+ if (typeof value === "string" && value.trim()) {
8370
+ const parsed = Number(value);
8371
+ if (Number.isFinite(parsed)) {
8372
+ return parsed;
8373
+ }
8374
+ }
8375
+ }
8376
+ };
8377
+ var parseMaybeJSON = (value) => {
8378
+ try {
8379
+ return JSON.parse(value);
8380
+ } catch {
8381
+ return;
8382
+ }
8383
+ };
8384
+ var flattenPayload = (value) => {
8385
+ if (!isRecord(value)) {
8386
+ return {};
8387
+ }
8388
+ const data = isRecord(value.data) ? value.data : undefined;
8389
+ const payload = isRecord(value.payload) ? value.payload : undefined;
8390
+ const event = isRecord(value.event) ? value.event : undefined;
8391
+ return {
8392
+ ...value,
8393
+ ...payload,
8394
+ ...event,
8395
+ ...data,
8396
+ ...isRecord(data?.payload) ? data.payload : undefined
8397
+ };
8398
+ };
8399
+ var toBase64 = (bytes) => Buffer.from(new Uint8Array(bytes)).toString("base64");
8400
+ var timingSafeEqual = (left, right) => {
8401
+ const encoder = new TextEncoder;
8402
+ const leftBytes = encoder.encode(left);
8403
+ const rightBytes = encoder.encode(right);
8404
+ if (leftBytes.length !== rightBytes.length) {
8405
+ return false;
8406
+ }
8407
+ let diff = 0;
8408
+ for (let index = 0;index < leftBytes.length; index += 1) {
8409
+ diff |= leftBytes[index] ^ rightBytes[index];
8410
+ }
8411
+ return diff === 0;
8412
+ };
8413
+ var signHmacSHA1Base64 = async (secret, payload) => {
8414
+ const encoder = new TextEncoder;
8415
+ const key = await crypto.subtle.importKey("raw", encoder.encode(secret), {
8416
+ hash: "SHA-1",
8417
+ name: "HMAC"
8418
+ }, false, ["sign"]);
8419
+ const signature = await crypto.subtle.sign("HMAC", key, encoder.encode(payload));
8420
+ return toBase64(signature);
8421
+ };
8422
+ var sortedParamsForSignature = (body) => Object.entries(flattenPayload(body)).filter(([, value]) => value !== undefined && value !== null).sort(([left], [right]) => left.localeCompare(right)).map(([key, value]) => `${key}${String(value)}`).join("");
8423
+ var normalizeList = (values, fallback) => new Set((values ?? fallback).map(normalizeToken).filter(Boolean));
8424
+ var metadataValue = (metadata, keys) => {
8425
+ for (const key of keys) {
8426
+ const value = metadata?.[key];
8427
+ if (typeof value === "string" && value.trim()) {
8428
+ return value.trim();
8429
+ }
8430
+ }
8431
+ };
8432
+ var resolveTransferTarget = (event, policy) => {
8433
+ if (typeof event.target === "string" && event.target.trim()) {
8434
+ return event.target.trim();
8435
+ }
8436
+ const metadataTarget = metadataValue(event.metadata, [
8437
+ "transferTarget",
8438
+ "target",
8439
+ "queue",
8440
+ "department"
8441
+ ]);
8442
+ if (metadataTarget) {
8443
+ return metadataTarget;
8444
+ }
8445
+ if (typeof policy.transferTarget === "function") {
8446
+ const target = policy.transferTarget(event);
8447
+ return typeof target === "string" && target.trim() ? target.trim() : undefined;
8448
+ }
8449
+ return typeof policy.transferTarget === "string" && policy.transferTarget.trim() ? policy.transferTarget.trim() : undefined;
8450
+ };
8451
+ var mergeMetadata = (event, policy) => ({
8452
+ ...policy.includeProviderPayload ? {
8453
+ answeredBy: event.answeredBy,
8454
+ durationMs: event.durationMs,
8455
+ provider: event.provider,
8456
+ reason: event.reason,
8457
+ sipCode: event.sipCode,
8458
+ status: event.status
8459
+ } : undefined,
8460
+ ...policy.metadata,
8461
+ ...event.metadata
8462
+ });
8463
+ var withDecisionDefaults = (decision, input) => {
8464
+ if (typeof decision === "string") {
8465
+ return buildDecision(decision, input);
8466
+ }
8467
+ return {
8468
+ ...buildDecision(decision.action, input),
8469
+ ...decision,
8470
+ confidence: decision.confidence ?? "high",
8471
+ metadata: {
8472
+ ...mergeMetadata(input.event, input.policy),
8473
+ ...decision.metadata
8474
+ },
8475
+ source: decision.source ?? input.source,
8476
+ target: decision.target ?? (decision.action === "transfer" ? resolveTransferTarget(input.event, input.policy) : undefined)
8477
+ };
8478
+ };
8479
+ var dispositionForAction = (action) => {
8480
+ switch (action) {
8481
+ case "complete":
8482
+ return "completed";
8483
+ case "escalate":
8484
+ return "escalated";
8485
+ case "no-answer":
8486
+ return "no-answer";
8487
+ case "transfer":
8488
+ return "transferred";
8489
+ case "voicemail":
8490
+ return "voicemail";
8491
+ default:
8492
+ return;
8493
+ }
8494
+ };
8495
+ var buildDecision = (action, input) => ({
8496
+ action,
8497
+ confidence: action === "ignore" ? "low" : "high",
8498
+ disposition: dispositionForAction(action),
8499
+ metadata: mergeMetadata(input.event, input.policy),
8500
+ reason: input.event.reason,
8501
+ source: input.source,
8502
+ target: action === "transfer" ? resolveTransferTarget(input.event, input.policy) : undefined
8503
+ });
8504
+ var createVoiceTelephonyOutcomePolicy = (policy = {}) => ({
8505
+ completedStatuses: policy.completedStatuses ?? DEFAULT_COMPLETED_STATUSES,
8506
+ escalationStatuses: policy.escalationStatuses ?? DEFAULT_ESCALATION_STATUSES,
8507
+ failedAsNoAnswer: policy.failedAsNoAnswer ?? true,
8508
+ failedStatuses: policy.failedStatuses ?? DEFAULT_FAILED_STATUSES,
8509
+ includeProviderPayload: policy.includeProviderPayload ?? true,
8510
+ machineDetectionVoicemailValues: policy.machineDetectionVoicemailValues ?? DEFAULT_MACHINE_VOICEMAIL_VALUES,
8511
+ metadata: policy.metadata,
8512
+ minAnsweredDurationMs: policy.minAnsweredDurationMs,
8513
+ noAnswerOnZeroDuration: policy.noAnswerOnZeroDuration ?? true,
8514
+ noAnswerSipCodes: policy.noAnswerSipCodes ?? DEFAULT_NO_ANSWER_SIP_CODES,
8515
+ noAnswerStatuses: policy.noAnswerStatuses ?? DEFAULT_NO_ANSWER_STATUSES,
8516
+ statusMap: policy.statusMap,
8517
+ transferStatuses: policy.transferStatuses ?? DEFAULT_TRANSFER_STATUSES,
8518
+ transferTarget: policy.transferTarget,
8519
+ voicemailStatuses: policy.voicemailStatuses ?? DEFAULT_VOICEMAIL_STATUSES
8520
+ });
8521
+ var resolveVoiceTelephonyOutcome = (event, policyInput = {}) => {
8522
+ const policy = createVoiceTelephonyOutcomePolicy(policyInput);
8523
+ const status = normalizeToken(event.status);
8524
+ const provider = normalizeToken(event.provider);
8525
+ const answeredBy = normalizeToken(event.answeredBy);
8526
+ const target = resolveTransferTarget(event, policy);
8527
+ if (status) {
8528
+ const mapped = policy.statusMap?.[status] ?? (provider ? policy.statusMap?.[`${provider}:${status}`] : undefined);
8529
+ if (mapped) {
8530
+ return withDecisionDefaults(mapped, {
8531
+ event,
8532
+ policy,
8533
+ source: "policy"
8534
+ });
8535
+ }
8536
+ }
8537
+ if (answeredBy && normalizeList(policy.machineDetectionVoicemailValues, []).has(answeredBy)) {
8538
+ return buildDecision("voicemail", { event, policy, source: "answered-by" });
8539
+ }
8540
+ if (typeof event.sipCode === "number" && policy.noAnswerSipCodes.includes(event.sipCode)) {
8541
+ return buildDecision("no-answer", { event, policy, source: "sip" });
8542
+ }
8543
+ if (target && status && normalizeList(policy.transferStatuses, []).has(status)) {
8544
+ return buildDecision("transfer", { event, policy, source: "status" });
8545
+ }
8546
+ if (status && normalizeList(policy.voicemailStatuses, []).has(status)) {
8547
+ return buildDecision("voicemail", { event, policy, source: "status" });
8548
+ }
8549
+ if (status && normalizeList(policy.escalationStatuses, []).has(status)) {
8550
+ return buildDecision("escalate", { event, policy, source: "status" });
8551
+ }
8552
+ if (status && (policy.failedAsNoAnswer ? normalizeList(policy.noAnswerStatuses, []).has(status) || normalizeList(policy.failedStatuses, []).has(status) : normalizeList(policy.noAnswerStatuses, []).has(status))) {
8553
+ return buildDecision("no-answer", { event, policy, source: "status" });
8554
+ }
8555
+ if (policy.noAnswerOnZeroDuration && typeof event.durationMs === "number" && event.durationMs <= 0) {
8556
+ return buildDecision("no-answer", { event, policy, source: "duration" });
8557
+ }
8558
+ if (typeof policy.minAnsweredDurationMs === "number" && typeof event.durationMs === "number" && event.durationMs < policy.minAnsweredDurationMs) {
8559
+ return {
8560
+ ...buildDecision("no-answer", { event, policy, source: "duration" }),
8561
+ confidence: "medium"
8562
+ };
8563
+ }
8564
+ if (status && normalizeList(policy.completedStatuses, []).has(status)) {
8565
+ return buildDecision("complete", { event, policy, source: "status" });
8566
+ }
8567
+ if (target) {
8568
+ return {
8569
+ ...buildDecision("transfer", { event, policy, source: "explicit-target" }),
8570
+ confidence: "medium"
8571
+ };
8572
+ }
8573
+ return buildDecision("ignore", { event, policy, source: "status" });
8574
+ };
8575
+ var voiceTelephonyOutcomeToRouteResult = (decision, result) => {
8576
+ switch (decision.action) {
8577
+ case "complete":
8578
+ return { complete: true, result };
8579
+ case "escalate":
8580
+ return {
8581
+ escalate: {
8582
+ metadata: decision.metadata,
8583
+ reason: decision.reason ?? "telephony-escalation"
8584
+ },
8585
+ result
8586
+ };
8587
+ case "no-answer":
8588
+ return {
8589
+ noAnswer: {
8590
+ metadata: decision.metadata
8591
+ },
8592
+ result
8593
+ };
8594
+ case "transfer":
8595
+ if (!decision.target) {
8596
+ return { result };
8597
+ }
8598
+ return {
8599
+ result,
8600
+ transfer: {
8601
+ metadata: decision.metadata,
8602
+ reason: decision.reason,
8603
+ target: decision.target
8604
+ }
8605
+ };
8606
+ case "voicemail":
8607
+ return {
8608
+ result,
8609
+ voicemail: {
8610
+ metadata: decision.metadata
8611
+ }
8612
+ };
8613
+ default:
8614
+ return { result };
8615
+ }
8616
+ };
8617
+ var applyVoiceTelephonyOutcome = async (api, decision, result) => {
8618
+ switch (decision.action) {
8619
+ case "complete":
8620
+ await api.complete(result);
8621
+ break;
8622
+ case "escalate":
8623
+ await api.escalate({
8624
+ metadata: decision.metadata,
8625
+ reason: decision.reason ?? "telephony-escalation",
8626
+ result
8627
+ });
8628
+ break;
8629
+ case "no-answer":
8630
+ await api.markNoAnswer({
8631
+ metadata: decision.metadata,
8632
+ result
8633
+ });
8634
+ break;
8635
+ case "transfer":
8636
+ if (!decision.target) {
8637
+ return;
8638
+ }
8639
+ await api.transfer({
8640
+ metadata: decision.metadata,
8641
+ reason: decision.reason,
8642
+ result,
8643
+ target: decision.target
8644
+ });
8645
+ break;
8646
+ case "voicemail":
8647
+ await api.markVoicemail({
8648
+ metadata: decision.metadata,
8649
+ result
8650
+ });
8651
+ break;
8652
+ default:
8653
+ break;
8654
+ }
8655
+ };
8656
+ var parseRequestBodyText = (input) => {
8657
+ const { contentType, text } = input;
8658
+ if (!text) {
8659
+ return {};
8660
+ }
8661
+ if (contentType.includes("application/json")) {
8662
+ return parseMaybeJSON(text) ?? {};
8663
+ }
8664
+ if (contentType.includes("application/x-www-form-urlencoded") || contentType.includes("multipart/form-data")) {
8665
+ return Object.fromEntries(new URLSearchParams(text));
8666
+ }
8667
+ return parseMaybeJSON(text) ?? Object.fromEntries(new URLSearchParams(text));
8668
+ };
8669
+ var readRequestBody = async (request) => {
8670
+ const contentType = request.headers.get("content-type") ?? "";
8671
+ const text = await request.text();
8672
+ return {
8673
+ body: parseRequestBodyText({ contentType, text }),
8674
+ rawBody: text
8675
+ };
8676
+ };
8677
+ var signVoiceTwilioWebhook = async (input) => signHmacSHA1Base64(input.authToken, `${input.url}${sortedParamsForSignature(input.body ?? {})}`);
8678
+ var verifyVoiceTwilioWebhookSignature = async (input) => {
8679
+ if (!input.authToken) {
8680
+ return { ok: false, reason: "missing-secret" };
8681
+ }
8682
+ const signature = input.headers.get("x-twilio-signature");
8683
+ if (!signature) {
8684
+ return { ok: false, reason: "missing-signature" };
8685
+ }
8686
+ const expected = await signVoiceTwilioWebhook({
8687
+ authToken: input.authToken,
8688
+ body: input.body,
8689
+ url: input.url
8690
+ });
8691
+ return timingSafeEqual(signature, expected) ? { ok: true } : { ok: false, reason: "invalid-signature" };
8692
+ };
8693
+ var resolveVerificationUrl = (option, input) => typeof option === "function" ? option(input) : option ?? input.request.url;
8694
+ var verifyVoiceTelephonyWebhook = async (input) => {
8695
+ if (input.options.verify) {
8696
+ return input.options.verify({
8697
+ body: input.body,
8698
+ headers: input.request.headers,
8699
+ provider: input.provider,
8700
+ query: input.query,
8701
+ rawBody: input.rawBody,
8702
+ request: input.request
8703
+ });
8704
+ }
8705
+ if (!input.options.signingSecret) {
8706
+ return input.options.requireVerification ? { ok: false, reason: "missing-secret" } : { ok: true };
8707
+ }
8708
+ if (input.provider !== "twilio") {
8709
+ return { ok: false, reason: "unsupported-provider" };
8710
+ }
8711
+ return verifyVoiceTwilioWebhookSignature({
8712
+ authToken: input.options.signingSecret,
8713
+ body: input.body,
8714
+ headers: input.request.headers,
8715
+ url: resolveVerificationUrl(input.options.verificationUrl, {
8716
+ query: input.query,
8717
+ request: input.request
8718
+ })
8719
+ });
8720
+ };
8721
+ var durationMsFromSeconds = (value) => typeof value === "number" ? value * 1000 : undefined;
8722
+ var parseVoiceTelephonyWebhookEvent = (input) => {
8723
+ const payload = flattenPayload(input.body);
8724
+ const provider = firstString(payload, ["provider", "Provider"]) ?? input.provider;
8725
+ const status = firstString(payload, [
8726
+ "CallStatus",
8727
+ "call_status",
8728
+ "callStatus",
8729
+ "DialCallStatus",
8730
+ "dial_call_status",
8731
+ "status",
8732
+ "event_type",
8733
+ "type"
8734
+ ]);
8735
+ const durationMs = firstNumber(payload, ["durationMs", "duration_ms"]) ?? durationMsFromSeconds(firstNumber(payload, [
8736
+ "CallDuration",
8737
+ "call_duration",
8738
+ "callDuration",
8739
+ "DialCallDuration",
8740
+ "dial_call_duration",
8741
+ "duration"
8742
+ ]));
8743
+ const sipCode = firstNumber(payload, [
8744
+ "SipResponseCode",
8745
+ "sip_response_code",
8746
+ "sipCode",
8747
+ "sip_code",
8748
+ "hangupCauseCode"
8749
+ ]);
8750
+ const from = firstString(payload, ["From", "from", "caller_id", "callerId"]);
8751
+ const to = firstString(payload, ["To", "to", "called_number", "calledNumber"]);
8752
+ const target = firstString(payload, [
8753
+ "transferTarget",
8754
+ "TransferTarget",
8755
+ "target",
8756
+ "queue",
8757
+ "department"
8758
+ ]);
8759
+ return {
8760
+ answeredBy: firstString(payload, [
8761
+ "AnsweredBy",
8762
+ "answered_by",
8763
+ "answeredBy",
8764
+ "machineDetection",
8765
+ "machine_detection"
8766
+ ]),
8767
+ durationMs,
8768
+ from,
8769
+ metadata: {
8770
+ ...input.query,
8771
+ ...payload
8772
+ },
8773
+ provider,
8774
+ reason: firstString(payload, [
8775
+ "Reason",
8776
+ "reason",
8777
+ "HangupCause",
8778
+ "hangup_cause",
8779
+ "hangupCause"
8780
+ ]),
8781
+ sipCode,
8782
+ status,
8783
+ target,
8784
+ to
8785
+ };
8786
+ };
8787
+ var defaultSessionId = (input) => {
8788
+ const payload = flattenPayload(input.body);
8789
+ const metadataSessionId = input.event.metadata?.sessionId;
8790
+ return firstString(input.query, ["sessionId", "session_id"]) ?? firstString(payload, [
8791
+ "sessionId",
8792
+ "session_id",
8793
+ "SessionId",
8794
+ "CallSid",
8795
+ "call_sid",
8796
+ "callSid",
8797
+ "CallUUID",
8798
+ "call_uuid",
8799
+ "callControlId",
8800
+ "call_control_id"
8801
+ ]) ?? (typeof metadataSessionId === "string" ? metadataSessionId : undefined);
8802
+ };
8803
+ var defaultIdempotencyKey = (input) => {
8804
+ const payload = flattenPayload(input.body);
8805
+ const eventId = firstString(payload, [
8806
+ "id",
8807
+ "event_id",
8808
+ "eventId",
8809
+ "EventSid",
8810
+ "event_sid",
8811
+ "MessageSid",
8812
+ "message_sid",
8813
+ "CallSid",
8814
+ "call_sid",
8815
+ "CallUUID",
8816
+ "call_uuid",
8817
+ "callControlId",
8818
+ "call_control_id"
8819
+ ]);
8820
+ const status = normalizeToken(input.event.status) ?? "unknown";
8821
+ if (eventId) {
8822
+ return `${input.provider}:${eventId}:${status}`;
8823
+ }
8824
+ if (input.sessionId) {
8825
+ return `${input.provider}:${input.sessionId}:${status}`;
8826
+ }
8827
+ };
8828
+ var createVoiceTelephonyWebhookHandler = (options = {}) => async (input) => {
8829
+ const provider = options.provider ?? "generic";
8830
+ const query = input.query ?? {};
8831
+ const { body, rawBody } = await readRequestBody(input.request);
8832
+ const verification = await verifyVoiceTelephonyWebhook({
8833
+ body,
8834
+ options,
8835
+ provider,
8836
+ query,
8837
+ rawBody,
8838
+ request: input.request
8839
+ });
8840
+ if (!verification.ok) {
8841
+ throw new VoiceTelephonyWebhookVerificationError(verification);
8842
+ }
8843
+ const event = options.parse ? await options.parse({
8844
+ body,
8845
+ headers: input.request.headers,
8846
+ provider,
8847
+ query,
8848
+ request: input.request
8849
+ }) : parseVoiceTelephonyWebhookEvent({
8850
+ body,
8851
+ headers: input.request.headers,
8852
+ provider,
8853
+ query,
8854
+ request: input.request
8855
+ });
8856
+ const sessionId = await (options.resolveSessionId?.({
8857
+ body,
8858
+ event,
8859
+ query,
8860
+ request: input.request
8861
+ }) ?? defaultSessionId({ body, event, query }));
8862
+ const idempotencyEnabled = options.idempotency?.enabled !== false;
8863
+ const idempotencyKey = idempotencyEnabled ? await (options.idempotency?.key?.({
8864
+ body,
8865
+ event,
8866
+ provider,
8867
+ query,
8868
+ request: input.request,
8869
+ sessionId
8870
+ }) ?? defaultIdempotencyKey({ body, event, provider, sessionId })) : undefined;
8871
+ const idempotencyStore = options.idempotency?.store;
8872
+ if (idempotencyKey && idempotencyStore) {
8873
+ const existing = await idempotencyStore.get(idempotencyKey);
8874
+ if (existing) {
8875
+ const duplicateDecision = {
8876
+ ...existing,
8877
+ duplicate: true
8878
+ };
8879
+ await options.onDecision?.({
8880
+ ...duplicateDecision,
8881
+ context: options.context,
8882
+ request: input.request
8883
+ });
8884
+ return duplicateDecision;
8885
+ }
8886
+ }
8887
+ const decision = resolveVoiceTelephonyOutcome(event, options.policy);
8888
+ const resultResolver = options.result;
8889
+ const result = typeof resultResolver === "function" ? await resultResolver({
8890
+ decision,
8891
+ event,
8892
+ sessionId
8893
+ }) : resultResolver;
8894
+ const routeResult = voiceTelephonyOutcomeToRouteResult(decision, result);
8895
+ const shouldApply = typeof options.apply === "function" ? options.apply({
8896
+ applied: false,
8897
+ decision,
8898
+ event,
8899
+ routeResult,
8900
+ sessionId
8901
+ }) : options.apply === true;
8902
+ let applied = false;
8903
+ if (shouldApply && decision.action !== "ignore" && options.getSessionHandle) {
8904
+ const api = await options.getSessionHandle({
8905
+ context: options.context,
8906
+ decision,
8907
+ event,
8908
+ request: input.request,
8909
+ sessionId
8910
+ });
8911
+ if (api) {
8912
+ await applyVoiceTelephonyOutcome(api, decision, result);
8913
+ applied = true;
8914
+ }
8915
+ }
8916
+ const webhookDecision = {
8917
+ applied,
8918
+ decision,
8919
+ event,
8920
+ idempotencyKey,
8921
+ routeResult,
8922
+ sessionId
8923
+ };
8924
+ if (idempotencyKey && idempotencyStore) {
8925
+ const now = Date.now();
8926
+ await idempotencyStore.set(idempotencyKey, {
8927
+ ...webhookDecision,
8928
+ createdAt: now,
8929
+ updatedAt: now
8930
+ });
8931
+ }
8932
+ await options.onDecision?.({
8933
+ ...webhookDecision,
8934
+ context: options.context,
8935
+ request: input.request
8936
+ });
8937
+ return webhookDecision;
8938
+ };
8939
+ var createVoiceTelephonyWebhookRoutes = (options = {}) => {
8940
+ const path = options.path ?? "/api/voice/telephony/webhook";
8941
+ const handler = createVoiceTelephonyWebhookHandler(options);
8942
+ return new Elysia({
8943
+ name: options.name ?? "absolutejs-voice-telephony-webhooks"
8944
+ }).post(path, async ({ query, request }) => {
8945
+ try {
8946
+ return await handler({ query, request });
8947
+ } catch (error) {
8948
+ if (error instanceof VoiceTelephonyWebhookVerificationError) {
8949
+ return new Response(JSON.stringify({ verification: error.result }), {
8950
+ headers: {
8951
+ "content-type": "application/json"
8952
+ },
8953
+ status: 401
8954
+ });
8955
+ }
8956
+ throw error;
8957
+ }
8958
+ }, {
8959
+ parse: "none"
8960
+ });
8961
+ };
8962
+
8963
+ // src/telephony/twilio.ts
7550
8964
  var TWILIO_MULAW_SAMPLE_RATE = 8000;
7551
8965
  var VOICE_PCM_SAMPLE_RATE = 16000;
7552
8966
  var escapeXml2 = (value) => value.replaceAll("&", "&amp;").replaceAll('"', "&quot;").replaceAll("'", "&apos;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
8967
+ var resolveRequestOrigin = (request) => {
8968
+ const url = new URL(request.url);
8969
+ const forwardedHost = request.headers.get("x-forwarded-host");
8970
+ const forwardedProto = request.headers.get("x-forwarded-proto");
8971
+ const host = forwardedHost ?? request.headers.get("host") ?? url.host;
8972
+ const protocol = forwardedProto ?? url.protocol.replace(":", "");
8973
+ return `${protocol}://${host}`;
8974
+ };
8975
+ var resolveTwilioStreamUrl = async (options, input) => {
8976
+ if (typeof options.twiml?.streamUrl === "function") {
8977
+ return options.twiml.streamUrl(input);
8978
+ }
8979
+ if (typeof options.twiml?.streamUrl === "string") {
8980
+ return options.twiml.streamUrl;
8981
+ }
8982
+ const origin = resolveRequestOrigin(input.request);
8983
+ const wsOrigin = origin.replace(/^http:/, "ws:").replace(/^https:/, "wss:");
8984
+ return `${wsOrigin}${input.streamPath}`;
8985
+ };
8986
+ var resolveTwilioStreamParameters = async (parameters, input) => {
8987
+ if (typeof parameters === "function") {
8988
+ return parameters(input);
8989
+ }
8990
+ return parameters;
8991
+ };
8992
+ var joinUrlPath = (origin, path) => `${origin.replace(/\/$/, "")}${path.startsWith("/") ? path : `/${path}`}`;
8993
+ var escapeHtml2 = (value) => value.replaceAll("&", "&amp;").replaceAll('"', "&quot;").replaceAll("'", "&#39;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
8994
+ var getWebhookVerificationUrl = (webhook, input) => {
8995
+ if (!webhook?.verificationUrl) {
8996
+ return;
8997
+ }
8998
+ if (typeof webhook.verificationUrl === "function") {
8999
+ return webhook.verificationUrl(input);
9000
+ }
9001
+ return webhook.verificationUrl;
9002
+ };
9003
+ var buildTwilioVoiceSetupStatus = async (options, input) => {
9004
+ const origin = resolveRequestOrigin(input.request);
9005
+ const stream = await resolveTwilioStreamUrl(options, input);
9006
+ const twiml = joinUrlPath(origin, input.twimlPath);
9007
+ const webhook = joinUrlPath(origin, input.webhookPath);
9008
+ const verificationUrl = getWebhookVerificationUrl(options.webhook, input);
9009
+ const missing = Object.entries(options.setup?.requiredEnv ?? {}).filter((entry) => !entry[1]).map(([name]) => name);
9010
+ const signingConfigured = Boolean(options.webhook?.signingSecret || options.webhook?.verify);
9011
+ const warnings = [
9012
+ ...stream.startsWith("wss://") ? [] : ["Twilio media streams should use wss:// in production."],
9013
+ ...signingConfigured ? [] : ["Webhook signature verification is not configured."],
9014
+ ...verificationUrl || !signingConfigured ? [] : ["Webhook signing is configured without an explicit verification URL."]
9015
+ ];
9016
+ return {
9017
+ generatedAt: Date.now(),
9018
+ missing,
9019
+ provider: "twilio",
9020
+ ready: missing.length === 0 && signingConfigured && warnings.length === 0,
9021
+ signing: {
9022
+ configured: signingConfigured,
9023
+ mode: options.webhook?.verify ? "custom" : options.webhook?.signingSecret ? "twilio-signature" : "none",
9024
+ verificationUrl
9025
+ },
9026
+ urls: {
9027
+ stream,
9028
+ twiml,
9029
+ webhook
9030
+ },
9031
+ warnings
9032
+ };
9033
+ };
9034
+ var renderTwilioVoiceSetupHTML = (status, title) => `<main style="font-family: ui-sans-serif, system-ui; max-width: 860px; margin: 40px auto; padding: 0 20px;">
9035
+ <p style="letter-spacing: .12em; text-transform: uppercase; color: #52606d;">Twilio setup</p>
9036
+ <h1>${escapeHtml2(title)}</h1>
9037
+ <p><strong>Status:</strong> ${status.ready ? "Ready" : "Needs attention"}</p>
9038
+ <section>
9039
+ <h2>URLs</h2>
9040
+ <ul>
9041
+ <li><strong>TwiML:</strong> <code>${escapeHtml2(status.urls.twiml)}</code></li>
9042
+ <li><strong>Media stream:</strong> <code>${escapeHtml2(status.urls.stream)}</code></li>
9043
+ <li><strong>Status webhook:</strong> <code>${escapeHtml2(status.urls.webhook)}</code></li>
9044
+ </ul>
9045
+ </section>
9046
+ <section>
9047
+ <h2>Signing</h2>
9048
+ <p>Mode: <code>${status.signing.mode}</code></p>
9049
+ ${status.signing.verificationUrl ? `<p>Verification URL: <code>${escapeHtml2(status.signing.verificationUrl)}</code></p>` : ""}
9050
+ </section>
9051
+ ${status.missing.length ? `<section><h2>Missing env</h2><ul>${status.missing.map((name) => `<li><code>${escapeHtml2(name)}</code></li>`).join("")}</ul></section>` : ""}
9052
+ ${status.warnings.length ? `<section><h2>Warnings</h2><ul>${status.warnings.map((warning) => `<li>${escapeHtml2(warning)}</li>`).join("")}</ul></section>` : ""}
9053
+ </main>`;
9054
+ var extractTwilioStreamUrl = (twiml) => twiml.match(/<Stream\b[^>]*\surl="([^"]+)"/i)?.[1]?.replaceAll("&amp;", "&");
9055
+ var createSmokeCheck = (name, status, message, details) => ({
9056
+ details,
9057
+ message,
9058
+ name,
9059
+ status
9060
+ });
9061
+ var renderTwilioVoiceSmokeHTML = (report, title) => `<main style="font-family: ui-sans-serif, system-ui; max-width: 860px; margin: 40px auto; padding: 0 20px;">
9062
+ <p style="letter-spacing: .12em; text-transform: uppercase; color: #52606d;">Twilio smoke test</p>
9063
+ <h1>${escapeHtml2(title)}</h1>
9064
+ <p><strong>Status:</strong> ${report.pass ? "Pass" : "Fail"}</p>
9065
+ <section>
9066
+ <h2>Checks</h2>
9067
+ <ul>
9068
+ ${report.checks.map((check) => `<li><strong>${escapeHtml2(check.name)}</strong>: ${escapeHtml2(check.status)}${check.message ? ` - ${escapeHtml2(check.message)}` : ""}</li>`).join("")}
9069
+ </ul>
9070
+ </section>
9071
+ <section>
9072
+ <h2>Observed URLs</h2>
9073
+ <ul>
9074
+ <li><strong>TwiML:</strong> <code>${escapeHtml2(report.setup.urls.twiml)}</code></li>
9075
+ <li><strong>Stream:</strong> <code>${escapeHtml2(report.twiml?.streamUrl ?? report.setup.urls.stream)}</code></li>
9076
+ <li><strong>Webhook:</strong> <code>${escapeHtml2(report.setup.urls.webhook)}</code></li>
9077
+ </ul>
9078
+ </section>
9079
+ </main>`;
9080
+ var runTwilioVoiceSmokeTest = async (input) => {
9081
+ const setup = await buildTwilioVoiceSetupStatus(input.options, input);
9082
+ const checks = [];
9083
+ const twimlUrl = new URL(setup.urls.twiml);
9084
+ twimlUrl.searchParams.set("scenarioId", input.options.smoke?.scenarioId ?? "smoke");
9085
+ twimlUrl.searchParams.set("sessionId", input.options.smoke?.sessionId ?? "smoke-session");
9086
+ const twimlResponse = await input.app.handle(new Request(twimlUrl, {
9087
+ headers: input.request.headers
9088
+ }));
9089
+ const twiml = await twimlResponse.text();
9090
+ const streamUrl = extractTwilioStreamUrl(twiml);
9091
+ checks.push(createSmokeCheck("twiml", twimlResponse.ok && Boolean(streamUrl) ? "pass" : "fail", streamUrl ? "TwiML includes a media stream URL." : 'TwiML is missing <Stream url="...">.', {
9092
+ status: twimlResponse.status,
9093
+ streamUrl
9094
+ }));
9095
+ checks.push(createSmokeCheck("stream-url", streamUrl?.startsWith("wss://") ? "pass" : "fail", streamUrl?.startsWith("wss://") ? "Media stream URL uses wss://." : "Media stream URL should use wss:// for Twilio.", {
9096
+ streamUrl
9097
+ }));
9098
+ const webhookBody = {
9099
+ CallSid: input.options.smoke?.callSid ?? "CA_SMOKE_TEST",
9100
+ CallStatus: input.options.smoke?.status ?? "busy",
9101
+ SipResponseCode: String(input.options.smoke?.sipCode ?? 486)
9102
+ };
9103
+ const webhookHeaders = new Headers({
9104
+ "content-type": "application/x-www-form-urlencoded"
9105
+ });
9106
+ const verificationUrl = setup.signing.verificationUrl ?? setup.urls.webhook;
9107
+ if (input.options.webhook?.signingSecret) {
9108
+ webhookHeaders.set("x-twilio-signature", await signVoiceTwilioWebhook({
9109
+ authToken: input.options.webhook.signingSecret,
9110
+ body: webhookBody,
9111
+ url: verificationUrl
9112
+ }));
9113
+ }
9114
+ const webhookResponse = await input.app.handle(new Request(setup.urls.webhook, {
9115
+ body: new URLSearchParams(webhookBody),
9116
+ headers: webhookHeaders,
9117
+ method: "POST"
9118
+ }));
9119
+ const webhookText = await webhookResponse.text();
9120
+ const webhookPayload = (() => {
9121
+ try {
9122
+ return JSON.parse(webhookText);
9123
+ } catch {
9124
+ return webhookText;
9125
+ }
9126
+ })();
9127
+ checks.push(createSmokeCheck("webhook", webhookResponse.ok ? "pass" : "fail", webhookResponse.ok ? "Synthetic Twilio status callback was accepted." : "Synthetic Twilio status callback failed.", {
9128
+ status: webhookResponse.status
9129
+ }));
9130
+ for (const warning of setup.warnings) {
9131
+ checks.push(createSmokeCheck("setup-warning", "warn", warning));
9132
+ }
9133
+ for (const name of setup.missing) {
9134
+ checks.push(createSmokeCheck("missing-env", "fail", `${name} is missing.`));
9135
+ }
9136
+ return {
9137
+ checks,
9138
+ generatedAt: Date.now(),
9139
+ pass: checks.every((check) => check.status !== "fail"),
9140
+ provider: "twilio",
9141
+ setup,
9142
+ twiml: {
9143
+ status: twimlResponse.status,
9144
+ streamUrl
9145
+ },
9146
+ webhook: {
9147
+ body: webhookPayload,
9148
+ status: webhookResponse.status
9149
+ }
9150
+ };
9151
+ };
7553
9152
  var normalizeOnTurn = (handler) => {
7554
9153
  if (handler.length > 1) {
7555
9154
  const directHandler = handler;
@@ -7651,7 +9250,7 @@ var bytesToInt16Array = (bytes) => {
7651
9250
  return output;
7652
9251
  };
7653
9252
  var decodeTwilioMulawBase64 = (payload) => {
7654
- const bytes = Uint8Array.from(Buffer2.from(payload, "base64"));
9253
+ const bytes = Uint8Array.from(Buffer3.from(payload, "base64"));
7655
9254
  const samples = new Int16Array(bytes.length);
7656
9255
  for (let index = 0;index < bytes.length; index += 1) {
7657
9256
  samples[index] = decodeMulawSample(bytes[index] ?? 0);
@@ -7663,7 +9262,7 @@ var encodeTwilioMulawBase64 = (samples) => {
7663
9262
  for (let index = 0;index < samples.length; index += 1) {
7664
9263
  bytes[index] = encodeMulawSample(samples[index] ?? 0);
7665
9264
  }
7666
- return Buffer2.from(bytes).toString("base64");
9265
+ return Buffer3.from(bytes).toString("base64");
7667
9266
  };
7668
9267
  var transcodeTwilioInboundPayloadToPCM16 = (payload) => {
7669
9268
  const narrowband = decodeTwilioMulawBase64(payload);
@@ -7672,7 +9271,7 @@ var transcodeTwilioInboundPayloadToPCM16 = (payload) => {
7672
9271
  };
7673
9272
  var transcodePCMToTwilioOutboundPayload = (chunk, format) => {
7674
9273
  if (format.container === "raw" && format.encoding === "mulaw" && format.channels === 1 && format.sampleRateHz === TWILIO_MULAW_SAMPLE_RATE) {
7675
- return Buffer2.from(chunk).toString("base64");
9274
+ return Buffer3.from(chunk).toString("base64");
7676
9275
  }
7677
9276
  if (format.encoding !== "pcm_s16le") {
7678
9277
  throw new Error(`Unsupported outbound telephony audio format: ${format.container}/${format.encoding}`);
@@ -7713,7 +9312,7 @@ var createTwilioSocketAdapter = (socket, getState) => ({
7713
9312
  return;
7714
9313
  }
7715
9314
  if (message.type === "audio") {
7716
- const payload = transcodePCMToTwilioOutboundPayload(Uint8Array.from(Buffer2.from(message.chunkBase64, "base64")), message.format);
9315
+ const payload = transcodePCMToTwilioOutboundPayload(Uint8Array.from(Buffer3.from(message.chunkBase64, "base64")), message.format);
7717
9316
  state.hasOutboundAudioSinceLastInbound = true;
7718
9317
  state.reviewRecorder?.recordTwilioOutbound({
7719
9318
  bytes: payload.length,
@@ -7926,6 +9525,148 @@ var createTwilioMediaStreamBridge = (socket, options) => {
7926
9525
  }
7927
9526
  };
7928
9527
  };
9528
+ var createTwilioVoiceRoutes = (options) => {
9529
+ const streamPath = options.streamPath ?? "/api/voice/twilio/stream";
9530
+ const twimlPath = options.twiml?.path ?? "/api/voice/twilio";
9531
+ const webhookPath = options.webhook?.path ?? "/api/voice/twilio/webhook";
9532
+ const setupPath = options.setup?.path === false ? false : options.setup?.path ?? "/api/voice/twilio/setup";
9533
+ const smokePath = options.smoke?.path === false ? false : options.smoke?.path ?? "/api/voice/twilio/smoke";
9534
+ const bridges = new WeakMap;
9535
+ const webhookPolicy = options.webhook?.policy ?? options.outcomePolicy ?? createVoiceTelephonyOutcomePolicy();
9536
+ const app = new Elysia2({
9537
+ name: options.name ?? "absolutejs-voice-twilio"
9538
+ }).get(twimlPath, async ({ query, request }) => {
9539
+ const streamUrl = await resolveTwilioStreamUrl(options, {
9540
+ query,
9541
+ request,
9542
+ streamPath
9543
+ });
9544
+ const parameters = await resolveTwilioStreamParameters(options.twiml?.parameters, {
9545
+ query,
9546
+ request
9547
+ });
9548
+ return new Response(createTwilioVoiceResponse({
9549
+ parameters,
9550
+ streamName: options.twiml?.streamName,
9551
+ streamUrl,
9552
+ track: options.twiml?.track
9553
+ }), {
9554
+ headers: {
9555
+ "content-type": "text/xml; charset=utf-8"
9556
+ }
9557
+ });
9558
+ }).post(twimlPath, async ({ query, request }) => {
9559
+ const streamUrl = await resolveTwilioStreamUrl(options, {
9560
+ query,
9561
+ request,
9562
+ streamPath
9563
+ });
9564
+ const parameters = await resolveTwilioStreamParameters(options.twiml?.parameters, {
9565
+ query,
9566
+ request
9567
+ });
9568
+ return new Response(createTwilioVoiceResponse({
9569
+ parameters,
9570
+ streamName: options.twiml?.streamName,
9571
+ streamUrl,
9572
+ track: options.twiml?.track
9573
+ }), {
9574
+ headers: {
9575
+ "content-type": "text/xml; charset=utf-8"
9576
+ }
9577
+ });
9578
+ }).ws(streamPath, {
9579
+ close: async (ws, _code, reason) => {
9580
+ const bridge = bridges.get(ws);
9581
+ bridges.delete(ws);
9582
+ await bridge?.close(reason);
9583
+ },
9584
+ message: async (ws, raw) => {
9585
+ let bridge = bridges.get(ws);
9586
+ if (!bridge) {
9587
+ bridge = createTwilioMediaStreamBridge({
9588
+ close: (code, reason) => {
9589
+ ws.close(code, reason);
9590
+ },
9591
+ send: (data) => {
9592
+ ws.send(data);
9593
+ }
9594
+ }, options);
9595
+ bridges.set(ws, bridge);
9596
+ }
9597
+ await bridge.handleMessage(raw);
9598
+ }
9599
+ }).use(createVoiceTelephonyWebhookRoutes({
9600
+ ...options.webhook ?? {},
9601
+ context: options.context,
9602
+ path: webhookPath,
9603
+ policy: webhookPolicy,
9604
+ provider: "twilio"
9605
+ }));
9606
+ if (!setupPath) {
9607
+ if (!smokePath) {
9608
+ return app;
9609
+ }
9610
+ return app.get(smokePath, async ({ query, request }) => {
9611
+ const report = await runTwilioVoiceSmokeTest({
9612
+ app,
9613
+ options,
9614
+ query,
9615
+ request,
9616
+ streamPath,
9617
+ twimlPath,
9618
+ webhookPath
9619
+ });
9620
+ if (query.format === "html") {
9621
+ return new Response(renderTwilioVoiceSmokeHTML(report, options.smoke?.title ?? "AbsoluteJS Twilio Voice Smoke Test"), {
9622
+ headers: {
9623
+ "content-type": "text/html; charset=utf-8"
9624
+ }
9625
+ });
9626
+ }
9627
+ return report;
9628
+ });
9629
+ }
9630
+ const withSetup = app.get(setupPath, async ({ query, request }) => {
9631
+ const status = await buildTwilioVoiceSetupStatus(options, {
9632
+ query,
9633
+ request,
9634
+ streamPath,
9635
+ twimlPath,
9636
+ webhookPath
9637
+ });
9638
+ if (query.format === "html") {
9639
+ return new Response(renderTwilioVoiceSetupHTML(status, options.setup?.title ?? "AbsoluteJS Twilio Voice Setup"), {
9640
+ headers: {
9641
+ "content-type": "text/html; charset=utf-8"
9642
+ }
9643
+ });
9644
+ }
9645
+ return status;
9646
+ });
9647
+ if (!smokePath) {
9648
+ return withSetup;
9649
+ }
9650
+ return withSetup.get(smokePath, async ({ query, request }) => {
9651
+ const report = await runTwilioVoiceSmokeTest({
9652
+ app,
9653
+ options,
9654
+ query,
9655
+ request,
9656
+ streamPath,
9657
+ twimlPath,
9658
+ webhookPath
9659
+ });
9660
+ if (query.format === "html") {
9661
+ return new Response(renderTwilioVoiceSmokeHTML(report, options.smoke?.title ?? "AbsoluteJS Twilio Voice Smoke Test"), {
9662
+ headers: {
9663
+ "content-type": "text/html; charset=utf-8"
9664
+ }
9665
+ });
9666
+ }
9667
+ return report;
9668
+ });
9669
+ };
7929
9670
 
7930
9671
  // src/testing/telephony.ts
7931
9672
  var DEFAULT_PCM16_FORMAT = {
@@ -8191,7 +9932,7 @@ var runVoiceTelephonyBenchmark = async (scenarios = getDefaultVoiceTelephonyBenc
8191
9932
  };
8192
9933
  };
8193
9934
  // src/testing/tts.ts
8194
- var DEFAULT_REALTIME_FORMAT = {
9935
+ var DEFAULT_REALTIME_FORMAT2 = {
8195
9936
  channels: 1,
8196
9937
  container: "raw",
8197
9938
  encoding: "pcm_s16le",
@@ -8250,7 +9991,7 @@ var runTTSAdapterFixture = async (adapter, fixture, options = {}) => {
8250
9991
  let audioDurationMs = 0;
8251
9992
  let audioChunkCount = 0;
8252
9993
  const session = adapter.kind === "realtime" ? await adapter.open({
8253
- format: options.realtimeFormat ?? DEFAULT_REALTIME_FORMAT,
9994
+ format: options.realtimeFormat ?? DEFAULT_REALTIME_FORMAT2,
8254
9995
  sessionId: `tts-benchmark:${fixture.id}`,
8255
9996
  ...openOptions ?? {}
8256
9997
  }) : await adapter.open({
@@ -8417,6 +10158,7 @@ export {
8417
10158
  getDefaultTTSBenchmarkFixtures,
8418
10159
  evaluateSTTBenchmarkAcceptance,
8419
10160
  createVoiceProviderFailureSimulator,
10161
+ createVoiceIOProviderFailureSimulator,
8420
10162
  createVoiceCallReviewRecorder,
8421
10163
  createVoiceCallReviewFromLiveTelephonyReport,
8422
10164
  createTelephonyVoiceTestFixtures,