@absolutejs/voice 0.0.22-beta.31 → 0.0.22-beta.311

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 (240) hide show
  1. package/README.md +3250 -73
  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/competitiveCoverage.d.ts +141 -0
  77. package/dist/dataControl.d.ts +180 -0
  78. package/dist/deliveryRuntime.d.ts +158 -0
  79. package/dist/deliverySinkRoutes.d.ts +117 -0
  80. package/dist/demoReadyRoutes.d.ts +98 -0
  81. package/dist/diagnosticsRoutes.d.ts +44 -0
  82. package/dist/evalRoutes.d.ts +219 -0
  83. package/dist/fileStore.d.ts +14 -2
  84. package/dist/guardrails.d.ts +128 -0
  85. package/dist/incidentBundle.d.ts +116 -0
  86. package/dist/index.d.ts +146 -13
  87. package/dist/index.js +28245 -5219
  88. package/dist/latencySlo.d.ts +56 -0
  89. package/dist/liveLatency.d.ts +78 -0
  90. package/dist/liveOps.d.ts +190 -0
  91. package/dist/mediaPipeline.d.ts +125 -0
  92. package/dist/mediaPipelineRoutes.d.ts +93 -0
  93. package/dist/modelAdapters.d.ts +60 -2
  94. package/dist/observabilityExport.d.ts +481 -0
  95. package/dist/openaiTTS.d.ts +18 -0
  96. package/dist/operationsRecord.d.ts +254 -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 +559 -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/providerDecisionTraces.d.ts +130 -0
  113. package/dist/providerHealth.d.ts +1 -0
  114. package/dist/providerOrchestration.d.ts +109 -0
  115. package/dist/providerRoutingContract.d.ts +71 -0
  116. package/dist/providerSlo.d.ts +142 -0
  117. package/dist/providerStackRecommendations.d.ts +187 -0
  118. package/dist/qualityRoutes.d.ts +76 -0
  119. package/dist/queue.d.ts +9 -0
  120. package/dist/react/VoiceAgentSquadStatus.d.ts +5 -0
  121. package/dist/react/VoiceDeliveryRuntime.d.ts +7 -0
  122. package/dist/react/VoiceOpsActionCenter.d.ts +5 -0
  123. package/dist/react/VoiceOpsStatus.d.ts +6 -0
  124. package/dist/react/VoicePlatformCoverage.d.ts +6 -0
  125. package/dist/react/VoiceProofTrends.d.ts +6 -0
  126. package/dist/react/VoiceProviderCapabilities.d.ts +6 -0
  127. package/dist/react/VoiceProviderContracts.d.ts +6 -0
  128. package/dist/react/VoiceProviderSimulationControls.d.ts +5 -0
  129. package/dist/react/VoiceProviderStatus.d.ts +6 -0
  130. package/dist/react/VoiceReadinessFailures.d.ts +6 -0
  131. package/dist/react/VoiceRoutingStatus.d.ts +6 -0
  132. package/dist/react/VoiceTraceTimeline.d.ts +6 -0
  133. package/dist/react/VoiceTurnLatency.d.ts +6 -0
  134. package/dist/react/VoiceTurnQuality.d.ts +6 -0
  135. package/dist/react/index.d.ts +32 -0
  136. package/dist/react/index.js +5059 -31
  137. package/dist/react/useVoiceAgentSquadStatus.d.ts +8 -0
  138. package/dist/react/useVoiceCampaignDialerProof.d.ts +10 -0
  139. package/dist/react/useVoiceController.d.ts +1 -0
  140. package/dist/react/useVoiceDeliveryRuntime.d.ts +13 -0
  141. package/dist/react/useVoiceLiveOps.d.ts +9 -0
  142. package/dist/react/useVoiceOpsActionCenter.d.ts +11 -0
  143. package/dist/react/useVoiceOpsStatus.d.ts +8 -0
  144. package/dist/react/useVoicePlatformCoverage.d.ts +8 -0
  145. package/dist/react/useVoiceProofTrends.d.ts +8 -0
  146. package/dist/react/useVoiceProviderCapabilities.d.ts +8 -0
  147. package/dist/react/useVoiceProviderContracts.d.ts +8 -0
  148. package/dist/react/useVoiceProviderSimulationControls.d.ts +10 -0
  149. package/dist/react/useVoiceReadinessFailures.d.ts +8 -0
  150. package/dist/react/useVoiceRoutingStatus.d.ts +8 -0
  151. package/dist/react/useVoiceStream.d.ts +1 -0
  152. package/dist/react/useVoiceTraceTimeline.d.ts +8 -0
  153. package/dist/react/useVoiceTurnLatency.d.ts +9 -0
  154. package/dist/react/useVoiceTurnQuality.d.ts +8 -0
  155. package/dist/react/useVoiceWorkflowStatus.d.ts +8 -0
  156. package/dist/readinessProfiles.d.ts +38 -0
  157. package/dist/realtimeChannel.d.ts +136 -0
  158. package/dist/realtimeProviderContracts.d.ts +133 -0
  159. package/dist/reconnectContract.d.ts +88 -0
  160. package/dist/resilienceRoutes.d.ts +143 -0
  161. package/dist/sessionReplay.d.ts +12 -0
  162. package/dist/simulationSuite.d.ts +143 -0
  163. package/dist/sloCalibration.d.ts +185 -0
  164. package/dist/sqliteStore.d.ts +13 -2
  165. package/dist/svelte/createVoiceAgentSquadStatus.d.ts +9 -0
  166. package/dist/svelte/createVoiceCampaignDialerProof.d.ts +9 -0
  167. package/dist/svelte/createVoiceDeliveryRuntime.d.ts +11 -0
  168. package/dist/svelte/createVoiceLiveOps.d.ts +13 -0
  169. package/dist/svelte/createVoiceOpsActionCenter.d.ts +10 -0
  170. package/dist/svelte/createVoiceOpsStatus.d.ts +9 -0
  171. package/dist/svelte/createVoicePlatformCoverage.d.ts +7 -0
  172. package/dist/svelte/createVoiceProofTrends.d.ts +7 -0
  173. package/dist/svelte/createVoiceProviderCapabilities.d.ts +10 -0
  174. package/dist/svelte/createVoiceProviderContracts.d.ts +10 -0
  175. package/dist/svelte/createVoiceProviderSimulationControls.d.ts +11 -0
  176. package/dist/svelte/createVoiceProviderStatus.d.ts +4 -2
  177. package/dist/svelte/createVoiceReadinessFailures.d.ts +7 -0
  178. package/dist/svelte/createVoiceRoutingStatus.d.ts +10 -0
  179. package/dist/svelte/createVoiceTraceTimeline.d.ts +10 -0
  180. package/dist/svelte/createVoiceTurnLatency.d.ts +11 -0
  181. package/dist/svelte/createVoiceTurnQuality.d.ts +10 -0
  182. package/dist/svelte/createVoiceWorkflowStatus.d.ts +8 -0
  183. package/dist/svelte/index.d.ts +17 -0
  184. package/dist/svelte/index.js +4924 -420
  185. package/dist/telephony/contract.d.ts +61 -0
  186. package/dist/telephony/matrix.d.ts +97 -0
  187. package/dist/telephony/plivo.d.ts +303 -0
  188. package/dist/telephony/security.d.ts +182 -0
  189. package/dist/telephony/telnyx.d.ts +291 -0
  190. package/dist/telephony/twilio.d.ts +135 -2
  191. package/dist/telephonyOutcome.d.ts +273 -0
  192. package/dist/testing/index.d.ts +1 -0
  193. package/dist/testing/index.js +1767 -44
  194. package/dist/testing/ioProviderSimulator.d.ts +41 -0
  195. package/dist/toolContract.d.ts +161 -0
  196. package/dist/toolRuntime.d.ts +50 -0
  197. package/dist/trace.d.ts +19 -1
  198. package/dist/traceDeliveryRoutes.d.ts +86 -0
  199. package/dist/traceTimeline.d.ts +97 -0
  200. package/dist/turnLatency.d.ts +95 -0
  201. package/dist/turnQuality.d.ts +94 -0
  202. package/dist/types.d.ts +97 -3
  203. package/dist/voiceMonitoring.d.ts +444 -0
  204. package/dist/vue/VoiceDeliveryRuntime.d.ts +30 -0
  205. package/dist/vue/VoiceOpsActionCenter.d.ts +13 -0
  206. package/dist/vue/VoiceOpsStatus.d.ts +30 -0
  207. package/dist/vue/VoicePlatformCoverage.d.ts +23 -0
  208. package/dist/vue/VoiceProofTrends.d.ts +21 -0
  209. package/dist/vue/VoiceProviderCapabilities.d.ts +51 -0
  210. package/dist/vue/VoiceProviderContracts.d.ts +21 -0
  211. package/dist/vue/VoiceProviderSimulationControls.d.ts +88 -0
  212. package/dist/vue/VoiceProviderStatus.d.ts +51 -0
  213. package/dist/vue/VoiceReadinessFailures.d.ts +21 -0
  214. package/dist/vue/VoiceRoutingStatus.d.ts +51 -0
  215. package/dist/vue/VoiceTurnLatency.d.ts +69 -0
  216. package/dist/vue/VoiceTurnQuality.d.ts +51 -0
  217. package/dist/vue/index.d.ts +30 -0
  218. package/dist/vue/index.js +4828 -56
  219. package/dist/vue/useVoiceAgentSquadStatus.d.ts +9 -0
  220. package/dist/vue/useVoiceCampaignDialerProof.d.ts +11 -0
  221. package/dist/vue/useVoiceController.d.ts +2 -1
  222. package/dist/vue/useVoiceDeliveryRuntime.d.ts +13 -0
  223. package/dist/vue/useVoiceLiveOps.d.ts +9 -0
  224. package/dist/vue/useVoiceOpsActionCenter.d.ts +11 -0
  225. package/dist/vue/useVoiceOpsStatus.d.ts +9 -0
  226. package/dist/vue/useVoicePlatformCoverage.d.ts +9 -0
  227. package/dist/vue/useVoiceProofTrends.d.ts +9 -0
  228. package/dist/vue/useVoiceProviderCapabilities.d.ts +9 -0
  229. package/dist/vue/useVoiceProviderContracts.d.ts +9 -0
  230. package/dist/vue/useVoiceProviderSimulationControls.d.ts +24 -0
  231. package/dist/vue/useVoiceProviderStatus.d.ts +1 -1
  232. package/dist/vue/useVoiceReadinessFailures.d.ts +775 -0
  233. package/dist/vue/useVoiceRoutingStatus.d.ts +8 -0
  234. package/dist/vue/useVoiceStream.d.ts +2 -1
  235. package/dist/vue/useVoiceTraceTimeline.d.ts +9 -0
  236. package/dist/vue/useVoiceTurnLatency.d.ts +10 -0
  237. package/dist/vue/useVoiceTurnQuality.d.ts +9 -0
  238. package/dist/vue/useVoiceWorkflowStatus.d.ts +9 -0
  239. package/dist/workflowContract.d.ts +91 -0
  240. 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,235 @@ 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 isVoiceProviderRoutingPolicyPreset = (value) => value === "balanced" || value === "cost-cap" || value === "cost-first" || value === "latency-first" || value === "quality-first";
