@absolutejs/voice 0.0.22-beta.30 → 0.0.22-beta.300

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 (237) hide show
  1. package/README.md +3232 -82
  2. package/dist/agent.d.ts +62 -0
  3. package/dist/agentSquadContract.d.ts +98 -0
  4. package/dist/angular/index.d.ts +16 -0
  5. package/dist/angular/index.js +3498 -1128
  6. package/dist/angular/voice-agent-squad-status.service.d.ts +12 -0
  7. package/dist/angular/voice-campaign-dialer-proof.service.d.ts +14 -0
  8. package/dist/angular/voice-controller.service.d.ts +1 -0
  9. package/dist/angular/voice-delivery-runtime.component.d.ts +17 -0
  10. package/dist/angular/voice-delivery-runtime.service.d.ts +16 -0
  11. package/dist/angular/voice-live-ops.service.d.ts +11 -0
  12. package/dist/angular/voice-ops-action-center.service.d.ts +13 -0
  13. package/dist/angular/voice-ops-status.component.d.ts +15 -0
  14. package/dist/angular/voice-ops-status.service.d.ts +12 -0
  15. package/dist/angular/voice-platform-coverage.service.d.ts +12 -0
  16. package/dist/angular/voice-proof-trends.service.d.ts +12 -0
  17. package/dist/angular/voice-provider-capabilities.service.d.ts +12 -0
  18. package/dist/angular/voice-provider-contracts.service.d.ts +12 -0
  19. package/dist/angular/voice-readiness-failures.service.d.ts +13 -0
  20. package/dist/angular/voice-routing-status.service.d.ts +11 -0
  21. package/dist/angular/voice-stream.service.d.ts +1 -0
  22. package/dist/angular/voice-trace-timeline.service.d.ts +12 -0
  23. package/dist/angular/voice-turn-latency.service.d.ts +13 -0
  24. package/dist/angular/voice-turn-quality.service.d.ts +12 -0
  25. package/dist/angular/voice-workflow-status.service.d.ts +12 -0
  26. package/dist/audit.d.ts +128 -0
  27. package/dist/auditDeliveryRoutes.d.ts +85 -0
  28. package/dist/auditExport.d.ts +34 -0
  29. package/dist/auditRoutes.d.ts +66 -0
  30. package/dist/auditSinks.d.ts +151 -0
  31. package/dist/bargeInRoutes.d.ts +56 -0
  32. package/dist/campaign.d.ts +768 -0
  33. package/dist/campaignDialers.d.ts +111 -0
  34. package/dist/client/actions.d.ts +83 -0
  35. package/dist/client/agentSquadStatus.d.ts +37 -0
  36. package/dist/client/agentSquadStatusWidget.d.ts +24 -0
  37. package/dist/client/bargeInMonitor.d.ts +7 -0
  38. package/dist/client/campaignDialerProof.d.ts +23 -0
  39. package/dist/client/deliveryRuntime.d.ts +34 -0
  40. package/dist/client/deliveryRuntimeWidget.d.ts +37 -0
  41. package/dist/client/duplex.d.ts +1 -1
  42. package/dist/client/htmxBootstrap.js +703 -13
  43. package/dist/client/index.d.ts +70 -0
  44. package/dist/client/index.js +5257 -19
  45. package/dist/client/liveOps.d.ts +22 -0
  46. package/dist/client/liveOpsWidget.d.ts +23 -0
  47. package/dist/client/liveTurnLatency.d.ts +41 -0
  48. package/dist/client/opsActionCenter.d.ts +54 -0
  49. package/dist/client/opsActionCenterWidget.d.ts +29 -0
  50. package/dist/client/opsActionHistory.d.ts +19 -0
  51. package/dist/client/opsActionHistoryWidget.d.ts +11 -0
  52. package/dist/client/opsStatus.d.ts +19 -0
  53. package/dist/client/opsStatusWidget.d.ts +40 -0
  54. package/dist/client/platformCoverage.d.ts +19 -0
  55. package/dist/client/platformCoverageWidget.d.ts +37 -0
  56. package/dist/client/proofTrends.d.ts +19 -0
  57. package/dist/client/proofTrendsWidget.d.ts +37 -0
  58. package/dist/client/providerCapabilities.d.ts +19 -0
  59. package/dist/client/providerCapabilitiesWidget.d.ts +32 -0
  60. package/dist/client/providerContracts.d.ts +19 -0
  61. package/dist/client/providerContractsWidget.d.ts +37 -0
  62. package/dist/client/providerSimulationControls.d.ts +33 -0
  63. package/dist/client/providerSimulationControlsWidget.d.ts +20 -0
  64. package/dist/client/providerStatusWidget.d.ts +32 -0
  65. package/dist/client/readinessFailures.d.ts +19 -0
  66. package/dist/client/readinessFailuresWidget.d.ts +42 -0
  67. package/dist/client/routingStatus.d.ts +19 -0
  68. package/dist/client/routingStatusWidget.d.ts +28 -0
  69. package/dist/client/traceTimeline.d.ts +19 -0
  70. package/dist/client/traceTimelineWidget.d.ts +36 -0
  71. package/dist/client/turnLatency.d.ts +22 -0
  72. package/dist/client/turnLatencyWidget.d.ts +33 -0
  73. package/dist/client/turnQuality.d.ts +19 -0
  74. package/dist/client/turnQualityWidget.d.ts +32 -0
  75. package/dist/client/workflowStatus.d.ts +19 -0
  76. package/dist/dataControl.d.ts +180 -0
  77. package/dist/deliveryRuntime.d.ts +158 -0
  78. package/dist/deliverySinkRoutes.d.ts +117 -0
  79. package/dist/demoReadyRoutes.d.ts +98 -0
  80. package/dist/diagnosticsRoutes.d.ts +44 -0
  81. package/dist/evalRoutes.d.ts +219 -0
  82. package/dist/fileStore.d.ts +14 -2
  83. package/dist/guardrails.d.ts +128 -0
  84. package/dist/handoff.d.ts +15 -1
  85. package/dist/incidentBundle.d.ts +116 -0
  86. package/dist/index.d.ts +140 -15
  87. package/dist/index.js +27685 -5100
  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/modelAdapters.d.ts +60 -2
  92. package/dist/observabilityExport.d.ts +481 -0
  93. package/dist/openaiRealtime.d.ts +27 -0
  94. package/dist/openaiTTS.d.ts +18 -0
  95. package/dist/operationsRecord.d.ts +254 -0
  96. package/dist/opsActionAuditRoutes.d.ts +99 -0
  97. package/dist/opsConsoleRoutes.d.ts +80 -0
  98. package/dist/opsRecovery.d.ts +137 -0
  99. package/dist/opsStatus.d.ts +76 -0
  100. package/dist/opsStatusRoutes.d.ts +33 -0
  101. package/dist/outcomeContract.d.ts +146 -0
  102. package/dist/phoneAgent.d.ts +139 -0
  103. package/dist/phoneAgentProductionSmoke.d.ts +115 -0
  104. package/dist/platformCoverage.d.ts +91 -0
  105. package/dist/postCallAnalysis.d.ts +98 -0
  106. package/dist/postgresStore.d.ts +13 -2
  107. package/dist/productionReadiness.d.ts +559 -0
  108. package/dist/proofTrends.d.ts +133 -0
  109. package/dist/providerAdapters.d.ts +48 -0
  110. package/dist/providerCapabilities.d.ts +92 -0
  111. package/dist/providerDecisionTraces.d.ts +130 -0
  112. package/dist/providerHealth.d.ts +1 -0
  113. package/dist/providerOrchestration.d.ts +109 -0
  114. package/dist/providerRoutingContract.d.ts +71 -0
  115. package/dist/providerSlo.d.ts +142 -0
  116. package/dist/providerStackRecommendations.d.ts +187 -0
  117. package/dist/qualityRoutes.d.ts +76 -0
  118. package/dist/queue.d.ts +61 -0
  119. package/dist/react/VoiceAgentSquadStatus.d.ts +5 -0
  120. package/dist/react/VoiceDeliveryRuntime.d.ts +7 -0
  121. package/dist/react/VoiceOpsActionCenter.d.ts +5 -0
  122. package/dist/react/VoiceOpsStatus.d.ts +6 -0
  123. package/dist/react/VoicePlatformCoverage.d.ts +6 -0
  124. package/dist/react/VoiceProofTrends.d.ts +6 -0
  125. package/dist/react/VoiceProviderCapabilities.d.ts +6 -0
  126. package/dist/react/VoiceProviderContracts.d.ts +6 -0
  127. package/dist/react/VoiceProviderSimulationControls.d.ts +5 -0
  128. package/dist/react/VoiceProviderStatus.d.ts +6 -0
  129. package/dist/react/VoiceReadinessFailures.d.ts +6 -0
  130. package/dist/react/VoiceRoutingStatus.d.ts +6 -0
  131. package/dist/react/VoiceTraceTimeline.d.ts +6 -0
  132. package/dist/react/VoiceTurnLatency.d.ts +6 -0
  133. package/dist/react/VoiceTurnQuality.d.ts +6 -0
  134. package/dist/react/index.d.ts +32 -0
  135. package/dist/react/index.js +5059 -31
  136. package/dist/react/useVoiceAgentSquadStatus.d.ts +8 -0
  137. package/dist/react/useVoiceCampaignDialerProof.d.ts +10 -0
  138. package/dist/react/useVoiceController.d.ts +1 -0
  139. package/dist/react/useVoiceDeliveryRuntime.d.ts +13 -0
  140. package/dist/react/useVoiceLiveOps.d.ts +9 -0
  141. package/dist/react/useVoiceOpsActionCenter.d.ts +11 -0
  142. package/dist/react/useVoiceOpsStatus.d.ts +8 -0
  143. package/dist/react/useVoicePlatformCoverage.d.ts +8 -0
  144. package/dist/react/useVoiceProofTrends.d.ts +8 -0
  145. package/dist/react/useVoiceProviderCapabilities.d.ts +8 -0
  146. package/dist/react/useVoiceProviderContracts.d.ts +8 -0
  147. package/dist/react/useVoiceProviderSimulationControls.d.ts +10 -0
  148. package/dist/react/useVoiceReadinessFailures.d.ts +8 -0
  149. package/dist/react/useVoiceRoutingStatus.d.ts +8 -0
  150. package/dist/react/useVoiceStream.d.ts +1 -0
  151. package/dist/react/useVoiceTraceTimeline.d.ts +8 -0
  152. package/dist/react/useVoiceTurnLatency.d.ts +9 -0
  153. package/dist/react/useVoiceTurnQuality.d.ts +8 -0
  154. package/dist/react/useVoiceWorkflowStatus.d.ts +8 -0
  155. package/dist/readinessProfiles.d.ts +38 -0
  156. package/dist/reconnectContract.d.ts +88 -0
  157. package/dist/resilienceRoutes.d.ts +143 -0
  158. package/dist/sessionReplay.d.ts +12 -0
  159. package/dist/simulationSuite.d.ts +143 -0
  160. package/dist/sloCalibration.d.ts +185 -0
  161. package/dist/sqliteStore.d.ts +13 -2
  162. package/dist/svelte/createVoiceAgentSquadStatus.d.ts +9 -0
  163. package/dist/svelte/createVoiceCampaignDialerProof.d.ts +9 -0
  164. package/dist/svelte/createVoiceDeliveryRuntime.d.ts +11 -0
  165. package/dist/svelte/createVoiceLiveOps.d.ts +13 -0
  166. package/dist/svelte/createVoiceOpsActionCenter.d.ts +10 -0
  167. package/dist/svelte/createVoiceOpsStatus.d.ts +9 -0
  168. package/dist/svelte/createVoicePlatformCoverage.d.ts +7 -0
  169. package/dist/svelte/createVoiceProofTrends.d.ts +7 -0
  170. package/dist/svelte/createVoiceProviderCapabilities.d.ts +10 -0
  171. package/dist/svelte/createVoiceProviderContracts.d.ts +10 -0
  172. package/dist/svelte/createVoiceProviderSimulationControls.d.ts +11 -0
  173. package/dist/svelte/createVoiceProviderStatus.d.ts +4 -2
  174. package/dist/svelte/createVoiceReadinessFailures.d.ts +7 -0
  175. package/dist/svelte/createVoiceRoutingStatus.d.ts +10 -0
  176. package/dist/svelte/createVoiceTraceTimeline.d.ts +10 -0
  177. package/dist/svelte/createVoiceTurnLatency.d.ts +11 -0
  178. package/dist/svelte/createVoiceTurnQuality.d.ts +10 -0
  179. package/dist/svelte/createVoiceWorkflowStatus.d.ts +8 -0
  180. package/dist/svelte/index.d.ts +17 -0
  181. package/dist/svelte/index.js +4924 -420
  182. package/dist/telephony/contract.d.ts +61 -0
  183. package/dist/telephony/matrix.d.ts +97 -0
  184. package/dist/telephony/plivo.d.ts +303 -0
  185. package/dist/telephony/security.d.ts +182 -0
  186. package/dist/telephony/telnyx.d.ts +291 -0
  187. package/dist/telephony/twilio.d.ts +135 -2
  188. package/dist/telephonyOutcome.d.ts +273 -0
  189. package/dist/testing/index.d.ts +1 -0
  190. package/dist/testing/index.js +1861 -44
  191. package/dist/testing/ioProviderSimulator.d.ts +41 -0
  192. package/dist/toolContract.d.ts +161 -0
  193. package/dist/toolRuntime.d.ts +50 -0
  194. package/dist/trace.d.ts +19 -1
  195. package/dist/traceDeliveryRoutes.d.ts +86 -0
  196. package/dist/traceTimeline.d.ts +97 -0
  197. package/dist/turnLatency.d.ts +95 -0
  198. package/dist/turnQuality.d.ts +94 -0
  199. package/dist/types.d.ts +127 -3
  200. package/dist/voiceMonitoring.d.ts +444 -0
  201. package/dist/vue/VoiceDeliveryRuntime.d.ts +30 -0
  202. package/dist/vue/VoiceOpsActionCenter.d.ts +13 -0
  203. package/dist/vue/VoiceOpsStatus.d.ts +30 -0
  204. package/dist/vue/VoicePlatformCoverage.d.ts +23 -0
  205. package/dist/vue/VoiceProofTrends.d.ts +21 -0
  206. package/dist/vue/VoiceProviderCapabilities.d.ts +51 -0
  207. package/dist/vue/VoiceProviderContracts.d.ts +21 -0
  208. package/dist/vue/VoiceProviderSimulationControls.d.ts +88 -0
  209. package/dist/vue/VoiceProviderStatus.d.ts +51 -0
  210. package/dist/vue/VoiceReadinessFailures.d.ts +21 -0
  211. package/dist/vue/VoiceRoutingStatus.d.ts +51 -0
  212. package/dist/vue/VoiceTurnLatency.d.ts +69 -0
  213. package/dist/vue/VoiceTurnQuality.d.ts +51 -0
  214. package/dist/vue/index.d.ts +30 -0
  215. package/dist/vue/index.js +4828 -56
  216. package/dist/vue/useVoiceAgentSquadStatus.d.ts +9 -0
  217. package/dist/vue/useVoiceCampaignDialerProof.d.ts +11 -0
  218. package/dist/vue/useVoiceController.d.ts +2 -1
  219. package/dist/vue/useVoiceDeliveryRuntime.d.ts +13 -0
  220. package/dist/vue/useVoiceLiveOps.d.ts +9 -0
  221. package/dist/vue/useVoiceOpsActionCenter.d.ts +11 -0
  222. package/dist/vue/useVoiceOpsStatus.d.ts +9 -0
  223. package/dist/vue/useVoicePlatformCoverage.d.ts +9 -0
  224. package/dist/vue/useVoiceProofTrends.d.ts +9 -0
  225. package/dist/vue/useVoiceProviderCapabilities.d.ts +9 -0
  226. package/dist/vue/useVoiceProviderContracts.d.ts +9 -0
  227. package/dist/vue/useVoiceProviderSimulationControls.d.ts +24 -0
  228. package/dist/vue/useVoiceProviderStatus.d.ts +1 -1
  229. package/dist/vue/useVoiceReadinessFailures.d.ts +775 -0
  230. package/dist/vue/useVoiceRoutingStatus.d.ts +8 -0
  231. package/dist/vue/useVoiceStream.d.ts +2 -1
  232. package/dist/vue/useVoiceTraceTimeline.d.ts +9 -0
  233. package/dist/vue/useVoiceTurnLatency.d.ts +10 -0
  234. package/dist/vue/useVoiceTurnQuality.d.ts +9 -0
  235. package/dist/vue/useVoiceWorkflowStatus.d.ts +9 -0
  236. package/dist/workflowContract.d.ts +91 -0
  237. 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("");
