@absolutejs/voice 0.0.22-beta.6 → 0.0.22-beta.60

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 (74) hide show
  1. package/README.md +205 -0
  2. package/dist/angular/index.d.ts +4 -0
  3. package/dist/angular/index.js +587 -43
  4. package/dist/angular/voice-app-kit-status.service.d.ts +12 -0
  5. package/dist/angular/voice-ops-status.component.d.ts +15 -0
  6. package/dist/angular/voice-provider-status.service.d.ts +12 -0
  7. package/dist/angular/voice-routing-status.service.d.ts +11 -0
  8. package/dist/angular/voice-stream.service.d.ts +2 -0
  9. package/dist/angular/voice-workflow-status.service.d.ts +12 -0
  10. package/dist/appKit.d.ts +92 -0
  11. package/dist/assistantHealth.d.ts +81 -0
  12. package/dist/client/actions.d.ts +22 -0
  13. package/dist/client/appKitStatus.d.ts +19 -0
  14. package/dist/client/connection.d.ts +3 -0
  15. package/dist/client/htmxBootstrap.js +44 -2
  16. package/dist/client/index.d.ts +14 -0
  17. package/dist/client/index.js +713 -2
  18. package/dist/client/opsStatusWidget.d.ts +40 -0
  19. package/dist/client/providerStatus.d.ts +19 -0
  20. package/dist/client/providerStatusWidget.d.ts +32 -0
  21. package/dist/client/routingStatus.d.ts +19 -0
  22. package/dist/client/routingStatusWidget.d.ts +28 -0
  23. package/dist/client/workflowStatus.d.ts +19 -0
  24. package/dist/diagnosticsRoutes.d.ts +44 -0
  25. package/dist/evalRoutes.d.ts +213 -0
  26. package/dist/handoff.d.ts +54 -0
  27. package/dist/handoffHealth.d.ts +94 -0
  28. package/dist/index.d.ts +32 -4
  29. package/dist/index.js +4222 -133
  30. package/dist/modelAdapters.d.ts +75 -0
  31. package/dist/opsConsoleRoutes.d.ts +77 -0
  32. package/dist/opsWebhook.d.ts +126 -0
  33. package/dist/providerAdapters.d.ts +48 -0
  34. package/dist/providerHealth.d.ts +79 -0
  35. package/dist/qualityRoutes.d.ts +76 -0
  36. package/dist/queue.d.ts +52 -0
  37. package/dist/react/VoiceOpsStatus.d.ts +6 -0
  38. package/dist/react/VoiceProviderStatus.d.ts +6 -0
  39. package/dist/react/VoiceRoutingStatus.d.ts +6 -0
  40. package/dist/react/index.d.ts +7 -0
  41. package/dist/react/index.js +1024 -11
  42. package/dist/react/useVoiceAppKitStatus.d.ts +8 -0
  43. package/dist/react/useVoiceController.d.ts +2 -0
  44. package/dist/react/useVoiceProviderStatus.d.ts +8 -0
  45. package/dist/react/useVoiceRoutingStatus.d.ts +8 -0
  46. package/dist/react/useVoiceStream.d.ts +2 -0
  47. package/dist/react/useVoiceWorkflowStatus.d.ts +8 -0
  48. package/dist/resilienceRoutes.d.ts +117 -0
  49. package/dist/sessionReplay.d.ts +175 -0
  50. package/dist/svelte/createVoiceAppKitStatus.d.ts +8 -0
  51. package/dist/svelte/createVoiceOpsStatus.d.ts +9 -0
  52. package/dist/svelte/createVoiceProviderStatus.d.ts +10 -0
  53. package/dist/svelte/createVoiceRoutingStatus.d.ts +10 -0
  54. package/dist/svelte/createVoiceWorkflowStatus.d.ts +8 -0
  55. package/dist/svelte/index.d.ts +5 -0
  56. package/dist/svelte/index.js +736 -3
  57. package/dist/testing/index.d.ts +2 -0
  58. package/dist/testing/index.js +1537 -7
  59. package/dist/testing/ioProviderSimulator.d.ts +41 -0
  60. package/dist/testing/providerSimulator.d.ts +44 -0
  61. package/dist/trace.d.ts +1 -1
  62. package/dist/types.d.ts +84 -2
  63. package/dist/vue/VoiceOpsStatus.d.ts +30 -0
  64. package/dist/vue/VoiceProviderStatus.d.ts +51 -0
  65. package/dist/vue/VoiceRoutingStatus.d.ts +51 -0
  66. package/dist/vue/index.d.ts +7 -0
  67. package/dist/vue/index.js +1062 -25
  68. package/dist/vue/useVoiceAppKitStatus.d.ts +9 -0
  69. package/dist/vue/useVoiceProviderStatus.d.ts +9 -0
  70. package/dist/vue/useVoiceRoutingStatus.d.ts +8 -0
  71. package/dist/vue/useVoiceStream.d.ts +2 -0
  72. package/dist/vue/useVoiceWorkflowStatus.d.ts +9 -0
  73. package/dist/workflowContract.d.ts +91 -0
  74. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -2992,6 +2992,289 @@ var toVoiceSessionSummary = (session) => ({
2992
2992
  // src/session.ts
2993
2993
  import { Buffer } from "buffer";
2994
2994
 
2995
+ // src/handoff.ts
2996
+ var toHex3 = (bytes) => Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
2997
+ var signHandoffBody = async (input) => {
2998
+ const encoder = new TextEncoder;
2999
+ const key = await crypto.subtle.importKey("raw", encoder.encode(input.secret), {
3000
+ hash: "SHA-256",
3001
+ name: "HMAC"
3002
+ }, false, ["sign"]);
3003
+ const signature = await crypto.subtle.sign("HMAC", key, encoder.encode(`${input.timestamp}.${input.body}`));
3004
+ return `sha256=${toHex3(new Uint8Array(signature))}`;
3005
+ };
3006
+ var toErrorMessage2 = (error) => error instanceof Error ? error.message : String(error);
3007
+ var createSkippedDelivery = (adapter) => ({
3008
+ adapterId: adapter.id,
3009
+ adapterKind: adapter.kind,
3010
+ status: "skipped"
3011
+ });
3012
+ var aggregateHandoffStatus = (deliveries) => {
3013
+ const statuses = Object.values(deliveries).map((delivery) => delivery.status);
3014
+ if (statuses.some((status) => status === "failed")) {
3015
+ return "failed";
3016
+ }
3017
+ if (statuses.some((status) => status === "delivered")) {
3018
+ return "delivered";
3019
+ }
3020
+ return "skipped";
3021
+ };
3022
+ var createHandoffDeliveryId = (input) => [
3023
+ "voice-handoff",
3024
+ input.sessionId,
3025
+ input.action,
3026
+ Date.now(),
3027
+ crypto.randomUUID()
3028
+ ].join(":");
3029
+ var resolveHandoffDeliveryError = (deliveries) => Object.values(deliveries).map((delivery) => delivery.error).find(Boolean);
3030
+ var defaultWebhookBody = (input) => ({
3031
+ action: input.action,
3032
+ metadata: input.metadata,
3033
+ reason: input.reason,
3034
+ result: input.result,
3035
+ session: {
3036
+ id: input.session.id,
3037
+ scenarioId: input.session.scenarioId,
3038
+ status: input.session.status
3039
+ },
3040
+ source: "absolutejs-voice",
3041
+ target: input.target
3042
+ });
3043
+ var deliverVoiceHandoff = async (input) => {
3044
+ if (!input.config || input.config.adapters.length === 0) {
3045
+ return;
3046
+ }
3047
+ const deliveries = {};
3048
+ for (const adapter of input.config.adapters) {
3049
+ if (adapter.actions && !adapter.actions.includes(input.handoff.action)) {
3050
+ deliveries[adapter.id] = createSkippedDelivery(adapter);
3051
+ continue;
3052
+ }
3053
+ try {
3054
+ const result = await adapter.handoff(input.handoff);
3055
+ deliveries[adapter.id] = {
3056
+ ...result,
3057
+ adapterId: adapter.id,
3058
+ adapterKind: adapter.kind
3059
+ };
3060
+ } catch (error) {
3061
+ deliveries[adapter.id] = {
3062
+ adapterId: adapter.id,
3063
+ adapterKind: adapter.kind,
3064
+ error: toErrorMessage2(error),
3065
+ status: "failed"
3066
+ };
3067
+ if (input.config.failMode === "throw") {
3068
+ throw error;
3069
+ }
3070
+ }
3071
+ }
3072
+ return {
3073
+ action: input.handoff.action,
3074
+ deliveries,
3075
+ status: aggregateHandoffStatus(deliveries)
3076
+ };
3077
+ };
3078
+ var createVoiceHandoffDeliveryRecord = (input) => {
3079
+ const now = Date.now();
3080
+ return {
3081
+ action: input.action,
3082
+ context: input.context,
3083
+ createdAt: now,
3084
+ deliveryAttempts: 0,
3085
+ deliveryStatus: "pending",
3086
+ id: input.id ?? createHandoffDeliveryId({
3087
+ action: input.action,
3088
+ sessionId: input.session.id
3089
+ }),
3090
+ metadata: input.metadata,
3091
+ reason: input.reason,
3092
+ result: input.result,
3093
+ session: input.session,
3094
+ sessionId: input.session.id,
3095
+ target: input.target,
3096
+ updatedAt: now
3097
+ };
3098
+ };
3099
+ var applyVoiceHandoffDeliveryResult = (delivery, result) => ({
3100
+ ...delivery,
3101
+ deliveredAt: result.status === "delivered" || result.status === "skipped" ? Date.now() : delivery.deliveredAt,
3102
+ deliveries: result.deliveries,
3103
+ deliveryAttempts: (delivery.deliveryAttempts ?? 0) + 1,
3104
+ deliveryError: result.status === "failed" ? resolveHandoffDeliveryError(result.deliveries) : undefined,
3105
+ deliveryStatus: result.status,
3106
+ updatedAt: Date.now()
3107
+ });
3108
+ var deliverVoiceHandoffDelivery = async (options) => {
3109
+ const result = await deliverVoiceHandoff({
3110
+ config: {
3111
+ adapters: options.adapters,
3112
+ failMode: options.failMode
3113
+ },
3114
+ handoff: {
3115
+ action: options.delivery.action,
3116
+ api: options.api,
3117
+ context: options.delivery.context,
3118
+ metadata: options.delivery.metadata,
3119
+ reason: options.delivery.reason,
3120
+ result: options.delivery.result,
3121
+ session: options.delivery.session,
3122
+ target: options.delivery.target
3123
+ }
3124
+ });
3125
+ return result ? applyVoiceHandoffDeliveryResult(options.delivery, result) : {
3126
+ ...options.delivery,
3127
+ deliveryAttempts: (options.delivery.deliveryAttempts ?? 0) + 1,
3128
+ deliveryStatus: "skipped",
3129
+ updatedAt: Date.now()
3130
+ };
3131
+ };
3132
+ var createVoiceMemoryHandoffDeliveryStore = () => {
3133
+ const deliveries = new Map;
3134
+ return {
3135
+ get: async (id) => deliveries.get(id),
3136
+ list: async () => [...deliveries.values()].sort((left, right) => left.createdAt - right.createdAt || left.id.localeCompare(right.id)),
3137
+ remove: async (id) => {
3138
+ deliveries.delete(id);
3139
+ },
3140
+ set: async (id, delivery) => {
3141
+ deliveries.set(id, delivery);
3142
+ }
3143
+ };
3144
+ };
3145
+ var createVoiceWebhookHandoffAdapter = (options) => ({
3146
+ actions: options.actions,
3147
+ handoff: async (input) => {
3148
+ const fetchImpl = options.fetch ?? globalThis.fetch;
3149
+ if (typeof fetchImpl !== "function") {
3150
+ return {
3151
+ deliveredTo: options.url,
3152
+ error: "Handoff delivery failed: fetch is not available in this runtime.",
3153
+ status: "failed"
3154
+ };
3155
+ }
3156
+ const body = JSON.stringify(await options.body?.(input) ?? defaultWebhookBody(input));
3157
+ const headers = {
3158
+ "content-type": "application/json",
3159
+ ...options.headers
3160
+ };
3161
+ if (options.signingSecret) {
3162
+ const timestamp = String(Date.now());
3163
+ headers["x-absolutejs-timestamp"] = timestamp;
3164
+ headers["x-absolutejs-signature"] = await signHandoffBody({
3165
+ body,
3166
+ secret: options.signingSecret,
3167
+ timestamp
3168
+ });
3169
+ }
3170
+ const controller = options.timeoutMs && options.timeoutMs > 0 ? new AbortController : undefined;
3171
+ const timeout = controller && options.timeoutMs ? setTimeout(() => controller.abort(), options.timeoutMs) : undefined;
3172
+ try {
3173
+ const response = await fetchImpl(options.url, {
3174
+ body,
3175
+ headers,
3176
+ method: options.method ?? "POST",
3177
+ signal: controller?.signal
3178
+ });
3179
+ if (!response.ok) {
3180
+ return {
3181
+ deliveredTo: options.url,
3182
+ error: `Handoff delivery failed with response ${response.status}.`,
3183
+ status: "failed"
3184
+ };
3185
+ }
3186
+ return {
3187
+ deliveredAt: Date.now(),
3188
+ deliveredTo: options.url,
3189
+ status: "delivered"
3190
+ };
3191
+ } finally {
3192
+ if (timeout) {
3193
+ clearTimeout(timeout);
3194
+ }
3195
+ }
3196
+ },
3197
+ id: options.id,
3198
+ kind: options.kind ?? "webhook"
3199
+ });
3200
+ var escapeXml = (value) => value.replaceAll("&", "&amp;").replaceAll('"', "&quot;").replaceAll("'", "&apos;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
3201
+ var defaultTwilioTransferTwiML = (input) => {
3202
+ if (!input.target) {
3203
+ return "<Response><Hangup /></Response>";
3204
+ }
3205
+ return `<Response><Dial>${escapeXml(input.target)}</Dial></Response>`;
3206
+ };
3207
+ var resolveTwilioCallSid = async (resolver, input) => {
3208
+ if (typeof resolver === "function") {
3209
+ return resolver(input);
3210
+ }
3211
+ if (typeof resolver === "string" && resolver.length > 0) {
3212
+ return resolver;
3213
+ }
3214
+ const metadataSid = typeof input.metadata?.callSid === "string" ? input.metadata.callSid : undefined;
3215
+ const sessionMetadata = input.session.metadata && typeof input.session.metadata === "object" ? input.session.metadata : undefined;
3216
+ const sessionSid = typeof sessionMetadata?.callSid === "string" ? sessionMetadata.callSid : undefined;
3217
+ return metadataSid ?? sessionSid;
3218
+ };
3219
+ var createVoiceTwilioRedirectHandoffAdapter = (options) => ({
3220
+ actions: options.actions ?? ["transfer"],
3221
+ handoff: async (input) => {
3222
+ const fetchImpl = options.fetch ?? globalThis.fetch;
3223
+ const callSid = await resolveTwilioCallSid(options.callSid, input);
3224
+ if (!callSid) {
3225
+ return {
3226
+ error: "Twilio handoff requires a callSid.",
3227
+ status: "failed"
3228
+ };
3229
+ }
3230
+ if (typeof fetchImpl !== "function") {
3231
+ return {
3232
+ error: "Twilio handoff failed: fetch is not available in this runtime.",
3233
+ status: "failed"
3234
+ };
3235
+ }
3236
+ const url = `https://api.twilio.com/2010-04-01/Accounts/${encodeURIComponent(options.accountSid)}/Calls/${encodeURIComponent(callSid)}.json`;
3237
+ const body = new URLSearchParams({
3238
+ Twiml: await (options.buildTwiML?.(input) ?? defaultTwilioTransferTwiML(input))
3239
+ });
3240
+ const auth = btoa(`${options.accountSid}:${options.authToken}`);
3241
+ const controller = options.timeoutMs && options.timeoutMs > 0 ? new AbortController : undefined;
3242
+ const timeout = controller && options.timeoutMs ? setTimeout(() => controller.abort(), options.timeoutMs) : undefined;
3243
+ try {
3244
+ const response = await fetchImpl(url, {
3245
+ body,
3246
+ headers: {
3247
+ authorization: `Basic ${auth}`,
3248
+ "content-type": "application/x-www-form-urlencoded"
3249
+ },
3250
+ method: "POST",
3251
+ signal: controller?.signal
3252
+ });
3253
+ if (!response.ok) {
3254
+ return {
3255
+ deliveredTo: url,
3256
+ error: `Twilio handoff failed with response ${response.status}.`,
3257
+ status: "failed"
3258
+ };
3259
+ }
3260
+ return {
3261
+ deliveredAt: Date.now(),
3262
+ deliveredTo: url,
3263
+ metadata: {
3264
+ callSid
3265
+ },
3266
+ status: "delivered"
3267
+ };
3268
+ } finally {
3269
+ if (timeout) {
3270
+ clearTimeout(timeout);
3271
+ }
3272
+ }
3273
+ },
3274
+ id: options.id ?? "twilio-redirect",
3275
+ kind: "twilio-redirect"
3276
+ });
3277
+
2995
3278
  // src/turnDetection.ts
2996
3279
  var DEFAULT_SILENCE_MS = 700;
2997
3280
  var DEFAULT_SPEECH_THRESHOLD = 0.015;
@@ -3288,6 +3571,7 @@ var pushCallLifecycleEvent = (session, input) => {
3288
3571
  }
3289
3572
  return lifecycle;
3290
3573
  };
3574
+ var getLatestCallLifecycleEvent = (session) => session.call?.events.at(-1);
3291
3575
  var createVoiceSession = (options) => {
3292
3576
  const logger = resolveLogger(options.logger);
3293
3577
  const reconnect = {
@@ -3388,6 +3672,64 @@ var createVoiceSession = (options) => {
3388
3672
  });
3389
3673
  }
3390
3674
  };
3675
+ const sendCallLifecycle = async (session) => {
3676
+ const event = getLatestCallLifecycleEvent(session);
3677
+ if (!event) {
3678
+ return;
3679
+ }
3680
+ await send({
3681
+ event,
3682
+ sessionId: options.id,
3683
+ type: "call_lifecycle"
3684
+ });
3685
+ };
3686
+ const runHandoff = async (input) => {
3687
+ const queuedDelivery = options.handoff?.deliveryQueue ? createVoiceHandoffDeliveryRecord({
3688
+ action: input.action,
3689
+ context: options.context,
3690
+ metadata: input.metadata,
3691
+ reason: input.reason,
3692
+ result: input.result,
3693
+ session: input.session,
3694
+ target: input.target
3695
+ }) : undefined;
3696
+ if (queuedDelivery) {
3697
+ await options.handoff?.deliveryQueue?.set(queuedDelivery.id, queuedDelivery);
3698
+ }
3699
+ if (options.handoff?.enqueueOnly) {
3700
+ return;
3701
+ }
3702
+ const result = await deliverVoiceHandoff({
3703
+ config: options.handoff,
3704
+ handoff: {
3705
+ action: input.action,
3706
+ api,
3707
+ context: options.context,
3708
+ metadata: input.metadata,
3709
+ reason: input.reason,
3710
+ result: input.result,
3711
+ session: input.session,
3712
+ target: input.target
3713
+ }
3714
+ });
3715
+ if (!result) {
3716
+ return;
3717
+ }
3718
+ if (queuedDelivery) {
3719
+ const updatedDelivery = applyVoiceHandoffDeliveryResult(queuedDelivery, result);
3720
+ await options.handoff?.deliveryQueue?.set(updatedDelivery.id, updatedDelivery);
3721
+ }
3722
+ await appendTrace({
3723
+ metadata: input.metadata,
3724
+ payload: {
3725
+ ...result,
3726
+ reason: input.reason,
3727
+ target: input.target
3728
+ },
3729
+ session: input.session,
3730
+ type: "call.handoff"
3731
+ });
3732
+ };
3391
3733
  const readSession = async () => options.store.getOrCreate(options.id);
3392
3734
  const writeSession = async (mutate) => {
3393
3735
  const session = await options.store.getOrCreate(options.id);
@@ -3578,6 +3920,7 @@ var createVoiceSession = (options) => {
3578
3920
  await appendTrace({
3579
3921
  payload: {
3580
3922
  disposition,
3923
+ metadata: input.metadata,
3581
3924
  reason: input.reason,
3582
3925
  target: input.target,
3583
3926
  type: "end"
@@ -3585,6 +3928,7 @@ var createVoiceSession = (options) => {
3585
3928
  session,
3586
3929
  type: "call.lifecycle"
3587
3930
  });
3931
+ await sendCallLifecycle(session);
3588
3932
  await send({
3589
3933
  sessionId: options.id,
3590
3934
  type: "complete"
@@ -3664,6 +4008,15 @@ var createVoiceSession = (options) => {
3664
4008
  session,
3665
4009
  type: "call.lifecycle"
3666
4010
  });
4011
+ await sendCallLifecycle(session);
4012
+ await runHandoff({
4013
+ action: "transfer",
4014
+ metadata: input.metadata,
4015
+ reason: input.reason,
4016
+ result: input.result,
4017
+ session,
4018
+ target: input.target
4019
+ });
3667
4020
  await completeInternal(input.result, {
3668
4021
  disposition: "transferred",
3669
4022
  invokeOnComplete: false,
@@ -3689,6 +4042,14 @@ var createVoiceSession = (options) => {
3689
4042
  session,
3690
4043
  type: "call.lifecycle"
3691
4044
  });
4045
+ await sendCallLifecycle(session);
4046
+ await runHandoff({
4047
+ action: "escalate",
4048
+ metadata: input.metadata,
4049
+ reason: input.reason,
4050
+ result: input.result,
4051
+ session
4052
+ });
3692
4053
  await completeInternal(input.result, {
3693
4054
  disposition: "escalated",
3694
4055
  invokeOnComplete: false,
@@ -3711,6 +4072,13 @@ var createVoiceSession = (options) => {
3711
4072
  session,
3712
4073
  type: "call.lifecycle"
3713
4074
  });
4075
+ await sendCallLifecycle(session);
4076
+ await runHandoff({
4077
+ action: "no-answer",
4078
+ metadata: input?.metadata,
4079
+ result: input?.result,
4080
+ session
4081
+ });
3714
4082
  await completeInternal(input?.result, {
3715
4083
  disposition: "no-answer",
3716
4084
  invokeOnComplete: false,
@@ -3732,6 +4100,13 @@ var createVoiceSession = (options) => {
3732
4100
  session,
3733
4101
  type: "call.lifecycle"
3734
4102
  });
4103
+ await sendCallLifecycle(session);
4104
+ await runHandoff({
4105
+ action: "voicemail",
4106
+ metadata: input?.metadata,
4107
+ result: input?.result,
4108
+ session
4109
+ });
3735
4110
  await completeInternal(input?.result, {
3736
4111
  disposition: "voicemail",
3737
4112
  invokeOnComplete: false,
@@ -4518,6 +4893,7 @@ var createVoiceSession = (options) => {
4518
4893
  session,
4519
4894
  type: "call.lifecycle"
4520
4895
  });
4896
+ await sendCallLifecycle(session);
4521
4897
  }
4522
4898
  await send({
4523
4899
  sessionId: options.id,
@@ -4755,6 +5131,14 @@ var isVoiceClientMessage = (value) => {
4755
5131
  return false;
4756
5132
  }
4757
5133
  switch (value.type) {
5134
+ case "call_control":
5135
+ if (!("action" in value)) {
5136
+ return false;
5137
+ }
5138
+ if (value.action !== "complete" && value.action !== "escalate" && value.action !== "no-answer" && value.action !== "transfer" && value.action !== "voicemail") {
5139
+ return false;
5140
+ }
5141
+ return (!("metadata" in value) || value.metadata === undefined || value.metadata !== null && typeof value.metadata === "object") && (!("reason" in value) || value.reason === undefined || typeof value.reason === "string") && (!("target" in value) || value.target === undefined || typeof value.target === "string");
4758
5142
  case "close":
4759
5143
  return true;
4760
5144
  case "end_turn":
@@ -4895,6 +5279,7 @@ var voice = (config) => {
4895
5279
  audioConditioning: sessionOptions.audioConditioning,
4896
5280
  context,
4897
5281
  id: sessionId,
5282
+ handoff: config.handoff,
4898
5283
  languageStrategy: config.languageStrategy,
4899
5284
  lexicon,
4900
5285
  logger: sessionOptions.logger,
@@ -5006,6 +5391,42 @@ var voice = (config) => {
5006
5391
  await current.close(message.reason);
5007
5392
  runtime.activeSessions.delete(sessionState.sessionId);
5008
5393
  }
5394
+ if (message.type === "call_control" && current) {
5395
+ if (message.action === "transfer") {
5396
+ if (message.target) {
5397
+ await current.transfer({
5398
+ metadata: message.metadata,
5399
+ reason: message.reason,
5400
+ target: message.target
5401
+ });
5402
+ } else {
5403
+ ws.send(JSON.stringify({
5404
+ message: "call_control transfer requires target",
5405
+ recoverable: true,
5406
+ type: "error"
5407
+ }));
5408
+ }
5409
+ }
5410
+ if (message.action === "escalate") {
5411
+ await current.escalate({
5412
+ metadata: message.metadata,
5413
+ reason: message.reason ?? "client-requested-escalation"
5414
+ });
5415
+ }
5416
+ if (message.action === "voicemail") {
5417
+ await current.markVoicemail({
5418
+ metadata: message.metadata
5419
+ });
5420
+ }
5421
+ if (message.action === "no-answer") {
5422
+ await current.markNoAnswer({
5423
+ metadata: message.metadata
5424
+ });
5425
+ }
5426
+ if (message.action === "complete") {
5427
+ await current.complete();
5428
+ }
5429
+ }
5009
5430
  if (message.type === "start" && message.sessionId && message.sessionId !== sessionState.sessionId) {
5010
5431
  const currentSession = runtime.activeSessions.get(sessionState.sessionId);
5011
5432
  if (currentSession) {
@@ -5052,9 +5473,15 @@ var voice = (config) => {
5052
5473
  }
5053
5474
  }).use(htmxRoutes());
5054
5475
  };
5476
+ // src/appKit.ts
5477
+ import { Elysia as Elysia11 } from "elysia";
5478
+
5479
+ // src/assistantHealth.ts
5480
+ import { Elysia as Elysia3 } from "elysia";
5481
+
5055
5482
  // src/agent.ts
5056
5483
  var normalizeText3 = (value) => typeof value === "string" ? value.trim() : "";
5057
- var toErrorMessage2 = (error) => error instanceof Error ? error.message : String(error);
5484
+ var toErrorMessage3 = (error) => error instanceof Error ? error.message : String(error);
5058
5485
  var createHistoryMessages = (session, turn) => {
5059
5486
  const messages = [];
5060
5487
  for (const previousTurn of session.turns) {
@@ -5235,7 +5662,7 @@ var createVoiceAgent = (options) => {
5235
5662
  toolCallId: toolCall.id
5236
5663
  });
5237
5664
  } catch (error) {
5238
- const errorMessage = toErrorMessage2(error);
5665
+ const errorMessage = toErrorMessage3(error);
5239
5666
  toolResults.push({
5240
5667
  error: errorMessage,
5241
5668
  status: "error",
@@ -6101,52 +6528,350 @@ var summarizeVoiceAssistantRuns = async (input) => {
6101
6528
  totalRuns: assistantRuns.length
6102
6529
  };
6103
6530
  };
6104
- // src/fileStore.ts
6105
- import { mkdir, readFile, readdir, rename, rm, writeFile } from "fs/promises";
6106
- import { join } from "path";
6107
6531
 
6108
- // src/trace.ts
6109
- var createVoiceTraceEventId = (event) => [
6110
- event.sessionId,
6111
- event.turnId ?? "session",
6112
- event.type,
6113
- String(event.at ?? Date.now()),
6114
- crypto.randomUUID()
6115
- ].map(encodeURIComponent).join(":");
6116
- var createVoiceTraceEvent = (event) => ({
6117
- ...event,
6118
- at: event.at,
6119
- id: event.id ?? createVoiceTraceEventId({
6120
- at: event.at,
6121
- sessionId: event.sessionId,
6122
- turnId: event.turnId,
6123
- type: event.type
6124
- })
6125
- });
6126
- var createVoiceTraceSinkDeliveryId = (events) => {
6127
- const firstEvent = events[0];
6128
- return [
6129
- firstEvent?.sessionId ?? "trace",
6130
- firstEvent?.traceId ?? "sink",
6131
- String(firstEvent?.at ?? Date.now()),
6132
- crypto.randomUUID()
6133
- ].map(encodeURIComponent).join(":");
6134
- };
6135
- var createVoiceTraceSinkDeliveryRecord = (input) => {
6136
- const createdAt = input.createdAt ?? Date.now();
6137
- return {
6138
- createdAt,
6139
- deliveredAt: input.deliveredAt,
6140
- deliveryAttempts: input.deliveryAttempts,
6141
- deliveryError: input.deliveryError,
6142
- deliveryStatus: input.deliveryStatus ?? "pending",
6143
- events: input.events,
6144
- id: input.id ?? createVoiceTraceSinkDeliveryId(input.events),
6145
- sinkDeliveries: input.sinkDeliveries,
6146
- updatedAt: input.updatedAt ?? createdAt
6532
+ // src/providerHealth.ts
6533
+ import { Elysia as Elysia2 } from "elysia";
6534
+ var getString = (value) => typeof value === "string" ? value : undefined;
6535
+ var getNumber = (value) => typeof value === "number" && Number.isFinite(value) ? value : undefined;
6536
+ var isProviderStatus = (value) => value === "success" || value === "fallback" || value === "error";
6537
+ var summarizeVoiceProviderHealth = async (input) => {
6538
+ const options = Array.isArray(input) ? { events: input } : input;
6539
+ const events = options.events ?? await options.store?.list() ?? [];
6540
+ const providers = options.providers ?? [];
6541
+ const providerSet = new Set(providers);
6542
+ const now = options.now ?? Date.now();
6543
+ const entries = new Map;
6544
+ const isAllowedProvider = (value) => typeof value === "string" && (providerSet.size === 0 || providerSet.has(value));
6545
+ const getEntry = (provider) => {
6546
+ const existing = entries.get(provider);
6547
+ if (existing) {
6548
+ return existing;
6549
+ }
6550
+ const entry = {
6551
+ elapsedCount: 0,
6552
+ elapsedTotal: 0,
6553
+ errorCount: 0,
6554
+ fallbackCount: 0,
6555
+ provider,
6556
+ rateLimited: false,
6557
+ recommended: false,
6558
+ runCount: 0,
6559
+ status: "idle",
6560
+ timeoutCount: 0
6561
+ };
6562
+ entries.set(provider, entry);
6563
+ return entry;
6147
6564
  };
6148
- };
6149
- var matchesTraceFilter = (event, filter) => {
6565
+ for (const provider of providers) {
6566
+ getEntry(provider);
6567
+ }
6568
+ const hasProviderRouterEvents = events.some((event) => event.type === "session.error" && isAllowedProvider(event.payload.provider) && isProviderStatus(event.payload.providerStatus));
6569
+ for (const event of events) {
6570
+ if (event.type === "assistant.run") {
6571
+ if (hasProviderRouterEvents) {
6572
+ continue;
6573
+ }
6574
+ const provider2 = event.payload.variantId;
6575
+ if (!isAllowedProvider(provider2)) {
6576
+ continue;
6577
+ }
6578
+ const entry2 = getEntry(provider2);
6579
+ entry2.runCount += 1;
6580
+ const elapsedMs = getNumber(event.payload.elapsedMs);
6581
+ if (elapsedMs !== undefined) {
6582
+ entry2.elapsedCount += 1;
6583
+ entry2.elapsedTotal += elapsedMs;
6584
+ }
6585
+ continue;
6586
+ }
6587
+ if (event.type !== "session.error") {
6588
+ continue;
6589
+ }
6590
+ const provider = event.payload.provider;
6591
+ if (!isAllowedProvider(provider)) {
6592
+ continue;
6593
+ }
6594
+ const providerStatus = isProviderStatus(event.payload.providerStatus) ? event.payload.providerStatus : undefined;
6595
+ const applyProviderHealth = () => {
6596
+ const entry2 = getEntry(provider);
6597
+ const providerHealth = event.payload.providerHealth;
6598
+ if (providerHealth && typeof providerHealth === "object") {
6599
+ const suppressedUntil2 = getNumber(providerHealth.suppressedUntil);
6600
+ if (suppressedUntil2 !== undefined) {
6601
+ entry2.suppressedUntil = suppressedUntil2;
6602
+ }
6603
+ }
6604
+ const suppressedUntil = getNumber(event.payload.suppressedUntil);
6605
+ if (suppressedUntil !== undefined) {
6606
+ entry2.suppressedUntil = suppressedUntil;
6607
+ }
6608
+ const suppressionRemainingMs = getNumber(event.payload.suppressionRemainingMs);
6609
+ if (suppressionRemainingMs !== undefined) {
6610
+ entry2.suppressionRemainingMs = suppressionRemainingMs;
6611
+ }
6612
+ return entry2;
6613
+ };
6614
+ if (providerStatus === "success" || providerStatus === "fallback") {
6615
+ const entry2 = applyProviderHealth();
6616
+ entry2.runCount += 1;
6617
+ entry2.lastSuccessAt = event.at;
6618
+ if (providerStatus === "success") {
6619
+ entry2.lastError = undefined;
6620
+ entry2.rateLimited = false;
6621
+ entry2.suppressedUntil = undefined;
6622
+ entry2.suppressionRemainingMs = undefined;
6623
+ }
6624
+ const elapsedMs = getNumber(event.payload.elapsedMs);
6625
+ if (elapsedMs !== undefined) {
6626
+ entry2.elapsedCount += 1;
6627
+ entry2.elapsedTotal += elapsedMs;
6628
+ }
6629
+ const selectedProvider = event.payload.selectedProvider;
6630
+ if (providerStatus === "fallback" && isAllowedProvider(selectedProvider) && selectedProvider !== provider) {
6631
+ getEntry(selectedProvider).fallbackCount += 1;
6632
+ }
6633
+ continue;
6634
+ }
6635
+ const entry = applyProviderHealth();
6636
+ entry.errorCount += 1;
6637
+ if (event.payload.timedOut === true) {
6638
+ entry.timeoutCount += 1;
6639
+ }
6640
+ entry.lastError = getString(event.payload.error);
6641
+ entry.lastErrorAt = event.at;
6642
+ entry.rateLimited ||= event.payload.rateLimited === true;
6643
+ }
6644
+ const summaries = [...entries.values()].map((entry) => {
6645
+ const hadSuppression = typeof entry.suppressedUntil === "number" || typeof entry.suppressionRemainingMs === "number";
6646
+ const suppressionRemainingMs = typeof entry.suppressedUntil === "number" ? Math.max(0, entry.suppressedUntil - now) : entry.suppressionRemainingMs;
6647
+ const activeSuppression = typeof suppressionRemainingMs === "number" && suppressionRemainingMs > 0;
6648
+ const recoverable = hadSuppression && !activeSuppression;
6649
+ const averageElapsedMs = entry.elapsedCount > 0 ? Math.round(entry.elapsedTotal / entry.elapsedCount) : undefined;
6650
+ const status = activeSuppression ? "suppressed" : recoverable ? "recoverable" : entry.rateLimited ? "rate-limited" : entry.errorCount > 0 && (!entry.lastSuccessAt || !entry.lastErrorAt || entry.lastErrorAt > entry.lastSuccessAt) ? "degraded" : entry.runCount > 0 ? "healthy" : "idle";
6651
+ return {
6652
+ averageElapsedMs,
6653
+ errorCount: entry.errorCount,
6654
+ fallbackCount: entry.fallbackCount,
6655
+ lastError: entry.lastError,
6656
+ lastErrorAt: entry.lastErrorAt,
6657
+ lastSuccessAt: entry.lastSuccessAt,
6658
+ provider: entry.provider,
6659
+ rateLimited: entry.rateLimited,
6660
+ recommended: false,
6661
+ runCount: entry.runCount,
6662
+ status,
6663
+ suppressionRemainingMs: activeSuppression ? suppressionRemainingMs : undefined,
6664
+ suppressedUntil: entry.suppressedUntil,
6665
+ timeoutCount: entry.timeoutCount
6666
+ };
6667
+ });
6668
+ const recommended = summaries.filter((entry) => entry.status === "healthy").sort((left, right) => (left.averageElapsedMs ?? Number.MAX_SAFE_INTEGER) - (right.averageElapsedMs ?? Number.MAX_SAFE_INTEGER))[0];
6669
+ if (recommended) {
6670
+ recommended.recommended = true;
6671
+ }
6672
+ return summaries;
6673
+ };
6674
+ var escapeHtml3 = (value) => value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
6675
+ var renderVoiceProviderHealthHTML = (providers) => providers.length === 0 ? '<p class="voice-provider-empty">No provider status yet.</p>' : [
6676
+ '<div class="voice-provider-health">',
6677
+ ...providers.map((provider) => {
6678
+ const suppressionSeconds = typeof provider.suppressionRemainingMs === "number" ? Math.ceil(provider.suppressionRemainingMs / 1000) : undefined;
6679
+ return [
6680
+ `<article class="voice-provider-card ${escapeHtml3(provider.status)}">`,
6681
+ '<div class="voice-provider-card-header">',
6682
+ `<strong>${escapeHtml3(provider.provider)}</strong>`,
6683
+ `<span>${escapeHtml3(provider.status)}${provider.recommended ? " \xB7 recommended" : ""}</span>`,
6684
+ "</div>",
6685
+ "<dl>",
6686
+ `<div><dt>Runs</dt><dd>${String(provider.runCount)}</dd></div>`,
6687
+ `<div><dt>Avg latency</dt><dd>${String(provider.averageElapsedMs ?? 0)}ms</dd></div>`,
6688
+ `<div><dt>Errors</dt><dd>${String(provider.errorCount)}</dd></div>`,
6689
+ `<div><dt>Timeouts</dt><dd>${String(provider.timeoutCount)}</dd></div>`,
6690
+ `<div><dt>Fallbacks</dt><dd>${String(provider.fallbackCount)}</dd></div>`,
6691
+ "</dl>",
6692
+ suppressionSeconds ? `<p>Temporarily suppressed for ${String(suppressionSeconds)}s.</p>` : "",
6693
+ provider.lastError ? `<p>${escapeHtml3(provider.lastError)}</p>` : "",
6694
+ "</article>"
6695
+ ].join("");
6696
+ }),
6697
+ "</div>"
6698
+ ].join("");
6699
+ var createVoiceProviderHealthJSONHandler = (options) => async () => summarizeVoiceProviderHealth(options);
6700
+ var createVoiceProviderHealthHTMLHandler = (options) => async () => {
6701
+ const providers = await summarizeVoiceProviderHealth(options);
6702
+ const render = options.render ?? renderVoiceProviderHealthHTML;
6703
+ const body = await render(providers);
6704
+ return new Response(body, {
6705
+ headers: {
6706
+ "Content-Type": "text/html; charset=utf-8",
6707
+ ...options.headers
6708
+ }
6709
+ });
6710
+ };
6711
+ var createVoiceProviderHealthRoutes = (options) => {
6712
+ const path = options.path ?? "/api/provider-status";
6713
+ const htmlPath = options.htmlPath === undefined ? `${path}/htmx` : options.htmlPath;
6714
+ const routes = new Elysia2({
6715
+ name: options.name ?? "absolutejs-voice-provider-health"
6716
+ }).get(path, createVoiceProviderHealthJSONHandler(options));
6717
+ if (htmlPath) {
6718
+ routes.get(htmlPath, createVoiceProviderHealthHTMLHandler(options));
6719
+ }
6720
+ return routes;
6721
+ };
6722
+
6723
+ // src/assistantHealth.ts
6724
+ var escapeHtml4 = (value) => value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
6725
+ var renderCountMap = (values) => {
6726
+ const entries = Object.entries(values).sort((left, right) => right[1] - left[1]);
6727
+ if (entries.length === 0) {
6728
+ return '<p class="voice-assistant-health-empty">No data yet.</p>';
6729
+ }
6730
+ return [
6731
+ '<div class="voice-assistant-health-metrics">',
6732
+ ...entries.map(([label, value]) => `<div><span>${escapeHtml4(label)}</span><strong>${String(value)}</strong></div>`),
6733
+ "</div>"
6734
+ ].join("");
6735
+ };
6736
+ var getString2 = (value) => typeof value === "string" ? value : undefined;
6737
+ var getRecentFailures = (events, maxFailures, replayHref) => events.filter((event) => event.type === "session.error" && (event.payload.providerStatus === "error" || typeof event.payload.error === "string") || event.type === "assistant.guardrail" && event.payload.action === "blocked").toReversed().slice(0, maxFailures).map((event) => {
6738
+ const failure = {
6739
+ at: event.at,
6740
+ assistantId: getString2(event.payload.assistantId),
6741
+ error: getString2(event.payload.error),
6742
+ provider: getString2(event.payload.provider),
6743
+ rateLimited: event.payload.rateLimited === true ? true : undefined,
6744
+ sessionId: event.sessionId,
6745
+ status: getString2(event.payload.providerStatus),
6746
+ turnId: event.turnId,
6747
+ type: event.type
6748
+ };
6749
+ const href = replayHref === false ? undefined : typeof replayHref === "function" ? replayHref(failure) : `${replayHref ?? "/api/voice-sessions"}/${encodeURIComponent(event.sessionId)}/replay/htmx`;
6750
+ return {
6751
+ ...failure,
6752
+ replayHref: href
6753
+ };
6754
+ });
6755
+ var summarizeVoiceAssistantHealth = async (options) => {
6756
+ const events = options.events ?? await options.store?.list() ?? [];
6757
+ return {
6758
+ assistantRuns: await summarizeVoiceAssistantRuns({ events }),
6759
+ providerHealth: await summarizeVoiceProviderHealth({
6760
+ events,
6761
+ providers: options.providers
6762
+ }),
6763
+ recentFailures: getRecentFailures(events, options.maxFailures ?? 8, options.replayHref)
6764
+ };
6765
+ };
6766
+ var renderVoiceAssistantHealthHTML = (summary) => {
6767
+ const assistant = summary.assistantRuns.assistants[0];
6768
+ const failures = summary.recentFailures;
6769
+ return [
6770
+ '<div class="voice-assistant-health">',
6771
+ '<section class="voice-assistant-health-grid">',
6772
+ `<article><span>Runs</span><strong>${String(assistant?.runCount ?? 0)}</strong></article>`,
6773
+ `<article><span>Sessions</span><strong>${String(assistant?.sessions ?? 0)}</strong></article>`,
6774
+ `<article><span>Guardrails</span><strong>${String(assistant?.guardrailCount ?? 0)}</strong></article>`,
6775
+ `<article><span>Avg latency</span><strong>${String(assistant?.averageElapsedMs ?? 0)}ms</strong></article>`,
6776
+ "</section>",
6777
+ "<section>",
6778
+ "<h3>Provider Health</h3>",
6779
+ renderVoiceProviderHealthHTML(summary.providerHealth),
6780
+ "</section>",
6781
+ '<section class="voice-assistant-health-columns">',
6782
+ `<article><h3>Outcomes</h3>${renderCountMap(assistant?.outcomes ?? {})}</article>`,
6783
+ `<article><h3>Variants</h3>${renderCountMap(assistant?.variants ?? {})}</article>`,
6784
+ `<article><h3>Tools</h3>${renderCountMap(assistant?.toolCalls ?? {})}</article>`,
6785
+ `<article><h3>Artifact Plans</h3>${renderCountMap(assistant?.artifactPlans ?? {})}</article>`,
6786
+ "</section>",
6787
+ "<section>",
6788
+ "<h3>Recent Failures</h3>",
6789
+ failures.length === 0 ? '<p class="voice-assistant-health-empty">No failures yet.</p>' : [
6790
+ '<div class="voice-assistant-health-failures">',
6791
+ ...failures.map((failure) => [
6792
+ "<article>",
6793
+ `<strong>${escapeHtml4(failure.provider ?? failure.assistantId ?? failure.type)}</strong>`,
6794
+ `<span>${escapeHtml4(failure.status ?? (failure.rateLimited ? "rate-limited" : "error"))}</span>`,
6795
+ failure.error ? `<p>${escapeHtml4(failure.error)}</p>` : "",
6796
+ `<small>${escapeHtml4(failure.sessionId)}${failure.turnId ? ` / ${escapeHtml4(failure.turnId)}` : ""}</small>`,
6797
+ failure.replayHref ? `<p><a href="${escapeHtml4(failure.replayHref)}">Open replay</a></p>` : "",
6798
+ "</article>"
6799
+ ].join("")),
6800
+ "</div>"
6801
+ ].join(""),
6802
+ "</section>",
6803
+ "</div>"
6804
+ ].join("");
6805
+ };
6806
+ var createVoiceAssistantHealthJSONHandler = (options) => async () => summarizeVoiceAssistantHealth(options);
6807
+ var createVoiceAssistantHealthHTMLHandler = (options) => async () => {
6808
+ const summary = await summarizeVoiceAssistantHealth(options);
6809
+ const render = options.render ?? renderVoiceAssistantHealthHTML;
6810
+ const body = await render(summary);
6811
+ return new Response(body, {
6812
+ headers: {
6813
+ "Content-Type": "text/html; charset=utf-8",
6814
+ ...options.headers
6815
+ }
6816
+ });
6817
+ };
6818
+ var createVoiceAssistantHealthRoutes = (options) => {
6819
+ const path = options.path ?? "/api/assistant-health";
6820
+ const htmlPath = options.htmlPath === undefined ? `${path}/htmx` : options.htmlPath;
6821
+ const routes = new Elysia3({
6822
+ name: options.name ?? "absolutejs-voice-assistant-health"
6823
+ }).get(path, createVoiceAssistantHealthJSONHandler(options));
6824
+ if (htmlPath) {
6825
+ routes.get(htmlPath, createVoiceAssistantHealthHTMLHandler(options));
6826
+ }
6827
+ return routes;
6828
+ };
6829
+
6830
+ // src/diagnosticsRoutes.ts
6831
+ import { Elysia as Elysia4 } from "elysia";
6832
+
6833
+ // src/trace.ts
6834
+ var createVoiceTraceEventId = (event) => [
6835
+ event.sessionId,
6836
+ event.turnId ?? "session",
6837
+ event.type,
6838
+ String(event.at ?? Date.now()),
6839
+ crypto.randomUUID()
6840
+ ].map(encodeURIComponent).join(":");
6841
+ var createVoiceTraceEvent = (event) => ({
6842
+ ...event,
6843
+ at: event.at,
6844
+ id: event.id ?? createVoiceTraceEventId({
6845
+ at: event.at,
6846
+ sessionId: event.sessionId,
6847
+ turnId: event.turnId,
6848
+ type: event.type
6849
+ })
6850
+ });
6851
+ var createVoiceTraceSinkDeliveryId = (events) => {
6852
+ const firstEvent = events[0];
6853
+ return [
6854
+ firstEvent?.sessionId ?? "trace",
6855
+ firstEvent?.traceId ?? "sink",
6856
+ String(firstEvent?.at ?? Date.now()),
6857
+ crypto.randomUUID()
6858
+ ].map(encodeURIComponent).join(":");
6859
+ };
6860
+ var createVoiceTraceSinkDeliveryRecord = (input) => {
6861
+ const createdAt = input.createdAt ?? Date.now();
6862
+ return {
6863
+ createdAt,
6864
+ deliveredAt: input.deliveredAt,
6865
+ deliveryAttempts: input.deliveryAttempts,
6866
+ deliveryError: input.deliveryError,
6867
+ deliveryStatus: input.deliveryStatus ?? "pending",
6868
+ events: input.events,
6869
+ id: input.id ?? createVoiceTraceSinkDeliveryId(input.events),
6870
+ sinkDeliveries: input.sinkDeliveries,
6871
+ updatedAt: input.updatedAt ?? createdAt
6872
+ };
6873
+ };
6874
+ var matchesTraceFilter = (event, filter) => {
6150
6875
  if (filter.sessionId !== undefined && event.sessionId !== filter.sessionId) {
6151
6876
  return false;
6152
6877
  }
@@ -6207,7 +6932,7 @@ var sleep3 = async (delayMs) => {
6207
6932
  }
6208
6933
  await new Promise((resolve2) => setTimeout(resolve2, delayMs));
6209
6934
  };
6210
- var toHex3 = (bytes) => Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
6935
+ var toHex4 = (bytes) => Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
6211
6936
  var signVoiceTraceSinkBody = async (input) => {
6212
6937
  const encoder = new TextEncoder;
6213
6938
  const key = await crypto.subtle.importKey("raw", encoder.encode(input.secret), {
@@ -6216,7 +6941,7 @@ var signVoiceTraceSinkBody = async (input) => {
6216
6941
  }, false, ["sign"]);
6217
6942
  const payload = encoder.encode(`${input.timestamp}.${input.body}`);
6218
6943
  const signature = await crypto.subtle.sign("HMAC", key, payload);
6219
- return `sha256=${toHex3(new Uint8Array(signature))}`;
6944
+ return `sha256=${toHex4(new Uint8Array(signature))}`;
6220
6945
  };
6221
6946
  var createVoiceTraceSinkDeliveryError = (input) => {
6222
6947
  if (input.response) {
@@ -6437,7 +7162,7 @@ var exportVoiceTrace = async (input) => {
6437
7162
  };
6438
7163
  };
6439
7164
  var toNumber = (value) => typeof value === "number" && Number.isFinite(value) ? value : 0;
6440
- var escapeHtml3 = (value) => value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
7165
+ var escapeHtml5 = (value) => value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
6441
7166
  var formatTraceValue = (value) => {
6442
7167
  if (value === undefined || value === null) {
6443
7168
  return "";
@@ -6715,10 +7440,10 @@ var renderVoiceTraceHTML = (events, options = {}) => {
6715
7440
  const offset = summary.startedAt === undefined ? event.at : Math.max(0, event.at - summary.startedAt);
6716
7441
  return [
6717
7442
  "<tr>",
6718
- `<td>${escapeHtml3(String(offset))}</td>`,
6719
- `<td>${escapeHtml3(event.type)}</td>`,
6720
- `<td>${escapeHtml3(event.turnId ?? "")}</td>`,
6721
- `<td><code>${escapeHtml3(JSON.stringify(event.payload))}</code></td>`,
7443
+ `<td>${escapeHtml5(String(offset))}</td>`,
7444
+ `<td>${escapeHtml5(event.type)}</td>`,
7445
+ `<td>${escapeHtml5(event.turnId ?? "")}</td>`,
7446
+ `<td><code>${escapeHtml5(JSON.stringify(event.payload))}</code></td>`,
6722
7447
  "</tr>"
6723
7448
  ].join("");
6724
7449
  }).join(`
@@ -6729,7 +7454,7 @@ var renderVoiceTraceHTML = (events, options = {}) => {
6729
7454
  "<head>",
6730
7455
  '<meta charset="utf-8" />',
6731
7456
  '<meta name="viewport" content="width=device-width, initial-scale=1" />',
6732
- `<title>${escapeHtml3(options.title ?? "Voice Trace")}</title>`,
7457
+ `<title>${escapeHtml5(options.title ?? "Voice Trace")}</title>`,
6733
7458
  "<style>",
6734
7459
  "body{font-family:ui-sans-serif,system-ui,sans-serif;margin:2rem;line-height:1.45;background:#f8f7f2;color:#181713}",
6735
7460
  "main{max-width:1100px;margin:auto}",
@@ -6743,7 +7468,7 @@ var renderVoiceTraceHTML = (events, options = {}) => {
6743
7468
  "</style>",
6744
7469
  "</head>",
6745
7470
  "<body><main>",
6746
- `<h1>${escapeHtml3(options.title ?? `Voice Trace ${summary.sessionId ?? ""}`.trim())}</h1>`,
7471
+ `<h1>${escapeHtml5(options.title ?? `Voice Trace ${summary.sessionId ?? ""}`.trim())}</h1>`,
6747
7472
  `<p class="${evaluation.pass ? "pass" : "fail"}">QA: ${evaluation.pass ? "pass" : "fail"}</p>`,
6748
7473
  '<section class="summary">',
6749
7474
  `<div class="card"><strong>Events</strong><br>${summary.eventCount}</div>`,
@@ -6757,19 +7482,2255 @@ var renderVoiceTraceHTML = (events, options = {}) => {
6757
7482
  eventRows,
6758
7483
  "</tbody></table>",
6759
7484
  "<h2>Markdown Export</h2>",
6760
- `<pre>${escapeHtml3(markdown)}</pre>`,
7485
+ `<pre>${escapeHtml5(markdown)}</pre>`,
6761
7486
  "</main></body></html>"
6762
7487
  ].join(`
6763
7488
  `);
6764
7489
  };
6765
- var buildVoiceTraceReplay = (events, options = {}) => ({
6766
- evaluation: evaluateVoiceTrace(options.redact ? redactVoiceTraceEvents(events, options.redact) : events, options.evaluation),
6767
- html: renderVoiceTraceHTML(events, options),
6768
- markdown: renderVoiceTraceMarkdown(events, options),
6769
- summary: summarizeVoiceTrace(options.redact ? redactVoiceTraceEvents(events, options.redact) : events)
7490
+ var buildVoiceTraceReplay = (events, options = {}) => ({
7491
+ evaluation: evaluateVoiceTrace(options.redact ? redactVoiceTraceEvents(events, options.redact) : events, options.evaluation),
7492
+ html: renderVoiceTraceHTML(events, options),
7493
+ markdown: renderVoiceTraceMarkdown(events, options),
7494
+ summary: summarizeVoiceTrace(options.redact ? redactVoiceTraceEvents(events, options.redact) : events)
7495
+ });
7496
+
7497
+ // src/diagnosticsRoutes.ts
7498
+ var escapeHtml6 = (value) => value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
7499
+ var getString3 = (value) => typeof value === "string" && value.trim() ? value : undefined;
7500
+ var getNumber2 = (value) => {
7501
+ const parsed = typeof value === "number" ? value : typeof value === "string" ? Number(value) : undefined;
7502
+ return typeof parsed === "number" && Number.isFinite(parsed) ? parsed : undefined;
7503
+ };
7504
+ var getBoolean = (value) => value === true || value === "true" || value === "1";
7505
+ var parseTraceTypeFilter = (value) => {
7506
+ if (typeof value !== "string" || !value.trim()) {
7507
+ return;
7508
+ }
7509
+ const types = value.split(",").map((entry) => entry.trim()).filter(Boolean);
7510
+ return types.length <= 1 ? types[0] : types;
7511
+ };
7512
+ var resolveVoiceDiagnosticsTraceFilter = (query) => ({
7513
+ limit: getNumber2(query.limit),
7514
+ scenarioId: getString3(query.scenarioId),
7515
+ sessionId: getString3(query.sessionId),
7516
+ traceId: getString3(query.traceId),
7517
+ turnId: getString3(query.turnId),
7518
+ type: parseTraceTypeFilter(query.type)
7519
+ });
7520
+ var filterByDiagnosticsQuery = (events, query) => {
7521
+ const provider = getString3(query.provider);
7522
+ const status = getString3(query.status);
7523
+ const since = getNumber2(query.since);
7524
+ const until = getNumber2(query.until);
7525
+ return filterVoiceTraceEvents(events, resolveVoiceDiagnosticsTraceFilter(query)).filter((event) => (!provider || event.payload.provider === provider) && (!status || event.payload.providerStatus === status || event.payload.status === status) && (since === undefined || event.at >= since) && (until === undefined || event.at <= until));
7526
+ };
7527
+ var buildVoiceDiagnosticsMarkdown = (events, options = {}) => {
7528
+ const summary = summarizeVoiceTrace(events);
7529
+ const evaluation = evaluateVoiceTrace(events, options.evaluation);
7530
+ const trace = renderVoiceTraceMarkdown(events, {
7531
+ evaluation: options.evaluation,
7532
+ title: options.title ?? `Voice Diagnostics ${summary.sessionId ?? ""}`.trim()
7533
+ });
7534
+ return [
7535
+ `# ${options.title ?? "Voice Diagnostics Bug Report"}`,
7536
+ "",
7537
+ `Session: ${summary.sessionId ?? "unknown"}`,
7538
+ `Pass: ${evaluation.pass ? "yes" : "no"}`,
7539
+ `Events: ${summary.eventCount}`,
7540
+ `Turns: ${summary.turnCount}`,
7541
+ `Errors: ${summary.errorCount}`,
7542
+ `Tool errors: ${summary.toolErrorCount}`,
7543
+ `Estimated cost units: ${summary.cost.estimatedRelativeCostUnits}`,
7544
+ "",
7545
+ "## Issues",
7546
+ "",
7547
+ evaluation.issues.length ? evaluation.issues.map((issue) => `- [${issue.severity}] ${issue.code}: ${issue.message}`).join(`
7548
+ `) : "- none",
7549
+ "",
7550
+ "## Trace",
7551
+ "",
7552
+ trace
7553
+ ].join(`
7554
+ `);
7555
+ };
7556
+ var renderDiagnosticsIndex = (input) => {
7557
+ const sessions = new Map;
7558
+ for (const event of input.events) {
7559
+ sessions.set(event.sessionId, [...sessions.get(event.sessionId) ?? [], event]);
7560
+ }
7561
+ const rows = [...sessions.entries()].sort(([, left], [, right]) => (right.at(-1)?.at ?? 0) - (left.at(-1)?.at ?? 0)).slice(0, 50).map(([sessionId, events]) => {
7562
+ const summary = summarizeVoiceTrace(events);
7563
+ const encoded = encodeURIComponent(sessionId);
7564
+ return `<tr><td>${escapeHtml6(sessionId)}</td><td>${summary.eventCount}</td><td>${summary.turnCount}</td><td>${summary.errorCount}</td><td><a href="${input.basePath}/html?sessionId=${encoded}&redact=true">HTML</a> \xB7 <a href="${input.basePath}/markdown?sessionId=${encoded}&redact=true">Markdown</a> \xB7 <a href="${input.basePath}/json?sessionId=${encoded}&redact=true">JSON</a></td></tr>`;
7565
+ }).join("");
7566
+ return `<!doctype html><html lang="en"><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /><title>${escapeHtml6(input.title)}</title><style>body{font-family:ui-sans-serif,system-ui,sans-serif;margin:2rem;background:#f8f7f2;color:#181713}main{max-width:1100px;margin:auto}table{width:100%;border-collapse:collapse;background:white}td,th{border-bottom:1px solid #eee;padding:.7rem;text-align:left}a{color:#9a3412}</style></head><body><main><h1>${escapeHtml6(input.title)}</h1><p>Recent voice trace diagnostics. Exports support filters: sessionId, traceId, turnId, scenarioId, type, provider, status, since, until, limit, redact.</p><table><thead><tr><th>Session</th><th>Events</th><th>Turns</th><th>Errors</th><th>Exports</th></tr></thead><tbody>${rows}</tbody></table></main></body></html>`;
7567
+ };
7568
+ var withRedaction = (events, query, defaultRedact) => {
7569
+ const shouldRedact = query.redact === undefined ? defaultRedact : getBoolean(query.redact);
7570
+ return shouldRedact ? redactVoiceTraceEvents(events, shouldRedact) : events;
7571
+ };
7572
+ var createVoiceDiagnosticsRoutes = (options) => {
7573
+ const path = options.path ?? "/diagnostics";
7574
+ const title = options.title ?? "AbsoluteJS Voice Diagnostics";
7575
+ const routes = new Elysia4({
7576
+ name: options.name ?? "absolutejs-voice-diagnostics"
7577
+ });
7578
+ routes.get(path, async () => {
7579
+ const events = await options.store.list();
7580
+ return new Response(renderDiagnosticsIndex({ basePath: path, events, title }), {
7581
+ headers: {
7582
+ "Content-Type": "text/html; charset=utf-8",
7583
+ ...options.headers
7584
+ }
7585
+ });
7586
+ });
7587
+ routes.get(`${path}/json`, async ({ query }) => {
7588
+ const events = filterByDiagnosticsQuery(await options.store.list(), query);
7589
+ const redacted = withRedaction(events, query, options.redact);
7590
+ return Response.json({
7591
+ ...await exportVoiceTrace({
7592
+ filter: resolveVoiceDiagnosticsTraceFilter(query),
7593
+ redact: false,
7594
+ store: {
7595
+ ...options.store,
7596
+ list: async () => redacted
7597
+ }
7598
+ }),
7599
+ filteredCount: events.length,
7600
+ redacted: redacted !== events
7601
+ });
7602
+ });
7603
+ routes.get(`${path}/markdown`, async ({ query }) => {
7604
+ const events = withRedaction(filterByDiagnosticsQuery(await options.store.list(), query), query, options.redact ?? true);
7605
+ const body = buildVoiceDiagnosticsMarkdown(events, {
7606
+ evaluation: options.evaluation,
7607
+ title
7608
+ });
7609
+ return new Response(body, {
7610
+ headers: {
7611
+ "Content-Type": "text/markdown; charset=utf-8",
7612
+ ...options.headers
7613
+ }
7614
+ });
7615
+ });
7616
+ routes.get(`${path}/html`, async ({ query }) => {
7617
+ const events = withRedaction(filterByDiagnosticsQuery(await options.store.list(), query), query, options.redact ?? true);
7618
+ const body = renderVoiceTraceHTML(events, {
7619
+ evaluation: options.evaluation,
7620
+ title
7621
+ });
7622
+ return new Response(body, {
7623
+ headers: {
7624
+ "Content-Type": "text/html; charset=utf-8",
7625
+ ...options.headers
7626
+ }
7627
+ });
7628
+ });
7629
+ return routes;
7630
+ };
7631
+
7632
+ // src/evalRoutes.ts
7633
+ import { Elysia as Elysia7 } from "elysia";
7634
+ import { mkdir } from "fs/promises";
7635
+ import { dirname } from "path";
7636
+
7637
+ // src/qualityRoutes.ts
7638
+ import { Elysia as Elysia6 } from "elysia";
7639
+
7640
+ // src/handoffHealth.ts
7641
+ import { Elysia as Elysia5 } from "elysia";
7642
+ var escapeHtml7 = (value) => value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
7643
+ var getString4 = (value) => typeof value === "string" && value.length > 0 ? value : undefined;
7644
+ var isStatus = (value) => value === "delivered" || value === "failed" || value === "skipped";
7645
+ var increment2 = (record, key) => {
7646
+ record[key] = (record[key] ?? 0) + 1;
7647
+ };
7648
+ var normalizeDelivery = (adapterId, value) => {
7649
+ const record = value && typeof value === "object" ? value : {};
7650
+ return {
7651
+ adapterId: getString4(record.adapterId) ?? adapterId,
7652
+ adapterKind: getString4(record.adapterKind),
7653
+ deliveredAt: typeof record.deliveredAt === "number" ? record.deliveredAt : undefined,
7654
+ deliveredTo: getString4(record.deliveredTo),
7655
+ error: getString4(record.error),
7656
+ status: isStatus(record.status) ? record.status : "failed"
7657
+ };
7658
+ };
7659
+ var normalizeDeliveries = (payload) => {
7660
+ const deliveries = payload.deliveries;
7661
+ if (!deliveries || typeof deliveries !== "object") {
7662
+ return [];
7663
+ }
7664
+ return Object.entries(deliveries).map(([adapterId, value]) => normalizeDelivery(adapterId, value));
7665
+ };
7666
+ var resolveReplayHref = (event, replayHref) => {
7667
+ if (replayHref === false) {
7668
+ return;
7669
+ }
7670
+ if (typeof replayHref === "function") {
7671
+ return replayHref(event);
7672
+ }
7673
+ return `${replayHref ?? "/api/voice-sessions"}/${encodeURIComponent(event.sessionId)}/replay/htmx`;
7674
+ };
7675
+ var summarizeVoiceHandoffHealth = async (options = {}) => {
7676
+ const sourceEvents = options.events ?? await options.store?.list() ?? [];
7677
+ const search = options.q?.trim().toLowerCase();
7678
+ const byAction = {};
7679
+ const byAdapter = {};
7680
+ const byStatus = {
7681
+ delivered: 0,
7682
+ failed: 0,
7683
+ skipped: 0
7684
+ };
7685
+ const events = sourceEvents.filter((event) => event.type === "call.handoff").map((event) => {
7686
+ const status = isStatus(event.payload.status) ? event.payload.status : "failed";
7687
+ const deliveries = normalizeDeliveries(event.payload);
7688
+ const item = {
7689
+ action: getString4(event.payload.action),
7690
+ at: event.at,
7691
+ deliveries,
7692
+ reason: getString4(event.payload.reason),
7693
+ sessionId: event.sessionId,
7694
+ status,
7695
+ target: getString4(event.payload.target)
7696
+ };
7697
+ return {
7698
+ ...item,
7699
+ replayHref: resolveReplayHref(item, options.replayHref)
7700
+ };
7701
+ }).filter((event) => {
7702
+ if (options.status && options.status !== "all" && event.status !== options.status) {
7703
+ return false;
7704
+ }
7705
+ if (!search) {
7706
+ return true;
7707
+ }
7708
+ return [
7709
+ event.action,
7710
+ event.reason,
7711
+ event.sessionId,
7712
+ event.status,
7713
+ event.target,
7714
+ ...event.deliveries.flatMap((delivery) => [
7715
+ delivery.adapterId,
7716
+ delivery.adapterKind,
7717
+ delivery.deliveredTo,
7718
+ delivery.error,
7719
+ delivery.status
7720
+ ])
7721
+ ].some((value) => value?.toLowerCase().includes(search));
7722
+ }).sort((left, right) => right.at - left.at).slice(0, options.limit ?? 50);
7723
+ for (const event of events) {
7724
+ byStatus[event.status] += 1;
7725
+ if (event.action) {
7726
+ increment2(byAction, event.action);
7727
+ }
7728
+ for (const delivery of event.deliveries) {
7729
+ byAdapter[delivery.adapterId] ??= {
7730
+ delivered: 0,
7731
+ failed: 0,
7732
+ skipped: 0
7733
+ };
7734
+ byAdapter[delivery.adapterId][delivery.status] += 1;
7735
+ }
7736
+ }
7737
+ return {
7738
+ byAction,
7739
+ byAdapter,
7740
+ byStatus,
7741
+ events,
7742
+ failed: byStatus.failed,
7743
+ total: events.length
7744
+ };
7745
+ };
7746
+ var renderMetricGrid = (summary) => [
7747
+ '<section class="voice-handoff-health-grid">',
7748
+ `<article><span>Total</span><strong>${String(summary.total)}</strong></article>`,
7749
+ `<article><span>Delivered</span><strong>${String(summary.byStatus.delivered)}</strong></article>`,
7750
+ `<article><span>Failed</span><strong>${String(summary.byStatus.failed)}</strong></article>`,
7751
+ `<article><span>Skipped</span><strong>${String(summary.byStatus.skipped)}</strong></article>`,
7752
+ "</section>"
7753
+ ].join("");
7754
+ var renderActionSummary = (summary) => {
7755
+ const actions = Object.entries(summary.byAction).sort((left, right) => right[1] - left[1]);
7756
+ const adapters = Object.entries(summary.byAdapter).sort(([left], [right]) => left.localeCompare(right));
7757
+ return [
7758
+ '<section class="voice-handoff-health-columns">',
7759
+ "<article><h3>Actions</h3>",
7760
+ actions.length === 0 ? "<p>No handoff actions yet.</p>" : `<ul>${actions.map(([action, count]) => `<li>${escapeHtml7(action)}: ${String(count)}</li>`).join("")}</ul>`,
7761
+ "</article>",
7762
+ "<article><h3>Adapters</h3>",
7763
+ adapters.length === 0 ? "<p>No adapter deliveries yet.</p>" : `<ul>${adapters.map(([adapterId, counts]) => `<li>${escapeHtml7(adapterId)}: ${String(counts.delivered)} delivered / ${String(counts.failed)} failed / ${String(counts.skipped)} skipped</li>`).join("")}</ul>`,
7764
+ "</article>",
7765
+ "</section>"
7766
+ ].join("");
7767
+ };
7768
+ var renderVoiceHandoffHealthHTML = (summary) => [
7769
+ '<div class="voice-handoff-health">',
7770
+ renderMetricGrid(summary),
7771
+ renderActionSummary(summary),
7772
+ "<section>",
7773
+ "<h3>Recent Handoffs</h3>",
7774
+ summary.events.length === 0 ? '<p class="voice-handoff-health-empty">No handoffs found.</p>' : [
7775
+ '<div class="voice-handoff-health-events">',
7776
+ ...summary.events.map((event) => [
7777
+ `<article class="${escapeHtml7(event.status)}">`,
7778
+ '<div class="voice-handoff-health-event-header">',
7779
+ `<strong>${escapeHtml7(event.action ?? "handoff")}</strong>`,
7780
+ `<span>${escapeHtml7(event.status)}</span>`,
7781
+ "</div>",
7782
+ `<p><small>${escapeHtml7(event.sessionId)}</small></p>`,
7783
+ event.target ? `<p>Target: ${escapeHtml7(event.target)}</p>` : "",
7784
+ event.reason ? `<p>Reason: ${escapeHtml7(event.reason)}</p>` : "",
7785
+ event.deliveries.length ? `<ul>${event.deliveries.map((delivery) => [
7786
+ "<li>",
7787
+ `${escapeHtml7(delivery.adapterId)}: ${escapeHtml7(delivery.status)}`,
7788
+ delivery.deliveredTo ? ` to ${escapeHtml7(delivery.deliveredTo)}` : "",
7789
+ delivery.error ? ` (${escapeHtml7(delivery.error)})` : "",
7790
+ "</li>"
7791
+ ].join("")).join("")}</ul>` : "",
7792
+ event.replayHref ? `<p><a href="${escapeHtml7(event.replayHref)}">Open replay</a></p>` : "",
7793
+ "</article>"
7794
+ ].join("")),
7795
+ "</div>"
7796
+ ].join(""),
7797
+ "</section>",
7798
+ "</div>"
7799
+ ].join("");
7800
+ var createVoiceHandoffHealthJSONHandler = (options = {}) => async ({ query }) => summarizeVoiceHandoffHealth({
7801
+ ...options,
7802
+ limit: typeof query?.limit === "string" ? Number(query.limit) : options.limit,
7803
+ q: query?.q ?? options.q,
7804
+ status: query?.status === "delivered" || query?.status === "failed" || query?.status === "skipped" || query?.status === "all" ? query.status : options.status
7805
+ });
7806
+ var createVoiceHandoffHealthHTMLHandler = (options = {}) => async ({ query }) => {
7807
+ const summary = await summarizeVoiceHandoffHealth({
7808
+ ...options,
7809
+ limit: typeof query?.limit === "string" ? Number(query.limit) : options.limit,
7810
+ q: query?.q ?? options.q,
7811
+ status: query?.status === "delivered" || query?.status === "failed" || query?.status === "skipped" || query?.status === "all" ? query.status : options.status
7812
+ });
7813
+ const body = await (options.render?.(summary) ?? renderVoiceHandoffHealthHTML(summary));
7814
+ return new Response(body, {
7815
+ headers: {
7816
+ "Content-Type": "text/html; charset=utf-8",
7817
+ ...options.headers
7818
+ }
7819
+ });
7820
+ };
7821
+ var createVoiceHandoffHealthRoutes = (options = {}) => {
7822
+ const path = options.path ?? "/api/voice-handoffs";
7823
+ const htmlPath = options.htmlPath === undefined ? `${path}/htmx` : options.htmlPath;
7824
+ const routes = new Elysia5({
7825
+ name: options.name ?? "absolutejs-voice-handoff-health"
7826
+ }).get(path, createVoiceHandoffHealthJSONHandler(options));
7827
+ if (htmlPath) {
7828
+ routes.get(htmlPath, createVoiceHandoffHealthHTMLHandler(options));
7829
+ }
7830
+ return routes;
7831
+ };
7832
+
7833
+ // src/qualityRoutes.ts
7834
+ var DEFAULT_THRESHOLDS = {
7835
+ maxDuplicateTurnRate: 0,
7836
+ maxEmptyTurnRate: 0.02,
7837
+ maxHandoffFailureRate: 0,
7838
+ maxMissingAssistantReplyRate: 0.05,
7839
+ maxProviderAverageLatencyMs: 3000,
7840
+ maxProviderErrorRate: 0.05,
7841
+ maxProviderFallbackRate: 0.25,
7842
+ maxProviderTimeoutRate: 0.03
7843
+ };
7844
+ var getString5 = (value) => typeof value === "string" ? value : undefined;
7845
+ var getNumber3 = (value) => typeof value === "number" && Number.isFinite(value) ? value : undefined;
7846
+ var rate = (count, total) => count / Math.max(1, total);
7847
+ var roundMetric2 = (value) => Math.round(value * 1e4) / 1e4;
7848
+ var createMetric = (input) => ({
7849
+ ...input,
7850
+ actual: roundMetric2(input.actual),
7851
+ pass: input.actual <= input.threshold
7852
+ });
7853
+ var evaluateVoiceQuality = async (input) => {
7854
+ const events = filterVoiceTraceEvents(input.events ?? await input.store?.list() ?? []);
7855
+ const thresholds = {
7856
+ ...DEFAULT_THRESHOLDS,
7857
+ ...input.thresholds
7858
+ };
7859
+ const committedTurns = events.filter((event) => event.type === "turn.committed");
7860
+ const assistantReplies = events.filter((event) => event.type === "turn.assistant");
7861
+ const sessionIdsWithAssistantReply = new Set(assistantReplies.map((event) => event.sessionId));
7862
+ const sessionsWithTurns = new Set(committedTurns.map((event) => event.sessionId));
7863
+ const emptyTurns = committedTurns.filter((event) => !getString5(event.payload.text)?.trim());
7864
+ const turnTextsBySession = new Map;
7865
+ let duplicateTurns = 0;
7866
+ for (const turn of committedTurns) {
7867
+ const normalized = getString5(turn.payload.text)?.trim().toLowerCase();
7868
+ if (!normalized) {
7869
+ continue;
7870
+ }
7871
+ const seen = turnTextsBySession.get(turn.sessionId) ?? new Set;
7872
+ if (seen.has(normalized)) {
7873
+ duplicateTurns += 1;
7874
+ }
7875
+ seen.add(normalized);
7876
+ turnTextsBySession.set(turn.sessionId, seen);
7877
+ }
7878
+ const missingAssistantReplySessions = [...sessionsWithTurns].filter((sessionId) => !sessionIdsWithAssistantReply.has(sessionId)).length;
7879
+ const providerEvents = events.filter((event) => event.type === "session.error" && typeof event.payload.provider === "string" && typeof event.payload.providerStatus === "string");
7880
+ const providerErrors = providerEvents.filter((event) => event.payload.providerStatus === "error");
7881
+ const providerFallbacks = providerEvents.filter((event) => event.payload.providerStatus === "fallback");
7882
+ const providerTimeouts = providerEvents.filter((event) => event.payload.timedOut === true);
7883
+ const providerLatencies = providerEvents.map((event) => getNumber3(event.payload.elapsedMs)).filter((value) => value !== undefined);
7884
+ const averageProviderLatencyMs = providerLatencies.length > 0 ? providerLatencies.reduce((sum, value) => sum + value, 0) / providerLatencies.length : 0;
7885
+ const handoffHealth = await summarizeVoiceHandoffHealth({ events });
7886
+ const metrics = {
7887
+ duplicateTurnRate: createMetric({
7888
+ actual: rate(duplicateTurns, committedTurns.length),
7889
+ label: "Duplicate turn rate",
7890
+ threshold: thresholds.maxDuplicateTurnRate,
7891
+ unit: "rate"
7892
+ }),
7893
+ emptyTurnRate: createMetric({
7894
+ actual: rate(emptyTurns.length, committedTurns.length),
7895
+ label: "Empty turn rate",
7896
+ threshold: thresholds.maxEmptyTurnRate,
7897
+ unit: "rate"
7898
+ }),
7899
+ handoffFailureRate: createMetric({
7900
+ actual: rate(handoffHealth.failed, handoffHealth.total),
7901
+ label: "Handoff failure rate",
7902
+ threshold: thresholds.maxHandoffFailureRate,
7903
+ unit: "rate"
7904
+ }),
7905
+ missingAssistantReplyRate: createMetric({
7906
+ actual: rate(missingAssistantReplySessions, sessionsWithTurns.size),
7907
+ label: "Missing assistant reply rate",
7908
+ threshold: thresholds.maxMissingAssistantReplyRate,
7909
+ unit: "rate"
7910
+ }),
7911
+ providerAverageLatencyMs: createMetric({
7912
+ actual: averageProviderLatencyMs,
7913
+ label: "Average provider latency",
7914
+ threshold: thresholds.maxProviderAverageLatencyMs,
7915
+ unit: "ms"
7916
+ }),
7917
+ providerErrorRate: createMetric({
7918
+ actual: rate(providerErrors.length, providerEvents.length),
7919
+ label: "Provider error rate",
7920
+ threshold: thresholds.maxProviderErrorRate,
7921
+ unit: "rate"
7922
+ }),
7923
+ providerFallbackRate: createMetric({
7924
+ actual: rate(providerFallbacks.length, providerEvents.length),
7925
+ label: "Provider fallback rate",
7926
+ threshold: thresholds.maxProviderFallbackRate,
7927
+ unit: "rate"
7928
+ }),
7929
+ providerTimeoutRate: createMetric({
7930
+ actual: rate(providerTimeouts.length, providerEvents.length),
7931
+ label: "Provider timeout rate",
7932
+ threshold: thresholds.maxProviderTimeoutRate,
7933
+ unit: "rate"
7934
+ })
7935
+ };
7936
+ const status = Object.values(metrics).every((metric) => metric.pass) ? "pass" : "fail";
7937
+ return {
7938
+ checkedAt: Date.now(),
7939
+ eventCount: events.length,
7940
+ metrics,
7941
+ status,
7942
+ thresholds
7943
+ };
7944
+ };
7945
+ var escapeHtml8 = (value) => value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
7946
+ var formatMetricValue = (metric) => metric.unit === "rate" ? `${(metric.actual * 100).toFixed(2)}%` : metric.unit === "ms" ? `${Math.round(metric.actual)}ms` : String(metric.actual);
7947
+ var formatThreshold = (metric) => metric.unit === "rate" ? `${(metric.threshold * 100).toFixed(2)}%` : metric.unit === "ms" ? `${Math.round(metric.threshold)}ms` : String(metric.threshold);
7948
+ var renderVoiceQualityHTML = (report, options = {}) => {
7949
+ const rows = Object.entries(report.metrics).map(([key, metric]) => `<tr class="${metric.pass ? "pass" : "fail"}"><td>${escapeHtml8(metric.label)}</td><td>${escapeHtml8(formatMetricValue(metric))}</td><td>${escapeHtml8(formatThreshold(metric))}</td><td>${metric.pass ? "pass" : "fail"}</td><td><code>${escapeHtml8(key)}</code></td></tr>`).join("");
7950
+ const links = options.links?.length ? `<nav>${options.links.map((link) => `<a href="${escapeHtml8(link.href)}">${escapeHtml8(link.label)}</a>`).join("")}</nav>` : "";
7951
+ return `<!doctype html><html lang="en"><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /><title>AbsoluteJS Voice Quality</title><style>body{font-family:ui-sans-serif,system-ui,sans-serif;margin:2rem;background:#f8f7f2;color:#181713}main{max-width:1100px;margin:auto}nav{display:flex;flex-wrap:wrap;gap:.5rem;margin:0 0 1.25rem}nav a{background:#181713;border-radius:999px;color:white;padding:.35rem .7rem;text-decoration:none}.status{border-radius:999px;display:inline-flex;padding:.35rem .75rem;font-weight:800}.status.pass{background:#dcfce7;color:#166534}.status.fail{background:#fee2e2;color:#991b1b}table{border-collapse:collapse;width:100%;background:white;margin-top:1rem}td,th{border-bottom:1px solid #eee;padding:.75rem;text-align:left}.pass td{border-left:4px solid #16a34a}.fail td{border-left:4px solid #dc2626}code{background:#f3f4f6;padding:.15rem .3rem;border-radius:.3rem}</style></head><body><main>${links}<h1>Voice quality gates</h1><p class="status ${report.status}">${report.status}</p><p>${report.eventCount} event(s) checked.</p><table><thead><tr><th>Metric</th><th>Actual</th><th>Threshold</th><th>Status</th><th>Key</th></tr></thead><tbody>${rows}</tbody></table></main></body></html>`;
7952
+ };
7953
+ var createVoiceQualityRoutes = (options) => {
7954
+ const path = options.path ?? "/quality";
7955
+ const routes = new Elysia6({
7956
+ name: options.name ?? "absolutejs-voice-quality"
7957
+ });
7958
+ const getReport = () => evaluateVoiceQuality({
7959
+ events: options.events,
7960
+ store: options.store,
7961
+ thresholds: options.thresholds
7962
+ });
7963
+ routes.get(path, async () => {
7964
+ const report = await getReport();
7965
+ return new Response(renderVoiceQualityHTML(report, { links: options.links }), {
7966
+ headers: {
7967
+ "Content-Type": "text/html; charset=utf-8",
7968
+ ...options.headers
7969
+ }
7970
+ });
7971
+ });
7972
+ routes.get(`${path}/json`, async () => getReport());
7973
+ routes.get(`${path}/status`, async ({ set }) => {
7974
+ const report = await getReport();
7975
+ if (report.status === "fail") {
7976
+ set.status = 503;
7977
+ }
7978
+ return report;
7979
+ });
7980
+ return routes;
7981
+ };
7982
+
7983
+ // src/evalRoutes.ts
7984
+ var escapeHtml9 = (value) => value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
7985
+ var rate2 = (count, total) => count / Math.max(1, total);
7986
+ var normalizeSearchText = (value) => value.trim().toLowerCase();
7987
+ var getString6 = (value) => typeof value === "string" ? value : undefined;
7988
+ var getObject = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : undefined;
7989
+ var getPathValue = (value, path) => {
7990
+ let current = value;
7991
+ for (const part of path.split(".").filter(Boolean)) {
7992
+ const record = getObject(current);
7993
+ if (!record || !(part in record)) {
7994
+ return;
7995
+ }
7996
+ current = record[part];
7997
+ }
7998
+ return current;
7999
+ };
8000
+ var includesAll = (haystack, needles) => {
8001
+ const normalized = normalizeSearchText(haystack);
8002
+ return needles.filter((needle) => !normalized.includes(normalizeSearchText(needle)));
8003
+ };
8004
+ var sessionTime = (events) => {
8005
+ const sorted = filterVoiceTraceEvents(events);
8006
+ return {
8007
+ endedAt: sorted.at(-1)?.at,
8008
+ startedAt: sorted[0]?.at
8009
+ };
8010
+ };
8011
+ var bucketKey = (timestamp) => new Date(timestamp).toISOString().slice(0, 10);
8012
+ var buildTrend = (sessions) => {
8013
+ const buckets = new Map;
8014
+ for (const session of sessions) {
8015
+ const endedAt = session.endedAt ?? session.startedAt ?? session.quality.checkedAt;
8016
+ const key = bucketKey(endedAt);
8017
+ const bucket = buckets.get(key) ?? {
8018
+ endedAt,
8019
+ failed: 0,
8020
+ key,
8021
+ passed: 0,
8022
+ total: 0
8023
+ };
8024
+ bucket.endedAt = Math.max(bucket.endedAt, endedAt);
8025
+ bucket.total += 1;
8026
+ if (session.status === "pass") {
8027
+ bucket.passed += 1;
8028
+ } else {
8029
+ bucket.failed += 1;
8030
+ }
8031
+ buckets.set(key, bucket);
8032
+ }
8033
+ return [...buckets.values()].sort((left, right) => right.endedAt - left.endedAt);
8034
+ };
8035
+ var runVoiceSessionEvals = async (options = {}) => {
8036
+ const events = filterVoiceTraceEvents(options.events ?? await options.store?.list() ?? []);
8037
+ const grouped = new Map;
8038
+ for (const event of events) {
8039
+ grouped.set(event.sessionId, [...grouped.get(event.sessionId) ?? [], event]);
8040
+ }
8041
+ const sessions = await Promise.all([...grouped.entries()].map(async ([sessionId, sessionEvents]) => {
8042
+ const sorted = filterVoiceTraceEvents(sessionEvents);
8043
+ const quality = await evaluateVoiceQuality({
8044
+ events: sorted,
8045
+ thresholds: options.thresholds
8046
+ });
8047
+ const { endedAt, startedAt } = sessionTime(sorted);
8048
+ const summary = summarizeVoiceTrace(sorted);
8049
+ const scenarioId = sorted.find((event) => event.scenarioId)?.scenarioId;
8050
+ return {
8051
+ endedAt,
8052
+ eventCount: sorted.length,
8053
+ quality,
8054
+ scenarioId,
8055
+ sessionId,
8056
+ startedAt,
8057
+ status: quality.status,
8058
+ summary
8059
+ };
8060
+ }));
8061
+ const limitedSessions = sessions.sort((left, right) => (right.endedAt ?? right.startedAt ?? 0) - (left.endedAt ?? left.startedAt ?? 0)).slice(0, options.limit ?? 100);
8062
+ const failed = limitedSessions.filter((session) => session.status === "fail").length;
8063
+ const passed = limitedSessions.length - failed;
8064
+ return {
8065
+ checkedAt: Date.now(),
8066
+ failed,
8067
+ passed,
8068
+ sessions: limitedSessions,
8069
+ status: failed > 0 ? "fail" : "pass",
8070
+ total: limitedSessions.length,
8071
+ trend: buildTrend(limitedSessions)
8072
+ };
8073
+ };
8074
+ var getSessionText = (events, type) => events.filter((event) => event.type === type).map((event) => getString6(event.payload.text)).filter((text) => Boolean(text?.trim())).join(`
8075
+ `);
8076
+ var countProviderErrors = (events) => events.filter((event) => event.type === "session.error" && (event.payload.providerStatus === "error" || typeof event.payload.provider === "string")).length;
8077
+ var evaluateScenarioSession = (scenario, sessionId, events) => {
8078
+ const issues = [];
8079
+ const committedText = getSessionText(events, "turn.committed");
8080
+ const assistantText = getSessionText(events, "turn.assistant");
8081
+ const lifecycleTypes = events.filter((event) => event.type === "call.lifecycle").map((event) => getString6(event.payload.type)).filter((type) => Boolean(type));
8082
+ const dispositions = events.filter((event) => event.type === "call.lifecycle").map((event) => getString6(event.payload.disposition)).filter((disposition) => Boolean(disposition));
8083
+ const handoffActions = events.filter((event) => event.type === "call.handoff").map((event) => getString6(event.payload.action)).filter((action) => Boolean(action));
8084
+ const turnCount = events.filter((event) => event.type === "turn.committed").length;
8085
+ const sessionErrorCount = events.filter((event) => event.type === "session.error").length;
8086
+ const providerErrorCount = countProviderErrors(events);
8087
+ const workflowContractEvents = events.filter((event) => event.type === "workflow.contract");
8088
+ for (const missing of includesAll(committedText, scenario.requiredTranscriptIncludes ?? [])) {
8089
+ issues.push(`Missing transcript text: ${missing}`);
8090
+ }
8091
+ for (const missing of includesAll(assistantText, scenario.requiredAssistantIncludes ?? [])) {
8092
+ issues.push(`Missing assistant text: ${missing}`);
8093
+ }
8094
+ for (const type of scenario.requiredLifecycleTypes ?? []) {
8095
+ if (!lifecycleTypes.includes(type)) {
8096
+ issues.push(`Missing lifecycle event: ${type}`);
8097
+ }
8098
+ }
8099
+ for (const type of scenario.forbiddenLifecycleTypes ?? []) {
8100
+ if (lifecycleTypes.includes(type)) {
8101
+ issues.push(`Forbidden lifecycle event occurred: ${type}`);
8102
+ }
8103
+ }
8104
+ for (const action of scenario.requiredHandoffActions ?? []) {
8105
+ if (!handoffActions.includes(action)) {
8106
+ issues.push(`Missing handoff action: ${action}`);
8107
+ }
8108
+ }
8109
+ for (const action of scenario.forbiddenHandoffActions ?? []) {
8110
+ if (handoffActions.includes(action)) {
8111
+ issues.push(`Forbidden handoff action occurred: ${action}`);
8112
+ }
8113
+ }
8114
+ if (scenario.requiredDisposition && !dispositions.includes(scenario.requiredDisposition)) {
8115
+ issues.push(`Missing disposition: ${scenario.requiredDisposition}`);
8116
+ }
8117
+ if (scenario.minTurns !== undefined && turnCount < scenario.minTurns) {
8118
+ issues.push(`Expected at least ${scenario.minTurns} turn(s), saw ${turnCount}.`);
8119
+ }
8120
+ if (scenario.maxSessionErrors !== undefined && sessionErrorCount > scenario.maxSessionErrors) {
8121
+ issues.push(`Expected at most ${scenario.maxSessionErrors} session error(s), saw ${sessionErrorCount}.`);
8122
+ }
8123
+ if (scenario.maxProviderErrors !== undefined && providerErrorCount > scenario.maxProviderErrors) {
8124
+ issues.push(`Expected at most ${scenario.maxProviderErrors} provider error(s), saw ${providerErrorCount}.`);
8125
+ }
8126
+ for (const path of scenario.requiredPayloadPaths ?? []) {
8127
+ if (events.every((event) => getPathValue(event.payload, path) === undefined)) {
8128
+ issues.push(`Missing payload path: ${path}`);
8129
+ }
8130
+ }
8131
+ for (const contractId of scenario.requiredWorkflowContracts ?? []) {
8132
+ const matching = workflowContractEvents.filter((event) => getString6(event.payload.contractId) === contractId);
8133
+ if (matching.length === 0) {
8134
+ issues.push(`Missing workflow contract: ${contractId}`);
8135
+ continue;
8136
+ }
8137
+ if (matching.some((event) => getString6(event.payload.status) !== "pass")) {
8138
+ issues.push(`Workflow contract failed: ${contractId}`);
8139
+ }
8140
+ }
8141
+ return {
8142
+ eventCount: events.length,
8143
+ issues,
8144
+ sessionId,
8145
+ status: issues.length > 0 ? "fail" : "pass"
8146
+ };
8147
+ };
8148
+ var runVoiceScenarioEvals = async (options = {}) => {
8149
+ const scenarios = options.scenarios ?? [];
8150
+ const events = filterVoiceTraceEvents(options.events ?? await options.store?.list() ?? []);
8151
+ const grouped = new Map;
8152
+ for (const event of events) {
8153
+ grouped.set(event.sessionId, [...grouped.get(event.sessionId) ?? [], event]);
8154
+ }
8155
+ const results = scenarios.map((scenario) => {
8156
+ const sessions = [...grouped.entries()].filter(([, sessionEvents]) => scenario.scenarioId ? sessionEvents.some((event) => event.scenarioId === scenario.scenarioId) : true).map(([sessionId, sessionEvents]) => evaluateScenarioSession(scenario, sessionId, filterVoiceTraceEvents(sessionEvents))).sort((left, right) => left.sessionId.localeCompare(right.sessionId));
8157
+ const issues = [];
8158
+ const minSessions = scenario.minSessions ?? 1;
8159
+ if (sessions.length < minSessions) {
8160
+ issues.push(`Expected at least ${minSessions} matching session(s), saw ${sessions.length}.`);
8161
+ }
8162
+ const failed2 = sessions.filter((session) => session.status === "fail").length;
8163
+ const passed2 = sessions.length - failed2;
8164
+ return {
8165
+ description: scenario.description,
8166
+ failed: failed2,
8167
+ id: scenario.id,
8168
+ issues,
8169
+ label: scenario.label ?? scenario.id,
8170
+ matchedSessions: sessions.length,
8171
+ passed: passed2,
8172
+ sessions,
8173
+ status: issues.length > 0 || failed2 > 0 ? "fail" : "pass"
8174
+ };
8175
+ });
8176
+ const failed = results.filter((scenario) => scenario.status === "fail").length;
8177
+ const passed = results.length - failed;
8178
+ return {
8179
+ checkedAt: Date.now(),
8180
+ failed,
8181
+ passed,
8182
+ scenarios: results,
8183
+ status: failed > 0 ? "fail" : "pass",
8184
+ total: results.length
8185
+ };
8186
+ };
8187
+ var resolveScenarioFixtures = async (options) => [...options.fixtures ?? [], ...await options.fixtureStore?.list() ?? []];
8188
+ var runVoiceScenarioFixtureEvals = async (options = {}) => {
8189
+ const fixtures = await resolveScenarioFixtures(options);
8190
+ const results = await Promise.all(fixtures.map(async (fixture) => {
8191
+ const report = await runVoiceScenarioEvals({
8192
+ events: fixture.events,
8193
+ scenarios: options.scenarios
8194
+ });
8195
+ return {
8196
+ description: fixture.description,
8197
+ fixtureId: fixture.id,
8198
+ label: fixture.label ?? fixture.id,
8199
+ report,
8200
+ status: report.status
8201
+ };
8202
+ }));
8203
+ const failed = results.filter((fixture) => fixture.status === "fail").length;
8204
+ const passed = results.length - failed;
8205
+ return {
8206
+ checkedAt: Date.now(),
8207
+ failed,
8208
+ fixtures: results,
8209
+ passed,
8210
+ status: failed > 0 ? "fail" : "pass",
8211
+ total: results.length
8212
+ };
8213
+ };
8214
+ var summarizeEvalBaseline = (report) => {
8215
+ const failedSessionIds = report.sessions.filter((session) => session.status === "fail").map((session) => session.sessionId).sort();
8216
+ return {
8217
+ failed: report.failed,
8218
+ failedSessionIds,
8219
+ passRate: rate2(report.passed, report.total),
8220
+ passed: report.passed,
8221
+ total: report.total
8222
+ };
8223
+ };
8224
+ var compareVoiceEvalBaseline = (currentReport, baselineReport, options = {}) => {
8225
+ const baseline = summarizeEvalBaseline(baselineReport);
8226
+ const current = summarizeEvalBaseline(currentReport);
8227
+ const maxFailedDelta = options.maxFailedDelta ?? 0;
8228
+ const maxPassRateDrop = options.maxPassRateDrop ?? 0;
8229
+ const failOnNewFailedSessions = options.failOnNewFailedSessions ?? true;
8230
+ const baselineFailed = new Set(baseline.failedSessionIds);
8231
+ const currentFailed = new Set(current.failedSessionIds);
8232
+ const newFailedSessionIds = current.failedSessionIds.filter((sessionId) => !baselineFailed.has(sessionId));
8233
+ const recoveredSessionIds = baseline.failedSessionIds.filter((sessionId) => !currentFailed.has(sessionId));
8234
+ const deltas = {
8235
+ failed: current.failed - baseline.failed,
8236
+ passRate: current.passRate - baseline.passRate,
8237
+ passed: current.passed - baseline.passed,
8238
+ total: current.total - baseline.total
8239
+ };
8240
+ const reasons = [];
8241
+ if (deltas.failed > maxFailedDelta) {
8242
+ reasons.push(`Failed sessions increased by ${deltas.failed}, above allowed delta ${maxFailedDelta}.`);
8243
+ }
8244
+ if (deltas.passRate < -maxPassRateDrop) {
8245
+ reasons.push(`Pass rate dropped by ${Math.abs(deltas.passRate).toFixed(4)}, above allowed drop ${maxPassRateDrop}.`);
8246
+ }
8247
+ if (failOnNewFailedSessions && newFailedSessionIds.length > 0) {
8248
+ reasons.push(`${newFailedSessionIds.length} session(s) failed that were not failing in the baseline.`);
8249
+ }
8250
+ return {
8251
+ baseline,
8252
+ checkedAt: Date.now(),
8253
+ current,
8254
+ deltas,
8255
+ newFailedSessionIds,
8256
+ recoveredSessionIds,
8257
+ reasons,
8258
+ status: reasons.length > 0 ? "fail" : "pass"
8259
+ };
8260
+ };
8261
+ var createVoiceFileEvalBaselineStore = (filePath) => ({
8262
+ get: async () => {
8263
+ const file = Bun.file(filePath);
8264
+ if (!await file.exists()) {
8265
+ return;
8266
+ }
8267
+ const text = await file.text();
8268
+ return text.trim() ? JSON.parse(text) : undefined;
8269
+ },
8270
+ set: async (report) => {
8271
+ await mkdir(dirname(filePath), { recursive: true });
8272
+ await Bun.write(filePath, JSON.stringify(report, null, 2));
8273
+ }
8274
+ });
8275
+ var createVoiceFileScenarioFixtureStore = (filePath) => ({
8276
+ list: async () => {
8277
+ const file = Bun.file(filePath);
8278
+ if (!await file.exists()) {
8279
+ return [];
8280
+ }
8281
+ const text = await file.text();
8282
+ if (!text.trim()) {
8283
+ return [];
8284
+ }
8285
+ const parsed = JSON.parse(text);
8286
+ return Array.isArray(parsed) ? parsed : parsed.fixtures ?? [];
8287
+ }
8288
+ });
8289
+ var formatTime = (value) => value === undefined ? "unknown" : new Date(value).toLocaleString();
8290
+ var formatPercent = (value) => `${(value * 100).toFixed(2)}%`;
8291
+ var renderVoiceEvalHTML = (report, options = {}) => {
8292
+ const title = options.title ?? "AbsoluteJS Voice Evals";
8293
+ const links = options.links?.length ? `<nav>${options.links.map((link) => `<a href="${escapeHtml9(link.href)}">${escapeHtml9(link.label)}</a>`).join("")}</nav>` : "";
8294
+ const trend = report.trend.length ? report.trend.map((bucket) => `<tr><td>${escapeHtml9(bucket.key)}</td><td>${bucket.total}</td><td>${bucket.passed}</td><td>${bucket.failed}</td></tr>`).join("") : '<tr><td colspan="4">No eval buckets yet.</td></tr>';
8295
+ const sessions = report.sessions.length ? report.sessions.map((session) => {
8296
+ const failedMetrics = Object.entries(session.quality.metrics).filter(([, metric]) => !metric.pass).map(([, metric]) => metric.label).join(", ");
8297
+ return `<tr class="${session.status}"><td>${escapeHtml9(session.sessionId)}</td><td>${escapeHtml9(session.status)}</td><td>${session.eventCount}</td><td>${session.summary.turnCount}</td><td>${session.summary.errorCount}</td><td>${escapeHtml9(formatTime(session.endedAt))}</td><td>${escapeHtml9(failedMetrics || "none")}</td></tr>`;
8298
+ }).join("") : '<tr><td colspan="7">No sessions found.</td></tr>';
8299
+ return `<!doctype html><html lang="en"><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /><title>${escapeHtml9(title)}</title><style>body{font-family:ui-sans-serif,system-ui,sans-serif;margin:2rem;background:#f8f7f2;color:#181713}main{max-width:1180px;margin:auto}nav{display:flex;gap:.5rem;flex-wrap:wrap;margin-bottom:1rem}nav a{background:#181713;border-radius:999px;color:white;padding:.35rem .7rem;text-decoration:none}.status{border-radius:999px;display:inline-flex;font-weight:800;padding:.35rem .75rem}.pass{color:#166534}.fail{color:#991b1b}.status.pass{background:#dcfce7}.status.fail{background:#fee2e2}.grid{display:grid;gap:1rem;grid-template-columns:repeat(auto-fit,minmax(160px,1fr));margin:1rem 0}.card{background:white;border:1px solid #e7e5e4;border-radius:1rem;padding:1rem}.card strong{display:block;font-size:2rem}table{border-collapse:collapse;background:white;width:100%;margin:1rem 0 2rem}td,th{border-bottom:1px solid #eee;padding:.75rem;text-align:left}tr.fail td{border-left:4px solid #dc2626}tr.pass td{border-left:4px solid #16a34a}</style></head><body><main>${links}<h1>${escapeHtml9(title)}</h1><p class="status ${report.status}">${report.status}</p><div class="grid"><article class="card"><span>Total</span><strong>${report.total}</strong></article><article class="card"><span>Passed</span><strong>${report.passed}</strong></article><article class="card"><span>Failed</span><strong>${report.failed}</strong></article></div><h2>Trend</h2><table><thead><tr><th>Day</th><th>Total</th><th>Passed</th><th>Failed</th></tr></thead><tbody>${trend}</tbody></table><h2>Session Eval Results</h2><table><thead><tr><th>Session</th><th>Status</th><th>Events</th><th>Turns</th><th>Errors</th><th>Last event</th><th>Failed metrics</th></tr></thead><tbody>${sessions}</tbody></table></main></body></html>`;
8300
+ };
8301
+ var renderVoiceEvalBaselineHTML = (comparison, options = {}) => {
8302
+ const title = options.title ?? "AbsoluteJS Voice Eval Baseline";
8303
+ const links = options.links?.length ? `<nav>${options.links.map((link) => `<a href="${escapeHtml9(link.href)}">${escapeHtml9(link.label)}</a>`).join("")}</nav>` : "";
8304
+ const reasons = comparison.reasons.length ? comparison.reasons.map((reason) => `<li>${escapeHtml9(reason)}</li>`).join("") : "<li>No baseline regressions detected.</li>";
8305
+ const newFailures = comparison.newFailedSessionIds.length ? comparison.newFailedSessionIds.map((id) => `<li>${escapeHtml9(id)}</li>`).join("") : "<li>none</li>";
8306
+ const recovered = comparison.recoveredSessionIds.length ? comparison.recoveredSessionIds.map((id) => `<li>${escapeHtml9(id)}</li>`).join("") : "<li>none</li>";
8307
+ return `<!doctype html><html lang="en"><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /><title>${escapeHtml9(title)}</title><style>body{font-family:ui-sans-serif,system-ui,sans-serif;margin:2rem;background:#f8f7f2;color:#181713}main{max-width:1000px;margin:auto}nav{display:flex;gap:.5rem;flex-wrap:wrap;margin-bottom:1rem}nav a{background:#181713;border-radius:999px;color:white;padding:.35rem .7rem;text-decoration:none}.status{border-radius:999px;display:inline-flex;font-weight:800;padding:.35rem .75rem}.pass{background:#dcfce7;color:#166534}.fail{background:#fee2e2;color:#991b1b}.grid{display:grid;gap:1rem;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));margin:1rem 0}.card{background:white;border:1px solid #e7e5e4;border-radius:1rem;padding:1rem}.card strong{display:block;font-size:2rem}section{background:white;border:1px solid #e7e5e4;border-radius:1rem;margin:1rem 0;padding:1rem}</style></head><body><main>${links}<h1>${escapeHtml9(title)}</h1><p class="status ${comparison.status}">${comparison.status}</p><div class="grid"><article class="card"><span>Baseline pass rate</span><strong>${escapeHtml9(formatPercent(comparison.baseline.passRate))}</strong></article><article class="card"><span>Current pass rate</span><strong>${escapeHtml9(formatPercent(comparison.current.passRate))}</strong></article><article class="card"><span>Failed delta</span><strong>${comparison.deltas.failed}</strong></article><article class="card"><span>Pass rate delta</span><strong>${escapeHtml9(formatPercent(comparison.deltas.passRate))}</strong></article></div><section><h2>Regression Reasons</h2><ul>${reasons}</ul></section><section><h2>New Failed Sessions</h2><ul>${newFailures}</ul></section><section><h2>Recovered Sessions</h2><ul>${recovered}</ul></section></main></body></html>`;
8308
+ };
8309
+ var renderVoiceScenarioEvalHTML = (report, options = {}) => {
8310
+ const title = options.title ?? "AbsoluteJS Voice Scenario Evals";
8311
+ const links = options.links?.length ? `<nav>${options.links.map((link) => `<a href="${escapeHtml9(link.href)}">${escapeHtml9(link.label)}</a>`).join("")}</nav>` : "";
8312
+ const scenarios = report.scenarios.length ? report.scenarios.map((scenario) => {
8313
+ const scenarioIssues = scenario.issues.length ? `<ul>${scenario.issues.map((issue) => `<li>${escapeHtml9(issue)}</li>`).join("")}</ul>` : "";
8314
+ const sessions = scenario.sessions.length ? scenario.sessions.map((session) => `<tr class="${session.status}"><td>${escapeHtml9(session.sessionId)}</td><td>${escapeHtml9(session.status)}</td><td>${session.eventCount}</td><td>${escapeHtml9(session.issues.join(", ") || "none")}</td></tr>`).join("") : '<tr><td colspan="4">No matching sessions.</td></tr>';
8315
+ return `<section class="scenario ${scenario.status}"><h2>${escapeHtml9(scenario.label)}</h2>${scenario.description ? `<p>${escapeHtml9(scenario.description)}</p>` : ""}<p class="status ${scenario.status}">${scenario.status}</p><p>${scenario.passed} passed, ${scenario.failed} failed, ${scenario.matchedSessions} matched.</p>${scenarioIssues}<table><thead><tr><th>Session</th><th>Status</th><th>Events</th><th>Issues</th></tr></thead><tbody>${sessions}</tbody></table></section>`;
8316
+ }).join("") : "<section><p>No scenarios configured.</p></section>";
8317
+ return `<!doctype html><html lang="en"><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /><title>${escapeHtml9(title)}</title><style>body{font-family:ui-sans-serif,system-ui,sans-serif;margin:2rem;background:#f8f7f2;color:#181713}main{max-width:1180px;margin:auto}nav{display:flex;gap:.5rem;flex-wrap:wrap;margin-bottom:1rem}nav a{background:#181713;border-radius:999px;color:white;padding:.35rem .7rem;text-decoration:none}.status{border-radius:999px;display:inline-flex;font-weight:800;padding:.35rem .75rem}.status.pass{background:#dcfce7;color:#166534}.status.fail{background:#fee2e2;color:#991b1b}.grid{display:grid;gap:1rem;grid-template-columns:repeat(auto-fit,minmax(160px,1fr));margin:1rem 0}.card,section{background:white;border:1px solid #e7e5e4;border-radius:1rem;padding:1rem}.card strong{display:block;font-size:2rem}section{margin:1rem 0}table{border-collapse:collapse;width:100%;margin-top:1rem}td,th{border-bottom:1px solid #eee;padding:.75rem;text-align:left}tr.fail td{border-left:4px solid #dc2626}tr.pass td{border-left:4px solid #16a34a}</style></head><body><main>${links}<h1>${escapeHtml9(title)}</h1><p class="status ${report.status}">${report.status}</p><div class="grid"><article class="card"><span>Total</span><strong>${report.total}</strong></article><article class="card"><span>Passed</span><strong>${report.passed}</strong></article><article class="card"><span>Failed</span><strong>${report.failed}</strong></article></div>${scenarios}</main></body></html>`;
8318
+ };
8319
+ var renderVoiceScenarioFixtureEvalHTML = (report, options = {}) => {
8320
+ const title = options.title ?? "AbsoluteJS Voice Fixture Evals";
8321
+ const links = options.links?.length ? `<nav>${options.links.map((link) => `<a href="${escapeHtml9(link.href)}">${escapeHtml9(link.label)}</a>`).join("")}</nav>` : "";
8322
+ const fixtures = report.fixtures.length ? report.fixtures.map((fixture) => {
8323
+ const scenarios = fixture.report.scenarios.map((scenario) => `<tr class="${scenario.status}"><td>${escapeHtml9(scenario.label)}</td><td>${escapeHtml9(scenario.status)}</td><td>${scenario.matchedSessions}</td><td>${escapeHtml9([...scenario.issues, ...scenario.sessions.flatMap((session) => session.issues)].join(", ") || "none")}</td></tr>`).join("");
8324
+ return `<section class="${fixture.status}"><h2>${escapeHtml9(fixture.label)}</h2>${fixture.description ? `<p>${escapeHtml9(fixture.description)}</p>` : ""}<p class="status ${fixture.status}">${fixture.status}</p><table><thead><tr><th>Scenario</th><th>Status</th><th>Sessions</th><th>Issues</th></tr></thead><tbody>${scenarios}</tbody></table></section>`;
8325
+ }).join("") : "<section><p>No scenario fixtures configured.</p></section>";
8326
+ return `<!doctype html><html lang="en"><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /><title>${escapeHtml9(title)}</title><style>body{font-family:ui-sans-serif,system-ui,sans-serif;margin:2rem;background:#f8f7f2;color:#181713}main{max-width:1180px;margin:auto}nav{display:flex;gap:.5rem;flex-wrap:wrap;margin-bottom:1rem}nav a{background:#181713;border-radius:999px;color:white;padding:.35rem .7rem;text-decoration:none}.status{border-radius:999px;display:inline-flex;font-weight:800;padding:.35rem .75rem}.status.pass{background:#dcfce7;color:#166534}.status.fail{background:#fee2e2;color:#991b1b}.grid{display:grid;gap:1rem;grid-template-columns:repeat(auto-fit,minmax(160px,1fr));margin:1rem 0}.card,section{background:white;border:1px solid #e7e5e4;border-radius:1rem;padding:1rem}.card strong{display:block;font-size:2rem}section{margin:1rem 0}table{border-collapse:collapse;width:100%;margin-top:1rem}td,th{border-bottom:1px solid #eee;padding:.75rem;text-align:left}tr.fail td{border-left:4px solid #dc2626}tr.pass td{border-left:4px solid #16a34a}</style></head><body><main>${links}<h1>${escapeHtml9(title)}</h1><p class="status ${report.status}">${report.status}</p><div class="grid"><article class="card"><span>Total</span><strong>${report.total}</strong></article><article class="card"><span>Passed</span><strong>${report.passed}</strong></article><article class="card"><span>Failed</span><strong>${report.failed}</strong></article></div>${fixtures}</main></body></html>`;
8327
+ };
8328
+ var createVoiceEvalRoutes = (options) => {
8329
+ const path = options.path ?? "/evals";
8330
+ const routes = new Elysia7({
8331
+ name: options.name ?? "absolutejs-voice-evals"
8332
+ });
8333
+ const getReport = () => runVoiceSessionEvals({
8334
+ events: options.events,
8335
+ limit: options.limit,
8336
+ store: options.store,
8337
+ thresholds: options.thresholds
8338
+ });
8339
+ const getBaseline = async () => typeof options.baseline === "function" ? options.baseline() : options.baseline ?? await options.baselineStore?.get();
8340
+ const getBaselineComparison = async () => {
8341
+ const [current, baseline] = await Promise.all([getReport(), getBaseline()]);
8342
+ return baseline ? compareVoiceEvalBaseline(current, baseline, options.baselineComparison) : undefined;
8343
+ };
8344
+ const getScenarioReport = () => runVoiceScenarioEvals({
8345
+ events: options.events,
8346
+ scenarios: options.scenarios,
8347
+ store: options.store
8348
+ });
8349
+ const getFixtureReport = () => runVoiceScenarioFixtureEvals({
8350
+ fixtures: options.fixtures,
8351
+ fixtureStore: options.fixtureStore,
8352
+ scenarios: options.scenarios
8353
+ });
8354
+ routes.get(path, async () => {
8355
+ const report = await getReport();
8356
+ return new Response(renderVoiceEvalHTML(report, {
8357
+ links: options.links,
8358
+ title: options.title
8359
+ }), {
8360
+ headers: {
8361
+ "Content-Type": "text/html; charset=utf-8",
8362
+ ...options.headers
8363
+ }
8364
+ });
8365
+ });
8366
+ routes.get(`${path}/json`, async () => getReport());
8367
+ routes.get(`${path}/status`, async ({ set }) => {
8368
+ const report = await getReport();
8369
+ if (report.status === "fail") {
8370
+ set.status = 503;
8371
+ }
8372
+ return report;
8373
+ });
8374
+ routes.get(`${path}/baseline`, async ({ set }) => {
8375
+ const comparison = await getBaselineComparison();
8376
+ if (!comparison) {
8377
+ set.status = 404;
8378
+ return Response.json({ error: "No voice eval baseline found." });
8379
+ }
8380
+ return new Response(renderVoiceEvalBaselineHTML(comparison, {
8381
+ links: options.links,
8382
+ title: `${options.title ?? "AbsoluteJS Voice Evals"} Baseline`
8383
+ }), {
8384
+ headers: {
8385
+ "Content-Type": "text/html; charset=utf-8",
8386
+ ...options.headers
8387
+ }
8388
+ });
8389
+ });
8390
+ routes.get(`${path}/baseline/json`, async ({ set }) => {
8391
+ const comparison = await getBaselineComparison();
8392
+ if (!comparison) {
8393
+ set.status = 404;
8394
+ return { error: "No voice eval baseline found." };
8395
+ }
8396
+ return comparison;
8397
+ });
8398
+ routes.get(`${path}/baseline/status`, async ({ set }) => {
8399
+ const comparison = await getBaselineComparison();
8400
+ if (!comparison) {
8401
+ set.status = 404;
8402
+ return { error: "No voice eval baseline found." };
8403
+ }
8404
+ if (comparison.status === "fail") {
8405
+ set.status = 503;
8406
+ }
8407
+ return comparison;
8408
+ });
8409
+ routes.post(`${path}/baseline`, async ({ set }) => {
8410
+ if (!options.baselineStore) {
8411
+ set.status = 501;
8412
+ return { error: "No voice eval baseline store configured." };
8413
+ }
8414
+ const report = await getReport();
8415
+ await options.baselineStore.set(report);
8416
+ return {
8417
+ baseline: report,
8418
+ status: "saved"
8419
+ };
8420
+ });
8421
+ routes.get(`${path}/scenarios`, async () => {
8422
+ const report = await getScenarioReport();
8423
+ return new Response(renderVoiceScenarioEvalHTML(report, {
8424
+ links: options.links,
8425
+ title: `${options.title ?? "AbsoluteJS Voice Evals"} Scenarios`
8426
+ }), {
8427
+ headers: {
8428
+ "Content-Type": "text/html; charset=utf-8",
8429
+ ...options.headers
8430
+ }
8431
+ });
8432
+ });
8433
+ routes.get(`${path}/scenarios/json`, async () => getScenarioReport());
8434
+ routes.get(`${path}/scenarios/status`, async ({ set }) => {
8435
+ const report = await getScenarioReport();
8436
+ if (report.status === "fail") {
8437
+ set.status = 503;
8438
+ }
8439
+ return report;
8440
+ });
8441
+ routes.get(`${path}/fixtures`, async () => {
8442
+ const report = await getFixtureReport();
8443
+ return new Response(renderVoiceScenarioFixtureEvalHTML(report, {
8444
+ links: options.links,
8445
+ title: `${options.title ?? "AbsoluteJS Voice Evals"} Fixtures`
8446
+ }), {
8447
+ headers: {
8448
+ "Content-Type": "text/html; charset=utf-8",
8449
+ ...options.headers
8450
+ }
8451
+ });
8452
+ });
8453
+ routes.get(`${path}/fixtures/json`, async () => getFixtureReport());
8454
+ routes.get(`${path}/fixtures/status`, async ({ set }) => {
8455
+ const report = await getFixtureReport();
8456
+ if (report.status === "fail") {
8457
+ set.status = 503;
8458
+ }
8459
+ return report;
8460
+ });
8461
+ return routes;
8462
+ };
8463
+
8464
+ // src/opsConsoleRoutes.ts
8465
+ import { Elysia as Elysia10 } from "elysia";
8466
+
8467
+ // src/resilienceRoutes.ts
8468
+ import { Elysia as Elysia8 } from "elysia";
8469
+ var escapeHtml10 = (value) => value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
8470
+ var getString7 = (value) => typeof value === "string" ? value : undefined;
8471
+ var getNumber4 = (value) => typeof value === "number" && Number.isFinite(value) ? value : undefined;
8472
+ var getBoolean2 = (value) => value === true;
8473
+ var isProviderStatus2 = (value) => value === "error" || value === "fallback" || value === "success";
8474
+ var listVoiceRoutingEvents = (events) => {
8475
+ const routingEvents = [];
8476
+ for (const event of events) {
8477
+ if (event.type !== "session.error") {
8478
+ continue;
8479
+ }
8480
+ const provider = getString7(event.payload.provider);
8481
+ const providerStatus = isProviderStatus2(event.payload.providerStatus) ? event.payload.providerStatus : undefined;
8482
+ if (!provider || !providerStatus) {
8483
+ continue;
8484
+ }
8485
+ const kind = getString7(event.payload.kind);
8486
+ routingEvents.push({
8487
+ at: event.at,
8488
+ attempt: getNumber4(event.payload.attempt),
8489
+ elapsedMs: getNumber4(event.payload.elapsedMs),
8490
+ error: getString7(event.payload.error),
8491
+ fallbackProvider: getString7(event.payload.fallbackProvider),
8492
+ kind: kind === "stt" || kind === "tts" ? kind : "llm",
8493
+ latencyBudgetMs: getNumber4(event.payload.latencyBudgetMs),
8494
+ operation: getString7(event.payload.operation),
8495
+ provider,
8496
+ routing: getString7(event.payload.routing),
8497
+ selectedProvider: getString7(event.payload.selectedProvider),
8498
+ sessionId: event.sessionId,
8499
+ status: providerStatus,
8500
+ suppressionRemainingMs: getNumber4(event.payload.suppressionRemainingMs),
8501
+ timedOut: getBoolean2(event.payload.timedOut),
8502
+ turnId: event.turnId
8503
+ });
8504
+ }
8505
+ return routingEvents.sort((left, right) => right.at - left.at);
8506
+ };
8507
+ var summarizeVoiceRoutingDecision = (events, options = {}) => {
8508
+ const routingEvents = listVoiceRoutingEvents(events).filter((event) => {
8509
+ if (options.kind && event.kind !== options.kind) {
8510
+ return false;
8511
+ }
8512
+ if (options.sessionId && event.sessionId !== options.sessionId) {
8513
+ return false;
8514
+ }
8515
+ return true;
8516
+ });
8517
+ const limited = typeof options.limit === "number" && options.limit >= 0 ? routingEvents.slice(0, options.limit) : routingEvents;
8518
+ return limited[0] ?? null;
8519
+ };
8520
+ var createVoiceRoutingDecisionSummary = async (options) => {
8521
+ const events = await options.store.list({
8522
+ sessionId: options.sessionId,
8523
+ type: "session.error"
8524
+ });
8525
+ return summarizeVoiceRoutingDecision(events, options);
8526
+ };
8527
+ var summarizeRoutingEvents = (events) => {
8528
+ const byKind = new Map;
8529
+ let errors = 0;
8530
+ let fallbacks = 0;
8531
+ let timeouts = 0;
8532
+ for (const event of events) {
8533
+ byKind.set(event.kind, (byKind.get(event.kind) ?? 0) + 1);
8534
+ if (event.status === "error") {
8535
+ errors += 1;
8536
+ }
8537
+ if (event.status === "fallback") {
8538
+ fallbacks += 1;
8539
+ }
8540
+ if (event.timedOut) {
8541
+ timeouts += 1;
8542
+ }
8543
+ }
8544
+ return {
8545
+ byKind,
8546
+ errors,
8547
+ fallbacks,
8548
+ timeouts,
8549
+ total: events.length
8550
+ };
8551
+ };
8552
+ var renderProviderCards = (title, providers) => {
8553
+ if (providers.length === 0) {
8554
+ return `<p class="muted">No ${escapeHtml10(title)} provider health yet.</p>`;
8555
+ }
8556
+ return `<div class="provider-grid">${providers.map((provider) => `
8557
+ <article class="card provider ${escapeHtml10(provider.status)}">
8558
+ <div class="card-header">
8559
+ <strong>${escapeHtml10(provider.provider)}</strong>
8560
+ <span>${escapeHtml10(provider.status)}${provider.recommended ? " \xB7 recommended" : ""}</span>
8561
+ </div>
8562
+ <dl>
8563
+ <div><dt>Runs</dt><dd>${provider.runCount}</dd></div>
8564
+ <div><dt>Avg latency</dt><dd>${provider.averageElapsedMs ?? 0}ms</dd></div>
8565
+ <div><dt>Errors</dt><dd>${provider.errorCount}</dd></div>
8566
+ <div><dt>Timeouts</dt><dd>${provider.timeoutCount}</dd></div>
8567
+ <div><dt>Fallbacks</dt><dd>${provider.fallbackCount}</dd></div>
8568
+ </dl>
8569
+ ${provider.lastError ? `<p class="muted">${escapeHtml10(provider.lastError)}</p>` : ""}
8570
+ </article>
8571
+ `).join("")}</div>`;
8572
+ };
8573
+ var renderTimeline2 = (events) => {
8574
+ if (events.length === 0) {
8575
+ return '<p class="muted">No provider routing events yet. Run the app or simulate provider failover.</p>';
8576
+ }
8577
+ return `<div class="timeline">${events.slice(0, 40).map((event) => `
8578
+ <article class="card event ${escapeHtml10(event.status ?? "unknown")}">
8579
+ <div class="card-header">
8580
+ <strong>${escapeHtml10(event.kind.toUpperCase())} ${escapeHtml10(event.operation ?? "generate")}</strong>
8581
+ <span>${new Date(event.at).toLocaleString()}</span>
8582
+ </div>
8583
+ <p>
8584
+ <span class="pill">${escapeHtml10(event.status ?? "unknown")}</span>
8585
+ <span class="pill">provider: ${escapeHtml10(event.provider ?? "unknown")}</span>
8586
+ ${event.fallbackProvider ? `<span class="pill">fallback: ${escapeHtml10(event.fallbackProvider)}</span>` : ""}
8587
+ ${event.timedOut ? '<span class="pill danger">timed out</span>' : ""}
8588
+ </p>
8589
+ <dl>
8590
+ <div><dt>Attempt</dt><dd>${event.attempt ?? 0}</dd></div>
8591
+ <div><dt>Elapsed</dt><dd>${event.elapsedMs ?? 0}ms</dd></div>
8592
+ <div><dt>Budget</dt><dd>${event.latencyBudgetMs ?? 0}ms</dd></div>
8593
+ <div><dt>Session</dt><dd>${escapeHtml10(event.sessionId)}</dd></div>
8594
+ </dl>
8595
+ ${event.error ? `<p class="muted">${escapeHtml10(event.error)}</p>` : ""}
8596
+ </article>
8597
+ `).join("")}</div>`;
8598
+ };
8599
+ var renderSimulationControls = (kind, simulation) => {
8600
+ if (!simulation) {
8601
+ return "";
8602
+ }
8603
+ const configuredProviders = simulation.providers.filter((provider) => provider.configured !== false);
8604
+ if (configuredProviders.length === 0) {
8605
+ return `<p class="muted">No ${kind.toUpperCase()} providers are configured for simulation.</p>`;
8606
+ }
8607
+ const pathPrefix = simulation.pathPrefix ?? `/api/${kind}-simulate`;
8608
+ const failureProviders = simulation.failureProviders ?? configuredProviders.map(({ provider }) => provider);
8609
+ const canFail = (provider) => configuredProviders.some((entry) => entry.provider === provider) && (!simulation.fallbackRequiredProvider || configuredProviders.some((entry) => entry.provider === simulation.fallbackRequiredProvider));
8610
+ return `<div class="simulate-panel" data-sim-kind="${kind}" data-sim-prefix="${escapeHtml10(pathPrefix)}">
8611
+ <p class="muted">${escapeHtml10(simulation.failureMessage ?? `Simulate ${kind.toUpperCase()} provider failure without changing provider credentials.`)}</p>
8612
+ <div class="simulate-actions">
8613
+ ${failureProviders.map((provider) => `<button type="button" data-provider-fail="${escapeHtml10(provider)}"${canFail(provider) ? "" : " disabled"}>Simulate ${escapeHtml10(provider)} ${kind.toUpperCase()} failure</button>`).join("")}
8614
+ ${configuredProviders.map((provider) => `<button type="button" data-provider-recover="${escapeHtml10(provider.provider)}">Mark ${escapeHtml10(provider.provider)} recovered</button>`).join("")}
8615
+ </div>
8616
+ ${simulation.fallbackRequiredProvider && !configuredProviders.some((entry) => entry.provider === simulation.fallbackRequiredProvider) ? `<p class="muted">${escapeHtml10(simulation.fallbackRequiredMessage ?? `Configure ${simulation.fallbackRequiredProvider} to enable fallback simulation.`)}</p>` : ""}
8617
+ <pre class="simulate-output" hidden></pre>
8618
+ </div>`;
8619
+ };
8620
+ var renderVoiceResilienceHTML = (input) => {
8621
+ const summary = summarizeRoutingEvents(input.routingEvents);
8622
+ const kindCounts = [...summary.byKind.entries()].map(([kind, count]) => `<span class="pill">${escapeHtml10(kind)}: ${String(count)}</span>`).join("");
8623
+ const links = input.links?.length ? input.links.map((link) => `<a href="${escapeHtml10(link.href)}">${escapeHtml10(link.label)}</a>`).join(" \xB7 ") : "";
8624
+ return `<!doctype html>
8625
+ <html lang="en">
8626
+ <head>
8627
+ <meta charset="utf-8" />
8628
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
8629
+ <title>${escapeHtml10(input.title ?? "AbsoluteJS Voice Resilience")}</title>
8630
+ <style>
8631
+ :root { color-scheme: dark; }
8632
+ body { background: radial-gradient(circle at top left, #172554, #09090b 36%, #050505); color: #f4f4f5; font-family: ui-sans-serif, system-ui, sans-serif; margin: 0; padding: 24px; }
8633
+ main { display: grid; gap: 16px; margin: 0 auto; max-width: 1180px; }
8634
+ section, .card { background: rgba(19, 22, 27, 0.92); border: 1px solid #27272a; border-radius: 20px; padding: 20px; }
8635
+ .hero { background: linear-gradient(135deg, rgba(14, 165, 233, 0.18), rgba(245, 158, 11, 0.12)); }
8636
+ .grid, .provider-grid { display: grid; gap: 14px; grid-template-columns: repeat(4, minmax(0, 1fr)); }
8637
+ .timeline { display: grid; gap: 12px; }
8638
+ .card-header { align-items: center; display: flex; gap: 12px; justify-content: space-between; }
8639
+ .card-header strong { font-size: 1.05rem; }
8640
+ .metric strong { display: block; font-size: 2rem; margin-top: 6px; }
8641
+ .muted, dt, span { color: #a1a1aa; }
8642
+ dl { display: grid; gap: 8px; grid-template-columns: repeat(4, minmax(0, 1fr)); }
8643
+ dl div { background: #0f1217; border: 1px solid #27272a; border-radius: 12px; padding: 10px; }
8644
+ dd { font-weight: 800; margin: 4px 0 0; }
8645
+ .pill { background: #0f1217; border: 1px solid #3f3f46; border-radius: 999px; color: #d4d4d8; display: inline-flex; margin: 3px 4px 3px 0; padding: 5px 9px; }
8646
+ .danger { border-color: rgba(239, 68, 68, 0.75); color: #fecaca; }
8647
+ .event.error { border-color: rgba(239, 68, 68, 0.7); }
8648
+ .event.fallback { border-color: rgba(245, 158, 11, 0.7); }
8649
+ .event.success, .provider.healthy { border-color: rgba(34, 197, 94, 0.5); }
8650
+ .provider.suppressed, .provider.degraded, .provider.rate-limited { border-color: rgba(239, 68, 68, 0.7); }
8651
+ .provider.recoverable { border-color: rgba(59, 130, 246, 0.7); }
8652
+ button { background: #f59e0b; border: 0; border-radius: 999px; color: #111827; cursor: pointer; font-weight: 800; padding: 10px 14px; }
8653
+ button:disabled { cursor: not-allowed; opacity: 0.45; }
8654
+ .simulate-actions { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 12px; }
8655
+ .simulate-output { background: #050505; border: 1px solid #27272a; border-radius: 14px; color: #d4d4d8; overflow: auto; padding: 12px; white-space: pre-wrap; }
8656
+ a { color: #f59e0b; }
8657
+ @media (max-width: 850px) { .grid, .provider-grid, dl { grid-template-columns: 1fr; } }
8658
+ </style>
8659
+ </head>
8660
+ <body>
8661
+ <main>
8662
+ <section class="hero">
8663
+ <h1>Provider routing and resilience</h1>
8664
+ <p>One view for the production reliability story: LLM failover, STT/TTS routing, latency budgets, timeouts, and fallback decisions.</p>
8665
+ ${links ? `<p>${links}</p>` : ""}
8666
+ <p>${kindCounts || '<span class="pill">No routing events yet</span>'}</p>
8667
+ </section>
8668
+ <section class="grid">
8669
+ <article class="card metric"><span>Total routing events</span><strong>${summary.total}</strong></article>
8670
+ <article class="card metric"><span>Fallbacks</span><strong>${summary.fallbacks}</strong></article>
8671
+ <article class="card metric"><span>Errors</span><strong>${summary.errors}</strong></article>
8672
+ <article class="card metric"><span>Timeouts</span><strong>${summary.timeouts}</strong></article>
8673
+ </section>
8674
+ <section>
8675
+ <h2>LLM provider health</h2>
8676
+ ${renderProviderCards("LLM", input.llmProviderHealth)}
8677
+ </section>
8678
+ <section>
8679
+ <h2>STT provider health</h2>
8680
+ ${renderSimulationControls("stt", input.sttSimulation)}
8681
+ ${renderProviderCards("STT", input.sttProviderHealth)}
8682
+ </section>
8683
+ <section>
8684
+ <h2>TTS provider health</h2>
8685
+ ${renderSimulationControls("tts", input.ttsSimulation)}
8686
+ ${renderProviderCards("TTS", input.ttsProviderHealth)}
8687
+ </section>
8688
+ <section>
8689
+ <h2>Routing timeline</h2>
8690
+ ${renderTimeline2(input.routingEvents)}
8691
+ </section>
8692
+ </main>
8693
+ <script>
8694
+ const showResult = (panel, result) => {
8695
+ const output = panel.querySelector(".simulate-output");
8696
+ if (!output) return;
8697
+ output.hidden = false;
8698
+ output.textContent = JSON.stringify(result, null, 2);
8699
+ };
8700
+ document.querySelectorAll("[data-sim-prefix]").forEach((panel) => {
8701
+ const prefix = panel.getAttribute("data-sim-prefix");
8702
+ panel.querySelectorAll("[data-provider-fail]").forEach((button) => {
8703
+ button.addEventListener("click", async () => {
8704
+ const provider = button.getAttribute("data-provider-fail");
8705
+ const response = await fetch(prefix + "/failure?provider=" + encodeURIComponent(provider || ""), { method: "POST" });
8706
+ showResult(panel, await response.json());
8707
+ if (response.ok) window.setTimeout(() => window.location.reload(), 450);
8708
+ });
8709
+ });
8710
+ panel.querySelectorAll("[data-provider-recover]").forEach((button) => {
8711
+ button.addEventListener("click", async () => {
8712
+ const provider = button.getAttribute("data-provider-recover");
8713
+ const response = await fetch(prefix + "/recovery?provider=" + encodeURIComponent(provider || ""), { method: "POST" });
8714
+ showResult(panel, await response.json());
8715
+ if (response.ok) window.setTimeout(() => window.location.reload(), 450);
8716
+ });
8717
+ });
8718
+ });
8719
+ </script>
8720
+ </body>
8721
+ </html>`;
8722
+ };
8723
+ var providerFromQuery = (value, providers) => typeof value === "string" && providers.some((provider) => provider.provider === value && provider.configured !== false) ? value : undefined;
8724
+ var registerSimulationRoutes = (routes, simulation, defaultPathPrefix) => {
8725
+ if (!simulation) {
8726
+ return routes;
8727
+ }
8728
+ const pathPrefix = simulation.pathPrefix ?? defaultPathPrefix;
8729
+ routes.post(`${pathPrefix}/failure`, async ({ query, set }) => {
8730
+ const provider = providerFromQuery(query.provider, simulation.providers);
8731
+ if (!provider) {
8732
+ set.status = 400;
8733
+ return {
8734
+ error: "Provider is not configured for simulation."
8735
+ };
8736
+ }
8737
+ if (simulation.failureProviders && !simulation.failureProviders.includes(provider)) {
8738
+ set.status = 400;
8739
+ return {
8740
+ error: `${provider} is not configured for failure simulation.`
8741
+ };
8742
+ }
8743
+ if (simulation.fallbackRequiredProvider && !simulation.providers.some((entry) => entry.provider === simulation.fallbackRequiredProvider && entry.configured !== false)) {
8744
+ set.status = 400;
8745
+ return {
8746
+ error: simulation.fallbackRequiredMessage ?? `Configure ${simulation.fallbackRequiredProvider} before simulating fallback.`
8747
+ };
8748
+ }
8749
+ return simulation.run(provider, "failure");
8750
+ });
8751
+ routes.post(`${pathPrefix}/recovery`, async ({ query, set }) => {
8752
+ const provider = providerFromQuery(query.provider, simulation.providers);
8753
+ if (!provider) {
8754
+ set.status = 400;
8755
+ return {
8756
+ error: "Provider is not configured for simulation."
8757
+ };
8758
+ }
8759
+ return simulation.run(provider, "recovery");
8760
+ });
8761
+ return routes;
8762
+ };
8763
+ var createVoiceResilienceRoutes = (options) => {
8764
+ const path = options.path ?? "/resilience";
8765
+ const routes = new Elysia8({
8766
+ name: options.name ?? "absolutejs-voice-resilience"
8767
+ }).get(path, async () => {
8768
+ const events = await options.store.list();
8769
+ const sttEvents = events.filter((event) => event.payload.kind === "stt");
8770
+ const ttsEvents = events.filter((event) => event.payload.kind === "tts");
8771
+ const data = {
8772
+ links: options.links,
8773
+ llmProviderHealth: await summarizeVoiceProviderHealth({
8774
+ events,
8775
+ providers: options.llmProviders ?? []
8776
+ }),
8777
+ routingEvents: listVoiceRoutingEvents(events),
8778
+ sttProviderHealth: await summarizeVoiceProviderHealth({
8779
+ events: sttEvents,
8780
+ providers: options.sttProviders ?? []
8781
+ }),
8782
+ sttSimulation: options.sttSimulation,
8783
+ title: options.title,
8784
+ ttsProviderHealth: await summarizeVoiceProviderHealth({
8785
+ events: ttsEvents,
8786
+ providers: options.ttsProviders ?? []
8787
+ }),
8788
+ ttsSimulation: options.ttsSimulation
8789
+ };
8790
+ const body = await (options.render ?? renderVoiceResilienceHTML)(data);
8791
+ return new Response(body, {
8792
+ headers: {
8793
+ "Content-Type": "text/html; charset=utf-8",
8794
+ ...options.headers
8795
+ }
8796
+ });
8797
+ });
8798
+ registerSimulationRoutes(routes, options.sttSimulation, "/api/stt-simulate");
8799
+ registerSimulationRoutes(routes, options.ttsSimulation, "/api/tts-simulate");
8800
+ return routes;
8801
+ };
8802
+
8803
+ // src/sessionReplay.ts
8804
+ import { Elysia as Elysia9 } from "elysia";
8805
+ var getString8 = (value) => typeof value === "string" ? value : undefined;
8806
+ var escapeHtml11 = (value) => value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
8807
+ var increment3 = (record, key) => {
8808
+ record[key] = (record[key] ?? 0) + 1;
8809
+ };
8810
+ var buildReplayTurns = (events) => {
8811
+ const turns = new Map;
8812
+ const getTurn = (turnId) => {
8813
+ const existing = turns.get(turnId);
8814
+ if (existing) {
8815
+ return existing;
8816
+ }
8817
+ const turn = {
8818
+ assistantReplies: [],
8819
+ errors: [],
8820
+ id: turnId,
8821
+ modelCalls: [],
8822
+ tools: [],
8823
+ transcripts: []
8824
+ };
8825
+ turns.set(turnId, turn);
8826
+ return turn;
8827
+ };
8828
+ for (const event of events) {
8829
+ const turnId = event.turnId ?? "session";
8830
+ const turn = getTurn(turnId);
8831
+ switch (event.type) {
8832
+ case "turn.transcript":
8833
+ turn.transcripts.push({
8834
+ isFinal: event.payload.isFinal === true,
8835
+ text: getString8(event.payload.text)
8836
+ });
8837
+ break;
8838
+ case "turn.committed":
8839
+ turn.committedText = getString8(event.payload.text);
8840
+ break;
8841
+ case "turn.assistant": {
8842
+ const text = getString8(event.payload.text);
8843
+ if (text) {
8844
+ turn.assistantReplies.push(text);
8845
+ }
8846
+ break;
8847
+ }
8848
+ case "agent.model":
8849
+ case "assistant.run":
8850
+ turn.modelCalls.push(event.payload);
8851
+ break;
8852
+ case "agent.tool":
8853
+ turn.tools.push(event.payload);
8854
+ break;
8855
+ case "session.error":
8856
+ turn.errors.push(event.payload);
8857
+ break;
8858
+ }
8859
+ }
8860
+ return [...turns.values()];
8861
+ };
8862
+ var summarizeVoiceSessionReplay = async (options) => {
8863
+ const sourceEvents = options.events ?? await options.store?.list({ sessionId: options.sessionId }) ?? [];
8864
+ const events = filterVoiceTraceEvents(sourceEvents, {
8865
+ sessionId: options.sessionId
8866
+ });
8867
+ const replay = buildVoiceTraceReplay(events, {
8868
+ evaluation: options.evaluation,
8869
+ redact: options.redact,
8870
+ title: options.title ?? `Voice Session ${options.sessionId}`
8871
+ });
8872
+ const startedAt = replay.summary.startedAt;
8873
+ return {
8874
+ evaluation: replay.evaluation,
8875
+ events,
8876
+ html: replay.html,
8877
+ markdown: replay.markdown,
8878
+ sessionId: options.sessionId,
8879
+ summary: replay.summary,
8880
+ timeline: events.map((event) => ({
8881
+ at: event.at,
8882
+ offsetMs: startedAt === undefined ? undefined : Math.max(0, event.at - startedAt),
8883
+ payload: event.payload,
8884
+ turnId: event.turnId,
8885
+ type: event.type
8886
+ })),
8887
+ turns: buildReplayTurns(events)
8888
+ };
8889
+ };
8890
+ var summarizeVoiceSessions = async (options = {}) => {
8891
+ const events = options.events ?? await options.store?.list() ?? [];
8892
+ const grouped = new Map;
8893
+ for (const event of events) {
8894
+ grouped.set(event.sessionId, [...grouped.get(event.sessionId) ?? [], event]);
8895
+ }
8896
+ const sessions = [...grouped.entries()].map(([sessionId, sessionEvents]) => {
8897
+ const sorted = filterVoiceTraceEvents(sessionEvents);
8898
+ const summary = buildVoiceTraceReplay(sorted, {
8899
+ evaluation: {
8900
+ requireAssistantReply: false,
8901
+ requireCompletedCall: false,
8902
+ requireTranscript: false,
8903
+ requireTurn: false
8904
+ }
8905
+ }).summary;
8906
+ const providerErrors = {};
8907
+ const providers = new Set;
8908
+ let latestOutcome;
8909
+ let errorCount = 0;
8910
+ for (const event of sorted) {
8911
+ const provider = getString8(event.payload.provider);
8912
+ if (provider) {
8913
+ providers.add(provider);
8914
+ }
8915
+ if (event.type === "session.error" && (event.payload.providerStatus === "error" || typeof event.payload.error === "string")) {
8916
+ errorCount += 1;
8917
+ increment3(providerErrors, provider ?? "unknown");
8918
+ }
8919
+ const outcome = getString8(event.payload.outcome);
8920
+ if (outcome) {
8921
+ latestOutcome = outcome;
8922
+ }
8923
+ }
8924
+ const item = {
8925
+ endedAt: summary.endedAt,
8926
+ errorCount,
8927
+ eventCount: summary.eventCount,
8928
+ latestOutcome,
8929
+ providerErrors,
8930
+ providers: [...providers].sort(),
8931
+ sessionId,
8932
+ startedAt: summary.startedAt,
8933
+ status: errorCount > 0 ? "failed" : "healthy",
8934
+ transcriptCount: summary.transcriptCount,
8935
+ turnCount: summary.turnCount
8936
+ };
8937
+ const replayHref = options.replayHref === false ? "" : typeof options.replayHref === "function" ? options.replayHref(item) : `${options.replayHref ?? "/api/voice-sessions"}/${encodeURIComponent(sessionId)}/replay/htmx`;
8938
+ return {
8939
+ ...item,
8940
+ replayHref
8941
+ };
8942
+ });
8943
+ const search = options.q?.trim().toLowerCase();
8944
+ return sessions.filter((session) => {
8945
+ if (options.status && options.status !== "all" && session.status !== options.status) {
8946
+ return false;
8947
+ }
8948
+ if (options.provider && !session.providers.includes(options.provider)) {
8949
+ return false;
8950
+ }
8951
+ if (!search) {
8952
+ return true;
8953
+ }
8954
+ return [
8955
+ session.sessionId,
8956
+ session.latestOutcome,
8957
+ session.status,
8958
+ ...session.providers
8959
+ ].some((value) => value?.toLowerCase().includes(search));
8960
+ }).sort((left, right) => (right.endedAt ?? right.startedAt ?? 0) - (left.endedAt ?? left.startedAt ?? 0)).slice(0, options.limit ?? 50);
8961
+ };
8962
+ var renderVoiceSessionsHTML = (sessions) => sessions.length === 0 ? '<p class="voice-sessions-empty">No voice sessions found.</p>' : [
8963
+ '<div class="voice-sessions-list">',
8964
+ ...sessions.map((session) => [
8965
+ `<article class="voice-session-card ${escapeHtml11(session.status)}">`,
8966
+ '<div class="voice-session-card-header">',
8967
+ `<strong>${escapeHtml11(session.sessionId)}</strong>`,
8968
+ `<span>${escapeHtml11(session.status)}</span>`,
8969
+ "</div>",
8970
+ "<dl>",
8971
+ `<div><dt>Events</dt><dd>${String(session.eventCount)}</dd></div>`,
8972
+ `<div><dt>Turns</dt><dd>${String(session.turnCount)}</dd></div>`,
8973
+ `<div><dt>Transcripts</dt><dd>${String(session.transcriptCount)}</dd></div>`,
8974
+ `<div><dt>Errors</dt><dd>${String(session.errorCount)}</dd></div>`,
8975
+ "</dl>",
8976
+ session.latestOutcome ? `<p>Outcome: ${escapeHtml11(session.latestOutcome)}</p>` : "",
8977
+ session.providers.length ? `<p>Providers: ${session.providers.map(escapeHtml11).join(", ")}</p>` : "",
8978
+ session.replayHref ? `<p><a href="${escapeHtml11(session.replayHref)}">Open replay</a></p>` : "",
8979
+ "</article>"
8980
+ ].join("")),
8981
+ "</div>"
8982
+ ].join("");
8983
+ var createVoiceSessionsJSONHandler = (options = {}) => async ({ query }) => summarizeVoiceSessions({
8984
+ ...options,
8985
+ limit: typeof query?.limit === "string" ? Number(query.limit) : options.limit,
8986
+ provider: query?.provider ?? options.provider,
8987
+ q: query?.q ?? options.q,
8988
+ status: query?.status === "failed" || query?.status === "healthy" || query?.status === "all" ? query.status : options.status
8989
+ });
8990
+ var createVoiceSessionsHTMLHandler = (options = {}) => async ({ query }) => {
8991
+ const sessions = await summarizeVoiceSessions({
8992
+ ...options,
8993
+ limit: typeof query?.limit === "string" ? Number(query.limit) : options.limit,
8994
+ provider: query?.provider ?? options.provider,
8995
+ q: query?.q ?? options.q,
8996
+ status: query?.status === "failed" || query?.status === "healthy" || query?.status === "all" ? query.status : options.status
8997
+ });
8998
+ const body = await (options.render?.(sessions) ?? renderVoiceSessionsHTML(sessions));
8999
+ return new Response(body, {
9000
+ headers: {
9001
+ "Content-Type": "text/html; charset=utf-8",
9002
+ ...options.headers
9003
+ }
9004
+ });
9005
+ };
9006
+ var createVoiceSessionListRoutes = (options = {}) => {
9007
+ const path = options.path ?? "/api/voice-sessions";
9008
+ const htmlPath = options.htmlPath === undefined ? `${path}/htmx` : options.htmlPath;
9009
+ const routes = new Elysia9({
9010
+ name: options.name ?? "absolutejs-voice-session-list"
9011
+ }).get(path, createVoiceSessionsJSONHandler(options));
9012
+ if (htmlPath) {
9013
+ routes.get(htmlPath, createVoiceSessionsHTMLHandler(options));
9014
+ }
9015
+ return routes;
9016
+ };
9017
+ var createVoiceSessionReplayJSONHandler = (options) => async ({ params }) => summarizeVoiceSessionReplay({
9018
+ ...options,
9019
+ sessionId: params.sessionId ?? ""
9020
+ });
9021
+ var createVoiceSessionReplayHTMLHandler = (options) => async ({ params }) => {
9022
+ const replay = await summarizeVoiceSessionReplay({
9023
+ ...options,
9024
+ sessionId: params.sessionId ?? ""
9025
+ });
9026
+ const body = await (options.render?.(replay) ?? replay.html);
9027
+ return new Response(body, {
9028
+ headers: {
9029
+ "Content-Type": "text/html; charset=utf-8",
9030
+ ...options.headers
9031
+ }
9032
+ });
9033
+ };
9034
+ var createVoiceSessionReplayRoutes = (options) => {
9035
+ const path = options.path ?? "/api/voice-sessions/:sessionId/replay";
9036
+ const htmlPath = options.htmlPath === undefined ? `${path}/htmx` : options.htmlPath;
9037
+ const routes = new Elysia9({
9038
+ name: options.name ?? "absolutejs-voice-session-replay"
9039
+ }).get(path, createVoiceSessionReplayJSONHandler(options));
9040
+ if (htmlPath) {
9041
+ routes.get(htmlPath, createVoiceSessionReplayHTMLHandler(options));
9042
+ }
9043
+ return routes;
9044
+ };
9045
+
9046
+ // src/opsConsoleRoutes.ts
9047
+ var DEFAULT_LINKS = [
9048
+ {
9049
+ description: "Quality gates for CI, deploy checks, and production readiness.",
9050
+ href: "/quality",
9051
+ label: "Quality",
9052
+ statusHref: "/quality/status"
9053
+ },
9054
+ {
9055
+ description: "Replay stored sessions against acceptance gates over time.",
9056
+ href: "/evals",
9057
+ label: "Evals",
9058
+ statusHref: "/evals/status"
9059
+ },
9060
+ {
9061
+ description: "Provider health, fallback paths, and failure simulation.",
9062
+ href: "/resilience",
9063
+ label: "Resilience"
9064
+ },
9065
+ {
9066
+ description: "Redacted trace exports for debugging and support handoffs.",
9067
+ href: "/diagnostics",
9068
+ label: "Diagnostics"
9069
+ },
9070
+ {
9071
+ description: "Recent sessions with replay links.",
9072
+ href: "/sessions",
9073
+ label: "Sessions"
9074
+ },
9075
+ {
9076
+ description: "Transfer and webhook delivery health.",
9077
+ href: "/handoffs",
9078
+ label: "Handoffs"
9079
+ }
9080
+ ];
9081
+ var escapeHtml12 = (value) => value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
9082
+ var countProviderStatuses = (providers) => {
9083
+ const degradedStatuses = new Set(["degraded", "rate-limited", "suppressed"]);
9084
+ const healthy = providers.filter((provider) => provider.status === "healthy").length;
9085
+ const degraded = providers.filter((provider) => degradedStatuses.has(provider.status)).length;
9086
+ return {
9087
+ degraded,
9088
+ healthy,
9089
+ total: providers.length
9090
+ };
9091
+ };
9092
+ var buildVoiceOpsConsoleReport = async (options) => {
9093
+ const events = await options.store.list();
9094
+ const providers = [
9095
+ ...await summarizeVoiceProviderHealth({
9096
+ events,
9097
+ providers: options.llmProviders
9098
+ }),
9099
+ ...await summarizeVoiceProviderHealth({
9100
+ events,
9101
+ providers: options.sttProviders
9102
+ }),
9103
+ ...await summarizeVoiceProviderHealth({
9104
+ events,
9105
+ providers: options.ttsProviders
9106
+ })
9107
+ ];
9108
+ const handoffs = await summarizeVoiceHandoffHealth({ events });
9109
+ const sessions = await summarizeVoiceSessions({
9110
+ events,
9111
+ limit: 8,
9112
+ status: "all"
9113
+ });
9114
+ const quality = await evaluateVoiceQuality({ events });
9115
+ const routingEvents = listVoiceRoutingEvents(events).slice(0, 10);
9116
+ const trace = summarizeVoiceTrace(events);
9117
+ return {
9118
+ checkedAt: Date.now(),
9119
+ eventCount: events.length,
9120
+ handoffs: {
9121
+ failed: handoffs.failed,
9122
+ total: handoffs.total
9123
+ },
9124
+ links: options.links ?? DEFAULT_LINKS,
9125
+ providers: countProviderStatuses(providers),
9126
+ quality,
9127
+ recentRoutingEvents: routingEvents,
9128
+ recentSessions: sessions,
9129
+ sessions: {
9130
+ failed: sessions.filter((session) => session.status === "failed").length,
9131
+ healthy: sessions.filter((session) => session.status === "healthy").length,
9132
+ total: sessions.length
9133
+ },
9134
+ trace
9135
+ };
9136
+ };
9137
+ var renderMetricCard = (input) => `<article class="metric"><span>${escapeHtml12(input.label)}</span><strong>${escapeHtml12(String(input.value))}</strong>${input.status ? `<p class="${escapeHtml12(input.status)}">${escapeHtml12(input.status)}</p>` : ""}${input.href ? `<a href="${escapeHtml12(input.href)}">Open</a>` : ""}</article>`;
9138
+ var renderVoiceOpsConsoleHTML = (report, options = {}) => {
9139
+ const links = report.links.map((link) => `<article class="surface">
9140
+ <div><h2>${escapeHtml12(link.label)}</h2>${link.description ? `<p>${escapeHtml12(link.description)}</p>` : ""}</div>
9141
+ <p><a href="${escapeHtml12(link.href)}">Open ${escapeHtml12(link.label)}</a>${link.statusHref ? ` \xB7 <a href="${escapeHtml12(link.statusHref)}">Status</a>` : ""}</p>
9142
+ </article>`).join("");
9143
+ const sessions = report.recentSessions.length ? report.recentSessions.map((session) => `<tr><td>${escapeHtml12(session.sessionId)}</td><td>${escapeHtml12(session.status)}</td><td>${session.turnCount}</td><td>${session.errorCount}</td><td>${session.replayHref ? `<a href="${escapeHtml12(session.replayHref)}">Replay</a>` : ""}</td></tr>`).join("") : '<tr><td colspan="5">No sessions yet.</td></tr>';
9144
+ const routing = report.recentRoutingEvents.length ? report.recentRoutingEvents.map((event) => `<tr><td>${escapeHtml12(event.kind)}</td><td>${escapeHtml12(event.provider ?? "unknown")}</td><td>${escapeHtml12(event.status ?? "unknown")}</td><td>${event.elapsedMs ?? 0}ms</td><td>${escapeHtml12(event.sessionId)}</td></tr>`).join("") : '<tr><td colspan="5">No provider routing events yet.</td></tr>';
9145
+ const title = options.title ?? "AbsoluteJS Voice Ops Console";
9146
+ return `<!doctype html><html lang="en"><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /><title>${escapeHtml12(title)}</title><style>body{font-family:ui-sans-serif,system-ui,sans-serif;background:#101316;color:#f6f2e8;margin:0}main{max-width:1180px;margin:auto;padding:32px}a{color:#fbbf24}header{display:flex;justify-content:space-between;gap:24px;align-items:flex-start;margin-bottom:24px}.eyebrow{color:#fbbf24;font-weight:800;letter-spacing:.08em;text-transform:uppercase}h1{font-size:clamp(2.2rem,5vw,4.5rem);line-height:.95;margin:.2rem 0 1rem}.muted{color:#a8b0b8}.grid{display:grid;gap:14px;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));margin:20px 0}.metric,.surface{background:#181d22;border:1px solid #2a323a;border-radius:20px;padding:18px}.metric strong{display:block;font-size:2.2rem;margin:.25rem 0}.pass,.healthy{color:#86efac}.fail,.failed,.degraded{color:#fca5a5}.surfaces{display:grid;gap:16px;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));margin:24px 0}table{width:100%;border-collapse:collapse;background:#181d22;border-radius:16px;overflow:hidden;margin:12px 0 28px}td,th{border-bottom:1px solid #2a323a;padding:12px;text-align:left}section{margin-top:30px}@media(max-width:700px){main{padding:20px}header{display:block}}</style></head><body><main><header><div><p class="eyebrow">Self-hosted voice operations</p><h1>${escapeHtml12(title)}</h1><p class="muted">One deployable control plane for quality gates, failover, traces, sessions, handoffs, and provider health.</p></div><p class="muted">Checked ${escapeHtml12(new Date(report.checkedAt).toLocaleString())}</p></header><div class="grid">${renderMetricCard({ label: "Quality", value: report.quality.status, status: report.quality.status, href: "/quality" })}${renderMetricCard({ label: "Events", value: report.eventCount, href: "/diagnostics" })}${renderMetricCard({ label: "Sessions", value: report.sessions.total, status: report.sessions.failed > 0 ? "failed" : "healthy", href: "/sessions" })}${renderMetricCard({ label: "Handoffs failed", value: report.handoffs.failed, status: report.handoffs.failed > 0 ? "failed" : "healthy", href: "/handoffs" })}${renderMetricCard({ label: "Providers degraded", value: report.providers.degraded, status: report.providers.degraded > 0 ? "degraded" : "healthy", href: "/resilience" })}</div><section><h2>Operational Surfaces</h2><div class="surfaces">${links}</div></section><section><h2>Recent Sessions</h2><table><thead><tr><th>Session</th><th>Status</th><th>Turns</th><th>Errors</th><th>Replay</th></tr></thead><tbody>${sessions}</tbody></table></section><section><h2>Recent Provider Routing</h2><table><thead><tr><th>Kind</th><th>Provider</th><th>Status</th><th>Elapsed</th><th>Session</th></tr></thead><tbody>${routing}</tbody></table></section></main></body></html>`;
9147
+ };
9148
+ var createVoiceOpsConsoleRoutes = (options) => {
9149
+ const path = options.path ?? "/ops-console";
9150
+ const routes = new Elysia10({
9151
+ name: options.name ?? "absolutejs-voice-ops-console"
9152
+ });
9153
+ const getReport = () => buildVoiceOpsConsoleReport(options);
9154
+ routes.get(path, async () => {
9155
+ const report = await getReport();
9156
+ return new Response(renderVoiceOpsConsoleHTML(report, { title: options.title }), {
9157
+ headers: {
9158
+ "Content-Type": "text/html; charset=utf-8",
9159
+ ...options.headers
9160
+ }
9161
+ });
9162
+ });
9163
+ routes.get(`${path}/json`, async () => getReport());
9164
+ return routes;
9165
+ };
9166
+
9167
+ // src/appKit.ts
9168
+ var DEFAULT_LINKS2 = [
9169
+ {
9170
+ description: "Integrated voice operations console.",
9171
+ href: "/ops-console",
9172
+ label: "Ops Console"
9173
+ },
9174
+ {
9175
+ description: "Production quality gates.",
9176
+ href: "/quality",
9177
+ label: "Quality",
9178
+ statusHref: "/quality/status"
9179
+ },
9180
+ {
9181
+ description: "Replay sessions against evals and workflow contracts.",
9182
+ href: "/evals",
9183
+ label: "Evals",
9184
+ statusHref: "/evals/status"
9185
+ },
9186
+ {
9187
+ description: "Provider routing, fallback, and resilience controls.",
9188
+ href: "/resilience",
9189
+ label: "Resilience"
9190
+ },
9191
+ {
9192
+ description: "Recent sessions and replay links.",
9193
+ href: "/sessions",
9194
+ label: "Sessions"
9195
+ },
9196
+ {
9197
+ description: "Handoff delivery health.",
9198
+ href: "/handoffs",
9199
+ label: "Handoffs"
9200
+ },
9201
+ {
9202
+ description: "Redacted traces and bug-report exports.",
9203
+ href: "/diagnostics",
9204
+ label: "Diagnostics"
9205
+ }
9206
+ ];
9207
+ var resolveLinks = (links) => links ?? DEFAULT_LINKS2;
9208
+ var toBasicLinks = (links) => links.map(({ href, label }) => ({ href, label }));
9209
+ var toOpsLinks = (links) => links.map((link) => ({
9210
+ description: link.description ?? link.label,
9211
+ href: link.href,
9212
+ label: link.label,
9213
+ statusHref: link.statusHref
9214
+ }));
9215
+ var toResilienceLinks = (links) => links.map(({ href, label }) => ({ href, label }));
9216
+ var countStatus = (statuses) => ({
9217
+ failed: statuses.filter((status) => status === "fail").length,
9218
+ passed: statuses.filter((status) => status === "pass").length,
9219
+ total: statuses.length
9220
+ });
9221
+ var summarizeVoiceAppKitStatus = async (options) => {
9222
+ const links = resolveLinks(options.links);
9223
+ const statusOptions = options.appStatus && typeof options.appStatus === "object" ? options.appStatus : undefined;
9224
+ const evalOptions = options.evals === false ? undefined : options.evals;
9225
+ const include = statusOptions?.include;
9226
+ const shouldInclude = (surface) => include?.[surface] !== false;
9227
+ const events = filterVoiceTraceEvents(await options.store.list());
9228
+ const [quality, workflows, providers, sessions, handoffs] = await Promise.all([
9229
+ options.quality === false || !shouldInclude("quality") ? undefined : evaluateVoiceQuality({
9230
+ events,
9231
+ thresholds: options.quality?.thresholds
9232
+ }),
9233
+ !evalOptions || !shouldInclude("workflows") ? undefined : (async () => {
9234
+ const fixtureReport = await runVoiceScenarioFixtureEvals({
9235
+ fixtures: evalOptions.fixtures,
9236
+ fixtureStore: evalOptions.fixtureStore,
9237
+ scenarios: evalOptions.scenarios
9238
+ });
9239
+ if ((statusOptions?.preferFixtureWorkflows ?? true) && fixtureReport.total > 0) {
9240
+ return {
9241
+ failed: fixtureReport.failed,
9242
+ source: "fixtures",
9243
+ status: fixtureReport.status,
9244
+ total: fixtureReport.total
9245
+ };
9246
+ }
9247
+ const liveReport = await runVoiceScenarioEvals({
9248
+ events,
9249
+ scenarios: evalOptions.scenarios
9250
+ });
9251
+ return {
9252
+ failed: liveReport.failed,
9253
+ source: "live",
9254
+ status: liveReport.status,
9255
+ total: liveReport.total
9256
+ };
9257
+ })(),
9258
+ options.providerHealth === false || !shouldInclude("providers") ? undefined : summarizeVoiceProviderHealth({
9259
+ events,
9260
+ providers: options.llmProviders
9261
+ }),
9262
+ options.sessions === false || !shouldInclude("sessions") ? undefined : summarizeVoiceSessions({
9263
+ events
9264
+ }),
9265
+ options.handoffs === false || !shouldInclude("handoffs") ? undefined : summarizeVoiceHandoffHealth({
9266
+ events
9267
+ })
9268
+ ]);
9269
+ const surfaces = {};
9270
+ const statuses = [];
9271
+ if (quality) {
9272
+ surfaces.quality = { status: quality.status };
9273
+ statuses.push(quality.status);
9274
+ }
9275
+ if (workflows) {
9276
+ const status = workflows.status;
9277
+ surfaces.workflows = {
9278
+ failed: workflows.failed,
9279
+ source: workflows.source,
9280
+ status,
9281
+ total: workflows.total
9282
+ };
9283
+ statuses.push(status);
9284
+ }
9285
+ if (providers) {
9286
+ const degraded = providers.filter((provider) => provider.status === "degraded" || provider.status === "rate-limited" || provider.status === "suppressed").length;
9287
+ const status = degraded > 0 ? "fail" : "pass";
9288
+ surfaces.providers = {
9289
+ degraded,
9290
+ status,
9291
+ total: providers.length
9292
+ };
9293
+ statuses.push(status);
9294
+ }
9295
+ if (sessions) {
9296
+ const failed = sessions.filter((session) => session.status === "failed").length;
9297
+ const status = failed > 0 ? "fail" : "pass";
9298
+ surfaces.sessions = {
9299
+ failed,
9300
+ status,
9301
+ total: sessions.length
9302
+ };
9303
+ statuses.push(status);
9304
+ }
9305
+ if (handoffs) {
9306
+ const status = handoffs.failed > 0 ? "fail" : "pass";
9307
+ surfaces.handoffs = {
9308
+ failed: handoffs.failed,
9309
+ status,
9310
+ total: handoffs.total
9311
+ };
9312
+ statuses.push(status);
9313
+ }
9314
+ return {
9315
+ checkedAt: Date.now(),
9316
+ links,
9317
+ status: statuses.includes("fail") ? "fail" : "pass",
9318
+ surfaces,
9319
+ ...countStatus(statuses)
9320
+ };
9321
+ };
9322
+ var createVoiceAppKitRoutes = (options) => {
9323
+ const routes = new Elysia11({
9324
+ name: options.name ?? "absolutejs-voice-app-kit"
9325
+ });
9326
+ const links = resolveLinks(options.links);
9327
+ const common = {
9328
+ headers: options.headers,
9329
+ store: options.store
9330
+ };
9331
+ const surfaces = [];
9332
+ if (options.appStatus !== false) {
9333
+ routes.get(options.appStatus?.path ?? "/app-kit/status", () => summarizeVoiceAppKitStatus(options));
9334
+ }
9335
+ if (options.providerHealth !== false) {
9336
+ surfaces.push("providerHealth");
9337
+ routes.use(createVoiceProviderHealthRoutes({
9338
+ ...common,
9339
+ providers: options.llmProviders,
9340
+ ...options.providerHealth
9341
+ }));
9342
+ }
9343
+ if (options.assistantHealth !== false) {
9344
+ surfaces.push("assistantHealth");
9345
+ routes.use(createVoiceAssistantHealthRoutes({
9346
+ ...common,
9347
+ providers: options.llmProviders,
9348
+ ...options.assistantHealth
9349
+ }));
9350
+ }
9351
+ if (options.quality !== false) {
9352
+ surfaces.push("quality");
9353
+ routes.use(createVoiceQualityRoutes({
9354
+ ...common,
9355
+ links: toBasicLinks(links),
9356
+ ...options.quality
9357
+ }));
9358
+ }
9359
+ if (options.evals !== false) {
9360
+ surfaces.push("evals");
9361
+ routes.use(createVoiceEvalRoutes({
9362
+ ...common,
9363
+ links: toBasicLinks(links),
9364
+ title: options.title ? `${options.title} Evals` : undefined,
9365
+ ...options.evals
9366
+ }));
9367
+ }
9368
+ if (options.sessions !== false) {
9369
+ surfaces.push("sessions");
9370
+ routes.use(createVoiceSessionListRoutes({
9371
+ ...common,
9372
+ htmlPath: "/sessions",
9373
+ path: "/api/voice-sessions",
9374
+ replayHref: "/sessions/:sessionId",
9375
+ ...options.sessions
9376
+ }));
9377
+ }
9378
+ if (options.sessionReplay !== false) {
9379
+ surfaces.push("sessionReplay");
9380
+ routes.use(createVoiceSessionReplayRoutes({
9381
+ ...common,
9382
+ htmlPath: "/sessions/:sessionId",
9383
+ path: "/api/voice-sessions/:sessionId/replay",
9384
+ ...options.sessionReplay
9385
+ }));
9386
+ }
9387
+ if (options.handoffs !== false) {
9388
+ surfaces.push("handoffs");
9389
+ routes.use(createVoiceHandoffHealthRoutes({
9390
+ ...common,
9391
+ htmlPath: "/handoffs",
9392
+ path: "/api/voice-handoffs",
9393
+ ...options.handoffs
9394
+ }));
9395
+ }
9396
+ if (options.diagnostics !== false) {
9397
+ surfaces.push("diagnostics");
9398
+ routes.use(createVoiceDiagnosticsRoutes({
9399
+ ...common,
9400
+ path: "/diagnostics",
9401
+ title: options.title ? `${options.title} Diagnostics` : undefined,
9402
+ ...options.diagnostics
9403
+ }));
9404
+ }
9405
+ if (options.resilience !== false) {
9406
+ surfaces.push("resilience");
9407
+ routes.use(createVoiceResilienceRoutes({
9408
+ ...common,
9409
+ links: toResilienceLinks(links),
9410
+ llmProviders: options.llmProviders,
9411
+ sttProviders: options.sttProviders,
9412
+ title: options.title ? `${options.title} Resilience` : undefined,
9413
+ ttsProviders: options.ttsProviders,
9414
+ ...options.resilience
9415
+ }));
9416
+ }
9417
+ if (options.opsConsole !== false) {
9418
+ surfaces.push("opsConsole");
9419
+ routes.use(createVoiceOpsConsoleRoutes({
9420
+ ...common,
9421
+ links: toOpsLinks(links),
9422
+ llmProviders: options.llmProviders,
9423
+ sttProviders: options.sttProviders,
9424
+ title: options.title,
9425
+ ttsProviders: options.ttsProviders,
9426
+ ...options.opsConsole
9427
+ }));
9428
+ }
9429
+ return {
9430
+ links,
9431
+ routes,
9432
+ surfaces,
9433
+ use: routes.use.bind(routes)
9434
+ };
9435
+ };
9436
+ var createVoiceAppKit = createVoiceAppKitRoutes;
9437
+ // src/workflowContract.ts
9438
+ var getObject2 = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : undefined;
9439
+ var getPathValue2 = (value, path) => {
9440
+ let current = value;
9441
+ for (const part of path.split(".").filter(Boolean)) {
9442
+ const record = getObject2(current);
9443
+ if (!record || !(part in record)) {
9444
+ return;
9445
+ }
9446
+ current = record[part];
9447
+ }
9448
+ return current;
9449
+ };
9450
+ var hasValue = (value, match) => {
9451
+ switch (match) {
9452
+ case "boolean":
9453
+ return typeof value === "boolean";
9454
+ case "number":
9455
+ return typeof value === "number" && Number.isFinite(value);
9456
+ case "string":
9457
+ return typeof value === "string";
9458
+ case "truthy":
9459
+ return Boolean(value);
9460
+ case "non-empty":
9461
+ default:
9462
+ return Array.isArray(value) ? value.length > 0 : typeof value === "string" ? value.trim().length > 0 : value !== undefined && value !== null;
9463
+ }
9464
+ };
9465
+ var resolveOutcome2 = (routeResult) => {
9466
+ if (routeResult.complete)
9467
+ return "complete";
9468
+ if (routeResult.transfer)
9469
+ return "transfer";
9470
+ if (routeResult.escalate)
9471
+ return "escalate";
9472
+ if (routeResult.voicemail)
9473
+ return "voicemail";
9474
+ if (routeResult.noAnswer)
9475
+ return "no-answer";
9476
+ return;
9477
+ };
9478
+ var validateVoiceWorkflowRouteResult = (definition, routeResult) => {
9479
+ const issues = [];
9480
+ const requiredFields = (definition.fields ?? []).filter((field) => field.required !== false).map((field) => field.path);
9481
+ const missingFields = [];
9482
+ const outcome = resolveOutcome2(routeResult);
9483
+ if (definition.outcome && outcome !== definition.outcome) {
9484
+ issues.push({
9485
+ code: "workflow.outcome_mismatch",
9486
+ message: `Expected workflow outcome ${definition.outcome}, saw ${outcome ?? "none"}.`
9487
+ });
9488
+ }
9489
+ for (const field of definition.fields ?? []) {
9490
+ if (field.required === false)
9491
+ continue;
9492
+ const paths = [field.path, ...field.aliases ?? []];
9493
+ const present = paths.some((path) => hasValue(getPathValue2(routeResult.result, path), field.match ?? "non-empty"));
9494
+ if (!present) {
9495
+ missingFields.push(field.path);
9496
+ issues.push({
9497
+ code: "workflow.missing_field",
9498
+ field: field.path,
9499
+ message: `Missing required workflow field: ${field.label ?? field.path}.`
9500
+ });
9501
+ }
9502
+ }
9503
+ issues.push(...definition.validate?.({
9504
+ result: routeResult.result,
9505
+ routeResult
9506
+ }) ?? []);
9507
+ return {
9508
+ contractId: definition.id,
9509
+ issues,
9510
+ missingFields,
9511
+ outcome,
9512
+ pass: issues.length === 0,
9513
+ requiredFields
9514
+ };
9515
+ };
9516
+ var createVoiceWorkflowScenario = (definition, overrides = {}) => ({
9517
+ description: definition.description,
9518
+ forbiddenHandoffActions: definition.forbiddenHandoffActions,
9519
+ id: definition.id,
9520
+ label: definition.label,
9521
+ maxProviderErrors: definition.maxProviderErrors,
9522
+ maxSessionErrors: definition.maxSessionErrors,
9523
+ minSessions: definition.minSessions,
9524
+ minTurns: definition.minTurns,
9525
+ requiredAssistantIncludes: definition.requiredAssistantIncludes,
9526
+ requiredDisposition: definition.requiredDisposition,
9527
+ requiredHandoffActions: definition.requiredHandoffActions,
9528
+ requiredLifecycleTypes: definition.requiredLifecycleTypes,
9529
+ requiredTranscriptIncludes: definition.requiredTranscriptIncludes,
9530
+ requiredWorkflowContracts: [definition.id],
9531
+ scenarioId: definition.scenarioId,
9532
+ ...overrides
9533
+ });
9534
+ var createVoiceWorkflowContract = (definition) => ({
9535
+ assertRouteResult: (routeResult) => {
9536
+ const validation = validateVoiceWorkflowRouteResult(definition, routeResult);
9537
+ if (!validation.pass) {
9538
+ throw new Error(`Voice workflow contract ${definition.id} failed: ${validation.issues.map((issue) => issue.message).join(" ")}`);
9539
+ }
9540
+ },
9541
+ definition,
9542
+ toScenarioEval: (overrides) => createVoiceWorkflowScenario(definition, overrides),
9543
+ validateRouteResult: (routeResult) => validateVoiceWorkflowRouteResult(definition, routeResult)
9544
+ });
9545
+ var presetDefinitions = {
9546
+ "appointment-booking": {
9547
+ description: "Appointment booking should complete with enough identity, appointment, and follow-up details to act on.",
9548
+ fields: [
9549
+ { aliases: ["name", "customer.name"], label: "Caller name", path: "caller.name" },
9550
+ {
9551
+ aliases: ["phone", "customer.phone"],
9552
+ label: "Caller phone",
9553
+ path: "caller.phone"
9554
+ },
9555
+ {
9556
+ aliases: ["appointment.start", "appointment.time", "scheduledAt"],
9557
+ label: "Appointment time",
9558
+ path: "appointment.startsAt"
9559
+ },
9560
+ {
9561
+ aliases: ["summary", "assistantSummary"],
9562
+ label: "Summary",
9563
+ path: "appointment.summary"
9564
+ }
9565
+ ],
9566
+ id: "appointment-booking",
9567
+ label: "Appointment booking",
9568
+ outcome: "complete",
9569
+ requiredDisposition: "completed"
9570
+ },
9571
+ "lead-qualification": {
9572
+ description: "Lead qualification should complete with contact, need, qualification, and next-step fields.",
9573
+ fields: [
9574
+ { aliases: ["name", "lead.name"], label: "Lead name", path: "contact.name" },
9575
+ {
9576
+ aliases: ["email", "lead.email"],
9577
+ label: "Lead email",
9578
+ path: "contact.email"
9579
+ },
9580
+ {
9581
+ aliases: ["need", "pain", "summary"],
9582
+ label: "Need",
9583
+ path: "qualification.need"
9584
+ },
9585
+ {
9586
+ aliases: ["qualified", "qualification.qualified"],
9587
+ label: "Qualified",
9588
+ match: "boolean",
9589
+ path: "qualification.isQualified"
9590
+ },
9591
+ {
9592
+ aliases: ["nextStep", "followUp"],
9593
+ label: "Next step",
9594
+ path: "qualification.nextStep"
9595
+ }
9596
+ ],
9597
+ id: "lead-qualification",
9598
+ label: "Lead qualification",
9599
+ outcome: "complete",
9600
+ requiredDisposition: "completed"
9601
+ },
9602
+ "support-triage": {
9603
+ description: "Support triage should capture identity, issue summary, severity, and the operational follow-up.",
9604
+ fields: [
9605
+ {
9606
+ aliases: ["name", "customer.name"],
9607
+ label: "Customer name",
9608
+ path: "customer.name"
9609
+ },
9610
+ {
9611
+ aliases: ["issue", "summary", "assistantSummary"],
9612
+ label: "Issue summary",
9613
+ path: "issue.summary"
9614
+ },
9615
+ {
9616
+ aliases: ["priority", "severity"],
9617
+ label: "Severity",
9618
+ path: "issue.severity"
9619
+ },
9620
+ {
9621
+ aliases: ["nextStep", "task.title"],
9622
+ label: "Next step",
9623
+ path: "resolution.nextStep"
9624
+ }
9625
+ ],
9626
+ id: "support-triage",
9627
+ label: "Support triage",
9628
+ outcome: "complete",
9629
+ requiredDisposition: "completed"
9630
+ },
9631
+ "transfer-handoff": {
9632
+ description: "Transfer handoff should produce a routed transfer plus handoff evidence.",
9633
+ fields: [
9634
+ {
9635
+ aliases: ["target", "callTarget"],
9636
+ label: "Transfer target",
9637
+ path: "transfer.target"
9638
+ },
9639
+ {
9640
+ aliases: ["reason", "callReason"],
9641
+ label: "Transfer reason",
9642
+ path: "transfer.reason"
9643
+ },
9644
+ {
9645
+ aliases: ["summary", "assistantSummary"],
9646
+ label: "Transfer summary",
9647
+ path: "transfer.summary"
9648
+ }
9649
+ ],
9650
+ id: "transfer-handoff",
9651
+ label: "Transfer handoff",
9652
+ outcome: "transfer",
9653
+ requiredDisposition: "transferred",
9654
+ requiredHandoffActions: ["transfer"]
9655
+ },
9656
+ "voicemail-callback": {
9657
+ description: "Voicemail callback should preserve enough caller and callback context for follow-up.",
9658
+ fields: [
9659
+ {
9660
+ aliases: ["name", "caller.name"],
9661
+ label: "Caller name",
9662
+ path: "voicemail.callerName"
9663
+ },
9664
+ {
9665
+ aliases: ["phone", "caller.phone"],
9666
+ label: "Callback phone",
9667
+ path: "voicemail.callbackPhone"
9668
+ },
9669
+ {
9670
+ aliases: ["message", "summary", "assistantSummary"],
9671
+ label: "Voicemail summary",
9672
+ path: "voicemail.summary"
9673
+ }
9674
+ ],
9675
+ id: "voicemail-callback",
9676
+ label: "Voicemail callback",
9677
+ outcome: "voicemail",
9678
+ requiredDisposition: "voicemail",
9679
+ requiredHandoffActions: ["voicemail"]
9680
+ }
9681
+ };
9682
+ var createVoiceWorkflowContractPreset = (name, options = {}) => {
9683
+ const preset = presetDefinitions[name];
9684
+ return createVoiceWorkflowContract({
9685
+ ...preset,
9686
+ ...options,
9687
+ fields: options.fields ?? preset.fields,
9688
+ id: options.id ?? preset.id
9689
+ });
9690
+ };
9691
+ var recordVoiceWorkflowContractTrace = async (input) => input.store.append({
9692
+ at: input.at ?? Date.now(),
9693
+ payload: {
9694
+ contractId: input.contractId ?? input.validation.contractId,
9695
+ issues: input.validation.issues,
9696
+ missingFields: input.validation.missingFields,
9697
+ outcome: input.validation.outcome,
9698
+ requiredFields: input.validation.requiredFields,
9699
+ status: input.validation.pass ? "pass" : "fail"
9700
+ },
9701
+ scenarioId: input.scenarioId,
9702
+ sessionId: input.sessionId,
9703
+ traceId: input.traceId,
9704
+ turnId: input.turnId,
9705
+ type: "workflow.contract"
6770
9706
  });
6771
-
9707
+ var createVoiceWorkflowContractHandler = (input) => {
9708
+ return async (session, turn, api, context) => {
9709
+ const legacyHandler = input.handler;
9710
+ const objectHandler = input.handler;
9711
+ const result = input.handler.length >= 4 ? await legacyHandler(session, turn, api, context) : await objectHandler({ api, context, session, turn });
9712
+ if (!result)
9713
+ return result;
9714
+ const resolved = input.resolveContract?.({ context, result, session, turn }) ?? input.contract;
9715
+ if (!resolved)
9716
+ return result;
9717
+ const contract = "validateRouteResult" in resolved ? resolved : createVoiceWorkflowContract(resolved);
9718
+ const validation = contract.validateRouteResult(result);
9719
+ if (input.store) {
9720
+ await recordVoiceWorkflowContractTrace({
9721
+ scenarioId: session.scenarioId,
9722
+ sessionId: session.id,
9723
+ store: input.store,
9724
+ turnId: turn.id,
9725
+ validation
9726
+ });
9727
+ }
9728
+ return result;
9729
+ };
9730
+ };
6772
9731
  // src/fileStore.ts
9732
+ import { mkdir as mkdir2, readFile, readdir, rename, rm, writeFile } from "fs/promises";
9733
+ import { join } from "path";
6773
9734
  var listJsonFiles = async (directory) => {
6774
9735
  try {
6775
9736
  const entries = await readdir(directory, {
@@ -6788,7 +9749,7 @@ var resolveFilePath = (directory, id) => join(directory, encodeStoreId(id));
6788
9749
  var createMemoryStoreId = (input) => `${input.assistantId}:${input.namespace}:${input.key}`;
6789
9750
  var readJsonFile = async (path) => JSON.parse(await readFile(path, "utf8"));
6790
9751
  var writeJsonFile = async (path, value, options) => {
6791
- await mkdir(options.directory, {
9752
+ await mkdir2(options.directory, {
6792
9753
  recursive: true
6793
9754
  });
6794
9755
  const tempPath = `${path}.${crypto.randomUUID()}.tmp`;
@@ -7085,6 +10046,47 @@ var createStoredVoiceExternalObjectMap = (mapping) => createVoiceExternalObjectM
7085
10046
  sourceType: mapping.sourceType
7086
10047
  });
7087
10048
  // src/modelAdapters.ts
10049
+ var resolveVoiceProviderRoutingPolicyPreset = (preset, options = {}) => {
10050
+ switch (preset) {
10051
+ case "balanced":
10052
+ return {
10053
+ fallbackMode: "provider-error",
10054
+ strategy: "balanced",
10055
+ weights: {
10056
+ cost: 1,
10057
+ latencyMs: 0.005,
10058
+ priority: 1,
10059
+ quality: 10,
10060
+ ...options.weights
10061
+ },
10062
+ ...options
10063
+ };
10064
+ case "cost-cap":
10065
+ return {
10066
+ fallbackMode: "provider-error",
10067
+ strategy: "prefer-cheapest",
10068
+ ...options
10069
+ };
10070
+ case "cost-first":
10071
+ return {
10072
+ fallbackMode: "provider-error",
10073
+ strategy: "prefer-cheapest",
10074
+ ...options
10075
+ };
10076
+ case "latency-first":
10077
+ return {
10078
+ fallbackMode: "provider-error",
10079
+ strategy: "prefer-fastest",
10080
+ ...options
10081
+ };
10082
+ case "quality-first":
10083
+ return {
10084
+ fallbackMode: "provider-error",
10085
+ strategy: "quality-first",
10086
+ ...options
10087
+ };
10088
+ }
10089
+ };
7088
10090
  var OUTPUT_SCHEMA = {
7089
10091
  additionalProperties: false,
7090
10092
  properties: {
@@ -7152,10 +10154,15 @@ var OUTPUT_SCHEMA = {
7152
10154
  },
7153
10155
  type: "object"
7154
10156
  };
7155
- var ROUTE_RESULT_INSTRUCTION = "Return a JSON object with assistantText, complete, transfer, escalate, voicemail, noAnswer, and result when you are not calling tools.";
10157
+ var ROUTE_RESULT_INSTRUCTION = "Return only a JSON object with assistantText, complete, transfer, escalate, voicemail, noAnswer, and result when you are not calling tools. Only set transfer, escalate, voicemail, or noAnswer when the user explicitly asks for that lifecycle outcome or a tool result says that exact outcome. Do not infer voicemail from generic words like voice, voice app, or voice integration.";
10158
+ var stripJSONCodeFence = (value) => {
10159
+ const trimmed = value.trim();
10160
+ const match = trimmed.match(/^```(?:json)?\s*([\s\S]*?)\s*```$/i);
10161
+ return match?.[1]?.trim() ?? value;
10162
+ };
7156
10163
  var parseJSON = (value) => {
7157
10164
  try {
7158
- const parsed = JSON.parse(value);
10165
+ const parsed = JSON.parse(stripJSONCodeFence(value));
7159
10166
  return parsed && typeof parsed === "object" ? parsed : {};
7160
10167
  } catch {
7161
10168
  return {
@@ -7170,11 +10177,27 @@ var parseJSONValue = (value) => {
7170
10177
  return value;
7171
10178
  }
7172
10179
  };
10180
+
10181
+ class VoiceProviderTimeoutError extends Error {
10182
+ provider;
10183
+ timeoutMs;
10184
+ constructor(provider, timeoutMs) {
10185
+ super(`Voice provider ${provider} exceeded ${timeoutMs}ms latency budget.`);
10186
+ this.name = "VoiceProviderTimeoutError";
10187
+ this.provider = provider;
10188
+ this.timeoutMs = timeoutMs;
10189
+ }
10190
+ }
7173
10191
  var getMessageToolCalls = (message) => {
7174
10192
  const toolCalls = message.metadata?.toolCalls;
7175
10193
  return Array.isArray(toolCalls) ? toolCalls.filter((toolCall) => toolCall && typeof toolCall === "object" && typeof toolCall.name === "string") : [];
7176
10194
  };
7177
10195
  var createHTTPError = (provider, response) => new Error(`${provider} voice assistant model failed: HTTP ${response.status}`);
10196
+ var sleep4 = (ms) => new Promise((resolve2) => {
10197
+ setTimeout(resolve2, ms);
10198
+ });
10199
+ var errorMessage = (error) => error instanceof Error ? error.message : String(error);
10200
+ var defaultIsRateLimitError = (error) => /(\b429\b|rate limit|quota|too many requests)/i.test(errorMessage(error));
7178
10201
  var normalizeRouteOutput = (output) => {
7179
10202
  const result = {};
7180
10203
  if (typeof output.assistantText === "string") {
@@ -7228,19 +10251,272 @@ var createJSONVoiceAssistantModel = (options) => ({
7228
10251
  return options.mapOutput?.(output) ?? normalizeRouteOutput(output);
7229
10252
  }
7230
10253
  });
7231
- var messageToOpenAIInput = (message) => {
7232
- if (message.role === "tool") {
10254
+ var createVoiceProviderRouter = (options) => {
10255
+ const providerIds = Object.keys(options.providers);
10256
+ const firstProvider = providerIds[0];
10257
+ const policy = typeof options.policy === "string" ? options.policy === "balanced" || options.policy === "cost-cap" || options.policy === "cost-first" || options.policy === "latency-first" || options.policy === "quality-first" ? resolveVoiceProviderRoutingPolicyPreset(options.policy) : {
10258
+ strategy: options.policy
10259
+ } : options.policy;
10260
+ const strategy = policy?.strategy ?? "prefer-selected";
10261
+ const fallbackMode = policy?.fallbackMode ?? options.fallbackMode ?? "provider-error";
10262
+ const healthOptions = typeof options.providerHealth === "object" ? options.providerHealth : options.providerHealth ? {} : undefined;
10263
+ const healthState = new Map;
10264
+ const now = () => healthOptions?.now?.() ?? Date.now();
10265
+ const failureThreshold = Math.max(1, healthOptions?.failureThreshold ?? 1);
10266
+ const cooldownMs = Math.max(0, healthOptions?.cooldownMs ?? 30000);
10267
+ const rateLimitCooldownMs = Math.max(0, healthOptions?.rateLimitCooldownMs ?? 60000);
10268
+ const getProviderTimeoutMs = (provider) => {
10269
+ const timeoutMs = options.providerProfiles?.[provider]?.timeoutMs ?? options.timeoutMs;
10270
+ return typeof timeoutMs === "number" && Number.isFinite(timeoutMs) && timeoutMs > 0 ? timeoutMs : undefined;
10271
+ };
10272
+ const getHealth = (provider) => {
10273
+ const existing = healthState.get(provider);
10274
+ if (existing) {
10275
+ return existing;
10276
+ }
10277
+ const next = {
10278
+ consecutiveFailures: 0,
10279
+ provider,
10280
+ status: "healthy"
10281
+ };
10282
+ healthState.set(provider, next);
10283
+ return next;
10284
+ };
10285
+ const cloneHealth = (provider) => {
10286
+ if (!healthOptions) {
10287
+ return;
10288
+ }
7233
10289
  return {
7234
- call_id: message.toolCallId ?? message.name ?? crypto.randomUUID(),
7235
- output: message.content,
7236
- type: "function_call_output"
10290
+ ...getHealth(provider)
7237
10291
  };
7238
- }
10292
+ };
10293
+ const getSuppressionRemainingMs = (provider) => {
10294
+ if (!healthOptions) {
10295
+ return;
10296
+ }
10297
+ const suppressedUntil = getHealth(provider).suppressedUntil;
10298
+ return typeof suppressedUntil === "number" ? Math.max(0, suppressedUntil - now()) : undefined;
10299
+ };
10300
+ const isSuppressed = (provider) => {
10301
+ if (!healthOptions) {
10302
+ return false;
10303
+ }
10304
+ const health = getHealth(provider);
10305
+ return typeof health.suppressedUntil === "number" && health.suppressedUntil > now();
10306
+ };
10307
+ const recordProviderSuccess = (provider) => {
10308
+ if (!healthOptions) {
10309
+ return;
10310
+ }
10311
+ const health = getHealth(provider);
10312
+ health.consecutiveFailures = 0;
10313
+ health.status = "healthy";
10314
+ health.suppressedUntil = undefined;
10315
+ return cloneHealth(provider);
10316
+ };
10317
+ const recordProviderError = (provider, isProviderError, rateLimited) => {
10318
+ if (!healthOptions || !isProviderError) {
10319
+ return cloneHealth(provider);
10320
+ }
10321
+ const currentTime = now();
10322
+ const health = getHealth(provider);
10323
+ health.consecutiveFailures += 1;
10324
+ health.lastFailureAt = currentTime;
10325
+ if (rateLimited) {
10326
+ health.lastRateLimitedAt = currentTime;
10327
+ }
10328
+ if (rateLimited || health.consecutiveFailures >= failureThreshold) {
10329
+ health.status = "suppressed";
10330
+ health.suppressedUntil = currentTime + (rateLimited ? rateLimitCooldownMs : cooldownMs);
10331
+ }
10332
+ return cloneHealth(provider);
10333
+ };
10334
+ const resolveAllowedProviders = async (input) => {
10335
+ const allowProviders = policy?.allowProviders ?? options.allowProviders;
10336
+ const allowed = typeof allowProviders === "function" ? await allowProviders(input) : allowProviders;
10337
+ return new Set(allowed ?? providerIds);
10338
+ };
10339
+ const passesBudgetFilters = (provider) => {
10340
+ const profile = options.providerProfiles?.[provider];
10341
+ if (typeof policy?.maxCost === "number" && typeof profile?.cost === "number" && profile.cost > policy.maxCost) {
10342
+ return false;
10343
+ }
10344
+ if (typeof policy?.maxLatencyMs === "number" && typeof profile?.latencyMs === "number" && profile.latencyMs > policy.maxLatencyMs) {
10345
+ return false;
10346
+ }
10347
+ if (typeof policy?.minQuality === "number" && typeof profile?.quality === "number" && profile.quality < policy.minQuality) {
10348
+ return false;
10349
+ }
10350
+ return true;
10351
+ };
10352
+ const getBalancedScore = (provider) => {
10353
+ const profile = options.providerProfiles?.[provider];
10354
+ if (policy?.scoreProvider) {
10355
+ return policy.scoreProvider(provider, profile);
10356
+ }
10357
+ const weights = policy?.weights ?? {};
10358
+ 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);
10359
+ };
10360
+ const sortProviders = (providers) => {
10361
+ if (strategy !== "prefer-cheapest" && strategy !== "prefer-fastest" && strategy !== "quality-first" && strategy !== "balanced") {
10362
+ return providers;
10363
+ }
10364
+ return [...providers].sort((left, right) => {
10365
+ const leftProfile = options.providerProfiles?.[left];
10366
+ const rightProfile = options.providerProfiles?.[right];
10367
+ if (strategy === "quality-first") {
10368
+ 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);
10369
+ }
10370
+ if (strategy === "balanced") {
10371
+ return getBalancedScore(left) - getBalancedScore(right);
10372
+ }
10373
+ const leftValue = strategy === "prefer-cheapest" ? leftProfile?.cost ?? Number.MAX_SAFE_INTEGER : leftProfile?.latencyMs ?? Number.MAX_SAFE_INTEGER;
10374
+ const rightValue = strategy === "prefer-cheapest" ? rightProfile?.cost ?? Number.MAX_SAFE_INTEGER : rightProfile?.latencyMs ?? Number.MAX_SAFE_INTEGER;
10375
+ return leftValue - rightValue || (leftProfile?.priority ?? Number.MAX_SAFE_INTEGER) - (rightProfile?.priority ?? Number.MAX_SAFE_INTEGER);
10376
+ });
10377
+ };
10378
+ const resolveOrder = async (input) => {
10379
+ const selectedProvider = await options.selectProvider?.(input);
10380
+ const allowedProviders = await resolveAllowedProviders(input);
10381
+ const fallbackOrder = typeof options.fallback === "function" ? await options.fallback(input) : options.fallback;
10382
+ const allowedRankedProviders = sortProviders([
10383
+ ...fallbackOrder ?? providerIds
10384
+ ]).filter((provider) => allowedProviders.has(provider));
10385
+ const rankedProviders = allowedRankedProviders.filter(passesBudgetFilters);
10386
+ const healthyRankedProviders = healthOptions ? rankedProviders.filter((provider) => !isSuppressed(provider)) : rankedProviders;
10387
+ const candidateRankedProviders = healthyRankedProviders.length ? healthyRankedProviders : rankedProviders;
10388
+ const preferred = selectedProvider && allowedProviders.has(selectedProvider) && passesBudgetFilters(selectedProvider) && (!healthOptions || !isSuppressed(selectedProvider)) ? selectedProvider : candidateRankedProviders[0] ?? firstProvider;
10389
+ const seen = new Set;
10390
+ const order = [];
10391
+ const candidates = strategy === "ordered" ? candidateRankedProviders : [
10392
+ preferred,
10393
+ ...candidateRankedProviders,
10394
+ ...providerIds.filter((provider) => !healthOptions || !isSuppressed(provider))
10395
+ ];
10396
+ for (const provider of candidates) {
10397
+ if (!provider || seen.has(provider) || !allowedProviders.has(provider) || !options.providers[provider]) {
10398
+ continue;
10399
+ }
10400
+ seen.add(provider);
10401
+ order.push(provider);
10402
+ }
10403
+ return {
10404
+ order,
10405
+ selectedProvider: preferred
10406
+ };
10407
+ };
10408
+ const emit = async (event, input) => {
10409
+ await options.onProviderEvent?.(event, input);
10410
+ };
10411
+ const runProvider = async (provider, model, input) => {
10412
+ const timeoutMs = getProviderTimeoutMs(provider);
10413
+ if (!timeoutMs) {
10414
+ return model.generate(input);
10415
+ }
10416
+ let timeout;
10417
+ try {
10418
+ return await Promise.race([
10419
+ model.generate(input),
10420
+ new Promise((_, reject) => {
10421
+ timeout = setTimeout(() => reject(new VoiceProviderTimeoutError(provider, timeoutMs)), timeoutMs);
10422
+ })
10423
+ ]);
10424
+ } finally {
10425
+ if (timeout) {
10426
+ clearTimeout(timeout);
10427
+ }
10428
+ }
10429
+ };
7239
10430
  return {
7240
- content: message.content,
7241
- role: message.role === "system" ? "developer" : message.role
10431
+ generate: async (input) => {
10432
+ const { order, selectedProvider } = await resolveOrder(input);
10433
+ if (!selectedProvider || order.length === 0) {
10434
+ throw new Error("Voice provider router has no available providers.");
10435
+ }
10436
+ let lastError;
10437
+ for (const [index, provider] of order.entries()) {
10438
+ const model = options.providers[provider];
10439
+ if (!model) {
10440
+ continue;
10441
+ }
10442
+ const startedAt = Date.now();
10443
+ try {
10444
+ const output = await runProvider(provider, model, input);
10445
+ const providerHealth = recordProviderSuccess(provider);
10446
+ await emit({
10447
+ at: Date.now(),
10448
+ attempt: index + 1,
10449
+ elapsedMs: Date.now() - startedAt,
10450
+ fallbackProvider: provider === selectedProvider ? undefined : provider,
10451
+ latencyBudgetMs: getProviderTimeoutMs(provider),
10452
+ provider,
10453
+ providerHealth,
10454
+ recovered: provider !== selectedProvider,
10455
+ selectedProvider,
10456
+ status: provider === selectedProvider ? "success" : "fallback"
10457
+ }, input);
10458
+ return output;
10459
+ } catch (error) {
10460
+ lastError = error;
10461
+ const hasNextProvider = index < order.length - 1;
10462
+ const isProviderError = options.isProviderError?.(error, provider) ?? true;
10463
+ const timedOut = options.isTimeoutError?.(error, provider) ?? error instanceof VoiceProviderTimeoutError;
10464
+ const rateLimited = options.isRateLimitError?.(error, provider) ?? defaultIsRateLimitError(error);
10465
+ const shouldFallback = fallbackMode === "provider-error" ? isProviderError : fallbackMode === "rate-limit" ? isProviderError && rateLimited : false;
10466
+ const providerHealth = recordProviderError(provider, isProviderError, rateLimited);
10467
+ const nextProvider = hasNextProvider ? order[index + 1] : undefined;
10468
+ await emit({
10469
+ at: Date.now(),
10470
+ attempt: index + 1,
10471
+ elapsedMs: Date.now() - startedAt,
10472
+ error: errorMessage(error),
10473
+ fallbackProvider: shouldFallback ? nextProvider : undefined,
10474
+ latencyBudgetMs: getProviderTimeoutMs(provider),
10475
+ provider,
10476
+ providerHealth,
10477
+ rateLimited,
10478
+ selectedProvider,
10479
+ suppressionRemainingMs: getSuppressionRemainingMs(provider),
10480
+ suppressedUntil: providerHealth?.suppressedUntil,
10481
+ status: "error",
10482
+ timedOut
10483
+ }, input);
10484
+ if (!hasNextProvider || !shouldFallback) {
10485
+ throw error;
10486
+ }
10487
+ }
10488
+ }
10489
+ throw lastError ?? new Error("Voice provider router did not run a provider.");
10490
+ }
7242
10491
  };
7243
10492
  };
10493
+ var messageToOpenAIInput = (message) => {
10494
+ if (message.role === "tool") {
10495
+ return [
10496
+ {
10497
+ call_id: message.toolCallId ?? message.name ?? crypto.randomUUID(),
10498
+ output: message.content,
10499
+ type: "function_call_output"
10500
+ }
10501
+ ];
10502
+ }
10503
+ const toolCalls = getMessageToolCalls(message);
10504
+ if (message.role === "assistant" && toolCalls.length) {
10505
+ return toolCalls.map((toolCall) => ({
10506
+ arguments: JSON.stringify(toolCall.args),
10507
+ call_id: toolCall.id ?? crypto.randomUUID(),
10508
+ name: toolCall.name,
10509
+ type: "function_call"
10510
+ }));
10511
+ }
10512
+ return [
10513
+ {
10514
+ content: message.content,
10515
+ role: message.role === "system" ? "developer" : message.role
10516
+ }
10517
+ ];
10518
+ };
10519
+ var messagesToOpenAIInput = (messages) => messages.flatMap(messageToOpenAIInput);
7244
10520
  var messageToAnthropicMessage = (message) => {
7245
10521
  if (message.role === "system") {
7246
10522
  return;
@@ -7410,7 +10686,7 @@ var createOpenAIVoiceAssistantModel = (options) => {
7410
10686
  generate: async (input) => {
7411
10687
  const response = await fetchImpl(`${baseUrl.replace(/\/$/, "")}/responses`, {
7412
10688
  body: JSON.stringify({
7413
- input: input.messages.map(messageToOpenAIInput),
10689
+ input: messagesToOpenAIInput(input.messages),
7414
10690
  instructions: [
7415
10691
  input.system,
7416
10692
  "Return a JSON object with assistantText, complete, transfer, escalate, voicemail, noAnswer, and result when you are not calling tools."
@@ -7574,64 +10850,492 @@ var extractGeminiToolCalls = (response) => {
7574
10850
  }
7575
10851
  return toolCalls;
7576
10852
  };
7577
- var createGeminiVoiceAssistantModel = (options) => {
7578
- const fetchImpl = options.fetch ?? globalThis.fetch;
7579
- const baseUrl = options.baseUrl ?? "https://generativelanguage.googleapis.com/v1beta";
7580
- const model = options.model ?? "gemini-2.5-flash";
10853
+ var createGeminiVoiceAssistantModel = (options) => {
10854
+ const fetchImpl = options.fetch ?? globalThis.fetch;
10855
+ const baseUrl = options.baseUrl ?? "https://generativelanguage.googleapis.com/v1beta";
10856
+ const model = options.model ?? "gemini-2.5-flash";
10857
+ const maxRetries = Math.max(0, options.maxRetries ?? 2);
10858
+ return {
10859
+ generate: async (input) => {
10860
+ const endpoint = `${baseUrl.replace(/\/$/, "")}/models/${encodeURIComponent(model)}:generateContent?key=${encodeURIComponent(options.apiKey)}`;
10861
+ let response;
10862
+ for (let attempt = 0;attempt <= maxRetries; attempt += 1) {
10863
+ response = await fetchImpl(endpoint, {
10864
+ body: JSON.stringify({
10865
+ contents: input.messages.map(messageToGeminiContent).filter(Boolean),
10866
+ generationConfig: {
10867
+ maxOutputTokens: options.maxOutputTokens,
10868
+ ...input.tools.length ? {} : {
10869
+ responseMimeType: "application/json",
10870
+ responseSchema: toGeminiSchema(OUTPUT_SCHEMA)
10871
+ },
10872
+ temperature: options.temperature
10873
+ },
10874
+ systemInstruction: {
10875
+ parts: [
10876
+ {
10877
+ text: [input.system, ROUTE_RESULT_INSTRUCTION].filter(Boolean).join(`
10878
+
10879
+ `)
10880
+ }
10881
+ ]
10882
+ },
10883
+ tools: input.tools.length ? [
10884
+ {
10885
+ functionDeclarations: input.tools.map((tool) => ({
10886
+ description: tool.description,
10887
+ name: tool.name,
10888
+ parameters: toGeminiSchema(tool.parameters ?? {
10889
+ additionalProperties: true,
10890
+ type: "object"
10891
+ })
10892
+ }))
10893
+ }
10894
+ ] : undefined
10895
+ }),
10896
+ headers: {
10897
+ "content-type": "application/json"
10898
+ },
10899
+ method: "POST"
10900
+ });
10901
+ if (response.ok || response.status !== 429 && response.status < 500 || attempt === maxRetries) {
10902
+ break;
10903
+ }
10904
+ const retryAfter = Number(response.headers.get("retry-after"));
10905
+ await sleep4(Number.isFinite(retryAfter) && retryAfter > 0 ? retryAfter * 1000 : 500 * 2 ** attempt);
10906
+ }
10907
+ if (!response) {
10908
+ throw new Error("Gemini voice assistant model failed: no response");
10909
+ }
10910
+ if (!response.ok) {
10911
+ throw createHTTPError("Gemini", response);
10912
+ }
10913
+ const body = await response.json();
10914
+ if (body.usageMetadata && typeof body.usageMetadata === "object") {
10915
+ await options.onUsage?.(body.usageMetadata);
10916
+ }
10917
+ const toolCalls = extractGeminiToolCalls(body);
10918
+ if (toolCalls.length) {
10919
+ return {
10920
+ assistantText: extractGeminiText(body) || undefined,
10921
+ toolCalls
10922
+ };
10923
+ }
10924
+ return normalizeRouteOutput(parseJSON(extractGeminiText(body)));
10925
+ }
10926
+ };
10927
+ };
10928
+ // src/providerAdapters.ts
10929
+ class VoiceIOProviderTimeoutError extends Error {
10930
+ provider;
10931
+ timeoutMs;
10932
+ constructor(kind, provider, timeoutMs) {
10933
+ super(`Voice ${kind} provider ${provider} exceeded ${timeoutMs}ms latency budget.`);
10934
+ this.name = "VoiceIOProviderTimeoutError";
10935
+ this.provider = provider;
10936
+ this.timeoutMs = timeoutMs;
10937
+ }
10938
+ }
10939
+ var errorMessage2 = (error) => error instanceof Error ? error.message : String(error);
10940
+ var createEmitter = () => {
10941
+ const listeners = new Map;
10942
+ return {
10943
+ emit: async (event, payload) => {
10944
+ await Promise.all([...listeners.get(event) ?? []].map((handler) => Promise.resolve(handler(payload))));
10945
+ },
10946
+ on: (event, handler) => {
10947
+ const set = listeners.get(event) ?? new Set;
10948
+ set.add(handler);
10949
+ listeners.set(event, set);
10950
+ return () => {
10951
+ set.delete(handler);
10952
+ };
10953
+ }
10954
+ };
10955
+ };
10956
+ var getTimeoutMs = (options, provider) => {
10957
+ const timeoutMs = options.providerProfiles?.[provider]?.timeoutMs ?? options.timeoutMs;
10958
+ return typeof timeoutMs === "number" && Number.isFinite(timeoutMs) && timeoutMs > 0 ? timeoutMs : undefined;
10959
+ };
10960
+ var withTimeout = async (input) => {
10961
+ if (!input.timeoutMs) {
10962
+ return input.run();
10963
+ }
10964
+ let timeout;
10965
+ try {
10966
+ return await Promise.race([
10967
+ Promise.resolve(input.run()),
10968
+ new Promise((_, reject) => {
10969
+ timeout = setTimeout(() => reject(new VoiceIOProviderTimeoutError(input.kind, input.provider, input.timeoutMs)), input.timeoutMs);
10970
+ })
10971
+ ]);
10972
+ } finally {
10973
+ if (timeout) {
10974
+ clearTimeout(timeout);
10975
+ }
10976
+ }
10977
+ };
10978
+ var isVoiceProviderRoutingPolicyPreset = (policy) => policy === "balanced" || policy === "cost-cap" || policy === "cost-first" || policy === "latency-first" || policy === "quality-first";
10979
+ var createResolver = (options) => {
10980
+ const providerIds = Object.keys(options.adapters);
10981
+ const firstProvider = providerIds[0];
10982
+ const policy = typeof options.policy === "string" ? isVoiceProviderRoutingPolicyPreset(options.policy) ? resolveVoiceProviderRoutingPolicyPreset(options.policy) : {
10983
+ strategy: options.policy
10984
+ } : options.policy;
10985
+ const strategy = policy?.strategy ?? "prefer-selected";
10986
+ const healthOptions = typeof options.providerHealth === "object" ? options.providerHealth : options.providerHealth ? {} : undefined;
10987
+ const healthState = new Map;
10988
+ const now = () => healthOptions?.now?.() ?? Date.now();
10989
+ const failureThreshold = Math.max(1, healthOptions?.failureThreshold ?? 1);
10990
+ const cooldownMs = Math.max(0, healthOptions?.cooldownMs ?? 30000);
10991
+ const getHealth = (provider) => {
10992
+ const existing = healthState.get(provider);
10993
+ if (existing) {
10994
+ return existing;
10995
+ }
10996
+ const next = {
10997
+ consecutiveFailures: 0,
10998
+ provider,
10999
+ status: "healthy"
11000
+ };
11001
+ healthState.set(provider, next);
11002
+ return next;
11003
+ };
11004
+ const cloneHealth = (provider) => {
11005
+ if (!healthOptions) {
11006
+ return;
11007
+ }
11008
+ return {
11009
+ ...getHealth(provider)
11010
+ };
11011
+ };
11012
+ const getSuppressionRemainingMs = (provider) => {
11013
+ if (!healthOptions) {
11014
+ return;
11015
+ }
11016
+ const suppressedUntil = getHealth(provider).suppressedUntil;
11017
+ return typeof suppressedUntil === "number" ? Math.max(0, suppressedUntil - now()) : undefined;
11018
+ };
11019
+ const isSuppressed = (provider) => {
11020
+ if (!healthOptions) {
11021
+ return false;
11022
+ }
11023
+ const suppressedUntil = getHealth(provider).suppressedUntil;
11024
+ return typeof suppressedUntil === "number" && suppressedUntil > now();
11025
+ };
11026
+ const recordSuccess = (provider) => {
11027
+ if (!healthOptions) {
11028
+ return;
11029
+ }
11030
+ const health = getHealth(provider);
11031
+ health.consecutiveFailures = 0;
11032
+ health.status = "healthy";
11033
+ health.suppressedUntil = undefined;
11034
+ return cloneHealth(provider);
11035
+ };
11036
+ const recordError = (provider, isProviderError) => {
11037
+ if (!healthOptions || !isProviderError) {
11038
+ return cloneHealth(provider);
11039
+ }
11040
+ const health = getHealth(provider);
11041
+ health.consecutiveFailures += 1;
11042
+ health.lastFailureAt = now();
11043
+ if (health.consecutiveFailures >= failureThreshold) {
11044
+ health.status = "suppressed";
11045
+ health.suppressedUntil = now() + cooldownMs;
11046
+ }
11047
+ return cloneHealth(provider);
11048
+ };
11049
+ const resolveAllowedProviders = async (input) => {
11050
+ const allowed = typeof policy?.allowProviders === "function" ? await policy.allowProviders(input) : policy?.allowProviders;
11051
+ return new Set(allowed ?? providerIds);
11052
+ };
11053
+ const passesBudgetFilters = (provider) => {
11054
+ const profile = options.providerProfiles?.[provider];
11055
+ if (typeof policy?.maxCost === "number" && typeof profile?.cost === "number" && profile.cost > policy.maxCost) {
11056
+ return false;
11057
+ }
11058
+ if (typeof policy?.maxLatencyMs === "number" && typeof profile?.latencyMs === "number" && profile.latencyMs > policy.maxLatencyMs) {
11059
+ return false;
11060
+ }
11061
+ if (typeof policy?.minQuality === "number" && typeof profile?.quality === "number" && profile.quality < policy.minQuality) {
11062
+ return false;
11063
+ }
11064
+ return true;
11065
+ };
11066
+ const getBalancedScore = (provider) => {
11067
+ const profile = options.providerProfiles?.[provider];
11068
+ if (policy?.scoreProvider) {
11069
+ return policy.scoreProvider(provider, profile);
11070
+ }
11071
+ const weights = policy?.weights ?? {};
11072
+ 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);
11073
+ };
11074
+ const sortProviders = (providers) => {
11075
+ if (strategy !== "prefer-cheapest" && strategy !== "prefer-fastest" && strategy !== "quality-first" && strategy !== "balanced") {
11076
+ return providers;
11077
+ }
11078
+ return [...providers].sort((left, right) => {
11079
+ const leftProfile = options.providerProfiles?.[left];
11080
+ const rightProfile = options.providerProfiles?.[right];
11081
+ if (strategy === "quality-first") {
11082
+ 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);
11083
+ }
11084
+ if (strategy === "balanced") {
11085
+ return getBalancedScore(left) - getBalancedScore(right);
11086
+ }
11087
+ const leftValue = strategy === "prefer-cheapest" ? leftProfile?.cost ?? Number.MAX_SAFE_INTEGER : leftProfile?.latencyMs ?? Number.MAX_SAFE_INTEGER;
11088
+ const rightValue = strategy === "prefer-cheapest" ? rightProfile?.cost ?? Number.MAX_SAFE_INTEGER : rightProfile?.latencyMs ?? Number.MAX_SAFE_INTEGER;
11089
+ return leftValue - rightValue || (leftProfile?.priority ?? Number.MAX_SAFE_INTEGER) - (rightProfile?.priority ?? Number.MAX_SAFE_INTEGER);
11090
+ });
11091
+ };
11092
+ const resolveOrder = async (input) => {
11093
+ const requestedProvider = await options.selectProvider?.(input);
11094
+ const selectedProvider = requestedProvider ?? firstProvider;
11095
+ const allowedProviders = await resolveAllowedProviders(input);
11096
+ const fallbackOrder = typeof options.fallback === "function" ? await options.fallback(input) : options.fallback;
11097
+ const candidates = [selectedProvider, ...fallbackOrder ?? providerIds];
11098
+ const seen = new Set;
11099
+ const orderedCandidates = candidates.filter((provider) => {
11100
+ if (!provider || seen.has(provider) || !options.adapters[provider]) {
11101
+ return false;
11102
+ }
11103
+ seen.add(provider);
11104
+ return true;
11105
+ });
11106
+ const rankedOrder = sortProviders(orderedCandidates).filter((provider) => allowedProviders.has(provider)).filter(passesBudgetFilters);
11107
+ const healthyOrder = healthOptions ? rankedOrder.filter((provider) => !isSuppressed(provider)) : rankedOrder;
11108
+ const order = healthyOrder.length ? healthyOrder : rankedOrder;
11109
+ const preferred = strategy === "prefer-selected" && selectedProvider && allowedProviders.has(selectedProvider) && passesBudgetFilters(selectedProvider) && (!healthOptions || !isSuppressed(selectedProvider)) ? selectedProvider : order[0];
11110
+ return {
11111
+ order,
11112
+ selectedProvider: preferred
11113
+ };
11114
+ };
11115
+ const emit = async (event, input) => {
11116
+ await options.onProviderEvent?.(event, input);
11117
+ };
11118
+ return {
11119
+ emit,
11120
+ getSuppressionRemainingMs,
11121
+ providerIds,
11122
+ recordError,
11123
+ recordSuccess,
11124
+ resolveOrder
11125
+ };
11126
+ };
11127
+ var createVoiceSTTProviderRouter = (options) => {
11128
+ const resolver = createResolver(options);
7581
11129
  return {
7582
- generate: async (input) => {
7583
- const endpoint = `${baseUrl.replace(/\/$/, "")}/models/${encodeURIComponent(model)}:generateContent?key=${encodeURIComponent(options.apiKey)}`;
7584
- const response = await fetchImpl(endpoint, {
7585
- body: JSON.stringify({
7586
- contents: input.messages.map(messageToGeminiContent).filter(Boolean),
7587
- generationConfig: {
7588
- maxOutputTokens: options.maxOutputTokens,
7589
- responseMimeType: "application/json",
7590
- responseSchema: toGeminiSchema(OUTPUT_SCHEMA),
7591
- temperature: options.temperature
7592
- },
7593
- systemInstruction: {
7594
- parts: [
7595
- {
7596
- text: [input.system, ROUTE_RESULT_INSTRUCTION].filter(Boolean).join(`
7597
-
7598
- `)
7599
- }
7600
- ]
7601
- },
7602
- tools: input.tools.length ? [
7603
- {
7604
- functionDeclarations: input.tools.map((tool) => ({
7605
- description: tool.description,
7606
- name: tool.name,
7607
- parameters: toGeminiSchema(tool.parameters ?? {
7608
- additionalProperties: true,
7609
- type: "object"
7610
- })
7611
- }))
7612
- }
7613
- ] : undefined
7614
- }),
7615
- headers: {
7616
- "content-type": "application/json"
7617
- },
7618
- method: "POST"
7619
- });
7620
- if (!response.ok) {
7621
- throw createHTTPError("Gemini", response);
11130
+ kind: "stt",
11131
+ open: async (input) => {
11132
+ const { order, selectedProvider } = await resolver.resolveOrder(input);
11133
+ if (!selectedProvider || order.length === 0) {
11134
+ throw new Error("Voice STT provider router has no available providers.");
11135
+ }
11136
+ let lastError;
11137
+ for (const [index, provider] of order.entries()) {
11138
+ const adapter = options.adapters[provider];
11139
+ if (!adapter) {
11140
+ continue;
11141
+ }
11142
+ const startedAt = Date.now();
11143
+ try {
11144
+ const session = await withTimeout({
11145
+ kind: "stt",
11146
+ operation: "open",
11147
+ provider,
11148
+ run: () => adapter.open(input),
11149
+ timeoutMs: getTimeoutMs(options, provider)
11150
+ });
11151
+ const providerHealth = resolver.recordSuccess(provider);
11152
+ await resolver.emit({
11153
+ at: Date.now(),
11154
+ attempt: index + 1,
11155
+ elapsedMs: Date.now() - startedAt,
11156
+ fallbackProvider: provider === selectedProvider ? undefined : provider,
11157
+ kind: "stt",
11158
+ latencyBudgetMs: getTimeoutMs(options, provider),
11159
+ operation: "open",
11160
+ provider,
11161
+ providerHealth,
11162
+ selectedProvider,
11163
+ status: provider === selectedProvider ? "success" : "fallback"
11164
+ }, input);
11165
+ return session;
11166
+ } catch (error) {
11167
+ lastError = error;
11168
+ const hasNextProvider = index < order.length - 1;
11169
+ const shouldFallback = options.isProviderError?.(error, provider) ?? true;
11170
+ const providerHealth = resolver.recordError(provider, shouldFallback);
11171
+ await resolver.emit({
11172
+ at: Date.now(),
11173
+ attempt: index + 1,
11174
+ elapsedMs: Date.now() - startedAt,
11175
+ error: errorMessage2(error),
11176
+ fallbackProvider: shouldFallback ? order[index + 1] : undefined,
11177
+ kind: "stt",
11178
+ latencyBudgetMs: getTimeoutMs(options, provider),
11179
+ operation: "open",
11180
+ provider,
11181
+ providerHealth,
11182
+ selectedProvider,
11183
+ status: "error",
11184
+ suppressionRemainingMs: resolver.getSuppressionRemainingMs(provider),
11185
+ suppressedUntil: providerHealth?.suppressedUntil,
11186
+ timedOut: error instanceof VoiceIOProviderTimeoutError
11187
+ }, input);
11188
+ if (!hasNextProvider || !shouldFallback) {
11189
+ throw error;
11190
+ }
11191
+ }
7622
11192
  }
7623
- const body = await response.json();
7624
- if (body.usageMetadata && typeof body.usageMetadata === "object") {
7625
- await options.onUsage?.(body.usageMetadata);
11193
+ throw lastError ?? new Error("Voice STT provider router did not open a provider.");
11194
+ }
11195
+ };
11196
+ };
11197
+ var createVoiceTTSProviderRouter = (options) => {
11198
+ const resolver = createResolver(options);
11199
+ return {
11200
+ kind: "tts",
11201
+ open: async (input) => {
11202
+ const { order, selectedProvider } = await resolver.resolveOrder(input);
11203
+ if (!selectedProvider || order.length === 0) {
11204
+ throw new Error("Voice TTS provider router has no available providers.");
11205
+ }
11206
+ const emitter = createEmitter();
11207
+ let activeSession;
11208
+ let activeProvider;
11209
+ let nextProviderIndex = 0;
11210
+ const attach = (session) => {
11211
+ session.on("audio", (event) => emitter.emit("audio", event));
11212
+ session.on("error", (event) => emitter.emit("error", event));
11213
+ session.on("close", (event) => emitter.emit("close", event));
11214
+ };
11215
+ const openProvider = async (provider, attempt) => {
11216
+ const adapter = options.adapters[provider];
11217
+ if (!adapter) {
11218
+ throw new Error(`Voice TTS provider ${provider} is not configured.`);
11219
+ }
11220
+ const startedAt = Date.now();
11221
+ const session = await withTimeout({
11222
+ kind: "tts",
11223
+ operation: "open",
11224
+ provider,
11225
+ run: () => adapter.open(input),
11226
+ timeoutMs: getTimeoutMs(options, provider)
11227
+ });
11228
+ attach(session);
11229
+ activeSession = session;
11230
+ activeProvider = provider;
11231
+ const providerHealth = resolver.recordSuccess(provider);
11232
+ await resolver.emit({
11233
+ at: Date.now(),
11234
+ attempt,
11235
+ elapsedMs: Date.now() - startedAt,
11236
+ fallbackProvider: provider === selectedProvider ? undefined : provider,
11237
+ kind: "tts",
11238
+ latencyBudgetMs: getTimeoutMs(options, provider),
11239
+ operation: "open",
11240
+ provider,
11241
+ providerHealth,
11242
+ selectedProvider,
11243
+ status: provider === selectedProvider ? "success" : "fallback"
11244
+ }, input);
11245
+ return session;
11246
+ };
11247
+ const failProvider = async (inputEvent) => {
11248
+ const shouldFallback = options.isProviderError?.(inputEvent.error, inputEvent.provider) ?? true;
11249
+ const providerHealth = resolver.recordError(inputEvent.provider, shouldFallback);
11250
+ await resolver.emit({
11251
+ at: Date.now(),
11252
+ attempt: inputEvent.attempt,
11253
+ elapsedMs: Date.now() - inputEvent.startedAt,
11254
+ error: errorMessage2(inputEvent.error),
11255
+ fallbackProvider: shouldFallback ? order[nextProviderIndex] : undefined,
11256
+ kind: "tts",
11257
+ latencyBudgetMs: getTimeoutMs(options, inputEvent.provider),
11258
+ operation: inputEvent.operation,
11259
+ provider: inputEvent.provider,
11260
+ providerHealth,
11261
+ selectedProvider,
11262
+ status: "error",
11263
+ suppressionRemainingMs: resolver.getSuppressionRemainingMs(inputEvent.provider),
11264
+ suppressedUntil: providerHealth?.suppressedUntil,
11265
+ timedOut: inputEvent.error instanceof VoiceIOProviderTimeoutError
11266
+ }, input);
11267
+ return shouldFallback;
11268
+ };
11269
+ for (const [index, provider] of order.entries()) {
11270
+ nextProviderIndex = index + 1;
11271
+ const startedAt = Date.now();
11272
+ try {
11273
+ await openProvider(provider, index + 1);
11274
+ break;
11275
+ } catch (error) {
11276
+ const shouldFallback = await failProvider({
11277
+ attempt: index + 1,
11278
+ error,
11279
+ operation: "open",
11280
+ provider,
11281
+ startedAt
11282
+ });
11283
+ if (!shouldFallback || index >= order.length - 1) {
11284
+ throw error;
11285
+ }
11286
+ }
7626
11287
  }
7627
- const toolCalls = extractGeminiToolCalls(body);
7628
- if (toolCalls.length) {
7629
- return {
7630
- assistantText: extractGeminiText(body) || undefined,
7631
- toolCalls
7632
- };
11288
+ if (!activeSession || !activeProvider) {
11289
+ throw new Error("Voice TTS provider router did not open a provider.");
7633
11290
  }
7634
- return normalizeRouteOutput(parseJSON(extractGeminiText(body)));
11291
+ const sendWithFallback = async (text) => {
11292
+ for (;; ) {
11293
+ const session = activeSession;
11294
+ const provider = activeProvider;
11295
+ if (!session || !provider) {
11296
+ throw new Error("Voice TTS provider router has no active provider.");
11297
+ }
11298
+ const startedAt = Date.now();
11299
+ try {
11300
+ await withTimeout({
11301
+ kind: "tts",
11302
+ operation: "send",
11303
+ provider,
11304
+ run: () => session.send(text),
11305
+ timeoutMs: getTimeoutMs(options, provider)
11306
+ });
11307
+ return;
11308
+ } catch (error) {
11309
+ const shouldFallback = await failProvider({
11310
+ attempt: nextProviderIndex,
11311
+ error,
11312
+ operation: "send",
11313
+ provider,
11314
+ startedAt
11315
+ });
11316
+ const nextProvider = order[nextProviderIndex];
11317
+ if (!shouldFallback || !nextProvider) {
11318
+ throw error;
11319
+ }
11320
+ nextProviderIndex += 1;
11321
+ await session.close("tts-provider-fallback").catch(() => {});
11322
+ await openProvider(nextProvider, nextProviderIndex);
11323
+ }
11324
+ }
11325
+ };
11326
+ return {
11327
+ close: async (reason) => {
11328
+ await activeSession?.close(reason);
11329
+ activeSession = undefined;
11330
+ activeProvider = undefined;
11331
+ await emitter.emit("close", {
11332
+ reason,
11333
+ type: "close"
11334
+ });
11335
+ },
11336
+ on: emitter.on,
11337
+ send: sendWithFallback
11338
+ };
7635
11339
  }
7636
11340
  };
7637
11341
  };
@@ -8116,6 +11820,169 @@ var createVoiceMemoryStore = () => {
8116
11820
  };
8117
11821
  return { get, getOrCreate, list, remove, set };
8118
11822
  };
11823
+ // src/opsWebhook.ts
11824
+ import { Elysia as Elysia12 } from "elysia";
11825
+ var toHex5 = (bytes) => Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
11826
+ var signVoiceOpsWebhookBody = async (input) => {
11827
+ const encoder = new TextEncoder;
11828
+ const key = await crypto.subtle.importKey("raw", encoder.encode(input.secret), {
11829
+ hash: "SHA-256",
11830
+ name: "HMAC"
11831
+ }, false, ["sign"]);
11832
+ const signature = await crypto.subtle.sign("HMAC", key, encoder.encode(`${input.timestamp}.${input.body}`));
11833
+ return `sha256=${toHex5(new Uint8Array(signature))}`;
11834
+ };
11835
+ var timingSafeEqual = (left, right) => {
11836
+ const encoder = new TextEncoder;
11837
+ const leftBytes = encoder.encode(left);
11838
+ const rightBytes = encoder.encode(right);
11839
+ if (leftBytes.length !== rightBytes.length) {
11840
+ return false;
11841
+ }
11842
+ let diff = 0;
11843
+ for (let index = 0;index < leftBytes.length; index += 1) {
11844
+ diff |= leftBytes[index] ^ rightBytes[index];
11845
+ }
11846
+ return diff === 0;
11847
+ };
11848
+ var resolveWebhookLink = async (resolver, event) => {
11849
+ if (typeof resolver === "function") {
11850
+ return resolver({
11851
+ event
11852
+ });
11853
+ }
11854
+ return resolver;
11855
+ };
11856
+ var joinBaseUrl = (baseUrl, path) => `${baseUrl.replace(/\/+$/, "")}/${path.replace(/^\/+/, "")}`;
11857
+ var asString = (value) => typeof value === "string" && value.length > 0 ? value : undefined;
11858
+ var buildVoiceOpsWebhookEntity = (event) => ({
11859
+ disposition: asString(event.payload.disposition),
11860
+ outcome: asString(event.payload.outcome),
11861
+ priority: asString(event.payload.priority),
11862
+ queue: asString(event.payload.queue),
11863
+ reviewId: asString(event.payload.reviewId),
11864
+ scenarioId: asString(event.payload.scenarioId),
11865
+ sessionId: asString(event.payload.sessionId),
11866
+ status: asString(event.payload.status),
11867
+ target: asString(event.payload.target),
11868
+ taskId: asString(event.payload.taskId)
11869
+ });
11870
+ var createVoiceOpsWebhookEnvelope = async (input) => {
11871
+ const entity = buildVoiceOpsWebhookEntity(input.event);
11872
+ const replayHref = await resolveWebhookLink(input.replayHref, input.event) ?? (input.baseUrl && entity.sessionId ? joinBaseUrl(input.baseUrl, `/api/voice-sessions/${encodeURIComponent(entity.sessionId)}/replay`) : undefined);
11873
+ const links = {
11874
+ event: await resolveWebhookLink(input.eventHref, input.event),
11875
+ replay: replayHref,
11876
+ review: await resolveWebhookLink(input.reviewHref, input.event),
11877
+ task: await resolveWebhookLink(input.taskHref, input.event)
11878
+ };
11879
+ return {
11880
+ entity,
11881
+ event: {
11882
+ createdAt: input.event.createdAt,
11883
+ id: input.event.id,
11884
+ payload: input.event.payload,
11885
+ type: input.event.type
11886
+ },
11887
+ links: links.event || links.replay || links.review || links.task ? links : undefined,
11888
+ schemaVersion: 1,
11889
+ source: "absolutejs-voice"
11890
+ };
11891
+ };
11892
+ var createVoiceOpsWebhookSink = (options) => createVoiceIntegrationHTTPSink({
11893
+ ...options,
11894
+ body: ({ event }) => createVoiceOpsWebhookEnvelope({
11895
+ baseUrl: options.baseUrl,
11896
+ event,
11897
+ eventHref: options.eventHref,
11898
+ replayHref: options.replayHref,
11899
+ reviewHref: options.reviewHref,
11900
+ taskHref: options.taskHref
11901
+ }),
11902
+ kind: options.kind ?? "ops-webhook"
11903
+ });
11904
+ var verifyVoiceOpsWebhookSignature = async (input) => {
11905
+ if (!input.secret) {
11906
+ return {
11907
+ ok: false,
11908
+ reason: "missing-secret"
11909
+ };
11910
+ }
11911
+ if (!input.signature) {
11912
+ return {
11913
+ ok: false,
11914
+ reason: "missing-signature"
11915
+ };
11916
+ }
11917
+ if (!input.signature.startsWith("sha256=")) {
11918
+ return {
11919
+ ok: false,
11920
+ reason: "unsupported-algorithm"
11921
+ };
11922
+ }
11923
+ if (!input.timestamp) {
11924
+ return {
11925
+ ok: false,
11926
+ reason: "missing-timestamp"
11927
+ };
11928
+ }
11929
+ const timestampMs = Number(input.timestamp);
11930
+ const toleranceMs = Math.max(0, input.toleranceMs ?? 5 * 60 * 1000);
11931
+ if (!Number.isFinite(timestampMs) || toleranceMs > 0 && Math.abs((input.now ?? Date.now()) - timestampMs) > toleranceMs) {
11932
+ return {
11933
+ ok: false,
11934
+ reason: "stale-timestamp"
11935
+ };
11936
+ }
11937
+ const expected = await signVoiceOpsWebhookBody({
11938
+ body: input.body,
11939
+ secret: input.secret,
11940
+ timestamp: input.timestamp
11941
+ });
11942
+ if (!timingSafeEqual(expected, input.signature)) {
11943
+ return {
11944
+ ok: false,
11945
+ reason: "invalid-signature"
11946
+ };
11947
+ }
11948
+ return {
11949
+ ok: true
11950
+ };
11951
+ };
11952
+ var createVoiceOpsWebhookReceiverRoutes = (options = {}) => {
11953
+ const path = options.path ?? "/api/voice-ops/webhook";
11954
+ return new Elysia12().post(path, async ({ body, request, set }) => {
11955
+ const bodyText = typeof body === "string" ? body : JSON.stringify(body);
11956
+ if (options.signingSecret) {
11957
+ const verification = await verifyVoiceOpsWebhookSignature({
11958
+ body: bodyText,
11959
+ secret: options.signingSecret,
11960
+ signature: request.headers.get("x-absolutejs-signature"),
11961
+ timestamp: request.headers.get("x-absolutejs-timestamp"),
11962
+ toleranceMs: options.toleranceMs
11963
+ });
11964
+ if (!verification.ok) {
11965
+ set.status = 401;
11966
+ return {
11967
+ ok: false,
11968
+ reason: verification.reason
11969
+ };
11970
+ }
11971
+ }
11972
+ const envelope = JSON.parse(bodyText);
11973
+ await options.onEnvelope?.({
11974
+ envelope,
11975
+ request
11976
+ });
11977
+ return {
11978
+ eventId: envelope.event?.id,
11979
+ ok: true,
11980
+ type: envelope.event?.type
11981
+ };
11982
+ }, {
11983
+ parse: "text"
11984
+ });
11985
+ };
8119
11986
  // src/queue.ts
8120
11987
  var releaseLeaseScript = `
8121
11988
  if redis.call("GET", KEYS[1]) == ARGV[1] then
@@ -8187,6 +12054,8 @@ var shouldDeadLetterSinkEvent = (event, sinks, maxFailures) => typeof maxFailure
8187
12054
  var shouldDeadLetterTask = (task, maxFailures) => typeof maxFailures === "number" && maxFailures > 0 && (task.processingAttempts ?? 0) >= maxFailures;
8188
12055
  var shouldProcessTraceDeliveryStatus = (status, allowed) => allowed.includes(status);
8189
12056
  var shouldDeadLetterTraceDelivery = (delivery, maxFailures) => typeof maxFailures === "number" && maxFailures > 0 && (delivery.deliveryAttempts ?? 0) >= maxFailures;
12057
+ var shouldProcessHandoffDeliveryStatus = (status, allowed) => allowed.includes(status);
12058
+ var shouldDeadLetterHandoffDelivery = (delivery, maxFailures) => typeof maxFailures === "number" && maxFailures > 0 && (delivery.deliveryAttempts ?? 0) >= maxFailures;
8190
12059
  var summarizeVoiceIntegrationEvents = (events, input = {}) => {
8191
12060
  const buildSummary = async () => {
8192
12061
  const deadLetterIds = new Set(input.deadLetters ? (await input.deadLetters.list()).map((event) => event.id) : []);
@@ -8268,6 +12137,48 @@ var summarizeVoiceTraceSinkDeliveries = (deliveries, input = {}) => {
8268
12137
  };
8269
12138
  return buildSummary();
8270
12139
  };
12140
+ var summarizeVoiceHandoffDeliveries = (deliveries, input = {}) => {
12141
+ const buildSummary = async () => {
12142
+ const deadLetterIds = new Set(input.deadLetters ? (await input.deadLetters.list()).map((delivery) => delivery.id) : []);
12143
+ const byAction = new Map;
12144
+ const summary = {
12145
+ byAction: [],
12146
+ deadLettered: 0,
12147
+ delivered: 0,
12148
+ failed: 0,
12149
+ pending: 0,
12150
+ retryEligible: 0,
12151
+ skipped: 0,
12152
+ total: deliveries.length
12153
+ };
12154
+ for (const delivery of deliveries) {
12155
+ byAction.set(delivery.action, (byAction.get(delivery.action) ?? 0) + 1);
12156
+ if (deadLetterIds.has(delivery.id)) {
12157
+ summary.deadLettered += 1;
12158
+ }
12159
+ switch (delivery.deliveryStatus) {
12160
+ case "delivered":
12161
+ summary.delivered += 1;
12162
+ break;
12163
+ case "failed":
12164
+ summary.failed += 1;
12165
+ if ((delivery.deliveryAttempts ?? 0) > 0) {
12166
+ summary.retryEligible += 1;
12167
+ }
12168
+ break;
12169
+ case "skipped":
12170
+ summary.skipped += 1;
12171
+ break;
12172
+ case "pending":
12173
+ summary.pending += 1;
12174
+ break;
12175
+ }
12176
+ }
12177
+ summary.byAction = [...byAction.entries()].sort((left, right) => right[1] - left[1]);
12178
+ return summary;
12179
+ };
12180
+ return buildSummary();
12181
+ };
8271
12182
  var summarizeVoiceOpsTaskQueue = (tasks, input = {}) => {
8272
12183
  const buildSummary = async () => {
8273
12184
  const deadLetterIds = new Set(input.deadLetters ? (await input.deadLetters.list()).map((task) => task.id) : []);
@@ -8697,6 +12608,108 @@ var createVoiceTraceSinkDeliveryWorkerLoop = (options) => {
8697
12608
  tick
8698
12609
  };
8699
12610
  };
12611
+ var createVoiceHandoffDeliveryWorker = (options) => {
12612
+ const allowedStatuses = options.statuses ?? ["pending", "failed"];
12613
+ const leaseMs = Math.max(1, options.leaseMs ?? 30000);
12614
+ return {
12615
+ drain: async () => {
12616
+ const result = {
12617
+ alreadyProcessed: 0,
12618
+ attempted: 0,
12619
+ deadLettered: 0,
12620
+ delivered: 0,
12621
+ failed: 0,
12622
+ skipped: 0
12623
+ };
12624
+ const deliveries = [...await options.deliveries.list()].sort((left, right) => left.createdAt - right.createdAt);
12625
+ for (const delivery of deliveries) {
12626
+ if (!shouldProcessHandoffDeliveryStatus(delivery.deliveryStatus, allowedStatuses)) {
12627
+ continue;
12628
+ }
12629
+ if (shouldDeadLetterHandoffDelivery(delivery, options.maxFailures)) {
12630
+ await options.deadLetters?.set(delivery.id, delivery);
12631
+ await options.onDeadLetter?.(delivery);
12632
+ result.deadLettered += 1;
12633
+ continue;
12634
+ }
12635
+ const claimed = await options.leases.claim({
12636
+ leaseMs,
12637
+ taskId: delivery.id,
12638
+ workerId: options.workerId
12639
+ });
12640
+ if (!claimed) {
12641
+ continue;
12642
+ }
12643
+ try {
12644
+ const idempotencyKey = `${delivery.id}:handoff`;
12645
+ if (options.idempotency && await options.idempotency.has(idempotencyKey)) {
12646
+ result.alreadyProcessed += 1;
12647
+ continue;
12648
+ }
12649
+ result.attempted += 1;
12650
+ const updatedDelivery = await deliverVoiceHandoffDelivery({
12651
+ adapters: options.adapters,
12652
+ api: options.api,
12653
+ delivery,
12654
+ failMode: options.failMode
12655
+ });
12656
+ await options.deliveries.set(updatedDelivery.id, updatedDelivery);
12657
+ if (updatedDelivery.deliveryStatus === "delivered" || updatedDelivery.deliveryStatus === "skipped") {
12658
+ await options.idempotency?.set(idempotencyKey, {
12659
+ ttlSeconds: options.idempotencyTtlSeconds
12660
+ });
12661
+ }
12662
+ if (updatedDelivery.deliveryStatus === "delivered") {
12663
+ result.delivered += 1;
12664
+ } else if (updatedDelivery.deliveryStatus === "skipped") {
12665
+ result.skipped += 1;
12666
+ } else if (updatedDelivery.deliveryStatus === "failed") {
12667
+ result.failed += 1;
12668
+ if (shouldDeadLetterHandoffDelivery(updatedDelivery, options.maxFailures)) {
12669
+ await options.deadLetters?.set(updatedDelivery.id, updatedDelivery);
12670
+ await options.onDeadLetter?.(updatedDelivery);
12671
+ result.deadLettered += 1;
12672
+ }
12673
+ }
12674
+ } finally {
12675
+ await options.leases.release({
12676
+ taskId: delivery.id,
12677
+ workerId: options.workerId
12678
+ });
12679
+ }
12680
+ }
12681
+ return result;
12682
+ }
12683
+ };
12684
+ };
12685
+ var createVoiceHandoffDeliveryWorkerLoop = (options) => {
12686
+ const pollIntervalMs = Math.max(1, options.pollIntervalMs ?? 1000);
12687
+ let timer;
12688
+ let running = false;
12689
+ const tick = async () => options.worker.drain();
12690
+ return {
12691
+ isRunning: () => running,
12692
+ start: () => {
12693
+ if (timer) {
12694
+ return;
12695
+ }
12696
+ running = true;
12697
+ timer = setInterval(() => {
12698
+ tick().catch((error) => {
12699
+ options.onError?.(error);
12700
+ });
12701
+ }, pollIntervalMs);
12702
+ },
12703
+ stop: () => {
12704
+ if (timer) {
12705
+ clearInterval(timer);
12706
+ timer = undefined;
12707
+ }
12708
+ running = false;
12709
+ },
12710
+ tick
12711
+ };
12712
+ };
8700
12713
  var createVoiceOpsTaskWorker = (options) => {
8701
12714
  const leaseMs = Math.max(1, options.leaseMs ?? 30000);
8702
12715
  const getTask = async (taskId) => {
@@ -8832,10 +12845,10 @@ var createVoiceOpsTaskProcessorWorker = (options) => ({
8832
12845
  result.completed += 1;
8833
12846
  } catch (error) {
8834
12847
  await options.onError?.(error, task);
8835
- const errorMessage = error instanceof Error ? error.message : String(error);
12848
+ const errorMessage3 = error instanceof Error ? error.message : String(error);
8836
12849
  const failedTask = failVoiceOpsTask(task, {
8837
12850
  actor: task.claimedBy ?? "ops-worker",
8838
- error: errorMessage
12851
+ error: errorMessage3
8839
12852
  });
8840
12853
  if (shouldDeadLetterTask(failedTask, options.maxFailures)) {
8841
12854
  const deadLetterTask = deadLetterVoiceOpsTask(failedTask, {
@@ -9648,7 +13661,7 @@ var createVoiceSTTRoutingCorrectionHandler = (mode = "generic") => {
9648
13661
  import { Buffer as Buffer2 } from "buffer";
9649
13662
  var TWILIO_MULAW_SAMPLE_RATE = 8000;
9650
13663
  var VOICE_PCM_SAMPLE_RATE = 16000;
9651
- var escapeXml = (value) => value.replaceAll("&", "&amp;").replaceAll('"', "&quot;").replaceAll("'", "&apos;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
13664
+ var escapeXml2 = (value) => value.replaceAll("&", "&amp;").replaceAll('"', "&quot;").replaceAll("'", "&apos;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
9652
13665
  var normalizeOnTurn2 = (handler) => {
9653
13666
  if (handler.length > 1) {
9654
13667
  const directHandler = handler;
@@ -9844,8 +13857,8 @@ var createTwilioSocketAdapter = (socket, getState) => ({
9844
13857
  }
9845
13858
  });
9846
13859
  var createTwilioVoiceResponse = (options) => {
9847
- const parameters = Object.entries(options.parameters ?? {}).filter((entry) => entry[1] !== undefined).map(([name, value]) => `<Parameter name="${escapeXml(name)}" value="${escapeXml(String(value))}" />`).join("");
9848
- return `<?xml version="1.0" encoding="UTF-8"?><Response><Connect><Stream url="${escapeXml(options.streamUrl)}"${options.track ? ` track="${escapeXml(options.track)}"` : ""}${options.streamName ? ` name="${escapeXml(options.streamName)}"` : ""}>${parameters}</Stream></Connect></Response>`;
13860
+ const parameters = Object.entries(options.parameters ?? {}).filter((entry) => entry[1] !== undefined).map(([name, value]) => `<Parameter name="${escapeXml2(name)}" value="${escapeXml2(String(value))}" />`).join("");
13861
+ return `<?xml version="1.0" encoding="UTF-8"?><Response><Connect><Stream url="${escapeXml2(options.streamUrl)}"${options.track ? ` track="${escapeXml2(options.track)}"` : ""}${options.streamName ? ` name="${escapeXml2(options.streamName)}"` : ""}>${parameters}</Stream></Connect></Response>`;
9849
13862
  };
9850
13863
  var createTwilioMediaStreamBridge = (socket, options) => {
9851
13864
  const runtimePreset = resolveVoiceRuntimePreset(options.preset);
@@ -10081,26 +14094,41 @@ export {
10081
14094
  withVoiceOpsTaskId,
10082
14095
  withVoiceIntegrationEventId,
10083
14096
  voice,
14097
+ verifyVoiceOpsWebhookSignature,
14098
+ validateVoiceWorkflowRouteResult,
10084
14099
  transcodeTwilioInboundPayloadToPCM16,
10085
14100
  transcodePCMToTwilioOutboundPayload,
10086
14101
  summarizeVoiceTraceSinkDeliveries,
10087
14102
  summarizeVoiceTrace,
14103
+ summarizeVoiceSessions,
14104
+ summarizeVoiceSessionReplay,
14105
+ summarizeVoiceRoutingDecision,
14106
+ summarizeVoiceProviderHealth,
10088
14107
  summarizeVoiceOpsTasks,
10089
14108
  summarizeVoiceOpsTaskQueue,
10090
14109
  summarizeVoiceOpsTaskAnalytics,
10091
14110
  summarizeVoiceIntegrationEvents,
14111
+ summarizeVoiceHandoffHealth,
14112
+ summarizeVoiceHandoffDeliveries,
10092
14113
  summarizeVoiceAssistantRuns,
14114
+ summarizeVoiceAssistantHealth,
14115
+ summarizeVoiceAppKitStatus,
10093
14116
  startVoiceOpsTask,
10094
14117
  shapeTelephonyAssistantText,
10095
14118
  selectVoiceTraceEventsForPrune,
14119
+ runVoiceSessionEvals,
14120
+ runVoiceScenarioFixtureEvals,
14121
+ runVoiceScenarioEvals,
10096
14122
  resolveVoiceTraceRedactionOptions,
10097
14123
  resolveVoiceSTTRoutingStrategy,
10098
14124
  resolveVoiceRuntimePreset,
14125
+ resolveVoiceProviderRoutingPolicyPreset,
10099
14126
  resolveVoiceOutcomeRecipe,
10100
14127
  resolveVoiceOpsTaskPolicy,
10101
14128
  resolveVoiceOpsTaskAssignment,
10102
14129
  resolveVoiceOpsTaskAgeBucket,
10103
14130
  resolveVoiceOpsPreset,
14131
+ resolveVoiceDiagnosticsTraceFilter,
10104
14132
  resolveVoiceAssistantMemoryNamespace,
10105
14133
  resolveTurnDetectionConfig,
10106
14134
  resolveAudioConditioningConfig,
@@ -10108,15 +14136,28 @@ export {
10108
14136
  reopenVoiceOpsTask,
10109
14137
  renderVoiceTraceMarkdown,
10110
14138
  renderVoiceTraceHTML,
14139
+ renderVoiceSessionsHTML,
14140
+ renderVoiceScenarioFixtureEvalHTML,
14141
+ renderVoiceScenarioEvalHTML,
14142
+ renderVoiceResilienceHTML,
14143
+ renderVoiceQualityHTML,
14144
+ renderVoiceProviderHealthHTML,
14145
+ renderVoiceOpsConsoleHTML,
14146
+ renderVoiceHandoffHealthHTML,
14147
+ renderVoiceEvalHTML,
14148
+ renderVoiceEvalBaselineHTML,
10111
14149
  renderVoiceCallReviewMarkdown,
10112
14150
  renderVoiceCallReviewHTML,
14151
+ renderVoiceAssistantHealthHTML,
10113
14152
  redactVoiceTraceText,
10114
14153
  redactVoiceTraceEvents,
10115
14154
  redactVoiceTraceEvent,
14155
+ recordVoiceWorkflowContractTrace,
10116
14156
  recordVoiceRuntimeOps,
10117
14157
  pruneVoiceTraceEvents,
10118
14158
  matchesVoiceOpsTaskAssignmentRule,
10119
14159
  markVoiceOpsTaskSLABreached,
14160
+ listVoiceRoutingEvents,
10120
14161
  listVoiceOpsTasks,
10121
14162
  isVoiceOpsTaskOverdue,
10122
14163
  heartbeatVoiceOpsTask,
@@ -10125,17 +14166,26 @@ export {
10125
14166
  failVoiceOpsTask,
10126
14167
  exportVoiceTrace,
10127
14168
  evaluateVoiceTrace,
14169
+ evaluateVoiceQuality,
10128
14170
  encodeTwilioMulawBase64,
10129
14171
  deliverVoiceTraceEventsToSinks,
10130
14172
  deliverVoiceIntegrationEventToSinks,
10131
14173
  deliverVoiceIntegrationEvent,
14174
+ deliverVoiceHandoffDelivery,
14175
+ deliverVoiceHandoff,
10132
14176
  decodeTwilioMulawBase64,
10133
14177
  deadLetterVoiceOpsTask,
10134
14178
  createVoiceZendeskTicketUpdateSink,
10135
14179
  createVoiceZendeskTicketSyncSinks,
10136
14180
  createVoiceZendeskTicketSink,
14181
+ createVoiceWorkflowScenario,
14182
+ createVoiceWorkflowContractPreset,
14183
+ createVoiceWorkflowContractHandler,
14184
+ createVoiceWorkflowContract,
14185
+ createVoiceWebhookHandoffAdapter,
10137
14186
  createVoiceWebhookDeliveryWorkerLoop,
10138
14187
  createVoiceWebhookDeliveryWorker,
14188
+ createVoiceTwilioRedirectHandoffAdapter,
10139
14189
  createVoiceTraceSinkStore,
10140
14190
  createVoiceTraceSinkDeliveryWorkerLoop,
10141
14191
  createVoiceTraceSinkDeliveryWorker,
@@ -10147,9 +14197,17 @@ export {
10147
14197
  createVoiceTaskUpdatedEvent,
10148
14198
  createVoiceTaskSLABreachedEvent,
10149
14199
  createVoiceTaskCreatedEvent,
14200
+ createVoiceTTSProviderRouter,
14201
+ createVoiceSessionsJSONHandler,
14202
+ createVoiceSessionsHTMLHandler,
14203
+ createVoiceSessionReplayRoutes,
14204
+ createVoiceSessionReplayJSONHandler,
14205
+ createVoiceSessionReplayHTMLHandler,
10150
14206
  createVoiceSessionRecord,
14207
+ createVoiceSessionListRoutes,
10151
14208
  createVoiceSession,
10152
14209
  createVoiceSTTRoutingCorrectionHandler,
14210
+ createVoiceSTTProviderRouter,
10153
14211
  createVoiceSQLiteTraceSinkDeliveryStore,
10154
14212
  createVoiceSQLiteTraceEventStore,
10155
14213
  createVoiceSQLiteTaskStore,
@@ -10159,9 +14217,16 @@ export {
10159
14217
  createVoiceSQLiteIntegrationEventStore,
10160
14218
  createVoiceSQLiteExternalObjectMapStore,
10161
14219
  createVoiceS3ReviewStore,
14220
+ createVoiceRoutingDecisionSummary,
10162
14221
  createVoiceReviewSavedEvent,
14222
+ createVoiceResilienceRoutes,
10163
14223
  createVoiceRedisTaskLeaseCoordinator,
10164
14224
  createVoiceRedisIdempotencyStore,
14225
+ createVoiceQualityRoutes,
14226
+ createVoiceProviderRouter,
14227
+ createVoiceProviderHealthRoutes,
14228
+ createVoiceProviderHealthJSONHandler,
14229
+ createVoiceProviderHealthHTMLHandler,
10165
14230
  createVoicePostgresTraceSinkDeliveryStore,
10166
14231
  createVoicePostgresTraceEventStore,
10167
14232
  createVoicePostgresTaskStore,
@@ -10170,13 +14235,18 @@ export {
10170
14235
  createVoicePostgresReviewStore,
10171
14236
  createVoicePostgresIntegrationEventStore,
10172
14237
  createVoicePostgresExternalObjectMapStore,
14238
+ createVoiceOpsWebhookSink,
14239
+ createVoiceOpsWebhookReceiverRoutes,
14240
+ createVoiceOpsWebhookEnvelope,
10173
14241
  createVoiceOpsTaskWorker,
10174
14242
  createVoiceOpsTaskProcessorWorkerLoop,
10175
14243
  createVoiceOpsTaskProcessorWorker,
10176
14244
  createVoiceOpsRuntime,
14245
+ createVoiceOpsConsoleRoutes,
10177
14246
  createVoiceMemoryTraceSinkDeliveryStore,
10178
14247
  createVoiceMemoryTraceEventStore,
10179
14248
  createVoiceMemoryStore,
14249
+ createVoiceMemoryHandoffDeliveryStore,
10180
14250
  createVoiceMemoryAssistantMemoryStore,
10181
14251
  createVoiceLinearIssueUpdateSink,
10182
14252
  createVoiceLinearIssueSyncSinks,
@@ -10189,18 +14259,28 @@ export {
10189
14259
  createVoiceHubSpotTaskSyncSinks,
10190
14260
  createVoiceHubSpotTaskSink,
10191
14261
  createVoiceHelpdeskTicketSink,
14262
+ createVoiceHandoffHealthRoutes,
14263
+ createVoiceHandoffHealthJSONHandler,
14264
+ createVoiceHandoffHealthHTMLHandler,
14265
+ createVoiceHandoffDeliveryWorkerLoop,
14266
+ createVoiceHandoffDeliveryWorker,
14267
+ createVoiceHandoffDeliveryRecord,
10192
14268
  createVoiceFileTraceSinkDeliveryStore,
10193
14269
  createVoiceFileTraceEventStore,
10194
14270
  createVoiceFileTaskStore,
10195
14271
  createVoiceFileSessionStore,
14272
+ createVoiceFileScenarioFixtureStore,
10196
14273
  createVoiceFileRuntimeStorage,
10197
14274
  createVoiceFileReviewStore,
10198
14275
  createVoiceFileIntegrationEventStore,
10199
14276
  createVoiceFileExternalObjectMapStore,
14277
+ createVoiceFileEvalBaselineStore,
10200
14278
  createVoiceFileAssistantMemoryStore,
10201
14279
  createVoiceExternalObjectMapId,
10202
14280
  createVoiceExternalObjectMap,
10203
14281
  createVoiceExperiment,
14282
+ createVoiceEvalRoutes,
14283
+ createVoiceDiagnosticsRoutes,
10204
14284
  createVoiceCallReviewRecorder,
10205
14285
  createVoiceCallReviewFromSession,
10206
14286
  createVoiceCallReviewFromLiveTelephonyReport,
@@ -10208,7 +14288,12 @@ export {
10208
14288
  createVoiceCRMActivitySink,
10209
14289
  createVoiceAssistantMemoryRecord,
10210
14290
  createVoiceAssistantMemoryHandle,
14291
+ createVoiceAssistantHealthRoutes,
14292
+ createVoiceAssistantHealthJSONHandler,
14293
+ createVoiceAssistantHealthHTMLHandler,
10211
14294
  createVoiceAssistant,
14295
+ createVoiceAppKitRoutes,
14296
+ createVoiceAppKit,
10212
14297
  createVoiceAgentTool,
10213
14298
  createVoiceAgentSquad,
10214
14299
  createVoiceAgent,
@@ -10229,13 +14314,17 @@ export {
10229
14314
  createAnthropicVoiceAssistantModel,
10230
14315
  conditionAudioChunk,
10231
14316
  completeVoiceOpsTask,
14317
+ compareVoiceEvalBaseline,
10232
14318
  claimVoiceOpsTask,
10233
14319
  buildVoiceTraceReplay,
10234
14320
  buildVoiceOpsTaskFromSLABreach,
10235
14321
  buildVoiceOpsTaskFromReview,
14322
+ buildVoiceOpsConsoleReport,
14323
+ buildVoiceDiagnosticsMarkdown,
10236
14324
  assignVoiceOpsTask,
10237
14325
  applyVoiceOpsTaskPolicy,
10238
14326
  applyVoiceOpsTaskAssignmentRule,
14327
+ applyVoiceHandoffDeliveryResult,
10239
14328
  applyRiskTieredPhraseHintCorrections,
10240
14329
  applyPhraseHintCorrections,
10241
14330
  TURN_PROFILE_DEFAULTS