3795
+ var resolveVoiceProviderRoutingPolicyPreset = (preset, options = {}) => {
3796
+ switch (preset) {
3797
+ case "balanced":
3798
+ return {
3799
+ fallbackMode: "provider-error",
3800
+ strategy: "balanced",
3801
+ weights: {
3802
+ cost: 1,
3803
+ latencyMs: 0.005,
3804
+ priority: 1,
3805
+ quality: 10,
3806
+ ...options.weights
3807
+ },
3808
+ ...options
3809
+ };
3810
+ case "cost-cap":
3811
+ return {
3812
+ fallbackMode: "provider-error",
3813
+ strategy: "prefer-cheapest",
3814
+ ...options
3815
+ };
3816
+ case "cost-first":
3817
+ return {
3818
+ fallbackMode: "provider-error",
3819
+ strategy: "prefer-cheapest",
3820
+ ...options
3821
+ };
3822
+ case "latency-first":
3823
+ return {
3824
+ fallbackMode: "provider-error",
3825
+ strategy: "prefer-fastest",
3826
+ ...options
3827
+ };
3828
+ case "quality-first":
3829
+ return {
3830
+ fallbackMode: "provider-error",
3831
+ strategy: "quality-first",
3832
+ ...options
3833
+ };
3834
+ }
3835
+ };
3836
+ var resolveVoiceProviderRoutingPolicy = (policy) => {
3837
+ if (!policy) {
3838
+ return;
3839
+ }
3840
+ if (typeof policy === "string") {
3841
+ return isVoiceProviderRoutingPolicyPreset(policy) ? resolveVoiceProviderRoutingPolicyPreset(policy) : {
3842
+ strategy: policy
3843
+ };
3844
+ }
3845
+ return policy;
3846
+ };
3847
+ var mergeDefinedProviderPolicyFields = (base, surface) => {
3848
+ const next = {
3849
+ ...base ?? {}
3850
+ };
3851
+ if (surface.allowProviders !== undefined) {
3852
+ next.allowProviders = surface.allowProviders;
3853
+ }
3854
+ if (surface.fallbackMode !== undefined) {
3855
+ next.fallbackMode = surface.fallbackMode;
3856
+ }
3857
+ if (surface.maxCost !== undefined) {
3858
+ next.maxCost = surface.maxCost;
3859
+ }
3860
+ if (surface.maxLatencyMs !== undefined) {
3861
+ next.maxLatencyMs = surface.maxLatencyMs;
3862
+ }
3863
+ if (surface.minQuality !== undefined) {
3864
+ next.minQuality = surface.minQuality;
3865
+ }
3866
+ if (surface.strategy !== undefined) {
3867
+ next.strategy = surface.strategy;
3868
+ }
3869
+ if (surface.weights !== undefined) {
3870
+ next.weights = {
3871
+ ...base?.weights ?? {},
3872
+ ...surface.weights
3873
+ };
3874
+ }
3875
+ return next;
3876
+ };
3877
+ var createVoiceProviderOrchestrationProfile = (options) => {
3878
+ const surfaceNames = Object.keys(options.surfaces);
3879
+ const defaultSurface = options.defaultSurface ?? surfaceNames[0];
3880
+ if (!defaultSurface || !options.surfaces[defaultSurface]) {
3881
+ throw new Error("Voice provider orchestration profile has no surfaces.");
3882
+ }
3883
+ return {
3884
+ defaultSurface,
3885
+ id: options.id,
3886
+ resolve: (surface = defaultSurface) => {
3887
+ const config = options.surfaces[surface];
3888
+ if (!config) {
3889
+ throw new Error(`Voice provider orchestration profile ${options.id} has no surface "${surface}".`);
3890
+ }
3891
+ const policy = mergeDefinedProviderPolicyFields(resolveVoiceProviderRoutingPolicy(config.policy), config);
3892
+ return {
3893
+ allowProviders: config.allowProviders,
3894
+ fallback: config.fallback,
3895
+ fallbackMode: config.fallbackMode,
3896
+ policy,
3897
+ providerHealth: config.providerHealth,
3898
+ providerProfiles: config.providerProfiles,
3899
+ timeoutMs: config.timeoutMs
3900
+ };
3901
+ },
3902
+ surfaces: options.surfaces
3903
+ };
3904
+ };
3514
3905
  var OUTPUT_SCHEMA = {
3515
3906
  additionalProperties: false,
3516
3907
  properties: {
@@ -3601,6 +3992,17 @@ var parseJSONValue = (value) => {
3601
3992
  return value;
3602
3993
  }
3603
3994
  };
3995
+
3996
+ class VoiceProviderTimeoutError extends Error {
3997
+ provider;
3998
+ timeoutMs;
3999
+ constructor(provider, timeoutMs) {
4000
+ super(`Voice provider ${provider} exceeded ${timeoutMs}ms latency budget.`);
4001
+ this.name = "VoiceProviderTimeoutError";
4002
+ this.provider = provider;
4003
+ this.timeoutMs = timeoutMs;
4004
+ }
4005
+ }
3604
4006
  var getMessageToolCalls = (message) => {
3605
4007
  const toolCalls = message.metadata?.toolCalls;
3606
4008
  return Array.isArray(toolCalls) ? toolCalls.filter((toolCall) => toolCall && typeof toolCall === "object" && typeof toolCall.name === "string") : [];
@@ -3667,17 +4069,25 @@ var createJSONVoiceAssistantModel = (options) => ({
3667
4069
  var createVoiceProviderRouter = (options) => {
3668
4070
  const providerIds = Object.keys(options.providers);
3669
4071
  const firstProvider = providerIds[0];
3670
- const policy = typeof options.policy === "string" ? {
3671
- strategy: options.policy
3672
- } : options.policy;
4072
+ const orchestrationSurface = options.orchestrationProfile?.resolve(options.orchestrationSurface);
4073
+ const policy = resolveVoiceProviderRoutingPolicy(options.policy) ?? resolveVoiceProviderRoutingPolicy(orchestrationSurface?.policy);
3673
4074
  const strategy = policy?.strategy ?? "prefer-selected";
3674
- const fallbackMode = policy?.fallbackMode ?? options.fallbackMode ?? "provider-error";
3675
- const healthOptions = typeof options.providerHealth === "object" ? options.providerHealth : options.providerHealth ? {} : undefined;
4075
+ const fallbackMode = policy?.fallbackMode ?? options.fallbackMode ?? orchestrationSurface?.fallbackMode ?? "provider-error";
4076
+ const providerProfiles = {
4077
+ ...orchestrationSurface?.providerProfiles ?? {},
4078
+ ...options.providerProfiles ?? {}
4079
+ };
4080
+ const providerHealthOption = options.providerHealth ?? orchestrationSurface?.providerHealth;
4081
+ const healthOptions = typeof providerHealthOption === "object" ? providerHealthOption : providerHealthOption ? {} : undefined;
3676
4082
  const healthState = new Map;
3677
4083
  const now = () => healthOptions?.now?.() ?? Date.now();
3678
4084
  const failureThreshold = Math.max(1, healthOptions?.failureThreshold ?? 1);
3679
4085
  const cooldownMs = Math.max(0, healthOptions?.cooldownMs ?? 30000);
3680
4086
  const rateLimitCooldownMs = Math.max(0, healthOptions?.rateLimitCooldownMs ?? 60000);
4087
+ const getProviderTimeoutMs = (provider) => {
4088
+ const timeoutMs = providerProfiles[provider]?.timeoutMs ?? options.timeoutMs ?? orchestrationSurface?.timeoutMs;
4089
+ return typeof timeoutMs === "number" && Number.isFinite(timeoutMs) && timeoutMs > 0 ? timeoutMs : undefined;
4090
+ };
3681
4091
  const getHealth = (provider) => {
3682
4092
  const existing = healthState.get(provider);
3683
4093
  if (existing) {
@@ -3741,17 +4151,44 @@ var createVoiceProviderRouter = (options) => {
3741
4151
  return cloneHealth(provider);
3742
4152
  };
3743
4153
  const resolveAllowedProviders = async (input) => {
3744
- const allowProviders = policy?.allowProviders ?? options.allowProviders;
4154
+ const allowProviders = policy?.allowProviders ?? options.allowProviders ?? orchestrationSurface?.allowProviders;
3745
4155
  const allowed = typeof allowProviders === "function" ? await allowProviders(input) : allowProviders;
3746
4156
  return new Set(allowed ?? providerIds);
3747
4157
  };
4158
+ const passesBudgetFilters = (provider) => {
4159
+ const profile = providerProfiles[provider];
4160
+ if (typeof policy?.maxCost === "number" && typeof profile?.cost === "number" && profile.cost > policy.maxCost) {
4161
+ return false;
4162
+ }
4163
+ if (typeof policy?.maxLatencyMs === "number" && typeof profile?.latencyMs === "number" && profile.latencyMs > policy.maxLatencyMs) {
4164
+ return false;
4165
+ }
4166
+ if (typeof policy?.minQuality === "number" && typeof profile?.quality === "number" && profile.quality < policy.minQuality) {
4167
+ return false;
4168
+ }
4169
+ return true;
4170
+ };
4171
+ const getBalancedScore = (provider) => {
4172
+ const profile = providerProfiles[provider];
4173
+ if (policy?.scoreProvider) {
4174
+ return policy.scoreProvider(provider, profile);
4175
+ }
4176
+ const weights = policy?.weights ?? {};
4177
+ 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);
4178
+ };
3748
4179
  const sortProviders = (providers) => {
3749
- if (strategy !== "prefer-cheapest" && strategy !== "prefer-fastest") {
4180
+ if (strategy !== "prefer-cheapest" && strategy !== "prefer-fastest" && strategy !== "quality-first" && strategy !== "balanced") {
3750
4181
  return providers;
3751
4182
  }
3752
4183
  return [...providers].sort((left, right) => {
3753
- const leftProfile = options.providerProfiles?.[left];
3754
- const rightProfile = options.providerProfiles?.[right];
4184
+ const leftProfile = providerProfiles[left];
4185
+ const rightProfile = providerProfiles[right];
4186
+ if (strategy === "quality-first") {
4187
+ 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);
4188
+ }
4189
+ if (strategy === "balanced") {
4190
+ return getBalancedScore(left) - getBalancedScore(right);
4191
+ }
3755
4192
  const leftValue = strategy === "prefer-cheapest" ? leftProfile?.cost ?? Number.MAX_SAFE_INTEGER : leftProfile?.latencyMs ?? Number.MAX_SAFE_INTEGER;
3756
4193
  const rightValue = strategy === "prefer-cheapest" ? rightProfile?.cost ?? Number.MAX_SAFE_INTEGER : rightProfile?.latencyMs ?? Number.MAX_SAFE_INTEGER;
3757
4194
  return leftValue - rightValue || (leftProfile?.priority ?? Number.MAX_SAFE_INTEGER) - (rightProfile?.priority ?? Number.MAX_SAFE_INTEGER);
@@ -3760,13 +4197,15 @@ var createVoiceProviderRouter = (options) => {
3760
4197
  const resolveOrder = async (input) => {
3761
4198
  const selectedProvider = await options.selectProvider?.(input);
3762
4199
  const allowedProviders = await resolveAllowedProviders(input);
3763
- const fallbackOrder = typeof options.fallback === "function" ? await options.fallback(input) : options.fallback;
3764
- const rankedProviders = sortProviders([
4200
+ const fallbackSource = options.fallback ?? orchestrationSurface?.fallback;
4201
+ const fallbackOrder = typeof fallbackSource === "function" ? await fallbackSource(input) : fallbackSource;
4202
+ const allowedRankedProviders = sortProviders([
3765
4203
  ...fallbackOrder ?? providerIds
3766
4204
  ]).filter((provider) => allowedProviders.has(provider));
4205
+ const rankedProviders = allowedRankedProviders.filter(passesBudgetFilters);
3767
4206
  const healthyRankedProviders = healthOptions ? rankedProviders.filter((provider) => !isSuppressed(provider)) : rankedProviders;
3768
4207
  const candidateRankedProviders = healthyRankedProviders.length ? healthyRankedProviders : rankedProviders;
3769
- const preferred = selectedProvider && allowedProviders.has(selectedProvider) && (!healthOptions || !isSuppressed(selectedProvider)) ? selectedProvider : candidateRankedProviders[0] ?? firstProvider;
4208
+ const preferred = selectedProvider && allowedProviders.has(selectedProvider) && passesBudgetFilters(selectedProvider) && (!healthOptions || !isSuppressed(selectedProvider)) ? selectedProvider : candidateRankedProviders[0] ?? firstProvider;
3770
4209
  const seen = new Set;
3771
4210
  const order = [];
3772
4211
  const candidates = strategy === "ordered" ? candidateRankedProviders : [
@@ -3789,6 +4228,25 @@ var createVoiceProviderRouter = (options) => {
3789
4228
  const emit = async (event, input) => {
3790
4229
  await options.onProviderEvent?.(event, input);
3791
4230
  };
4231
+ const runProvider = async (provider, model, input) => {
4232
+ const timeoutMs = getProviderTimeoutMs(provider);
4233
+ if (!timeoutMs) {
4234
+ return model.generate(input);
4235
+ }
4236
+ let timeout;
4237
+ try {
4238
+ return await Promise.race([
4239
+ model.generate(input),
4240
+ new Promise((_, reject) => {
4241
+ timeout = setTimeout(() => reject(new VoiceProviderTimeoutError(provider, timeoutMs)), timeoutMs);
4242
+ })
4243
+ ]);
4244
+ } finally {
4245
+ if (timeout) {
4246
+ clearTimeout(timeout);
4247
+ }
4248
+ }
4249
+ };
3792
4250
  return {
3793
4251
  generate: async (input) => {
3794
4252
  const { order, selectedProvider } = await resolveOrder(input);
@@ -3803,12 +4261,14 @@ var createVoiceProviderRouter = (options) => {
3803
4261
  }
3804
4262
  const startedAt = Date.now();
3805
4263
  try {
3806
- const output = await model.generate(input);
4264
+ const output = await runProvider(provider, model, input);
3807
4265
  const providerHealth = recordProviderSuccess(provider);
3808
4266
  await emit({
3809
4267
  at: Date.now(),
4268
+ attempt: index + 1,
3810
4269
  elapsedMs: Date.now() - startedAt,
3811
4270
  fallbackProvider: provider === selectedProvider ? undefined : provider,
4271
+ latencyBudgetMs: getProviderTimeoutMs(provider),
3812
4272
  provider,
3813
4273
  providerHealth,
3814
4274
  recovered: provider !== selectedProvider,
@@ -3820,22 +4280,26 @@ var createVoiceProviderRouter = (options) => {
3820
4280
  lastError = error;
3821
4281
  const hasNextProvider = index < order.length - 1;
3822
4282
  const isProviderError = options.isProviderError?.(error, provider) ?? true;
4283
+ const timedOut = options.isTimeoutError?.(error, provider) ?? error instanceof VoiceProviderTimeoutError;
3823
4284
  const rateLimited = options.isRateLimitError?.(error, provider) ?? defaultIsRateLimitError(error);
3824
4285
  const shouldFallback = fallbackMode === "provider-error" ? isProviderError : fallbackMode === "rate-limit" ? isProviderError && rateLimited : false;
3825
4286
  const providerHealth = recordProviderError(provider, isProviderError, rateLimited);
3826
4287
  const nextProvider = hasNextProvider ? order[index + 1] : undefined;
3827
4288
  await emit({
3828
4289
  at: Date.now(),
4290
+ attempt: index + 1,
3829
4291
  elapsedMs: Date.now() - startedAt,
3830
4292
  error: errorMessage(error),
3831
4293
  fallbackProvider: shouldFallback ? nextProvider : undefined,
4294
+ latencyBudgetMs: getProviderTimeoutMs(provider),
3832
4295
  provider,
3833
4296
  providerHealth,
3834
4297
  rateLimited,
3835
4298
  selectedProvider,
3836
4299
  suppressionRemainingMs: getSuppressionRemainingMs(provider),
3837
4300
  suppressedUntil: providerHealth?.suppressedUntil,
3838
- status: "error"
4301
+ status: "error",
4302
+ timedOut
3839
4303
  }, input);
3840
4304
  if (!hasNextProvider || !shouldFallback) {
3841
4305
  throw error;
@@ -4457,7 +4921,7 @@ var createVoiceMemoryStore = () => {
4457
4921
  };
4458
4922
 
4459
4923
  // src/session.ts
4460
- import { Buffer } from "buffer";
4924
+ import { Buffer as Buffer2 } from "buffer";
4461
4925
 
4462
4926
  // src/handoff.ts
4463
4927
  var toHex = (bytes) => Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
@@ -4776,6 +5240,12 @@ var DEFAULT_FORMAT = {
4776
5240
  encoding: "pcm_s16le",
4777
5241
  sampleRateHz: 16000
4778
5242
  };
5243
+ var DEFAULT_REALTIME_FORMAT = {
5244
+ channels: 1,
5245
+ container: "raw",
5246
+ encoding: "pcm_s16le",
5247
+ sampleRateHz: 24000
5248
+ };
4779
5249
  var toError = (value) => value instanceof Error ? value : new Error(String(value));
4780
5250
  var createEmptyCurrentTurn = () => ({
4781
5251
  finalText: "",
@@ -4788,7 +5258,7 @@ var createEmptyCurrentTurn = () => ({
4788
5258
  transcripts: []
4789
5259
  });
4790
5260
  var cloneTranscript = (transcript) => ({ ...transcript });
4791
- var encodeBase64 = (chunk) => Buffer.from(chunk).toString("base64");
5261
+ var encodeBase64 = (chunk) => Buffer2.from(chunk).toString("base64");
4792
5262
  var countWords2 = (text) => text.trim().split(/\s+/).filter(Boolean).length;
4793
5263
  var normalizeText2 = (text) => text.trim().replace(/\s+/g, " ");
4794
5264
  var getAudioChunkDurationMs = (chunk) => chunk.byteLength / (DEFAULT_FORMAT.sampleRateHz * DEFAULT_FORMAT.channels * 2) * 1000;
@@ -4959,7 +5429,7 @@ var createVoiceSession = (options) => {
4959
5429
  } : undefined;
4960
5430
  const appendTrace = async (input) => {
4961
5431
  await options.trace?.append({
4962
- at: Date.now(),
5432
+ at: input.at ?? Date.now(),
4963
5433
  metadata: input.metadata,
4964
5434
  payload: input.payload,
4965
5435
  scenarioId: input.session?.scenarioId ?? options.scenarioId,
@@ -4968,6 +5438,13 @@ var createVoiceSession = (options) => {
4968
5438
  type: input.type
4969
5439
  });
4970
5440
  };
5441
+ const appendTurnLatencyStage = async (input) => appendTrace({
5442
+ at: input.at,
5443
+ payload: { stage: input.stage },
5444
+ session: input.session,
5445
+ turnId: input.turnId,
5446
+ type: "turn_latency.stage"
5447
+ });
4971
5448
  const phraseHints = options.phraseHints ?? [];
4972
5449
  const lexicon = options.lexicon ?? [];
4973
5450
  let socket = options.socket;
@@ -5046,6 +5523,18 @@ var createVoiceSession = (options) => {
5046
5523
  type: "call_lifecycle"
5047
5524
  });
5048
5525
  };
5526
+ const sendReplay = async (session) => {
5527
+ await send({
5528
+ assistantTexts: session.turns.flatMap((turn) => turn.assistantText ? [turn.assistantText] : []),
5529
+ call: session.call,
5530
+ partial: session.currentTurn.partialText,
5531
+ scenarioId: session.scenarioId,
5532
+ sessionId: options.id,
5533
+ status: session.status,
5534
+ turns: session.turns,
5535
+ type: "replay"
5536
+ });
5537
+ };
5049
5538
  const runHandoff = async (input) => {
5050
5539
  const queuedDelivery = options.handoff?.deliveryQueue ? createVoiceHandoffDeliveryRecord({
5051
5540
  action: input.action,
@@ -5149,6 +5638,23 @@ var createVoiceSession = (options) => {
5149
5638
  });
5150
5639
  }
5151
5640
  };
5641
+ const sendAssistantAudio = async (chunk, input) => {
5642
+ 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));
5643
+ await send({
5644
+ chunkBase64: encodeBase64(normalizedChunk),
5645
+ format: input.format,
5646
+ receivedAt: input.receivedAt,
5647
+ turnId: activeTTSTurnId,
5648
+ type: "audio"
5649
+ });
5650
+ if (activeTTSTurnId) {
5651
+ await appendTurnLatencyStage({
5652
+ at: input.receivedAt,
5653
+ stage: "assistant_audio_received",
5654
+ turnId: activeTTSTurnId
5655
+ });
5656
+ }
5657
+ };
5152
5658
  const scheduleTurnCommit = (delayMs, reason, reset = true) => {
5153
5659
  if (!reset && silenceTimer) {
5154
5660
  return;
@@ -5850,8 +6356,12 @@ var createVoiceSession = (options) => {
5850
6356
  if (sttSession) {
5851
6357
  return sttSession;
5852
6358
  }
5853
- const openedSession = await options.stt.open({
5854
- format: DEFAULT_FORMAT,
6359
+ const inputAdapter = options.realtime ?? options.stt;
6360
+ if (!inputAdapter) {
6361
+ throw new Error("Voice session requires either an stt or realtime adapter.");
6362
+ }
6363
+ const openedSession = await inputAdapter.open({
6364
+ format: options.realtime ? options.realtimeInputFormat ?? DEFAULT_REALTIME_FORMAT : DEFAULT_FORMAT,
5855
6365
  languageStrategy: options.languageStrategy,
5856
6366
  lexicon,
5857
6367
  phraseHints,
@@ -5886,6 +6396,16 @@ var createVoiceSession = (options) => {
5886
6396
  openedSession.on("close", (event) => {
5887
6397
  runAdapterEvent("adapter.close", () => handleClose(event));
5888
6398
  });
6399
+ if (options.realtime) {
6400
+ openedSession.on("audio", ({ chunk, format, receivedAt }) => {
6401
+ runAdapterEvent("adapter.audio", async () => {
6402
+ await sendAssistantAudio(chunk, {
6403
+ format,
6404
+ receivedAt
6405
+ });
6406
+ });
6407
+ });
6408
+ }
5889
6409
  return openedSession;
5890
6410
  };
5891
6411
  const ensureTTSSession = async () => {
@@ -5910,13 +6430,9 @@ var createVoiceSession = (options) => {
5910
6430
  if (ttsSession !== openedSession) {
5911
6431
  return;
5912
6432
  }
5913
- 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));
5914
- await send({
5915
- chunkBase64: encodeBase64(normalizedChunk),
6433
+ await sendAssistantAudio(chunk, {
5916
6434
  format,
5917
- receivedAt,
5918
- turnId: activeTTSTurnId,
5919
- type: "audio"
6435
+ receivedAt
5920
6436
  });
5921
6437
  });
5922
6438
  });
@@ -5960,9 +6476,32 @@ var createVoiceSession = (options) => {
5960
6476
  });
5961
6477
  };
5962
6478
  const completeTurn = async (session, turn) => {
6479
+ const liveOpsControl = await options.liveOps?.getControl(options.id);
6480
+ if (liveOpsControl?.assistantPaused || liveOpsControl?.operatorTakeover) {
6481
+ await appendTrace({
6482
+ metadata: {
6483
+ source: "voice-live-ops"
6484
+ },
6485
+ payload: {
6486
+ action: "turn.skipped",
6487
+ control: liveOpsControl,
6488
+ reason: liveOpsControl.operatorTakeover ? "operator-takeover" : "assistant-paused",
6489
+ status: "skipped"
6490
+ },
6491
+ session,
6492
+ turnId: turn.id,
6493
+ type: "operator.action"
6494
+ });
6495
+ return;
6496
+ }
6497
+ const injectedInstruction = liveOpsControl?.injectedInstruction?.trim();
5963
6498
  const committedOutput = await options.route.onTurn({
5964
6499
  api,
5965
6500
  context: options.context,
6501
+ liveOps: liveOpsControl ? {
6502
+ control: liveOpsControl,
6503
+ injectedInstruction
6504
+ } : undefined,
5966
6505
  session,
5967
6506
  turn
5968
6507
  });
@@ -5976,6 +6515,7 @@ var createVoiceSession = (options) => {
5976
6515
  voicemail: committedOutput?.voicemail
5977
6516
  };
5978
6517
  if (output?.assistantText) {
6518
+ const assistantTextStartedAt = Date.now();
5979
6519
  await writeSession((currentSession) => {
5980
6520
  setTurnResult(currentSession, turn.id, {
5981
6521
  assistantText: output.assistantText
@@ -5986,10 +6526,17 @@ var createVoiceSession = (options) => {
5986
6526
  turnId: turn.id,
5987
6527
  type: "assistant"
5988
6528
  });
6529
+ await appendTurnLatencyStage({
6530
+ at: assistantTextStartedAt,
6531
+ session,
6532
+ stage: "assistant_text_started",
6533
+ turnId: turn.id
6534
+ });
5989
6535
  await appendTrace({
5990
6536
  payload: {
5991
6537
  text: output.assistantText,
5992
- ttsConfigured: Boolean(options.tts)
6538
+ ttsConfigured: Boolean(options.tts),
6539
+ realtimeConfigured: Boolean(options.realtime)
5993
6540
  },
5994
6541
  session,
5995
6542
  turnId: turn.id,
@@ -6000,7 +6547,18 @@ var createVoiceSession = (options) => {
6000
6547
  if (activeTTSSession) {
6001
6548
  const ttsStartedAt = Date.now();
6002
6549
  activeTTSTurnId = turn.id;
6550
+ await appendTurnLatencyStage({
6551
+ at: ttsStartedAt,
6552
+ session,
6553
+ stage: "tts_send_started",
6554
+ turnId: turn.id
6555
+ });
6003
6556
  await activeTTSSession.send(output.assistantText);
6557
+ await appendTurnLatencyStage({
6558
+ session,
6559
+ stage: "tts_send_completed",
6560
+ turnId: turn.id
6561
+ });
6004
6562
  await appendTrace({
6005
6563
  payload: {
6006
6564
  elapsedMs: Date.now() - ttsStartedAt,
@@ -6010,9 +6568,35 @@ var createVoiceSession = (options) => {
6010
6568
  turnId: turn.id,
6011
6569
  type: "turn.assistant"
6012
6570
  });
6571
+ } else if (options.realtime) {
6572
+ const activeRealtimeSession = await ensureAdapter();
6573
+ const realtimeStartedAt = Date.now();
6574
+ activeTTSTurnId = turn.id;
6575
+ await appendTurnLatencyStage({
6576
+ at: realtimeStartedAt,
6577
+ session,
6578
+ stage: "tts_send_started",
6579
+ turnId: turn.id
6580
+ });
6581
+ await activeRealtimeSession.send(output.assistantText);
6582
+ await appendTurnLatencyStage({
6583
+ session,
6584
+ stage: "tts_send_completed",
6585
+ turnId: turn.id
6586
+ });
6587
+ await appendTrace({
6588
+ payload: {
6589
+ elapsedMs: Date.now() - realtimeStartedAt,
6590
+ mode: "realtime",
6591
+ status: "sent"
6592
+ },
6593
+ session,
6594
+ turnId: turn.id,
6595
+ type: "turn.assistant"
6596
+ });
6013
6597
  }
6014
6598
  } catch (error) {
6015
- logger.warn("voice tts send failed", {
6599
+ logger.warn("voice assistant audio send failed", {
6016
6600
  error: toError(error).message,
6017
6601
  sessionId: options.id,
6018
6602
  turnId: turn.id
@@ -6020,7 +6604,7 @@ var createVoiceSession = (options) => {
6020
6604
  await appendTrace({
6021
6605
  payload: {
6022
6606
  error: toError(error).message,
6023
- status: "tts-send-failed"
6607
+ status: options.realtime ? "realtime-send-failed" : "tts-send-failed"
6024
6608
  },
6025
6609
  session,
6026
6610
  turnId: turn.id,
@@ -6197,11 +6781,35 @@ var createVoiceSession = (options) => {
6197
6781
  turnId: turn.id,
6198
6782
  type: "turn.cost"
6199
6783
  });
6784
+ const firstTranscriptAt = turn.transcripts.map((transcript) => transcript.endedAtMs ?? transcript.startedAtMs).filter((value) => typeof value === "number").sort((left, right) => left - right)[0];
6785
+ 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];
6786
+ if (firstTranscriptAt !== undefined) {
6787
+ await appendTurnLatencyStage({
6788
+ at: firstTranscriptAt,
6789
+ session: updatedSession,
6790
+ stage: "speech_detected",
6791
+ turnId: turn.id
6792
+ });
6793
+ }
6794
+ if (finalTranscriptAt !== undefined) {
6795
+ await appendTurnLatencyStage({
6796
+ at: finalTranscriptAt,
6797
+ session: updatedSession,
6798
+ stage: "final_transcript",
6799
+ turnId: turn.id
6800
+ });
6801
+ }
6802
+ await appendTurnLatencyStage({
6803
+ at: turn.committedAt,
6804
+ session: updatedSession,
6805
+ stage: "turn_committed",
6806
+ turnId: turn.id
6807
+ });
6200
6808
  await send({
6201
6809
  turn,
6202
6810
  type: "turn"
6203
6811
  });
6204
- if (options.sttLifecycle === "turn-scoped") {
6812
+ if (options.stt && options.sttLifecycle === "turn-scoped") {
6205
6813
  await closeAdapter("turn-commit");
6206
6814
  }
6207
6815
  await completeTurn(updatedSession, turn);
@@ -6264,6 +6872,7 @@ var createVoiceSession = (options) => {
6264
6872
  scenarioId: session.scenarioId,
6265
6873
  type: "session"
6266
6874
  });
6875
+ await sendReplay(session);
6267
6876
  if (shouldFireOnSession) {
6268
6877
  await options.route.onCallStart?.({
6269
6878
  api,
@@ -7640,10 +8249,981 @@ var runVoiceSessionBenchmarkSeries = async (input) => {
7640
8249
  });
7641
8250
  };
7642
8251
  // src/telephony/twilio.ts
7643
- import { Buffer as Buffer2 } from "buffer";
8252
+ import { Buffer as Buffer3 } from "buffer";
8253
+ import { Elysia as Elysia2 } from "elysia";
8254
+
8255
+ // src/telephonyOutcome.ts
8256
+ import { Elysia } from "elysia";
8257
+ var DEFAULT_COMPLETED_STATUSES = [
8258
+ "answered",
8259
+ "completed",
8260
+ "complete",
8261
+ "connected",
8262
+ "in-progress",
8263
+ "live"
8264
+ ];
8265
+ var DEFAULT_NO_ANSWER_STATUSES = [
8266
+ "busy",
8267
+ "canceled",
8268
+ "cancelled",
8269
+ "failed",
8270
+ "no-answer",
8271
+ "no_answer",
8272
+ "not-answered",
8273
+ "ring-no-answer",
8274
+ "timeout",
8275
+ "unanswered"
8276
+ ];
8277
+ var DEFAULT_VOICEMAIL_STATUSES = [
8278
+ "answering-machine",
8279
+ "machine",
8280
+ "voicemail",
8281
+ "voice-mail"
8282
+ ];
8283
+ var DEFAULT_TRANSFER_STATUSES = ["bridged", "forwarded", "transferred"];
8284
+ var DEFAULT_ESCALATION_STATUSES = ["escalated", "human-required", "operator"];
8285
+ var DEFAULT_FAILED_STATUSES = ["busy", "failed", "no-answer"];
8286
+ var DEFAULT_MACHINE_VOICEMAIL_VALUES = [
8287
+ "answering-machine",
8288
+ "fax",
8289
+ "machine",
8290
+ "machine-end-beep",
8291
+ "machine-end-other",
8292
+ "machine-start",
8293
+ "voicemail"
8294
+ ];
8295
+ var DEFAULT_NO_ANSWER_SIP_CODES = [408, 480, 486, 487, 603];
8296
+ var isRecord = (value) => Boolean(value) && typeof value === "object" && !Array.isArray(value);
8297
+ var uniqueSorted = (values) => Array.from(new Set(values)).sort();
8298
+ var findMissing = (values, required) => {
8299
+ if (!required?.length) {
8300
+ return [];
8301
+ }
8302
+ const valueSet = new Set(values);
8303
+ return required.filter((value) => !valueSet.has(value));
8304
+ };
8305
+
8306
+ class VoiceTelephonyWebhookVerificationError extends Error {
8307
+ result;
8308
+ constructor(result) {
8309
+ super(result.ok ? "telephony webhook verified" : result.reason);
8310
+ this.name = "VoiceTelephonyWebhookVerificationError";
8311
+ this.result = result;
8312
+ }
8313
+ }
8314
+ var createMemoryVoiceTelephonyWebhookIdempotencyStore = () => {
8315
+ const decisions = new Map;
8316
+ return {
8317
+ get: (key) => decisions.get(key),
8318
+ set: (key, decision) => {
8319
+ decisions.set(key, decision);
8320
+ }
8321
+ };
8322
+ };
8323
+ var isTelephonyWebhookProvider = (value) => value === "generic" || value === "plivo" || value === "telnyx" || value === "twilio";
8324
+ var isTelephonyOutcomeAction = (value) => value === "complete" || value === "escalate" || value === "ignore" || value === "no-answer" || value === "transfer" || value === "voicemail";
8325
+ var isCallDisposition = (value) => value === "completed" || value === "escalated" || value === "failed" || value === "no-answer" || value === "transferred" || value === "voicemail";
8326
+ var evaluateVoiceTelephonyWebhookNormalizationEvidence = (input = {}) => {
8327
+ const issues = [];
8328
+ const decisions = input.decisions ?? [];
8329
+ const verificationAttempts = input.verificationAttempts ?? [];
8330
+ const actions = uniqueSorted(decisions.map((decision) => decision.decision?.action ?? decision.action).filter(isTelephonyOutcomeAction));
8331
+ const dispositions = uniqueSorted(decisions.map((decision) => decision.decision?.disposition ?? decision.disposition).filter(isCallDisposition));
8332
+ const providers = uniqueSorted(decisions.map((decision) => decision.provider ?? decision.event?.provider).filter(isTelephonyWebhookProvider));
8333
+ const sources = uniqueSorted(decisions.map((decision) => decision.decision?.source ?? decision.source).filter((source) => typeof source === "string"));
8334
+ const applied = decisions.filter((decision) => decision.applied === true).length;
8335
+ const duplicateDecisions = decisions.filter((decision) => decision.duplicate === true);
8336
+ const duplicateProviders = uniqueSorted(duplicateDecisions.map((decision) => decision.provider ?? decision.event?.provider).filter(isTelephonyWebhookProvider));
8337
+ const duplicateIdempotencyKeys = new Set(duplicateDecisions.map((decision) => decision.idempotencyKey).filter((key) => typeof key === "string" && key.length > 0)).size;
8338
+ const duplicateCampaignOutcomesApplied = duplicateDecisions.filter((decision) => isRecord(decision.campaignOutcome) && decision.campaignOutcome.applied === true).length;
8339
+ const duplicateOutcomeReasons = uniqueSorted(duplicateDecisions.map((decision) => isRecord(decision.campaignOutcome) ? decision.campaignOutcome.reason : undefined).filter((reason) => typeof reason === "string"));
8340
+ const routeResults = decisions.filter((decision) => isRecord(decision.routeResult)).length;
8341
+ const missingSessionIds = decisions.filter((decision) => !decision.sessionId).length;
8342
+ const rejectedVerificationAttempts = verificationAttempts.filter((attempt) => attempt.rejected === true || attempt.status === 401 || attempt.verification?.ok === false && attempt.verification.reason === "invalid-signature");
8343
+ const rejectedVerificationProviders = uniqueSorted(rejectedVerificationAttempts.map((attempt) => attempt.provider).filter(isTelephonyWebhookProvider));
8344
+ const replayRejectedVerificationAttempts = rejectedVerificationAttempts.filter((attempt) => attempt.replayRejected === true);
8345
+ const replayRejectedVerificationProviders = uniqueSorted(replayRejectedVerificationAttempts.map((attempt) => attempt.provider).filter(isTelephonyWebhookProvider));
8346
+ const rejectedVerificationSideEffects = rejectedVerificationAttempts.reduce((total, attempt) => total + Math.max(0, attempt.sideEffects ?? 0), 0);
8347
+ if (input.minDecisions !== undefined && decisions.length < input.minDecisions) {
8348
+ issues.push(`Expected at least ${String(input.minDecisions)} telephony webhook decision(s), found ${String(decisions.length)}.`);
8349
+ }
8350
+ if (input.minApplied !== undefined && applied < input.minApplied) {
8351
+ issues.push(`Expected at least ${String(input.minApplied)} applied telephony webhook decision(s), found ${String(applied)}.`);
8352
+ }
8353
+ if (input.minDuplicates !== undefined && duplicateDecisions.length < input.minDuplicates) {
8354
+ issues.push(`Expected at least ${String(input.minDuplicates)} duplicate telephony webhook decision(s), found ${String(duplicateDecisions.length)}.`);
8355
+ }
8356
+ if (input.minDuplicateIdempotencyKeys !== undefined && duplicateIdempotencyKeys < input.minDuplicateIdempotencyKeys) {
8357
+ issues.push(`Expected at least ${String(input.minDuplicateIdempotencyKeys)} duplicate telephony webhook idempotency key(s), found ${String(duplicateIdempotencyKeys)}.`);
8358
+ }
8359
+ if (input.maxDuplicateCampaignOutcomesApplied !== undefined && duplicateCampaignOutcomesApplied > input.maxDuplicateCampaignOutcomesApplied) {
8360
+ issues.push(`Expected at most ${String(input.maxDuplicateCampaignOutcomesApplied)} duplicate telephony webhook campaign outcome application(s), found ${String(duplicateCampaignOutcomesApplied)}.`);
8361
+ }
8362
+ if (input.minRejectedVerificationAttempts !== undefined && rejectedVerificationAttempts.length < input.minRejectedVerificationAttempts) {
8363
+ issues.push(`Expected at least ${String(input.minRejectedVerificationAttempts)} rejected telephony webhook verification attempt(s), found ${String(rejectedVerificationAttempts.length)}.`);
8364
+ }
8365
+ if (input.maxRejectedVerificationSideEffects !== undefined && rejectedVerificationSideEffects > input.maxRejectedVerificationSideEffects) {
8366
+ issues.push(`Expected at most ${String(input.maxRejectedVerificationSideEffects)} rejected telephony webhook side effect(s), found ${String(rejectedVerificationSideEffects)}.`);
8367
+ }
8368
+ if (input.minReplayRejectedVerificationAttempts !== undefined && replayRejectedVerificationAttempts.length < input.minReplayRejectedVerificationAttempts) {
8369
+ issues.push(`Expected at least ${String(input.minReplayRejectedVerificationAttempts)} replay-rejected telephony webhook verification attempt(s), found ${String(replayRejectedVerificationAttempts.length)}.`);
8370
+ }
8371
+ if (input.maxMissingSessionIds !== undefined && missingSessionIds > input.maxMissingSessionIds) {
8372
+ issues.push(`Expected at most ${String(input.maxMissingSessionIds)} telephony webhook decision(s) without sessionId, found ${String(missingSessionIds)}.`);
8373
+ }
8374
+ if (input.requireRouteResults && routeResults < decisions.length) {
8375
+ issues.push(`Expected every telephony webhook decision to include a route result, found ${String(routeResults)} of ${String(decisions.length)}.`);
8376
+ }
8377
+ for (const provider of findMissing(providers, input.requiredProviders)) {
8378
+ issues.push(`Missing telephony webhook provider: ${provider}.`);
8379
+ }
8380
+ for (const provider of findMissing(duplicateProviders, input.requiredDuplicateProviders)) {
8381
+ issues.push(`Missing duplicate telephony webhook provider: ${provider}.`);
8382
+ }
8383
+ for (const provider of findMissing(rejectedVerificationProviders, input.requiredRejectedVerificationProviders)) {
8384
+ issues.push(`Missing rejected telephony webhook verification provider: ${provider}.`);
8385
+ }
8386
+ for (const provider of findMissing(replayRejectedVerificationProviders, input.requiredReplayRejectedVerificationProviders)) {
8387
+ issues.push(`Missing replay-rejected telephony webhook verification provider: ${provider}.`);
8388
+ }
8389
+ for (const action of findMissing(actions, input.requiredActions)) {
8390
+ issues.push(`Missing telephony webhook action: ${action}.`);
8391
+ }
8392
+ for (const disposition of findMissing(dispositions, input.requiredDispositions)) {
8393
+ issues.push(`Missing telephony webhook disposition: ${disposition}.`);
8394
+ }
8395
+ return {
8396
+ actions,
8397
+ applied,
8398
+ decisions: decisions.length,
8399
+ dispositions,
8400
+ duplicateCampaignOutcomesApplied,
8401
+ duplicateIdempotencyKeys,
8402
+ duplicateOutcomeReasons,
8403
+ duplicateProviders,
8404
+ duplicates: duplicateDecisions.length,
8405
+ issues,
8406
+ missingSessionIds,
8407
+ ok: issues.length === 0,
8408
+ providers,
8409
+ rejectedVerificationAttempts: rejectedVerificationAttempts.length,
8410
+ rejectedVerificationProviders,
8411
+ rejectedVerificationSideEffects,
8412
+ replayRejectedVerificationAttempts: replayRejectedVerificationAttempts.length,
8413
+ replayRejectedVerificationProviders,
8414
+ routeResults,
8415
+ sources,
8416
+ verificationAttempts: verificationAttempts.length
8417
+ };
8418
+ };
8419
+ var assertVoiceTelephonyWebhookNormalizationEvidence = (input = {}) => {
8420
+ const assertion = evaluateVoiceTelephonyWebhookNormalizationEvidence(input);
8421
+ if (!assertion.ok) {
8422
+ throw new Error(`Voice telephony webhook normalization evidence assertion failed: ${assertion.issues.join(" ")}`);
8423
+ }
8424
+ return assertion;
8425
+ };
8426
+ var normalizeToken = (value) => typeof value === "string" ? value.trim().toLowerCase().replace(/\s+/g, "-").replace(/_+/g, "-") : undefined;
8427
+ var firstString = (source, keys) => {
8428
+ for (const key of keys) {
8429
+ const value = source[key];
8430
+ if (typeof value === "string" && value.trim()) {
8431
+ return value.trim();
8432
+ }
8433
+ if (typeof value === "number" && Number.isFinite(value)) {
8434
+ return String(value);
8435
+ }
8436
+ }
8437
+ };
8438
+ var firstNumber = (source, keys) => {
8439
+ for (const key of keys) {
8440
+ const value = source[key];
8441
+ if (typeof value === "number" && Number.isFinite(value)) {
8442
+ return value;
8443
+ }
8444
+ if (typeof value === "string" && value.trim()) {
8445
+ const parsed = Number(value);
8446
+ if (Number.isFinite(parsed)) {
8447
+ return parsed;
8448
+ }
8449
+ }
8450
+ }
8451
+ };
8452
+ var parseMaybeJSON = (value) => {
8453
+ try {
8454
+ return JSON.parse(value);
8455
+ } catch {
8456
+ return;
8457
+ }
8458
+ };
8459
+ var flattenPayload = (value) => {
8460
+ if (!isRecord(value)) {
8461
+ return {};
8462
+ }
8463
+ const data = isRecord(value.data) ? value.data : undefined;
8464
+ const payload = isRecord(value.payload) ? value.payload : undefined;
8465
+ const event = isRecord(value.event) ? value.event : undefined;
8466
+ return {
8467
+ ...value,
8468
+ ...payload,
8469
+ ...event,
8470
+ ...data,
8471
+ ...isRecord(data?.payload) ? data.payload : undefined
8472
+ };
8473
+ };
8474
+ var toBase64 = (bytes) => Buffer.from(new Uint8Array(bytes)).toString("base64");
8475
+ var timingSafeEqual = (left, right) => {
8476
+ const encoder = new TextEncoder;
8477
+ const leftBytes = encoder.encode(left);
8478
+ const rightBytes = encoder.encode(right);
8479
+ if (leftBytes.length !== rightBytes.length) {
8480
+ return false;
8481
+ }
8482
+ let diff = 0;
8483
+ for (let index = 0;index < leftBytes.length; index += 1) {
8484
+ diff |= leftBytes[index] ^ rightBytes[index];
8485
+ }
8486
+ return diff === 0;
8487
+ };
8488
+ var signHmacSHA1Base64 = async (secret, payload) => {
8489
+ const encoder = new TextEncoder;
8490
+ const key = await crypto.subtle.importKey("raw", encoder.encode(secret), {
8491
+ hash: "SHA-1",
8492
+ name: "HMAC"
8493
+ }, false, ["sign"]);
8494
+ const signature = await crypto.subtle.sign("HMAC", key, encoder.encode(payload));
8495
+ return toBase64(signature);
8496
+ };
8497
+ 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("");
8498
+ var normalizeList = (values, fallback) => new Set((values ?? fallback).map(normalizeToken).filter(Boolean));
8499
+ var metadataValue = (metadata, keys) => {
8500
+ for (const key of keys) {
8501
+ const value = metadata?.[key];
8502
+ if (typeof value === "string" && value.trim()) {
8503
+ return value.trim();
8504
+ }
8505
+ }
8506
+ };
8507
+ var resolveTransferTarget = (event, policy) => {
8508
+ if (typeof event.target === "string" && event.target.trim()) {
8509
+ return event.target.trim();
8510
+ }
8511
+ const metadataTarget = metadataValue(event.metadata, [
8512
+ "transferTarget",
8513
+ "target",
8514
+ "queue",
8515
+ "department"
8516
+ ]);
8517
+ if (metadataTarget) {
8518
+ return metadataTarget;
8519
+ }
8520
+ if (typeof policy.transferTarget === "function") {
8521
+ const target = policy.transferTarget(event);
8522
+ return typeof target === "string" && target.trim() ? target.trim() : undefined;
8523
+ }
8524
+ return typeof policy.transferTarget === "string" && policy.transferTarget.trim() ? policy.transferTarget.trim() : undefined;
8525
+ };
8526
+ var mergeMetadata = (event, policy) => ({
8527
+ ...policy.includeProviderPayload ? {
8528
+ answeredBy: event.answeredBy,
8529
+ durationMs: event.durationMs,
8530
+ provider: event.provider,
8531
+ reason: event.reason,
8532
+ sipCode: event.sipCode,
8533
+ status: event.status
8534
+ } : undefined,
8535
+ ...policy.metadata,
8536
+ ...event.metadata
8537
+ });
8538
+ var withDecisionDefaults = (decision, input) => {
8539
+ if (typeof decision === "string") {
8540
+ return buildDecision(decision, input);
8541
+ }
8542
+ return {
8543
+ ...buildDecision(decision.action, input),
8544
+ ...decision,
8545
+ confidence: decision.confidence ?? "high",
8546
+ metadata: {
8547
+ ...mergeMetadata(input.event, input.policy),
8548
+ ...decision.metadata
8549
+ },
8550
+ source: decision.source ?? input.source,
8551
+ target: decision.target ?? (decision.action === "transfer" ? resolveTransferTarget(input.event, input.policy) : undefined)
8552
+ };
8553
+ };
8554
+ var dispositionForAction = (action) => {
8555
+ switch (action) {
8556
+ case "complete":
8557
+ return "completed";
8558
+ case "escalate":
8559
+ return "escalated";
8560
+ case "no-answer":
8561
+ return "no-answer";
8562
+ case "transfer":
8563
+ return "transferred";
8564
+ case "voicemail":
8565
+ return "voicemail";
8566
+ default:
8567
+ return;
8568
+ }
8569
+ };
8570
+ var buildDecision = (action, input) => ({
8571
+ action,
8572
+ confidence: action === "ignore" ? "low" : "high",
8573
+ disposition: dispositionForAction(action),
8574
+ metadata: mergeMetadata(input.event, input.policy),
8575
+ reason: input.event.reason,
8576
+ source: input.source,
8577
+ target: action === "transfer" ? resolveTransferTarget(input.event, input.policy) : undefined
8578
+ });
8579
+ var createVoiceTelephonyOutcomePolicy = (policy = {}) => ({
8580
+ completedStatuses: policy.completedStatuses ?? DEFAULT_COMPLETED_STATUSES,
8581
+ escalationStatuses: policy.escalationStatuses ?? DEFAULT_ESCALATION_STATUSES,
8582
+ failedAsNoAnswer: policy.failedAsNoAnswer ?? true,
8583
+ failedStatuses: policy.failedStatuses ?? DEFAULT_FAILED_STATUSES,
8584
+ includeProviderPayload: policy.includeProviderPayload ?? true,
8585
+ machineDetectionVoicemailValues: policy.machineDetectionVoicemailValues ?? DEFAULT_MACHINE_VOICEMAIL_VALUES,
8586
+ metadata: policy.metadata,
8587
+ minAnsweredDurationMs: policy.minAnsweredDurationMs,
8588
+ noAnswerOnZeroDuration: policy.noAnswerOnZeroDuration ?? true,
8589
+ noAnswerSipCodes: policy.noAnswerSipCodes ?? DEFAULT_NO_ANSWER_SIP_CODES,
8590
+ noAnswerStatuses: policy.noAnswerStatuses ?? DEFAULT_NO_ANSWER_STATUSES,
8591
+ statusMap: policy.statusMap,
8592
+ transferStatuses: policy.transferStatuses ?? DEFAULT_TRANSFER_STATUSES,
8593
+ transferTarget: policy.transferTarget,
8594
+ voicemailStatuses: policy.voicemailStatuses ?? DEFAULT_VOICEMAIL_STATUSES
8595
+ });
8596
+ var resolveVoiceTelephonyOutcome = (event, policyInput = {}) => {
8597
+ const policy = createVoiceTelephonyOutcomePolicy(policyInput);
8598
+ const status = normalizeToken(event.status);
8599
+ const provider = normalizeToken(event.provider);
8600
+ const answeredBy = normalizeToken(event.answeredBy);
8601
+ const target = resolveTransferTarget(event, policy);
8602
+ if (status) {
8603
+ const mapped = policy.statusMap?.[status] ?? (provider ? policy.statusMap?.[`${provider}:${status}`] : undefined);
8604
+ if (mapped) {
8605
+ return withDecisionDefaults(mapped, {
8606
+ event,
8607
+ policy,
8608
+ source: "policy"
8609
+ });
8610
+ }
8611
+ }
8612
+ if (answeredBy && normalizeList(policy.machineDetectionVoicemailValues, []).has(answeredBy)) {
8613
+ return buildDecision("voicemail", { event, policy, source: "answered-by" });
8614
+ }
8615
+ if (typeof event.sipCode === "number" && policy.noAnswerSipCodes.includes(event.sipCode)) {
8616
+ return buildDecision("no-answer", { event, policy, source: "sip" });
8617
+ }
8618
+ if (target && status && normalizeList(policy.transferStatuses, []).has(status)) {
8619
+ return buildDecision("transfer", { event, policy, source: "status" });
8620
+ }
8621
+ if (status && normalizeList(policy.voicemailStatuses, []).has(status)) {
8622
+ return buildDecision("voicemail", { event, policy, source: "status" });
8623
+ }
8624
+ if (status && normalizeList(policy.escalationStatuses, []).has(status)) {
8625
+ return buildDecision("escalate", { event, policy, source: "status" });
8626
+ }
8627
+ if (status && (policy.failedAsNoAnswer ? normalizeList(policy.noAnswerStatuses, []).has(status) || normalizeList(policy.failedStatuses, []).has(status) : normalizeList(policy.noAnswerStatuses, []).has(status))) {
8628
+ return buildDecision("no-answer", { event, policy, source: "status" });
8629
+ }
8630
+ if (policy.noAnswerOnZeroDuration && typeof event.durationMs === "number" && event.durationMs <= 0) {
8631
+ return buildDecision("no-answer", { event, policy, source: "duration" });
8632
+ }
8633
+ if (typeof policy.minAnsweredDurationMs === "number" && typeof event.durationMs === "number" && event.durationMs < policy.minAnsweredDurationMs) {
8634
+ return {
8635
+ ...buildDecision("no-answer", { event, policy, source: "duration" }),
8636
+ confidence: "medium"
8637
+ };
8638
+ }
8639
+ if (status && normalizeList(policy.completedStatuses, []).has(status)) {
8640
+ return buildDecision("complete", { event, policy, source: "status" });
8641
+ }
8642
+ if (target) {
8643
+ return {
8644
+ ...buildDecision("transfer", { event, policy, source: "explicit-target" }),
8645
+ confidence: "medium"
8646
+ };
8647
+ }
8648
+ return buildDecision("ignore", { event, policy, source: "status" });
8649
+ };
8650
+ var voiceTelephonyOutcomeToRouteResult = (decision, result) => {
8651
+ switch (decision.action) {
8652
+ case "complete":
8653
+ return { complete: true, result };
8654
+ case "escalate":
8655
+ return {
8656
+ escalate: {
8657
+ metadata: decision.metadata,
8658
+ reason: decision.reason ?? "telephony-escalation"
8659
+ },
8660
+ result
8661
+ };
8662
+ case "no-answer":
8663
+ return {
8664
+ noAnswer: {
8665
+ metadata: decision.metadata
8666
+ },
8667
+ result
8668
+ };
8669
+ case "transfer":
8670
+ if (!decision.target) {
8671
+ return { result };
8672
+ }
8673
+ return {
8674
+ result,
8675
+ transfer: {
8676
+ metadata: decision.metadata,
8677
+ reason: decision.reason,
8678
+ target: decision.target
8679
+ }
8680
+ };
8681
+ case "voicemail":
8682
+ return {
8683
+ result,
8684
+ voicemail: {
8685
+ metadata: decision.metadata
8686
+ }
8687
+ };
8688
+ default:
8689
+ return { result };
8690
+ }
8691
+ };
8692
+ var applyVoiceTelephonyOutcome = async (api, decision, result) => {
8693
+ switch (decision.action) {
8694
+ case "complete":
8695
+ await api.complete(result);
8696
+ break;
8697
+ case "escalate":
8698
+ await api.escalate({
8699
+ metadata: decision.metadata,
8700
+ reason: decision.reason ?? "telephony-escalation",
8701
+ result
8702
+ });
8703
+ break;
8704
+ case "no-answer":
8705
+ await api.markNoAnswer({
8706
+ metadata: decision.metadata,
8707
+ result
8708
+ });
8709
+ break;
8710
+ case "transfer":
8711
+ if (!decision.target) {
8712
+ return;
8713
+ }
8714
+ await api.transfer({
8715
+ metadata: decision.metadata,
8716
+ reason: decision.reason,
8717
+ result,
8718
+ target: decision.target
8719
+ });
8720
+ break;
8721
+ case "voicemail":
8722
+ await api.markVoicemail({
8723
+ metadata: decision.metadata,
8724
+ result
8725
+ });
8726
+ break;
8727
+ default:
8728
+ break;
8729
+ }
8730
+ };
8731
+ var parseRequestBodyText = (input) => {
8732
+ const { contentType, text } = input;
8733
+ if (!text) {
8734
+ return {};
8735
+ }
8736
+ if (contentType.includes("application/json")) {
8737
+ return parseMaybeJSON(text) ?? {};
8738
+ }
8739
+ if (contentType.includes("application/x-www-form-urlencoded") || contentType.includes("multipart/form-data")) {
8740
+ return Object.fromEntries(new URLSearchParams(text));
8741
+ }
8742
+ return parseMaybeJSON(text) ?? Object.fromEntries(new URLSearchParams(text));
8743
+ };
8744
+ var readRequestBody = async (request) => {
8745
+ const contentType = request.headers.get("content-type") ?? "";
8746
+ const text = await request.text();
8747
+ return {
8748
+ body: parseRequestBodyText({ contentType, text }),
8749
+ rawBody: text
8750
+ };
8751
+ };
8752
+ var signVoiceTwilioWebhook = async (input) => signHmacSHA1Base64(input.authToken, `${input.url}${sortedParamsForSignature(input.body ?? {})}`);
8753
+ var verifyVoiceTwilioWebhookSignature = async (input) => {
8754
+ if (!input.authToken) {
8755
+ return { ok: false, reason: "missing-secret" };
8756
+ }
8757
+ const signature = input.headers.get("x-twilio-signature");
8758
+ if (!signature) {
8759
+ return { ok: false, reason: "missing-signature" };
8760
+ }
8761
+ const expected = await signVoiceTwilioWebhook({
8762
+ authToken: input.authToken,
8763
+ body: input.body,
8764
+ url: input.url
8765
+ });
8766
+ return timingSafeEqual(signature, expected) ? { ok: true } : { ok: false, reason: "invalid-signature" };
8767
+ };
8768
+ var resolveVerificationUrl = (option, input) => typeof option === "function" ? option(input) : option ?? input.request.url;
8769
+ var verifyVoiceTelephonyWebhook = async (input) => {
8770
+ if (input.options.verify) {
8771
+ return input.options.verify({
8772
+ body: input.body,
8773
+ headers: input.request.headers,
8774
+ provider: input.provider,
8775
+ query: input.query,
8776
+ rawBody: input.rawBody,
8777
+ request: input.request
8778
+ });
8779
+ }
8780
+ if (!input.options.signingSecret) {
8781
+ return input.options.requireVerification ? { ok: false, reason: "missing-secret" } : { ok: true };
8782
+ }
8783
+ if (input.provider !== "twilio") {
8784
+ return { ok: false, reason: "unsupported-provider" };
8785
+ }
8786
+ return verifyVoiceTwilioWebhookSignature({
8787
+ authToken: input.options.signingSecret,
8788
+ body: input.body,
8789
+ headers: input.request.headers,
8790
+ url: resolveVerificationUrl(input.options.verificationUrl, {
8791
+ query: input.query,
8792
+ request: input.request
8793
+ })
8794
+ });
8795
+ };
8796
+ var durationMsFromSeconds = (value) => typeof value === "number" ? value * 1000 : undefined;
8797
+ var parseVoiceTelephonyWebhookEvent = (input) => {
8798
+ const payload = flattenPayload(input.body);
8799
+ const provider = firstString(payload, ["provider", "Provider"]) ?? input.provider;
8800
+ const status = firstString(payload, [
8801
+ "CallStatus",
8802
+ "call_status",
8803
+ "callStatus",
8804
+ "DialCallStatus",
8805
+ "dial_call_status",
8806
+ "status",
8807
+ "event_type",
8808
+ "type"
8809
+ ]);
8810
+ const durationMs = firstNumber(payload, ["durationMs", "duration_ms"]) ?? durationMsFromSeconds(firstNumber(payload, [
8811
+ "CallDuration",
8812
+ "call_duration",
8813
+ "callDuration",
8814
+ "DialCallDuration",
8815
+ "dial_call_duration",
8816
+ "duration"
8817
+ ]));
8818
+ const sipCode = firstNumber(payload, [
8819
+ "SipResponseCode",
8820
+ "sip_response_code",
8821
+ "sipCode",
8822
+ "sip_code",
8823
+ "hangupCauseCode"
8824
+ ]);
8825
+ const from = firstString(payload, ["From", "from", "caller_id", "callerId"]);
8826
+ const to = firstString(payload, ["To", "to", "called_number", "calledNumber"]);
8827
+ const target = firstString(payload, [
8828
+ "transferTarget",
8829
+ "TransferTarget",
8830
+ "target",
8831
+ "queue",
8832
+ "department"
8833
+ ]);
8834
+ return {
8835
+ answeredBy: firstString(payload, [
8836
+ "AnsweredBy",
8837
+ "answered_by",
8838
+ "answeredBy",
8839
+ "machineDetection",
8840
+ "machine_detection"
8841
+ ]),
8842
+ durationMs,
8843
+ from,
8844
+ metadata: {
8845
+ ...input.query,
8846
+ ...payload
8847
+ },
8848
+ provider,
8849
+ reason: firstString(payload, [
8850
+ "Reason",
8851
+ "reason",
8852
+ "HangupCause",
8853
+ "hangup_cause",
8854
+ "hangupCause"
8855
+ ]),
8856
+ sipCode,
8857
+ status,
8858
+ target,
8859
+ to
8860
+ };
8861
+ };
8862
+ var defaultSessionId = (input) => {
8863
+ const payload = flattenPayload(input.body);
8864
+ const metadataSessionId = input.event.metadata?.sessionId;
8865
+ return firstString(input.query, ["sessionId", "session_id"]) ?? firstString(payload, [
8866
+ "sessionId",
8867
+ "session_id",
8868
+ "SessionId",
8869
+ "CallSid",
8870
+ "call_sid",
8871
+ "callSid",
8872
+ "CallUUID",
8873
+ "call_uuid",
8874
+ "callControlId",
8875
+ "call_control_id"
8876
+ ]) ?? (typeof metadataSessionId === "string" ? metadataSessionId : undefined);
8877
+ };
8878
+ var defaultIdempotencyKey = (input) => {
8879
+ const payload = flattenPayload(input.body);
8880
+ const eventId = firstString(payload, [
8881
+ "id",
8882
+ "event_id",
8883
+ "eventId",
8884
+ "EventSid",
8885
+ "event_sid",
8886
+ "MessageSid",
8887
+ "message_sid",
8888
+ "CallSid",
8889
+ "call_sid",
8890
+ "CallUUID",
8891
+ "call_uuid",
8892
+ "callControlId",
8893
+ "call_control_id"
8894
+ ]);
8895
+ const status = normalizeToken(input.event.status) ?? "unknown";
8896
+ if (eventId) {
8897
+ return `${input.provider}:${eventId}:${status}`;
8898
+ }
8899
+ if (input.sessionId) {
8900
+ return `${input.provider}:${input.sessionId}:${status}`;
8901
+ }
8902
+ };
8903
+ var createVoiceTelephonyWebhookHandler = (options = {}) => async (input) => {
8904
+ const provider = options.provider ?? "generic";
8905
+ const query = input.query ?? {};
8906
+ const { body, rawBody } = await readRequestBody(input.request);
8907
+ const verification = await verifyVoiceTelephonyWebhook({
8908
+ body,
8909
+ options,
8910
+ provider,
8911
+ query,
8912
+ rawBody,
8913
+ request: input.request
8914
+ });
8915
+ if (!verification.ok) {
8916
+ throw new VoiceTelephonyWebhookVerificationError(verification);
8917
+ }
8918
+ const event = options.parse ? await options.parse({
8919
+ body,
8920
+ headers: input.request.headers,
8921
+ provider,
8922
+ query,
8923
+ request: input.request
8924
+ }) : parseVoiceTelephonyWebhookEvent({
8925
+ body,
8926
+ headers: input.request.headers,
8927
+ provider,
8928
+ query,
8929
+ request: input.request
8930
+ });
8931
+ const sessionId = await (options.resolveSessionId?.({
8932
+ body,
8933
+ event,
8934
+ query,
8935
+ request: input.request
8936
+ }) ?? defaultSessionId({ body, event, query }));
8937
+ const idempotencyEnabled = options.idempotency?.enabled !== false;
8938
+ const idempotencyKey = idempotencyEnabled ? await (options.idempotency?.key?.({
8939
+ body,
8940
+ event,
8941
+ provider,
8942
+ query,
8943
+ request: input.request,
8944
+ sessionId
8945
+ }) ?? defaultIdempotencyKey({ body, event, provider, sessionId })) : undefined;
8946
+ const idempotencyStore = options.idempotency?.store;
8947
+ if (idempotencyKey && idempotencyStore) {
8948
+ const existing = await idempotencyStore.get(idempotencyKey);
8949
+ if (existing) {
8950
+ const duplicateDecision = {
8951
+ ...existing,
8952
+ duplicate: true
8953
+ };
8954
+ await options.onDecision?.({
8955
+ ...duplicateDecision,
8956
+ context: options.context,
8957
+ request: input.request
8958
+ });
8959
+ return duplicateDecision;
8960
+ }
8961
+ }
8962
+ const decision = resolveVoiceTelephonyOutcome(event, options.policy);
8963
+ const resultResolver = options.result;
8964
+ const result = typeof resultResolver === "function" ? await resultResolver({
8965
+ decision,
8966
+ event,
8967
+ sessionId
8968
+ }) : resultResolver;
8969
+ const routeResult = voiceTelephonyOutcomeToRouteResult(decision, result);
8970
+ const shouldApply = typeof options.apply === "function" ? options.apply({
8971
+ applied: false,
8972
+ decision,
8973
+ event,
8974
+ routeResult,
8975
+ sessionId
8976
+ }) : options.apply === true;
8977
+ let applied = false;
8978
+ if (shouldApply && decision.action !== "ignore" && options.getSessionHandle) {
8979
+ const api = await options.getSessionHandle({
8980
+ context: options.context,
8981
+ decision,
8982
+ event,
8983
+ request: input.request,
8984
+ sessionId
8985
+ });
8986
+ if (api) {
8987
+ await applyVoiceTelephonyOutcome(api, decision, result);
8988
+ applied = true;
8989
+ }
8990
+ }
8991
+ const webhookDecision = {
8992
+ applied,
8993
+ decision,
8994
+ event,
8995
+ idempotencyKey,
8996
+ routeResult,
8997
+ sessionId
8998
+ };
8999
+ if (idempotencyKey && idempotencyStore) {
9000
+ const now = Date.now();
9001
+ await idempotencyStore.set(idempotencyKey, {
9002
+ ...webhookDecision,
9003
+ createdAt: now,
9004
+ updatedAt: now
9005
+ });
9006
+ }
9007
+ await options.onDecision?.({
9008
+ ...webhookDecision,
9009
+ context: options.context,
9010
+ request: input.request
9011
+ });
9012
+ return webhookDecision;
9013
+ };
9014
+ var createVoiceTelephonyWebhookRoutes = (options = {}) => {
9015
+ const path = options.path ?? "/api/voice/telephony/webhook";
9016
+ const handler = createVoiceTelephonyWebhookHandler(options);
9017
+ return new Elysia({
9018
+ name: options.name ?? "absolutejs-voice-telephony-webhooks"
9019
+ }).post(path, async ({ query, request }) => {
9020
+ try {
9021
+ return await handler({ query, request });
9022
+ } catch (error) {
9023
+ if (error instanceof VoiceTelephonyWebhookVerificationError) {
9024
+ return new Response(JSON.stringify({ verification: error.result }), {
9025
+ headers: {
9026
+ "content-type": "application/json"
9027
+ },
9028
+ status: 401
9029
+ });
9030
+ }
9031
+ throw error;
9032
+ }
9033
+ }, {
9034
+ parse: "none"
9035
+ });
9036
+ };
9037
+
9038
+ // src/telephony/twilio.ts
7644
9039
  var TWILIO_MULAW_SAMPLE_RATE = 8000;
7645
9040
  var VOICE_PCM_SAMPLE_RATE = 16000;
7646
9041
  var escapeXml2 = (value) => value.replaceAll("&", "&amp;").replaceAll('"', "&quot;").replaceAll("'", "&apos;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
9042
+ var resolveRequestOrigin = (request) => {
9043
+ const url = new URL(request.url);
9044
+ const forwardedHost = request.headers.get("x-forwarded-host");
9045
+ const forwardedProto = request.headers.get("x-forwarded-proto");
9046
+ const host = forwardedHost ?? request.headers.get("host") ?? url.host;
9047
+ const protocol = forwardedProto ?? url.protocol.replace(":", "");
9048
+ return `${protocol}://${host}`;
9049
+ };
9050
+ var resolveTwilioStreamUrl = async (options, input) => {
9051
+ if (typeof options.twiml?.streamUrl === "function") {
9052
+ return options.twiml.streamUrl(input);
9053
+ }
9054
+ if (typeof options.twiml?.streamUrl === "string") {
9055
+ return options.twiml.streamUrl;
9056
+ }
9057
+ const origin = resolveRequestOrigin(input.request);
9058
+ const wsOrigin = origin.replace(/^http:/, "ws:").replace(/^https:/, "wss:");
9059
+ return `${wsOrigin}${input.streamPath}`;
9060
+ };
9061
+ var resolveTwilioStreamParameters = async (parameters, input) => {
9062
+ if (typeof parameters === "function") {
9063
+ return parameters(input);
9064
+ }
9065
+ return parameters;
9066
+ };
9067
+ var joinUrlPath = (origin, path) => `${origin.replace(/\/$/, "")}${path.startsWith("/") ? path : `/${path}`}`;
9068
+ var escapeHtml2 = (value) => value.replaceAll("&", "&amp;").replaceAll('"', "&quot;").replaceAll("'", "&#39;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
9069
+ var getWebhookVerificationUrl = (webhook, input) => {
9070
+ if (!webhook?.verificationUrl) {
9071
+ return;
9072
+ }
9073
+ if (typeof webhook.verificationUrl === "function") {
9074
+ return webhook.verificationUrl(input);
9075
+ }
9076
+ return webhook.verificationUrl;
9077
+ };
9078
+ var buildTwilioVoiceSetupStatus = async (options, input) => {
9079
+ const origin = resolveRequestOrigin(input.request);
9080
+ const stream = await resolveTwilioStreamUrl(options, input);
9081
+ const twiml = joinUrlPath(origin, input.twimlPath);
9082
+ const webhook = joinUrlPath(origin, input.webhookPath);
9083
+ const verificationUrl = getWebhookVerificationUrl(options.webhook, input);
9084
+ const missing = Object.entries(options.setup?.requiredEnv ?? {}).filter((entry) => !entry[1]).map(([name]) => name);
9085
+ const signingConfigured = Boolean(options.webhook?.signingSecret || options.webhook?.verify);
9086
+ const warnings = [
9087
+ ...stream.startsWith("wss://") ? [] : ["Twilio media streams should use wss:// in production."],
9088
+ ...signingConfigured ? [] : ["Webhook signature verification is not configured."],
9089
+ ...verificationUrl || !signingConfigured ? [] : ["Webhook signing is configured without an explicit verification URL."]
9090
+ ];
9091
+ return {
9092
+ generatedAt: Date.now(),
9093
+ missing,
9094
+ provider: "twilio",
9095
+ ready: missing.length === 0 && signingConfigured && warnings.length === 0,
9096
+ signing: {
9097
+ configured: signingConfigured,
9098
+ mode: options.webhook?.verify ? "custom" : options.webhook?.signingSecret ? "twilio-signature" : "none",
9099
+ verificationUrl
9100
+ },
9101
+ urls: {
9102
+ stream,
9103
+ twiml,
9104
+ webhook
9105
+ },
9106
+ warnings
9107
+ };
9108
+ };
9109
+ var renderTwilioVoiceSetupHTML = (status, title) => `<main style="font-family: ui-sans-serif, system-ui; max-width: 860px; margin: 40px auto; padding: 0 20px;">
9110
+ <p style="letter-spacing: .12em; text-transform: uppercase; color: #52606d;">Twilio setup</p>
9111
+ <h1>${escapeHtml2(title)}</h1>
9112
+ <p><strong>Status:</strong> ${status.ready ? "Ready" : "Needs attention"}</p>
9113
+ <section>
9114
+ <h2>URLs</h2>
9115
+ <ul>
9116
+ <li><strong>TwiML:</strong> <code>${escapeHtml2(status.urls.twiml)}</code></li>
9117
+ <li><strong>Media stream:</strong> <code>${escapeHtml2(status.urls.stream)}</code></li>
9118
+ <li><strong>Status webhook:</strong> <code>${escapeHtml2(status.urls.webhook)}</code></li>
9119
+ </ul>
9120
+ </section>
9121
+ <section>
9122
+ <h2>Signing</h2>
9123
+ <p>Mode: <code>${status.signing.mode}</code></p>
9124
+ ${status.signing.verificationUrl ? `<p>Verification URL: <code>${escapeHtml2(status.signing.verificationUrl)}</code></p>` : ""}
9125
+ </section>
9126
+ ${status.missing.length ? `<section><h2>Missing env</h2><ul>${status.missing.map((name) => `<li><code>${escapeHtml2(name)}</code></li>`).join("")}</ul></section>` : ""}
9127
+ ${status.warnings.length ? `<section><h2>Warnings</h2><ul>${status.warnings.map((warning) => `<li>${escapeHtml2(warning)}</li>`).join("")}</ul></section>` : ""}
9128
+ </main>`;
9129
+ var extractTwilioStreamUrl = (twiml) => twiml.match(/<Stream\b[^>]*\surl="([^"]+)"/i)?.[1]?.replaceAll("&amp;", "&");
9130
+ var createSmokeCheck = (name, status, message, details) => ({
9131
+ details,
9132
+ message,
9133
+ name,
9134
+ status
9135
+ });
9136
+ var renderTwilioVoiceSmokeHTML = (report, title) => `<main style="font-family: ui-sans-serif, system-ui; max-width: 860px; margin: 40px auto; padding: 0 20px;">
9137
+ <p style="letter-spacing: .12em; text-transform: uppercase; color: #52606d;">Twilio smoke test</p>
9138
+ <h1>${escapeHtml2(title)}</h1>
9139
+ <p><strong>Status:</strong> ${report.pass ? "Pass" : "Fail"}</p>
9140
+ <section>
9141
+ <h2>Checks</h2>
9142
+ <ul>
9143
+ ${report.checks.map((check) => `<li><strong>${escapeHtml2(check.name)}</strong>: ${escapeHtml2(check.status)}${check.message ? ` - ${escapeHtml2(check.message)}` : ""}</li>`).join("")}
9144
+ </ul>
9145
+ </section>
9146
+ <section>
9147
+ <h2>Observed URLs</h2>
9148
+ <ul>
9149
+ <li><strong>TwiML:</strong> <code>${escapeHtml2(report.setup.urls.twiml)}</code></li>
9150
+ <li><strong>Stream:</strong> <code>${escapeHtml2(report.twiml?.streamUrl ?? report.setup.urls.stream)}</code></li>
9151
+ <li><strong>Webhook:</strong> <code>${escapeHtml2(report.setup.urls.webhook)}</code></li>
9152
+ </ul>
9153
+ </section>
9154
+ </main>`;
9155
+ var runTwilioVoiceSmokeTest = async (input) => {
9156
+ const setup = await buildTwilioVoiceSetupStatus(input.options, input);
9157
+ const checks = [];
9158
+ const twimlUrl = new URL(setup.urls.twiml);
9159
+ twimlUrl.searchParams.set("scenarioId", input.options.smoke?.scenarioId ?? "smoke");
9160
+ twimlUrl.searchParams.set("sessionId", input.options.smoke?.sessionId ?? "smoke-session");
9161
+ const twimlResponse = await input.app.handle(new Request(twimlUrl, {
9162
+ headers: input.request.headers
9163
+ }));
9164
+ const twiml = await twimlResponse.text();
9165
+ const streamUrl = extractTwilioStreamUrl(twiml);
9166
+ checks.push(createSmokeCheck("twiml", twimlResponse.ok && Boolean(streamUrl) ? "pass" : "fail", streamUrl ? "TwiML includes a media stream URL." : 'TwiML is missing <Stream url="...">.', {
9167
+ status: twimlResponse.status,
9168
+ streamUrl
9169
+ }));
9170
+ 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.", {
9171
+ streamUrl
9172
+ }));
9173
+ const webhookBody = {
9174
+ CallSid: input.options.smoke?.callSid ?? "CA_SMOKE_TEST",
9175
+ CallStatus: input.options.smoke?.status ?? "busy",
9176
+ SipResponseCode: String(input.options.smoke?.sipCode ?? 486)
9177
+ };
9178
+ const webhookHeaders = new Headers({
9179
+ "content-type": "application/x-www-form-urlencoded"
9180
+ });
9181
+ const verificationUrl = setup.signing.verificationUrl ?? setup.urls.webhook;
9182
+ if (input.options.webhook?.signingSecret) {
9183
+ webhookHeaders.set("x-twilio-signature", await signVoiceTwilioWebhook({
9184
+ authToken: input.options.webhook.signingSecret,
9185
+ body: webhookBody,
9186
+ url: verificationUrl
9187
+ }));
9188
+ }
9189
+ const webhookResponse = await input.app.handle(new Request(setup.urls.webhook, {
9190
+ body: new URLSearchParams(webhookBody),
9191
+ headers: webhookHeaders,
9192
+ method: "POST"
9193
+ }));
9194
+ const webhookText = await webhookResponse.text();
9195
+ const webhookPayload = (() => {
9196
+ try {
9197
+ return JSON.parse(webhookText);
9198
+ } catch {
9199
+ return webhookText;
9200
+ }
9201
+ })();
9202
+ checks.push(createSmokeCheck("webhook", webhookResponse.ok ? "pass" : "fail", webhookResponse.ok ? "Synthetic Twilio status callback was accepted." : "Synthetic Twilio status callback failed.", {
9203
+ status: webhookResponse.status
9204
+ }));
9205
+ for (const warning of setup.warnings) {
9206
+ checks.push(createSmokeCheck("setup-warning", "warn", warning));
9207
+ }
9208
+ for (const name of setup.missing) {
9209
+ checks.push(createSmokeCheck("missing-env", "fail", `${name} is missing.`));
9210
+ }
9211
+ return {
9212
+ checks,
9213
+ generatedAt: Date.now(),
9214
+ pass: checks.every((check) => check.status !== "fail"),
9215
+ provider: "twilio",
9216
+ setup,
9217
+ twiml: {
9218
+ status: twimlResponse.status,
9219
+ streamUrl
9220
+ },
9221
+ webhook: {
9222
+ body: webhookPayload,
9223
+ status: webhookResponse.status
9224
+ }
9225
+ };
9226
+ };
7647
9227
  var normalizeOnTurn = (handler) => {
7648
9228
  if (handler.length > 1) {
7649
9229
  const directHandler = handler;
@@ -7745,7 +9325,7 @@ var bytesToInt16Array = (bytes) => {
7745
9325
  return output;
7746
9326
  };
7747
9327
  var decodeTwilioMulawBase64 = (payload) => {
7748
- const bytes = Uint8Array.from(Buffer2.from(payload, "base64"));
9328
+ const bytes = Uint8Array.from(Buffer3.from(payload, "base64"));
7749
9329
  const samples = new Int16Array(bytes.length);
7750
9330
  for (let index = 0;index < bytes.length; index += 1) {
7751
9331
  samples[index] = decodeMulawSample(bytes[index] ?? 0);
@@ -7757,7 +9337,7 @@ var encodeTwilioMulawBase64 = (samples) => {
7757
9337
  for (let index = 0;index < samples.length; index += 1) {
7758
9338
  bytes[index] = encodeMulawSample(samples[index] ?? 0);
7759
9339
  }
7760
- return Buffer2.from(bytes).toString("base64");
9340
+ return Buffer3.from(bytes).toString("base64");
7761
9341
  };
7762
9342
  var transcodeTwilioInboundPayloadToPCM16 = (payload) => {
7763
9343
  const narrowband = decodeTwilioMulawBase64(payload);
@@ -7766,7 +9346,7 @@ var transcodeTwilioInboundPayloadToPCM16 = (payload) => {
7766
9346
  };
7767
9347
  var transcodePCMToTwilioOutboundPayload = (chunk, format) => {
7768
9348
  if (format.container === "raw" && format.encoding === "mulaw" && format.channels === 1 && format.sampleRateHz === TWILIO_MULAW_SAMPLE_RATE) {
7769
- return Buffer2.from(chunk).toString("base64");
9349
+ return Buffer3.from(chunk).toString("base64");
7770
9350
  }
7771
9351
  if (format.encoding !== "pcm_s16le") {
7772
9352
  throw new Error(`Unsupported outbound telephony audio format: ${format.container}/${format.encoding}`);
@@ -7807,7 +9387,7 @@ var createTwilioSocketAdapter = (socket, getState) => ({
7807
9387
  return;
7808
9388
  }
7809
9389
  if (message.type === "audio") {
7810
- const payload = transcodePCMToTwilioOutboundPayload(Uint8Array.from(Buffer2.from(message.chunkBase64, "base64")), message.format);
9390
+ const payload = transcodePCMToTwilioOutboundPayload(Uint8Array.from(Buffer3.from(message.chunkBase64, "base64")), message.format);
7811
9391
  state.hasOutboundAudioSinceLastInbound = true;
7812
9392
  state.reviewRecorder?.recordTwilioOutbound({
7813
9393
  bytes: payload.length,
@@ -8020,6 +9600,148 @@ var createTwilioMediaStreamBridge = (socket, options) => {
8020
9600
  }
8021
9601
  };
8022
9602
  };
9603
+ var createTwilioVoiceRoutes = (options) => {
9604
+ const streamPath = options.streamPath ?? "/api/voice/twilio/stream";
9605
+ const twimlPath = options.twiml?.path ?? "/api/voice/twilio";
9606
+ const webhookPath = options.webhook?.path ?? "/api/voice/twilio/webhook";
9607
+ const setupPath = options.setup?.path === false ? false : options.setup?.path ?? "/api/voice/twilio/setup";
9608
+ const smokePath = options.smoke?.path === false ? false : options.smoke?.path ?? "/api/voice/twilio/smoke";
9609
+ const bridges = new WeakMap;
9610
+ const webhookPolicy = options.webhook?.policy ?? options.outcomePolicy ?? createVoiceTelephonyOutcomePolicy();
9611
+ const app = new Elysia2({
9612
+ name: options.name ?? "absolutejs-voice-twilio"
9613
+ }).get(twimlPath, async ({ query, request }) => {
9614
+ const streamUrl = await resolveTwilioStreamUrl(options, {
9615
+ query,
9616
+ request,
9617
+ streamPath
9618
+ });
9619
+ const parameters = await resolveTwilioStreamParameters(options.twiml?.parameters, {
9620
+ query,
9621
+ request
9622
+ });
9623
+ return new Response(createTwilioVoiceResponse({
9624
+ parameters,
9625
+ streamName: options.twiml?.streamName,
9626
+ streamUrl,
9627
+ track: options.twiml?.track
9628
+ }), {
9629
+ headers: {
9630
+ "content-type": "text/xml; charset=utf-8"
9631
+ }
9632
+ });
9633
+ }).post(twimlPath, async ({ query, request }) => {
9634
+ const streamUrl = await resolveTwilioStreamUrl(options, {
9635
+ query,
9636
+ request,
9637
+ streamPath
9638
+ });
9639
+ const parameters = await resolveTwilioStreamParameters(options.twiml?.parameters, {
9640
+ query,
9641
+ request
9642
+ });
9643
+ return new Response(createTwilioVoiceResponse({
9644
+ parameters,
9645
+ streamName: options.twiml?.streamName,
9646
+ streamUrl,
9647
+ track: options.twiml?.track
9648
+ }), {
9649
+ headers: {
9650
+ "content-type": "text/xml; charset=utf-8"
9651
+ }
9652
+ });
9653
+ }).ws(streamPath, {
9654
+ close: async (ws, _code, reason) => {
9655
+ const bridge = bridges.get(ws);
9656
+ bridges.delete(ws);
9657
+ await bridge?.close(reason);
9658
+ },
9659
+ message: async (ws, raw) => {
9660
+ let bridge = bridges.get(ws);
9661
+ if (!bridge) {
9662
+ bridge = createTwilioMediaStreamBridge({
9663
+ close: (code, reason) => {
9664
+ ws.close(code, reason);
9665
+ },
9666
+ send: (data) => {
9667
+ ws.send(data);
9668
+ }
9669
+ }, options);
9670
+ bridges.set(ws, bridge);
9671
+ }
9672
+ await bridge.handleMessage(raw);
9673
+ }
9674
+ }).use(createVoiceTelephonyWebhookRoutes({
9675
+ ...options.webhook ?? {},
9676
+ context: options.context,
9677
+ path: webhookPath,
9678
+ policy: webhookPolicy,
9679
+ provider: "twilio"
9680
+ }));
9681
+ if (!setupPath) {
9682
+ if (!smokePath) {
9683
+ return app;
9684
+ }
9685
+ return app.get(smokePath, async ({ query, request }) => {
9686
+ const report = await runTwilioVoiceSmokeTest({
9687
+ app,
9688
+ options,
9689
+ query,
9690
+ request,
9691
+ streamPath,
9692
+ twimlPath,
9693
+ webhookPath
9694
+ });
9695
+ if (query.format === "html") {
9696
+ return new Response(renderTwilioVoiceSmokeHTML(report, options.smoke?.title ?? "AbsoluteJS Twilio Voice Smoke Test"), {
9697
+ headers: {
9698
+ "content-type": "text/html; charset=utf-8"
9699
+ }
9700
+ });
9701
+ }
9702
+ return report;
9703
+ });
9704
+ }
9705
+ const withSetup = app.get(setupPath, async ({ query, request }) => {
9706
+ const status = await buildTwilioVoiceSetupStatus(options, {
9707
+ query,
9708
+ request,
9709
+ streamPath,
9710
+ twimlPath,
9711
+ webhookPath
9712
+ });
9713
+ if (query.format === "html") {
9714
+ return new Response(renderTwilioVoiceSetupHTML(status, options.setup?.title ?? "AbsoluteJS Twilio Voice Setup"), {
9715
+ headers: {
9716
+ "content-type": "text/html; charset=utf-8"
9717
+ }
9718
+ });
9719
+ }
9720
+ return status;
9721
+ });
9722
+ if (!smokePath) {
9723
+ return withSetup;
9724
+ }
9725
+ return withSetup.get(smokePath, async ({ query, request }) => {
9726
+ const report = await runTwilioVoiceSmokeTest({
9727
+ app,
9728
+ options,
9729
+ query,
9730
+ request,
9731
+ streamPath,
9732
+ twimlPath,
9733
+ webhookPath
9734
+ });
9735
+ if (query.format === "html") {
9736
+ return new Response(renderTwilioVoiceSmokeHTML(report, options.smoke?.title ?? "AbsoluteJS Twilio Voice Smoke Test"), {
9737
+ headers: {
9738
+ "content-type": "text/html; charset=utf-8"
9739
+ }
9740
+ });
9741
+ }
9742
+ return report;
9743
+ });
9744
+ };
8023
9745
 
8024
9746
  // src/testing/telephony.ts
8025
9747
  var DEFAULT_PCM16_FORMAT = {
@@ -8285,7 +10007,7 @@ var runVoiceTelephonyBenchmark = async (scenarios = getDefaultVoiceTelephonyBenc
8285
10007
  };
8286
10008
  };
8287
10009
  // src/testing/tts.ts
8288
- var DEFAULT_REALTIME_FORMAT = {
10010
+ var DEFAULT_REALTIME_FORMAT2 = {
8289
10011
  channels: 1,
8290
10012
  container: "raw",
8291
10013
  encoding: "pcm_s16le",
@@ -8344,7 +10066,7 @@ var runTTSAdapterFixture = async (adapter, fixture, options = {}) => {
8344
10066
  let audioDurationMs = 0;
8345
10067
  let audioChunkCount = 0;
8346
10068
  const session = adapter.kind === "realtime" ? await adapter.open({
8347
- format: options.realtimeFormat ?? DEFAULT_REALTIME_FORMAT,
10069
+ format: options.realtimeFormat ?? DEFAULT_REALTIME_FORMAT2,
8348
10070
  sessionId: `tts-benchmark:${fixture.id}`,
8349
10071
  ...openOptions ?? {}
8350
10072
  }) : await adapter.open({
@@ -8511,6 +10233,7 @@ export {
8511
10233
  getDefaultTTSBenchmarkFixtures,
8512
10234
  evaluateSTTBenchmarkAcceptance,
8513
10235
  createVoiceProviderFailureSimulator,
10236
+ createVoiceIOProviderFailureSimulator,
8514
10237
  createVoiceCallReviewRecorder,
8515
10238
  createVoiceCallReviewFromLiveTelephonyReport,
8516
10239
  createTelephonyVoiceTestFixtures,