@@ -4486,6 +4950,14 @@ var aggregateHandoffStatus = (deliveries) => {
4486
4950
  }
4487
4951
  return "skipped";
4488
4952
  };
4953
+ var createHandoffDeliveryId = (input) => [
4954
+ "voice-handoff",
4955
+ input.sessionId,
4956
+ input.action,
4957
+ Date.now(),
4958
+ crypto.randomUUID()
4959
+ ].join(":");
4960
+ var resolveHandoffDeliveryError = (deliveries) => Object.values(deliveries).map((delivery) => delivery.error).find(Boolean);
4489
4961
  var defaultWebhookBody = (input) => ({
4490
4962
  action: input.action,
4491
4963
  metadata: input.metadata,
@@ -4534,6 +5006,73 @@ var deliverVoiceHandoff = async (input) => {
4534
5006
  status: aggregateHandoffStatus(deliveries)
4535
5007
  };
4536
5008
  };
5009
+ var createVoiceHandoffDeliveryRecord = (input) => {
5010
+ const now = Date.now();
5011
+ return {
5012
+ action: input.action,
5013
+ context: input.context,
5014
+ createdAt: now,
5015
+ deliveryAttempts: 0,
5016
+ deliveryStatus: "pending",
5017
+ id: input.id ?? createHandoffDeliveryId({
5018
+ action: input.action,
5019
+ sessionId: input.session.id
5020
+ }),
5021
+ metadata: input.metadata,
5022
+ reason: input.reason,
5023
+ result: input.result,
5024
+ session: input.session,
5025
+ sessionId: input.session.id,
5026
+ target: input.target,
5027
+ updatedAt: now
5028
+ };
5029
+ };
5030
+ var applyVoiceHandoffDeliveryResult = (delivery, result) => ({
5031
+ ...delivery,
5032
+ deliveredAt: result.status === "delivered" || result.status === "skipped" ? Date.now() : delivery.deliveredAt,
5033
+ deliveries: result.deliveries,
5034
+ deliveryAttempts: (delivery.deliveryAttempts ?? 0) + 1,
5035
+ deliveryError: result.status === "failed" ? resolveHandoffDeliveryError(result.deliveries) : undefined,
5036
+ deliveryStatus: result.status,
5037
+ updatedAt: Date.now()
5038
+ });
5039
+ var deliverVoiceHandoffDelivery = async (options) => {
5040
+ const result = await deliverVoiceHandoff({
5041
+ config: {
5042
+ adapters: options.adapters,
5043
+ failMode: options.failMode
5044
+ },
5045
+ handoff: {
5046
+ action: options.delivery.action,
5047
+ api: options.api,
5048
+ context: options.delivery.context,
5049
+ metadata: options.delivery.metadata,
5050
+ reason: options.delivery.reason,
5051
+ result: options.delivery.result,
5052
+ session: options.delivery.session,
5053
+ target: options.delivery.target
5054
+ }
5055
+ });
5056
+ return result ? applyVoiceHandoffDeliveryResult(options.delivery, result) : {
5057
+ ...options.delivery,
5058
+ deliveryAttempts: (options.delivery.deliveryAttempts ?? 0) + 1,
5059
+ deliveryStatus: "skipped",
5060
+ updatedAt: Date.now()
5061
+ };
5062
+ };
5063
+ var createVoiceMemoryHandoffDeliveryStore = () => {
5064
+ const deliveries = new Map;
5065
+ return {
5066
+ get: async (id) => deliveries.get(id),
5067
+ list: async () => [...deliveries.values()].sort((left, right) => left.createdAt - right.createdAt || left.id.localeCompare(right.id)),
5068
+ remove: async (id) => {
5069
+ deliveries.delete(id);
5070
+ },
5071
+ set: async (id, delivery) => {
5072
+ deliveries.set(id, delivery);
5073
+ }
5074
+ };
5075
+ };
4537
5076
  var createVoiceWebhookHandoffAdapter = (options) => ({
4538
5077
  actions: options.actions,
4539
5078
  handoff: async (input) => {
@@ -4701,6 +5240,12 @@ var DEFAULT_FORMAT = {
4701
5240
  encoding: "pcm_s16le",
4702
5241
  sampleRateHz: 16000
4703
5242
  };
5243
+ var DEFAULT_REALTIME_FORMAT = {
5244
+ channels: 1,
5245
+ container: "raw",
5246
+ encoding: "pcm_s16le",
5247
+ sampleRateHz: 24000
5248
+ };
4704
5249
  var toError = (value) => value instanceof Error ? value : new Error(String(value));
4705
5250
  var createEmptyCurrentTurn = () => ({
4706
5251
  finalText: "",
@@ -4713,7 +5258,7 @@ var createEmptyCurrentTurn = () => ({
4713
5258
  transcripts: []
4714
5259
  });
4715
5260
  var cloneTranscript = (transcript) => ({ ...transcript });
4716
- var encodeBase64 = (chunk) => Buffer.from(chunk).toString("base64");
5261
+ var encodeBase64 = (chunk) => Buffer2.from(chunk).toString("base64");
4717
5262
  var countWords2 = (text) => text.trim().split(/\s+/).filter(Boolean).length;
4718
5263
  var normalizeText2 = (text) => text.trim().replace(/\s+/g, " ");
4719
5264
  var getAudioChunkDurationMs = (chunk) => chunk.byteLength / (DEFAULT_FORMAT.sampleRateHz * DEFAULT_FORMAT.channels * 2) * 1000;
@@ -4884,7 +5429,7 @@ var createVoiceSession = (options) => {
4884
5429
  } : undefined;
4885
5430
  const appendTrace = async (input) => {
4886
5431
  await options.trace?.append({
4887
- at: Date.now(),
5432
+ at: input.at ?? Date.now(),
4888
5433
  metadata: input.metadata,
4889
5434
  payload: input.payload,
4890
5435
  scenarioId: input.session?.scenarioId ?? options.scenarioId,
@@ -4893,6 +5438,13 @@ var createVoiceSession = (options) => {
4893
5438
  type: input.type
4894
5439
  });
4895
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
+ });
4896
5448
  const phraseHints = options.phraseHints ?? [];
4897
5449
  const lexicon = options.lexicon ?? [];
4898
5450
  let socket = options.socket;
@@ -4971,7 +5523,34 @@ var createVoiceSession = (options) => {
4971
5523
  type: "call_lifecycle"
4972
5524
  });
4973
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
+ };
4974
5538
  const runHandoff = async (input) => {
5539
+ const queuedDelivery = options.handoff?.deliveryQueue ? createVoiceHandoffDeliveryRecord({
5540
+ action: input.action,
5541
+ context: options.context,
5542
+ metadata: input.metadata,
5543
+ reason: input.reason,
5544
+ result: input.result,
5545
+ session: input.session,
5546
+ target: input.target
5547
+ }) : undefined;
5548
+ if (queuedDelivery) {
5549
+ await options.handoff?.deliveryQueue?.set(queuedDelivery.id, queuedDelivery);
5550
+ }
5551
+ if (options.handoff?.enqueueOnly) {
5552
+ return;
5553
+ }
4975
5554
  const result = await deliverVoiceHandoff({
4976
5555
  config: options.handoff,
4977
5556
  handoff: {
@@ -4988,6 +5567,10 @@ var createVoiceSession = (options) => {
4988
5567
  if (!result) {
4989
5568
  return;
4990
5569
  }
5570
+ if (queuedDelivery) {
5571
+ const updatedDelivery = applyVoiceHandoffDeliveryResult(queuedDelivery, result);
5572
+ await options.handoff?.deliveryQueue?.set(updatedDelivery.id, updatedDelivery);
5573
+ }
4991
5574
  await appendTrace({
4992
5575
  metadata: input.metadata,
4993
5576
  payload: {
@@ -5055,6 +5638,23 @@ var createVoiceSession = (options) => {
5055
5638
  });
5056
5639
  }
5057
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
+ };
5058
5658
  const scheduleTurnCommit = (delayMs, reason, reset = true) => {
5059
5659
  if (!reset && silenceTimer) {
5060
5660
  return;
@@ -5756,8 +6356,12 @@ var createVoiceSession = (options) => {
5756
6356
  if (sttSession) {
5757
6357
  return sttSession;
5758
6358
  }
5759
- const openedSession = await options.stt.open({
5760
- 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,
5761
6365
  languageStrategy: options.languageStrategy,
5762
6366
  lexicon,
5763
6367
  phraseHints,
@@ -5792,6 +6396,16 @@ var createVoiceSession = (options) => {
5792
6396
  openedSession.on("close", (event) => {
5793
6397
  runAdapterEvent("adapter.close", () => handleClose(event));
5794
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
+ }
5795
6409
  return openedSession;
5796
6410
  };
5797
6411
  const ensureTTSSession = async () => {
@@ -5816,13 +6430,9 @@ var createVoiceSession = (options) => {
5816
6430
  if (ttsSession !== openedSession) {
5817
6431
  return;
5818
6432
  }
5819
- const normalizedChunk = chunk instanceof Uint8Array ? new Uint8Array(chunk) : chunk instanceof ArrayBuffer ? new Uint8Array(chunk.slice(0)) : new Uint8Array(chunk.buffer.slice(chunk.byteOffset, chunk.byteOffset + chunk.byteLength));
5820
- await send({
5821
- chunkBase64: encodeBase64(normalizedChunk),
6433
+ await sendAssistantAudio(chunk, {
5822
6434
  format,
5823
- receivedAt,
5824
- turnId: activeTTSTurnId,
5825
- type: "audio"
6435
+ receivedAt
5826
6436
  });
5827
6437
  });
5828
6438
  });
@@ -5866,9 +6476,32 @@ var createVoiceSession = (options) => {
5866
6476
  });
5867
6477
  };
5868
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();
5869
6498
  const committedOutput = await options.route.onTurn({
5870
6499
  api,
5871
6500
  context: options.context,
6501
+ liveOps: liveOpsControl ? {
6502
+ control: liveOpsControl,
6503
+ injectedInstruction
6504
+ } : undefined,
5872
6505
  session,
5873
6506
  turn
5874
6507
  });
@@ -5882,6 +6515,7 @@ var createVoiceSession = (options) => {
5882
6515
  voicemail: committedOutput?.voicemail
5883
6516
  };
5884
6517
  if (output?.assistantText) {
6518
+ const assistantTextStartedAt = Date.now();
5885
6519
  await writeSession((currentSession) => {
5886
6520
  setTurnResult(currentSession, turn.id, {
5887
6521
  assistantText: output.assistantText
@@ -5892,10 +6526,17 @@ var createVoiceSession = (options) => {
5892
6526
  turnId: turn.id,
5893
6527
  type: "assistant"
5894
6528
  });
6529
+ await appendTurnLatencyStage({
6530
+ at: assistantTextStartedAt,
6531
+ session,
6532
+ stage: "assistant_text_started",
6533
+ turnId: turn.id
6534
+ });
5895
6535
  await appendTrace({
5896
6536
  payload: {
5897
6537
  text: output.assistantText,
5898
- ttsConfigured: Boolean(options.tts)
6538
+ ttsConfigured: Boolean(options.tts),
6539
+ realtimeConfigured: Boolean(options.realtime)
5899
6540
  },
5900
6541
  session,
5901
6542
  turnId: turn.id,
@@ -5906,7 +6547,18 @@ var createVoiceSession = (options) => {
5906
6547
  if (activeTTSSession) {
5907
6548
  const ttsStartedAt = Date.now();
5908
6549
  activeTTSTurnId = turn.id;
6550
+ await appendTurnLatencyStage({
6551
+ at: ttsStartedAt,
6552
+ session,
6553
+ stage: "tts_send_started",
6554
+ turnId: turn.id
6555
+ });
5909
6556
  await activeTTSSession.send(output.assistantText);
6557
+ await appendTurnLatencyStage({
6558
+ session,
6559
+ stage: "tts_send_completed",
6560
+ turnId: turn.id
6561
+ });
5910
6562
  await appendTrace({
5911
6563
  payload: {
5912
6564
  elapsedMs: Date.now() - ttsStartedAt,
@@ -5916,9 +6568,35 @@ var createVoiceSession = (options) => {
5916
6568
  turnId: turn.id,
5917
6569
  type: "turn.assistant"
5918
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
+ });
5919
6597
  }
5920
6598
  } catch (error) {
5921
- logger.warn("voice tts send failed", {
6599
+ logger.warn("voice assistant audio send failed", {
5922
6600
  error: toError(error).message,
5923
6601
  sessionId: options.id,
5924
6602
  turnId: turn.id
@@ -5926,7 +6604,7 @@ var createVoiceSession = (options) => {
5926
6604
  await appendTrace({
5927
6605
  payload: {
5928
6606
  error: toError(error).message,
5929
- status: "tts-send-failed"
6607
+ status: options.realtime ? "realtime-send-failed" : "tts-send-failed"
5930
6608
  },
5931
6609
  session,
5932
6610
  turnId: turn.id,
@@ -6103,11 +6781,35 @@ var createVoiceSession = (options) => {
6103
6781
  turnId: turn.id,
6104
6782
  type: "turn.cost"
6105
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
+ });
6106
6808
  await send({
6107
6809
  turn,
6108
6810
  type: "turn"
6109
6811
  });
6110
- if (options.sttLifecycle === "turn-scoped") {
6812
+ if (options.stt && options.sttLifecycle === "turn-scoped") {
6111
6813
  await closeAdapter("turn-commit");
6112
6814
  }
6113
6815
  await completeTurn(updatedSession, turn);
@@ -6170,6 +6872,7 @@ var createVoiceSession = (options) => {
6170
6872
  scenarioId: session.scenarioId,
6171
6873
  type: "session"
6172
6874
  });
6875
+ await sendReplay(session);
6173
6876
  if (shouldFireOnSession) {
6174
6877
  await options.route.onCallStart?.({
6175
6878
  api,
@@ -7546,10 +8249,981 @@ var runVoiceSessionBenchmarkSeries = async (input) => {
7546
8249
  });
7547
8250
  };
7548
8251
  // src/telephony/twilio.ts
7549
- 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
7550
9039
  var TWILIO_MULAW_SAMPLE_RATE = 8000;
7551
9040
  var VOICE_PCM_SAMPLE_RATE = 16000;
7552
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
+ };
7553
9227
  var normalizeOnTurn = (handler) => {
7554
9228
  if (handler.length > 1) {
7555
9229
  const directHandler = handler;
@@ -7651,7 +9325,7 @@ var bytesToInt16Array = (bytes) => {
7651
9325
  return output;
7652
9326
  };
7653
9327
  var decodeTwilioMulawBase64 = (payload) => {
7654
- const bytes = Uint8Array.from(Buffer2.from(payload, "base64"));
9328
+ const bytes = Uint8Array.from(Buffer3.from(payload, "base64"));
7655
9329
  const samples = new Int16Array(bytes.length);
7656
9330
  for (let index = 0;index < bytes.length; index += 1) {
7657
9331
  samples[index] = decodeMulawSample(bytes[index] ?? 0);
@@ -7663,7 +9337,7 @@ var encodeTwilioMulawBase64 = (samples) => {
7663
9337
  for (let index = 0;index < samples.length; index += 1) {
7664
9338
  bytes[index] = encodeMulawSample(samples[index] ?? 0);
7665
9339
  }
7666
- return Buffer2.from(bytes).toString("base64");
9340
+ return Buffer3.from(bytes).toString("base64");
7667
9341
  };
7668
9342
  var transcodeTwilioInboundPayloadToPCM16 = (payload) => {
7669
9343
  const narrowband = decodeTwilioMulawBase64(payload);
@@ -7672,7 +9346,7 @@ var transcodeTwilioInboundPayloadToPCM16 = (payload) => {
7672
9346
  };
7673
9347
  var transcodePCMToTwilioOutboundPayload = (chunk, format) => {
7674
9348
  if (format.container === "raw" && format.encoding === "mulaw" && format.channels === 1 && format.sampleRateHz === TWILIO_MULAW_SAMPLE_RATE) {
7675
- return Buffer2.from(chunk).toString("base64");
9349
+ return Buffer3.from(chunk).toString("base64");
7676
9350
  }
7677
9351
  if (format.encoding !== "pcm_s16le") {
7678
9352
  throw new Error(`Unsupported outbound telephony audio format: ${format.container}/${format.encoding}`);
@@ -7713,7 +9387,7 @@ var createTwilioSocketAdapter = (socket, getState) => ({
7713
9387
  return;
7714
9388
  }
7715
9389
  if (message.type === "audio") {
7716
- 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);
7717
9391
  state.hasOutboundAudioSinceLastInbound = true;
7718
9392
  state.reviewRecorder?.recordTwilioOutbound({
7719
9393
  bytes: payload.length,
@@ -7926,6 +9600,148 @@ var createTwilioMediaStreamBridge = (socket, options) => {
7926
9600
  }
7927
9601
  };
7928
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
+ };
7929
9745
 
7930
9746
  // src/testing/telephony.ts
7931
9747
  var DEFAULT_PCM16_FORMAT = {
@@ -8191,7 +10007,7 @@ var runVoiceTelephonyBenchmark = async (scenarios = getDefaultVoiceTelephonyBenc
8191
10007
  };
8192
10008
  };
8193
10009
  // src/testing/tts.ts
8194
- var DEFAULT_REALTIME_FORMAT = {
10010
+ var DEFAULT_REALTIME_FORMAT2 = {
8195
10011
  channels: 1,
8196
10012
  container: "raw",
8197
10013
  encoding: "pcm_s16le",
@@ -8250,7 +10066,7 @@ var runTTSAdapterFixture = async (adapter, fixture, options = {}) => {
8250
10066
  let audioDurationMs = 0;
8251
10067
  let audioChunkCount = 0;
8252
10068
  const session = adapter.kind === "realtime" ? await adapter.open({
8253
- format: options.realtimeFormat ?? DEFAULT_REALTIME_FORMAT,
10069
+ format: options.realtimeFormat ?? DEFAULT_REALTIME_FORMAT2,
8254
10070
  sessionId: `tts-benchmark:${fixture.id}`,
8255
10071
  ...openOptions ?? {}
8256
10072
  }) : await adapter.open({
@@ -8417,6 +10233,7 @@ export {
8417
10233
  getDefaultTTSBenchmarkFixtures,
8418
10234
  evaluateSTTBenchmarkAcceptance,
8419
10235
  createVoiceProviderFailureSimulator,
10236
+ createVoiceIOProviderFailureSimulator,
8420
10237
  createVoiceCallReviewRecorder,
8421
10238
  createVoiceCallReviewFromLiveTelephonyReport,
8422
10239
  createTelephonyVoiceTestFixtures,