@absolutejs/voice 0.0.22-beta.4 → 0.0.22-beta.41

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 (44) hide show
  1. package/dist/angular/index.d.ts +1 -0
  2. package/dist/angular/index.js +172 -2
  3. package/dist/angular/voice-provider-status.service.d.ts +12 -0
  4. package/dist/angular/voice-stream.service.d.ts +2 -0
  5. package/dist/assistantHealth.d.ts +81 -0
  6. package/dist/client/actions.d.ts +22 -0
  7. package/dist/client/connection.d.ts +3 -0
  8. package/dist/client/htmxBootstrap.js +44 -2
  9. package/dist/client/index.d.ts +2 -0
  10. package/dist/client/index.js +125 -2
  11. package/dist/client/providerStatus.d.ts +19 -0
  12. package/dist/diagnosticsRoutes.d.ts +44 -0
  13. package/dist/handoff.d.ts +54 -0
  14. package/dist/handoffHealth.d.ts +94 -0
  15. package/dist/index.d.ts +26 -2
  16. package/dist/index.js +3551 -128
  17. package/dist/modelAdapters.d.ts +99 -0
  18. package/dist/opsConsoleRoutes.d.ts +77 -0
  19. package/dist/opsWebhook.d.ts +126 -0
  20. package/dist/providerAdapters.d.ts +37 -0
  21. package/dist/providerHealth.d.ts +79 -0
  22. package/dist/qualityRoutes.d.ts +76 -0
  23. package/dist/queue.d.ts +52 -0
  24. package/dist/react/index.d.ts +1 -0
  25. package/dist/react/index.js +148 -2
  26. package/dist/react/useVoiceController.d.ts +2 -0
  27. package/dist/react/useVoiceProviderStatus.d.ts +8 -0
  28. package/dist/react/useVoiceStream.d.ts +2 -0
  29. package/dist/resilienceRoutes.d.ts +106 -0
  30. package/dist/sessionReplay.d.ts +175 -0
  31. package/dist/svelte/createVoiceProviderStatus.d.ts +8 -0
  32. package/dist/svelte/index.d.ts +1 -0
  33. package/dist/svelte/index.js +127 -2
  34. package/dist/testing/index.d.ts +2 -0
  35. package/dist/testing/index.js +1468 -7
  36. package/dist/testing/ioProviderSimulator.d.ts +41 -0
  37. package/dist/testing/providerSimulator.d.ts +44 -0
  38. package/dist/trace.d.ts +1 -1
  39. package/dist/types.d.ts +84 -2
  40. package/dist/vue/index.d.ts +1 -0
  41. package/dist/vue/index.js +161 -2
  42. package/dist/vue/useVoiceProviderStatus.d.ts +9 -0
  43. package/dist/vue/useVoiceStream.d.ts +2 -0
  44. 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) {
@@ -5054,7 +5475,7 @@ var voice = (config) => {
5054
5475
  };
5055
5476
  // src/agent.ts
5056
5477
  var normalizeText3 = (value) => typeof value === "string" ? value.trim() : "";
5057
- var toErrorMessage2 = (error) => error instanceof Error ? error.message : String(error);
5478
+ var toErrorMessage3 = (error) => error instanceof Error ? error.message : String(error);
5058
5479
  var createHistoryMessages = (session, turn) => {
5059
5480
  const messages = [];
5060
5481
  for (const previousTurn of session.turns) {
@@ -5150,6 +5571,17 @@ var createVoiceAgent = (options) => {
5150
5571
  if (output.assistantText?.trim()) {
5151
5572
  messages.push({
5152
5573
  content: output.assistantText,
5574
+ metadata: output.toolCalls?.length ? {
5575
+ toolCalls: output.toolCalls
5576
+ } : undefined,
5577
+ role: "assistant"
5578
+ });
5579
+ } else if (output.toolCalls?.length) {
5580
+ messages.push({
5581
+ content: "",
5582
+ metadata: {
5583
+ toolCalls: output.toolCalls
5584
+ },
5153
5585
  role: "assistant"
5154
5586
  });
5155
5587
  }
@@ -5224,7 +5656,7 @@ var createVoiceAgent = (options) => {
5224
5656
  toolCallId: toolCall.id
5225
5657
  });
5226
5658
  } catch (error) {
5227
- const errorMessage = toErrorMessage2(error);
5659
+ const errorMessage = toErrorMessage3(error);
5228
5660
  toolResults.push({
5229
5661
  error: errorMessage,
5230
5662
  status: "error",
@@ -6090,53 +6522,352 @@ var summarizeVoiceAssistantRuns = async (input) => {
6090
6522
  totalRuns: assistantRuns.length
6091
6523
  };
6092
6524
  };
6093
- // src/fileStore.ts
6094
- import { mkdir, readFile, readdir, rename, rm, writeFile } from "fs/promises";
6095
- import { join } from "path";
6525
+ // src/assistantHealth.ts
6526
+ import { Elysia as Elysia3 } from "elysia";
6096
6527
 
6097
- // src/trace.ts
6098
- var createVoiceTraceEventId = (event) => [
6099
- event.sessionId,
6100
- event.turnId ?? "session",
6101
- event.type,
6102
- String(event.at ?? Date.now()),
6103
- crypto.randomUUID()
6104
- ].map(encodeURIComponent).join(":");
6105
- var createVoiceTraceEvent = (event) => ({
6106
- ...event,
6107
- at: event.at,
6108
- id: event.id ?? createVoiceTraceEventId({
6109
- at: event.at,
6110
- sessionId: event.sessionId,
6111
- turnId: event.turnId,
6112
- type: event.type
6113
- })
6114
- });
6115
- var createVoiceTraceSinkDeliveryId = (events) => {
6116
- const firstEvent = events[0];
6117
- return [
6118
- firstEvent?.sessionId ?? "trace",
6119
- firstEvent?.traceId ?? "sink",
6120
- String(firstEvent?.at ?? Date.now()),
6121
- crypto.randomUUID()
6122
- ].map(encodeURIComponent).join(":");
6123
- };
6124
- var createVoiceTraceSinkDeliveryRecord = (input) => {
6125
- const createdAt = input.createdAt ?? Date.now();
6126
- return {
6127
- createdAt,
6128
- deliveredAt: input.deliveredAt,
6129
- deliveryAttempts: input.deliveryAttempts,
6130
- deliveryError: input.deliveryError,
6131
- deliveryStatus: input.deliveryStatus ?? "pending",
6132
- events: input.events,
6133
- id: input.id ?? createVoiceTraceSinkDeliveryId(input.events),
6134
- sinkDeliveries: input.sinkDeliveries,
6135
- updatedAt: input.updatedAt ?? createdAt
6528
+ // src/providerHealth.ts
6529
+ import { Elysia as Elysia2 } from "elysia";
6530
+ var getString = (value) => typeof value === "string" ? value : undefined;
6531
+ var getNumber = (value) => typeof value === "number" && Number.isFinite(value) ? value : undefined;
6532
+ var isProviderStatus = (value) => value === "success" || value === "fallback" || value === "error";
6533
+ var summarizeVoiceProviderHealth = async (input) => {
6534
+ const options = Array.isArray(input) ? { events: input } : input;
6535
+ const events = options.events ?? await options.store?.list() ?? [];
6536
+ const providers = options.providers ?? [];
6537
+ const providerSet = new Set(providers);
6538
+ const now = options.now ?? Date.now();
6539
+ const entries = new Map;
6540
+ const isAllowedProvider = (value) => typeof value === "string" && (providerSet.size === 0 || providerSet.has(value));
6541
+ const getEntry = (provider) => {
6542
+ const existing = entries.get(provider);
6543
+ if (existing) {
6544
+ return existing;
6545
+ }
6546
+ const entry = {
6547
+ elapsedCount: 0,
6548
+ elapsedTotal: 0,
6549
+ errorCount: 0,
6550
+ fallbackCount: 0,
6551
+ provider,
6552
+ rateLimited: false,
6553
+ recommended: false,
6554
+ runCount: 0,
6555
+ status: "idle",
6556
+ timeoutCount: 0
6557
+ };
6558
+ entries.set(provider, entry);
6559
+ return entry;
6136
6560
  };
6137
- };
6138
- var matchesTraceFilter = (event, filter) => {
6139
- if (filter.sessionId !== undefined && event.sessionId !== filter.sessionId) {
6561
+ for (const provider of providers) {
6562
+ getEntry(provider);
6563
+ }
6564
+ const hasProviderRouterEvents = events.some((event) => event.type === "session.error" && isAllowedProvider(event.payload.provider) && isProviderStatus(event.payload.providerStatus));
6565
+ for (const event of events) {
6566
+ if (event.type === "assistant.run") {
6567
+ if (hasProviderRouterEvents) {
6568
+ continue;
6569
+ }
6570
+ const provider2 = event.payload.variantId;
6571
+ if (!isAllowedProvider(provider2)) {
6572
+ continue;
6573
+ }
6574
+ const entry2 = getEntry(provider2);
6575
+ entry2.runCount += 1;
6576
+ const elapsedMs = getNumber(event.payload.elapsedMs);
6577
+ if (elapsedMs !== undefined) {
6578
+ entry2.elapsedCount += 1;
6579
+ entry2.elapsedTotal += elapsedMs;
6580
+ }
6581
+ continue;
6582
+ }
6583
+ if (event.type !== "session.error") {
6584
+ continue;
6585
+ }
6586
+ const provider = event.payload.provider;
6587
+ if (!isAllowedProvider(provider)) {
6588
+ continue;
6589
+ }
6590
+ const providerStatus = isProviderStatus(event.payload.providerStatus) ? event.payload.providerStatus : undefined;
6591
+ const applyProviderHealth = () => {
6592
+ const entry2 = getEntry(provider);
6593
+ const providerHealth = event.payload.providerHealth;
6594
+ if (providerHealth && typeof providerHealth === "object") {
6595
+ const suppressedUntil2 = getNumber(providerHealth.suppressedUntil);
6596
+ if (suppressedUntil2 !== undefined) {
6597
+ entry2.suppressedUntil = suppressedUntil2;
6598
+ }
6599
+ }
6600
+ const suppressedUntil = getNumber(event.payload.suppressedUntil);
6601
+ if (suppressedUntil !== undefined) {
6602
+ entry2.suppressedUntil = suppressedUntil;
6603
+ }
6604
+ const suppressionRemainingMs = getNumber(event.payload.suppressionRemainingMs);
6605
+ if (suppressionRemainingMs !== undefined) {
6606
+ entry2.suppressionRemainingMs = suppressionRemainingMs;
6607
+ }
6608
+ return entry2;
6609
+ };
6610
+ if (providerStatus === "success" || providerStatus === "fallback") {
6611
+ const entry2 = applyProviderHealth();
6612
+ entry2.runCount += 1;
6613
+ entry2.lastSuccessAt = event.at;
6614
+ if (providerStatus === "success") {
6615
+ entry2.lastError = undefined;
6616
+ entry2.rateLimited = false;
6617
+ entry2.suppressedUntil = undefined;
6618
+ entry2.suppressionRemainingMs = undefined;
6619
+ }
6620
+ const elapsedMs = getNumber(event.payload.elapsedMs);
6621
+ if (elapsedMs !== undefined) {
6622
+ entry2.elapsedCount += 1;
6623
+ entry2.elapsedTotal += elapsedMs;
6624
+ }
6625
+ const selectedProvider = event.payload.selectedProvider;
6626
+ if (providerStatus === "fallback" && isAllowedProvider(selectedProvider) && selectedProvider !== provider) {
6627
+ getEntry(selectedProvider).fallbackCount += 1;
6628
+ }
6629
+ continue;
6630
+ }
6631
+ const entry = applyProviderHealth();
6632
+ entry.errorCount += 1;
6633
+ if (event.payload.timedOut === true) {
6634
+ entry.timeoutCount += 1;
6635
+ }
6636
+ entry.lastError = getString(event.payload.error);
6637
+ entry.lastErrorAt = event.at;
6638
+ entry.rateLimited ||= event.payload.rateLimited === true;
6639
+ }
6640
+ const summaries = [...entries.values()].map((entry) => {
6641
+ const hadSuppression = typeof entry.suppressedUntil === "number" || typeof entry.suppressionRemainingMs === "number";
6642
+ const suppressionRemainingMs = typeof entry.suppressedUntil === "number" ? Math.max(0, entry.suppressedUntil - now) : entry.suppressionRemainingMs;
6643
+ const activeSuppression = typeof suppressionRemainingMs === "number" && suppressionRemainingMs > 0;
6644
+ const recoverable = hadSuppression && !activeSuppression;
6645
+ const averageElapsedMs = entry.elapsedCount > 0 ? Math.round(entry.elapsedTotal / entry.elapsedCount) : undefined;
6646
+ 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";
6647
+ return {
6648
+ averageElapsedMs,
6649
+ errorCount: entry.errorCount,
6650
+ fallbackCount: entry.fallbackCount,
6651
+ lastError: entry.lastError,
6652
+ lastErrorAt: entry.lastErrorAt,
6653
+ lastSuccessAt: entry.lastSuccessAt,
6654
+ provider: entry.provider,
6655
+ rateLimited: entry.rateLimited,
6656
+ recommended: false,
6657
+ runCount: entry.runCount,
6658
+ status,
6659
+ suppressionRemainingMs: activeSuppression ? suppressionRemainingMs : undefined,
6660
+ suppressedUntil: entry.suppressedUntil,
6661
+ timeoutCount: entry.timeoutCount
6662
+ };
6663
+ });
6664
+ const recommended = summaries.filter((entry) => entry.status === "healthy").sort((left, right) => (left.averageElapsedMs ?? Number.MAX_SAFE_INTEGER) - (right.averageElapsedMs ?? Number.MAX_SAFE_INTEGER))[0];
6665
+ if (recommended) {
6666
+ recommended.recommended = true;
6667
+ }
6668
+ return summaries;
6669
+ };
6670
+ var escapeHtml3 = (value) => value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
6671
+ var renderVoiceProviderHealthHTML = (providers) => providers.length === 0 ? '<p class="voice-provider-empty">No provider status yet.</p>' : [
6672
+ '<div class="voice-provider-health">',
6673
+ ...providers.map((provider) => {
6674
+ const suppressionSeconds = typeof provider.suppressionRemainingMs === "number" ? Math.ceil(provider.suppressionRemainingMs / 1000) : undefined;
6675
+ return [
6676
+ `<article class="voice-provider-card ${escapeHtml3(provider.status)}">`,
6677
+ '<div class="voice-provider-card-header">',
6678
+ `<strong>${escapeHtml3(provider.provider)}</strong>`,
6679
+ `<span>${escapeHtml3(provider.status)}${provider.recommended ? " \xB7 recommended" : ""}</span>`,
6680
+ "</div>",
6681
+ "<dl>",
6682
+ `<div><dt>Runs</dt><dd>${String(provider.runCount)}</dd></div>`,
6683
+ `<div><dt>Avg latency</dt><dd>${String(provider.averageElapsedMs ?? 0)}ms</dd></div>`,
6684
+ `<div><dt>Errors</dt><dd>${String(provider.errorCount)}</dd></div>`,
6685
+ `<div><dt>Timeouts</dt><dd>${String(provider.timeoutCount)}</dd></div>`,
6686
+ `<div><dt>Fallbacks</dt><dd>${String(provider.fallbackCount)}</dd></div>`,
6687
+ "</dl>",
6688
+ suppressionSeconds ? `<p>Temporarily suppressed for ${String(suppressionSeconds)}s.</p>` : "",
6689
+ provider.lastError ? `<p>${escapeHtml3(provider.lastError)}</p>` : "",
6690
+ "</article>"
6691
+ ].join("");
6692
+ }),
6693
+ "</div>"
6694
+ ].join("");
6695
+ var createVoiceProviderHealthJSONHandler = (options) => async () => summarizeVoiceProviderHealth(options);
6696
+ var createVoiceProviderHealthHTMLHandler = (options) => async () => {
6697
+ const providers = await summarizeVoiceProviderHealth(options);
6698
+ const render = options.render ?? renderVoiceProviderHealthHTML;
6699
+ const body = await render(providers);
6700
+ return new Response(body, {
6701
+ headers: {
6702
+ "Content-Type": "text/html; charset=utf-8",
6703
+ ...options.headers
6704
+ }
6705
+ });
6706
+ };
6707
+ var createVoiceProviderHealthRoutes = (options) => {
6708
+ const path = options.path ?? "/api/provider-status";
6709
+ const htmlPath = options.htmlPath === undefined ? `${path}/htmx` : options.htmlPath;
6710
+ const routes = new Elysia2({
6711
+ name: options.name ?? "absolutejs-voice-provider-health"
6712
+ }).get(path, createVoiceProviderHealthJSONHandler(options));
6713
+ if (htmlPath) {
6714
+ routes.get(htmlPath, createVoiceProviderHealthHTMLHandler(options));
6715
+ }
6716
+ return routes;
6717
+ };
6718
+
6719
+ // src/assistantHealth.ts
6720
+ var escapeHtml4 = (value) => value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
6721
+ var renderCountMap = (values) => {
6722
+ const entries = Object.entries(values).sort((left, right) => right[1] - left[1]);
6723
+ if (entries.length === 0) {
6724
+ return '<p class="voice-assistant-health-empty">No data yet.</p>';
6725
+ }
6726
+ return [
6727
+ '<div class="voice-assistant-health-metrics">',
6728
+ ...entries.map(([label, value]) => `<div><span>${escapeHtml4(label)}</span><strong>${String(value)}</strong></div>`),
6729
+ "</div>"
6730
+ ].join("");
6731
+ };
6732
+ var getString2 = (value) => typeof value === "string" ? value : undefined;
6733
+ 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) => {
6734
+ const failure = {
6735
+ at: event.at,
6736
+ assistantId: getString2(event.payload.assistantId),
6737
+ error: getString2(event.payload.error),
6738
+ provider: getString2(event.payload.provider),
6739
+ rateLimited: event.payload.rateLimited === true ? true : undefined,
6740
+ sessionId: event.sessionId,
6741
+ status: getString2(event.payload.providerStatus),
6742
+ turnId: event.turnId,
6743
+ type: event.type
6744
+ };
6745
+ const href = replayHref === false ? undefined : typeof replayHref === "function" ? replayHref(failure) : `${replayHref ?? "/api/voice-sessions"}/${encodeURIComponent(event.sessionId)}/replay/htmx`;
6746
+ return {
6747
+ ...failure,
6748
+ replayHref: href
6749
+ };
6750
+ });
6751
+ var summarizeVoiceAssistantHealth = async (options) => {
6752
+ const events = options.events ?? await options.store?.list() ?? [];
6753
+ return {
6754
+ assistantRuns: await summarizeVoiceAssistantRuns({ events }),
6755
+ providerHealth: await summarizeVoiceProviderHealth({
6756
+ events,
6757
+ providers: options.providers
6758
+ }),
6759
+ recentFailures: getRecentFailures(events, options.maxFailures ?? 8, options.replayHref)
6760
+ };
6761
+ };
6762
+ var renderVoiceAssistantHealthHTML = (summary) => {
6763
+ const assistant = summary.assistantRuns.assistants[0];
6764
+ const failures = summary.recentFailures;
6765
+ return [
6766
+ '<div class="voice-assistant-health">',
6767
+ '<section class="voice-assistant-health-grid">',
6768
+ `<article><span>Runs</span><strong>${String(assistant?.runCount ?? 0)}</strong></article>`,
6769
+ `<article><span>Sessions</span><strong>${String(assistant?.sessions ?? 0)}</strong></article>`,
6770
+ `<article><span>Guardrails</span><strong>${String(assistant?.guardrailCount ?? 0)}</strong></article>`,
6771
+ `<article><span>Avg latency</span><strong>${String(assistant?.averageElapsedMs ?? 0)}ms</strong></article>`,
6772
+ "</section>",
6773
+ "<section>",
6774
+ "<h3>Provider Health</h3>",
6775
+ renderVoiceProviderHealthHTML(summary.providerHealth),
6776
+ "</section>",
6777
+ '<section class="voice-assistant-health-columns">',
6778
+ `<article><h3>Outcomes</h3>${renderCountMap(assistant?.outcomes ?? {})}</article>`,
6779
+ `<article><h3>Variants</h3>${renderCountMap(assistant?.variants ?? {})}</article>`,
6780
+ `<article><h3>Tools</h3>${renderCountMap(assistant?.toolCalls ?? {})}</article>`,
6781
+ `<article><h3>Artifact Plans</h3>${renderCountMap(assistant?.artifactPlans ?? {})}</article>`,
6782
+ "</section>",
6783
+ "<section>",
6784
+ "<h3>Recent Failures</h3>",
6785
+ failures.length === 0 ? '<p class="voice-assistant-health-empty">No failures yet.</p>' : [
6786
+ '<div class="voice-assistant-health-failures">',
6787
+ ...failures.map((failure) => [
6788
+ "<article>",
6789
+ `<strong>${escapeHtml4(failure.provider ?? failure.assistantId ?? failure.type)}</strong>`,
6790
+ `<span>${escapeHtml4(failure.status ?? (failure.rateLimited ? "rate-limited" : "error"))}</span>`,
6791
+ failure.error ? `<p>${escapeHtml4(failure.error)}</p>` : "",
6792
+ `<small>${escapeHtml4(failure.sessionId)}${failure.turnId ? ` / ${escapeHtml4(failure.turnId)}` : ""}</small>`,
6793
+ failure.replayHref ? `<p><a href="${escapeHtml4(failure.replayHref)}">Open replay</a></p>` : "",
6794
+ "</article>"
6795
+ ].join("")),
6796
+ "</div>"
6797
+ ].join(""),
6798
+ "</section>",
6799
+ "</div>"
6800
+ ].join("");
6801
+ };
6802
+ var createVoiceAssistantHealthJSONHandler = (options) => async () => summarizeVoiceAssistantHealth(options);
6803
+ var createVoiceAssistantHealthHTMLHandler = (options) => async () => {
6804
+ const summary = await summarizeVoiceAssistantHealth(options);
6805
+ const render = options.render ?? renderVoiceAssistantHealthHTML;
6806
+ const body = await render(summary);
6807
+ return new Response(body, {
6808
+ headers: {
6809
+ "Content-Type": "text/html; charset=utf-8",
6810
+ ...options.headers
6811
+ }
6812
+ });
6813
+ };
6814
+ var createVoiceAssistantHealthRoutes = (options) => {
6815
+ const path = options.path ?? "/api/assistant-health";
6816
+ const htmlPath = options.htmlPath === undefined ? `${path}/htmx` : options.htmlPath;
6817
+ const routes = new Elysia3({
6818
+ name: options.name ?? "absolutejs-voice-assistant-health"
6819
+ }).get(path, createVoiceAssistantHealthJSONHandler(options));
6820
+ if (htmlPath) {
6821
+ routes.get(htmlPath, createVoiceAssistantHealthHTMLHandler(options));
6822
+ }
6823
+ return routes;
6824
+ };
6825
+ // src/diagnosticsRoutes.ts
6826
+ import { Elysia as Elysia4 } from "elysia";
6827
+
6828
+ // src/trace.ts
6829
+ var createVoiceTraceEventId = (event) => [
6830
+ event.sessionId,
6831
+ event.turnId ?? "session",
6832
+ event.type,
6833
+ String(event.at ?? Date.now()),
6834
+ crypto.randomUUID()
6835
+ ].map(encodeURIComponent).join(":");
6836
+ var createVoiceTraceEvent = (event) => ({
6837
+ ...event,
6838
+ at: event.at,
6839
+ id: event.id ?? createVoiceTraceEventId({
6840
+ at: event.at,
6841
+ sessionId: event.sessionId,
6842
+ turnId: event.turnId,
6843
+ type: event.type
6844
+ })
6845
+ });
6846
+ var createVoiceTraceSinkDeliveryId = (events) => {
6847
+ const firstEvent = events[0];
6848
+ return [
6849
+ firstEvent?.sessionId ?? "trace",
6850
+ firstEvent?.traceId ?? "sink",
6851
+ String(firstEvent?.at ?? Date.now()),
6852
+ crypto.randomUUID()
6853
+ ].map(encodeURIComponent).join(":");
6854
+ };
6855
+ var createVoiceTraceSinkDeliveryRecord = (input) => {
6856
+ const createdAt = input.createdAt ?? Date.now();
6857
+ return {
6858
+ createdAt,
6859
+ deliveredAt: input.deliveredAt,
6860
+ deliveryAttempts: input.deliveryAttempts,
6861
+ deliveryError: input.deliveryError,
6862
+ deliveryStatus: input.deliveryStatus ?? "pending",
6863
+ events: input.events,
6864
+ id: input.id ?? createVoiceTraceSinkDeliveryId(input.events),
6865
+ sinkDeliveries: input.sinkDeliveries,
6866
+ updatedAt: input.updatedAt ?? createdAt
6867
+ };
6868
+ };
6869
+ var matchesTraceFilter = (event, filter) => {
6870
+ if (filter.sessionId !== undefined && event.sessionId !== filter.sessionId) {
6140
6871
  return false;
6141
6872
  }
6142
6873
  if (filter.turnId !== undefined && event.turnId !== filter.turnId) {
@@ -6196,7 +6927,7 @@ var sleep3 = async (delayMs) => {
6196
6927
  }
6197
6928
  await new Promise((resolve2) => setTimeout(resolve2, delayMs));
6198
6929
  };
6199
- var toHex3 = (bytes) => Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
6930
+ var toHex4 = (bytes) => Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
6200
6931
  var signVoiceTraceSinkBody = async (input) => {
6201
6932
  const encoder = new TextEncoder;
6202
6933
  const key = await crypto.subtle.importKey("raw", encoder.encode(input.secret), {
@@ -6205,7 +6936,7 @@ var signVoiceTraceSinkBody = async (input) => {
6205
6936
  }, false, ["sign"]);
6206
6937
  const payload = encoder.encode(`${input.timestamp}.${input.body}`);
6207
6938
  const signature = await crypto.subtle.sign("HMAC", key, payload);
6208
- return `sha256=${toHex3(new Uint8Array(signature))}`;
6939
+ return `sha256=${toHex4(new Uint8Array(signature))}`;
6209
6940
  };
6210
6941
  var createVoiceTraceSinkDeliveryError = (input) => {
6211
6942
  if (input.response) {
@@ -6426,7 +7157,7 @@ var exportVoiceTrace = async (input) => {
6426
7157
  };
6427
7158
  };
6428
7159
  var toNumber = (value) => typeof value === "number" && Number.isFinite(value) ? value : 0;
6429
- var escapeHtml3 = (value) => value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
7160
+ var escapeHtml5 = (value) => value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
6430
7161
  var formatTraceValue = (value) => {
6431
7162
  if (value === undefined || value === null) {
6432
7163
  return "";
@@ -6704,10 +7435,10 @@ var renderVoiceTraceHTML = (events, options = {}) => {
6704
7435
  const offset = summary.startedAt === undefined ? event.at : Math.max(0, event.at - summary.startedAt);
6705
7436
  return [
6706
7437
  "<tr>",
6707
- `<td>${escapeHtml3(String(offset))}</td>`,
6708
- `<td>${escapeHtml3(event.type)}</td>`,
6709
- `<td>${escapeHtml3(event.turnId ?? "")}</td>`,
6710
- `<td><code>${escapeHtml3(JSON.stringify(event.payload))}</code></td>`,
7438
+ `<td>${escapeHtml5(String(offset))}</td>`,
7439
+ `<td>${escapeHtml5(event.type)}</td>`,
7440
+ `<td>${escapeHtml5(event.turnId ?? "")}</td>`,
7441
+ `<td><code>${escapeHtml5(JSON.stringify(event.payload))}</code></td>`,
6711
7442
  "</tr>"
6712
7443
  ].join("");
6713
7444
  }).join(`
@@ -6718,7 +7449,7 @@ var renderVoiceTraceHTML = (events, options = {}) => {
6718
7449
  "<head>",
6719
7450
  '<meta charset="utf-8" />',
6720
7451
  '<meta name="viewport" content="width=device-width, initial-scale=1" />',
6721
- `<title>${escapeHtml3(options.title ?? "Voice Trace")}</title>`,
7452
+ `<title>${escapeHtml5(options.title ?? "Voice Trace")}</title>`,
6722
7453
  "<style>",
6723
7454
  "body{font-family:ui-sans-serif,system-ui,sans-serif;margin:2rem;line-height:1.45;background:#f8f7f2;color:#181713}",
6724
7455
  "main{max-width:1100px;margin:auto}",
@@ -6732,7 +7463,7 @@ var renderVoiceTraceHTML = (events, options = {}) => {
6732
7463
  "</style>",
6733
7464
  "</head>",
6734
7465
  "<body><main>",
6735
- `<h1>${escapeHtml3(options.title ?? `Voice Trace ${summary.sessionId ?? ""}`.trim())}</h1>`,
7466
+ `<h1>${escapeHtml5(options.title ?? `Voice Trace ${summary.sessionId ?? ""}`.trim())}</h1>`,
6736
7467
  `<p class="${evaluation.pass ? "pass" : "fail"}">QA: ${evaluation.pass ? "pass" : "fail"}</p>`,
6737
7468
  '<section class="summary">',
6738
7469
  `<div class="card"><strong>Events</strong><br>${summary.eventCount}</div>`,
@@ -6746,7 +7477,7 @@ var renderVoiceTraceHTML = (events, options = {}) => {
6746
7477
  eventRows,
6747
7478
  "</tbody></table>",
6748
7479
  "<h2>Markdown Export</h2>",
6749
- `<pre>${escapeHtml3(markdown)}</pre>`,
7480
+ `<pre>${escapeHtml5(markdown)}</pre>`,
6750
7481
  "</main></body></html>"
6751
7482
  ].join(`
6752
7483
  `);
@@ -6758,7 +7489,385 @@ var buildVoiceTraceReplay = (events, options = {}) => ({
6758
7489
  summary: summarizeVoiceTrace(options.redact ? redactVoiceTraceEvents(events, options.redact) : events)
6759
7490
  });
6760
7491
 
7492
+ // src/diagnosticsRoutes.ts
7493
+ var escapeHtml6 = (value) => value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
7494
+ var getString3 = (value) => typeof value === "string" && value.trim() ? value : undefined;
7495
+ var getNumber2 = (value) => {
7496
+ const parsed = typeof value === "number" ? value : typeof value === "string" ? Number(value) : undefined;
7497
+ return typeof parsed === "number" && Number.isFinite(parsed) ? parsed : undefined;
7498
+ };
7499
+ var getBoolean = (value) => value === true || value === "true" || value === "1";
7500
+ var parseTraceTypeFilter = (value) => {
7501
+ if (typeof value !== "string" || !value.trim()) {
7502
+ return;
7503
+ }
7504
+ const types = value.split(",").map((entry) => entry.trim()).filter(Boolean);
7505
+ return types.length <= 1 ? types[0] : types;
7506
+ };
7507
+ var resolveVoiceDiagnosticsTraceFilter = (query) => ({
7508
+ limit: getNumber2(query.limit),
7509
+ scenarioId: getString3(query.scenarioId),
7510
+ sessionId: getString3(query.sessionId),
7511
+ traceId: getString3(query.traceId),
7512
+ turnId: getString3(query.turnId),
7513
+ type: parseTraceTypeFilter(query.type)
7514
+ });
7515
+ var filterByDiagnosticsQuery = (events, query) => {
7516
+ const provider = getString3(query.provider);
7517
+ const status = getString3(query.status);
7518
+ const since = getNumber2(query.since);
7519
+ const until = getNumber2(query.until);
7520
+ 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));
7521
+ };
7522
+ var buildVoiceDiagnosticsMarkdown = (events, options = {}) => {
7523
+ const summary = summarizeVoiceTrace(events);
7524
+ const evaluation = evaluateVoiceTrace(events, options.evaluation);
7525
+ const trace = renderVoiceTraceMarkdown(events, {
7526
+ evaluation: options.evaluation,
7527
+ title: options.title ?? `Voice Diagnostics ${summary.sessionId ?? ""}`.trim()
7528
+ });
7529
+ return [
7530
+ `# ${options.title ?? "Voice Diagnostics Bug Report"}`,
7531
+ "",
7532
+ `Session: ${summary.sessionId ?? "unknown"}`,
7533
+ `Pass: ${evaluation.pass ? "yes" : "no"}`,
7534
+ `Events: ${summary.eventCount}`,
7535
+ `Turns: ${summary.turnCount}`,
7536
+ `Errors: ${summary.errorCount}`,
7537
+ `Tool errors: ${summary.toolErrorCount}`,
7538
+ `Estimated cost units: ${summary.cost.estimatedRelativeCostUnits}`,
7539
+ "",
7540
+ "## Issues",
7541
+ "",
7542
+ evaluation.issues.length ? evaluation.issues.map((issue) => `- [${issue.severity}] ${issue.code}: ${issue.message}`).join(`
7543
+ `) : "- none",
7544
+ "",
7545
+ "## Trace",
7546
+ "",
7547
+ trace
7548
+ ].join(`
7549
+ `);
7550
+ };
7551
+ var renderDiagnosticsIndex = (input) => {
7552
+ const sessions = new Map;
7553
+ for (const event of input.events) {
7554
+ sessions.set(event.sessionId, [...sessions.get(event.sessionId) ?? [], event]);
7555
+ }
7556
+ const rows = [...sessions.entries()].sort(([, left], [, right]) => (right.at(-1)?.at ?? 0) - (left.at(-1)?.at ?? 0)).slice(0, 50).map(([sessionId, events]) => {
7557
+ const summary = summarizeVoiceTrace(events);
7558
+ const encoded = encodeURIComponent(sessionId);
7559
+ 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>`;
7560
+ }).join("");
7561
+ 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>`;
7562
+ };
7563
+ var withRedaction = (events, query, defaultRedact) => {
7564
+ const shouldRedact = query.redact === undefined ? defaultRedact : getBoolean(query.redact);
7565
+ return shouldRedact ? redactVoiceTraceEvents(events, shouldRedact) : events;
7566
+ };
7567
+ var createVoiceDiagnosticsRoutes = (options) => {
7568
+ const path = options.path ?? "/diagnostics";
7569
+ const title = options.title ?? "AbsoluteJS Voice Diagnostics";
7570
+ const routes = new Elysia4({
7571
+ name: options.name ?? "absolutejs-voice-diagnostics"
7572
+ });
7573
+ routes.get(path, async () => {
7574
+ const events = await options.store.list();
7575
+ return new Response(renderDiagnosticsIndex({ basePath: path, events, title }), {
7576
+ headers: {
7577
+ "Content-Type": "text/html; charset=utf-8",
7578
+ ...options.headers
7579
+ }
7580
+ });
7581
+ });
7582
+ routes.get(`${path}/json`, async ({ query }) => {
7583
+ const events = filterByDiagnosticsQuery(await options.store.list(), query);
7584
+ const redacted = withRedaction(events, query, options.redact);
7585
+ return Response.json({
7586
+ ...await exportVoiceTrace({
7587
+ filter: resolveVoiceDiagnosticsTraceFilter(query),
7588
+ redact: false,
7589
+ store: {
7590
+ ...options.store,
7591
+ list: async () => redacted
7592
+ }
7593
+ }),
7594
+ filteredCount: events.length,
7595
+ redacted: redacted !== events
7596
+ });
7597
+ });
7598
+ routes.get(`${path}/markdown`, async ({ query }) => {
7599
+ const events = withRedaction(filterByDiagnosticsQuery(await options.store.list(), query), query, options.redact ?? true);
7600
+ const body = buildVoiceDiagnosticsMarkdown(events, {
7601
+ evaluation: options.evaluation,
7602
+ title
7603
+ });
7604
+ return new Response(body, {
7605
+ headers: {
7606
+ "Content-Type": "text/markdown; charset=utf-8",
7607
+ ...options.headers
7608
+ }
7609
+ });
7610
+ });
7611
+ routes.get(`${path}/html`, async ({ query }) => {
7612
+ const events = withRedaction(filterByDiagnosticsQuery(await options.store.list(), query), query, options.redact ?? true);
7613
+ const body = renderVoiceTraceHTML(events, {
7614
+ evaluation: options.evaluation,
7615
+ title
7616
+ });
7617
+ return new Response(body, {
7618
+ headers: {
7619
+ "Content-Type": "text/html; charset=utf-8",
7620
+ ...options.headers
7621
+ }
7622
+ });
7623
+ });
7624
+ return routes;
7625
+ };
7626
+ // src/sessionReplay.ts
7627
+ import { Elysia as Elysia5 } from "elysia";
7628
+ var getString4 = (value) => typeof value === "string" ? value : undefined;
7629
+ var escapeHtml7 = (value) => value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
7630
+ var increment2 = (record, key) => {
7631
+ record[key] = (record[key] ?? 0) + 1;
7632
+ };
7633
+ var buildReplayTurns = (events) => {
7634
+ const turns = new Map;
7635
+ const getTurn = (turnId) => {
7636
+ const existing = turns.get(turnId);
7637
+ if (existing) {
7638
+ return existing;
7639
+ }
7640
+ const turn = {
7641
+ assistantReplies: [],
7642
+ errors: [],
7643
+ id: turnId,
7644
+ modelCalls: [],
7645
+ tools: [],
7646
+ transcripts: []
7647
+ };
7648
+ turns.set(turnId, turn);
7649
+ return turn;
7650
+ };
7651
+ for (const event of events) {
7652
+ const turnId = event.turnId ?? "session";
7653
+ const turn = getTurn(turnId);
7654
+ switch (event.type) {
7655
+ case "turn.transcript":
7656
+ turn.transcripts.push({
7657
+ isFinal: event.payload.isFinal === true,
7658
+ text: getString4(event.payload.text)
7659
+ });
7660
+ break;
7661
+ case "turn.committed":
7662
+ turn.committedText = getString4(event.payload.text);
7663
+ break;
7664
+ case "turn.assistant": {
7665
+ const text = getString4(event.payload.text);
7666
+ if (text) {
7667
+ turn.assistantReplies.push(text);
7668
+ }
7669
+ break;
7670
+ }
7671
+ case "agent.model":
7672
+ case "assistant.run":
7673
+ turn.modelCalls.push(event.payload);
7674
+ break;
7675
+ case "agent.tool":
7676
+ turn.tools.push(event.payload);
7677
+ break;
7678
+ case "session.error":
7679
+ turn.errors.push(event.payload);
7680
+ break;
7681
+ }
7682
+ }
7683
+ return [...turns.values()];
7684
+ };
7685
+ var summarizeVoiceSessionReplay = async (options) => {
7686
+ const sourceEvents = options.events ?? await options.store?.list({ sessionId: options.sessionId }) ?? [];
7687
+ const events = filterVoiceTraceEvents(sourceEvents, {
7688
+ sessionId: options.sessionId
7689
+ });
7690
+ const replay = buildVoiceTraceReplay(events, {
7691
+ evaluation: options.evaluation,
7692
+ redact: options.redact,
7693
+ title: options.title ?? `Voice Session ${options.sessionId}`
7694
+ });
7695
+ const startedAt = replay.summary.startedAt;
7696
+ return {
7697
+ evaluation: replay.evaluation,
7698
+ events,
7699
+ html: replay.html,
7700
+ markdown: replay.markdown,
7701
+ sessionId: options.sessionId,
7702
+ summary: replay.summary,
7703
+ timeline: events.map((event) => ({
7704
+ at: event.at,
7705
+ offsetMs: startedAt === undefined ? undefined : Math.max(0, event.at - startedAt),
7706
+ payload: event.payload,
7707
+ turnId: event.turnId,
7708
+ type: event.type
7709
+ })),
7710
+ turns: buildReplayTurns(events)
7711
+ };
7712
+ };
7713
+ var summarizeVoiceSessions = async (options = {}) => {
7714
+ const events = options.events ?? await options.store?.list() ?? [];
7715
+ const grouped = new Map;
7716
+ for (const event of events) {
7717
+ grouped.set(event.sessionId, [...grouped.get(event.sessionId) ?? [], event]);
7718
+ }
7719
+ const sessions = [...grouped.entries()].map(([sessionId, sessionEvents]) => {
7720
+ const sorted = filterVoiceTraceEvents(sessionEvents);
7721
+ const summary = buildVoiceTraceReplay(sorted, {
7722
+ evaluation: {
7723
+ requireAssistantReply: false,
7724
+ requireCompletedCall: false,
7725
+ requireTranscript: false,
7726
+ requireTurn: false
7727
+ }
7728
+ }).summary;
7729
+ const providerErrors = {};
7730
+ const providers = new Set;
7731
+ let latestOutcome;
7732
+ let errorCount = 0;
7733
+ for (const event of sorted) {
7734
+ const provider = getString4(event.payload.provider);
7735
+ if (provider) {
7736
+ providers.add(provider);
7737
+ }
7738
+ if (event.type === "session.error" && (event.payload.providerStatus === "error" || typeof event.payload.error === "string")) {
7739
+ errorCount += 1;
7740
+ increment2(providerErrors, provider ?? "unknown");
7741
+ }
7742
+ const outcome = getString4(event.payload.outcome);
7743
+ if (outcome) {
7744
+ latestOutcome = outcome;
7745
+ }
7746
+ }
7747
+ const item = {
7748
+ endedAt: summary.endedAt,
7749
+ errorCount,
7750
+ eventCount: summary.eventCount,
7751
+ latestOutcome,
7752
+ providerErrors,
7753
+ providers: [...providers].sort(),
7754
+ sessionId,
7755
+ startedAt: summary.startedAt,
7756
+ status: errorCount > 0 ? "failed" : "healthy",
7757
+ transcriptCount: summary.transcriptCount,
7758
+ turnCount: summary.turnCount
7759
+ };
7760
+ const replayHref = options.replayHref === false ? "" : typeof options.replayHref === "function" ? options.replayHref(item) : `${options.replayHref ?? "/api/voice-sessions"}/${encodeURIComponent(sessionId)}/replay/htmx`;
7761
+ return {
7762
+ ...item,
7763
+ replayHref
7764
+ };
7765
+ });
7766
+ const search = options.q?.trim().toLowerCase();
7767
+ return sessions.filter((session) => {
7768
+ if (options.status && options.status !== "all" && session.status !== options.status) {
7769
+ return false;
7770
+ }
7771
+ if (options.provider && !session.providers.includes(options.provider)) {
7772
+ return false;
7773
+ }
7774
+ if (!search) {
7775
+ return true;
7776
+ }
7777
+ return [
7778
+ session.sessionId,
7779
+ session.latestOutcome,
7780
+ session.status,
7781
+ ...session.providers
7782
+ ].some((value) => value?.toLowerCase().includes(search));
7783
+ }).sort((left, right) => (right.endedAt ?? right.startedAt ?? 0) - (left.endedAt ?? left.startedAt ?? 0)).slice(0, options.limit ?? 50);
7784
+ };
7785
+ var renderVoiceSessionsHTML = (sessions) => sessions.length === 0 ? '<p class="voice-sessions-empty">No voice sessions found.</p>' : [
7786
+ '<div class="voice-sessions-list">',
7787
+ ...sessions.map((session) => [
7788
+ `<article class="voice-session-card ${escapeHtml7(session.status)}">`,
7789
+ '<div class="voice-session-card-header">',
7790
+ `<strong>${escapeHtml7(session.sessionId)}</strong>`,
7791
+ `<span>${escapeHtml7(session.status)}</span>`,
7792
+ "</div>",
7793
+ "<dl>",
7794
+ `<div><dt>Events</dt><dd>${String(session.eventCount)}</dd></div>`,
7795
+ `<div><dt>Turns</dt><dd>${String(session.turnCount)}</dd></div>`,
7796
+ `<div><dt>Transcripts</dt><dd>${String(session.transcriptCount)}</dd></div>`,
7797
+ `<div><dt>Errors</dt><dd>${String(session.errorCount)}</dd></div>`,
7798
+ "</dl>",
7799
+ session.latestOutcome ? `<p>Outcome: ${escapeHtml7(session.latestOutcome)}</p>` : "",
7800
+ session.providers.length ? `<p>Providers: ${session.providers.map(escapeHtml7).join(", ")}</p>` : "",
7801
+ session.replayHref ? `<p><a href="${escapeHtml7(session.replayHref)}">Open replay</a></p>` : "",
7802
+ "</article>"
7803
+ ].join("")),
7804
+ "</div>"
7805
+ ].join("");
7806
+ var createVoiceSessionsJSONHandler = (options = {}) => async ({ query }) => summarizeVoiceSessions({
7807
+ ...options,
7808
+ limit: typeof query?.limit === "string" ? Number(query.limit) : options.limit,
7809
+ provider: query?.provider ?? options.provider,
7810
+ q: query?.q ?? options.q,
7811
+ status: query?.status === "failed" || query?.status === "healthy" || query?.status === "all" ? query.status : options.status
7812
+ });
7813
+ var createVoiceSessionsHTMLHandler = (options = {}) => async ({ query }) => {
7814
+ const sessions = await summarizeVoiceSessions({
7815
+ ...options,
7816
+ limit: typeof query?.limit === "string" ? Number(query.limit) : options.limit,
7817
+ provider: query?.provider ?? options.provider,
7818
+ q: query?.q ?? options.q,
7819
+ status: query?.status === "failed" || query?.status === "healthy" || query?.status === "all" ? query.status : options.status
7820
+ });
7821
+ const body = await (options.render?.(sessions) ?? renderVoiceSessionsHTML(sessions));
7822
+ return new Response(body, {
7823
+ headers: {
7824
+ "Content-Type": "text/html; charset=utf-8",
7825
+ ...options.headers
7826
+ }
7827
+ });
7828
+ };
7829
+ var createVoiceSessionListRoutes = (options = {}) => {
7830
+ const path = options.path ?? "/api/voice-sessions";
7831
+ const htmlPath = options.htmlPath === undefined ? `${path}/htmx` : options.htmlPath;
7832
+ const routes = new Elysia5({
7833
+ name: options.name ?? "absolutejs-voice-session-list"
7834
+ }).get(path, createVoiceSessionsJSONHandler(options));
7835
+ if (htmlPath) {
7836
+ routes.get(htmlPath, createVoiceSessionsHTMLHandler(options));
7837
+ }
7838
+ return routes;
7839
+ };
7840
+ var createVoiceSessionReplayJSONHandler = (options) => async ({ params }) => summarizeVoiceSessionReplay({
7841
+ ...options,
7842
+ sessionId: params.sessionId ?? ""
7843
+ });
7844
+ var createVoiceSessionReplayHTMLHandler = (options) => async ({ params }) => {
7845
+ const replay = await summarizeVoiceSessionReplay({
7846
+ ...options,
7847
+ sessionId: params.sessionId ?? ""
7848
+ });
7849
+ const body = await (options.render?.(replay) ?? replay.html);
7850
+ return new Response(body, {
7851
+ headers: {
7852
+ "Content-Type": "text/html; charset=utf-8",
7853
+ ...options.headers
7854
+ }
7855
+ });
7856
+ };
7857
+ var createVoiceSessionReplayRoutes = (options) => {
7858
+ const path = options.path ?? "/api/voice-sessions/:sessionId/replay";
7859
+ const htmlPath = options.htmlPath === undefined ? `${path}/htmx` : options.htmlPath;
7860
+ const routes = new Elysia5({
7861
+ name: options.name ?? "absolutejs-voice-session-replay"
7862
+ }).get(path, createVoiceSessionReplayJSONHandler(options));
7863
+ if (htmlPath) {
7864
+ routes.get(htmlPath, createVoiceSessionReplayHTMLHandler(options));
7865
+ }
7866
+ return routes;
7867
+ };
6761
7868
  // src/fileStore.ts
7869
+ import { mkdir, readFile, readdir, rename, rm, writeFile } from "fs/promises";
7870
+ import { join } from "path";
6762
7871
  var listJsonFiles = async (directory) => {
6763
7872
  try {
6764
7873
  const entries = await readdir(directory, {
@@ -7006,73 +8115,2021 @@ var createVoiceFileAssistantMemoryStore = (options) => {
7006
8115
  throw error;
7007
8116
  }
7008
8117
  };
7009
- const list = async (input) => {
7010
- const files = await listJsonFiles(options.directory);
7011
- const records = await Promise.all(files.map((file) => readJsonFile(file)));
7012
- return records.filter((record) => record.assistantId === input.assistantId && (input.namespace === undefined || record.namespace === input.namespace)).sort((left, right) => right.updatedAt - left.updatedAt);
7013
- };
7014
- const set = async (input) => {
7015
- const existing = await get(input);
7016
- const record = createVoiceAssistantMemoryRecord({
7017
- ...input,
7018
- createdAt: input.createdAt ?? existing?.createdAt,
7019
- updatedAt: input.updatedAt
7020
- });
7021
- await writeJsonFile(resolveFilePath(options.directory, createMemoryStoreId(record)), record, options);
7022
- return record;
7023
- };
7024
- const remove = async (input) => {
7025
- await rm(resolveFilePath(options.directory, createMemoryStoreId(input)), {
7026
- force: true
7027
- });
7028
- };
7029
- return { delete: remove, get, list, set };
8118
+ const list = async (input) => {
8119
+ const files = await listJsonFiles(options.directory);
8120
+ const records = await Promise.all(files.map((file) => readJsonFile(file)));
8121
+ return records.filter((record) => record.assistantId === input.assistantId && (input.namespace === undefined || record.namespace === input.namespace)).sort((left, right) => right.updatedAt - left.updatedAt);
8122
+ };
8123
+ const set = async (input) => {
8124
+ const existing = await get(input);
8125
+ const record = createVoiceAssistantMemoryRecord({
8126
+ ...input,
8127
+ createdAt: input.createdAt ?? existing?.createdAt,
8128
+ updatedAt: input.updatedAt
8129
+ });
8130
+ await writeJsonFile(resolveFilePath(options.directory, createMemoryStoreId(record)), record, options);
8131
+ return record;
8132
+ };
8133
+ const remove = async (input) => {
8134
+ await rm(resolveFilePath(options.directory, createMemoryStoreId(input)), {
8135
+ force: true
8136
+ });
8137
+ };
8138
+ return { delete: remove, get, list, set };
8139
+ };
8140
+ var createVoiceFileRuntimeStorage = (options) => ({
8141
+ events: createVoiceFileIntegrationEventStore({
8142
+ ...options,
8143
+ directory: join(options.directory, "events")
8144
+ }),
8145
+ externalObjects: createVoiceFileExternalObjectMapStore({
8146
+ ...options,
8147
+ directory: join(options.directory, "external-objects")
8148
+ }),
8149
+ memories: createVoiceFileAssistantMemoryStore({
8150
+ ...options,
8151
+ directory: join(options.directory, "memories")
8152
+ }),
8153
+ reviews: createVoiceFileReviewStore({
8154
+ ...options,
8155
+ directory: join(options.directory, "reviews")
8156
+ }),
8157
+ session: createVoiceFileSessionStore({
8158
+ ...options,
8159
+ directory: join(options.directory, "sessions")
8160
+ }),
8161
+ tasks: createVoiceFileTaskStore({
8162
+ ...options,
8163
+ directory: join(options.directory, "tasks")
8164
+ }),
8165
+ traceDeliveries: createVoiceFileTraceSinkDeliveryStore({
8166
+ ...options,
8167
+ directory: join(options.directory, "trace-deliveries")
8168
+ }),
8169
+ traces: createVoiceFileTraceEventStore({
8170
+ ...options,
8171
+ directory: join(options.directory, "traces")
8172
+ })
8173
+ });
8174
+ var createStoredVoiceCallReviewArtifact = (id, artifact) => withVoiceCallReviewId(id, artifact);
8175
+ var createStoredVoiceOpsTask = (id, task) => withVoiceOpsTaskId(id, task);
8176
+ var createStoredVoiceIntegrationEvent = (id, event) => withVoiceIntegrationEventId(id, event);
8177
+ var createStoredVoiceExternalObjectMap = (mapping) => createVoiceExternalObjectMap({
8178
+ at: mapping.at,
8179
+ externalId: mapping.externalId,
8180
+ provider: mapping.provider,
8181
+ sinkId: mapping.sinkId,
8182
+ sourceId: mapping.sourceId,
8183
+ sourceType: mapping.sourceType
8184
+ });
8185
+ // src/modelAdapters.ts
8186
+ var OUTPUT_SCHEMA = {
8187
+ additionalProperties: false,
8188
+ properties: {
8189
+ assistantText: {
8190
+ type: "string"
8191
+ },
8192
+ complete: {
8193
+ type: "boolean"
8194
+ },
8195
+ escalate: {
8196
+ additionalProperties: false,
8197
+ properties: {
8198
+ metadata: {
8199
+ additionalProperties: true,
8200
+ type: "object"
8201
+ },
8202
+ reason: {
8203
+ type: "string"
8204
+ }
8205
+ },
8206
+ required: ["reason"],
8207
+ type: "object"
8208
+ },
8209
+ noAnswer: {
8210
+ additionalProperties: false,
8211
+ properties: {
8212
+ metadata: {
8213
+ additionalProperties: true,
8214
+ type: "object"
8215
+ }
8216
+ },
8217
+ type: "object"
8218
+ },
8219
+ result: {
8220
+ additionalProperties: true,
8221
+ type: "object"
8222
+ },
8223
+ transfer: {
8224
+ additionalProperties: false,
8225
+ properties: {
8226
+ metadata: {
8227
+ additionalProperties: true,
8228
+ type: "object"
8229
+ },
8230
+ reason: {
8231
+ type: "string"
8232
+ },
8233
+ target: {
8234
+ type: "string"
8235
+ }
8236
+ },
8237
+ required: ["target"],
8238
+ type: "object"
8239
+ },
8240
+ voicemail: {
8241
+ additionalProperties: false,
8242
+ properties: {
8243
+ metadata: {
8244
+ additionalProperties: true,
8245
+ type: "object"
8246
+ }
8247
+ },
8248
+ type: "object"
8249
+ }
8250
+ },
8251
+ type: "object"
8252
+ };
8253
+ 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.";
8254
+ var stripJSONCodeFence = (value) => {
8255
+ const trimmed = value.trim();
8256
+ const match = trimmed.match(/^```(?:json)?\s*([\s\S]*?)\s*```$/i);
8257
+ return match?.[1]?.trim() ?? value;
8258
+ };
8259
+ var parseJSON = (value) => {
8260
+ try {
8261
+ const parsed = JSON.parse(stripJSONCodeFence(value));
8262
+ return parsed && typeof parsed === "object" ? parsed : {};
8263
+ } catch {
8264
+ return {
8265
+ assistantText: value
8266
+ };
8267
+ }
8268
+ };
8269
+ var parseJSONValue = (value) => {
8270
+ try {
8271
+ return JSON.parse(value);
8272
+ } catch {
8273
+ return value;
8274
+ }
8275
+ };
8276
+
8277
+ class VoiceProviderTimeoutError extends Error {
8278
+ provider;
8279
+ timeoutMs;
8280
+ constructor(provider, timeoutMs) {
8281
+ super(`Voice provider ${provider} exceeded ${timeoutMs}ms latency budget.`);
8282
+ this.name = "VoiceProviderTimeoutError";
8283
+ this.provider = provider;
8284
+ this.timeoutMs = timeoutMs;
8285
+ }
8286
+ }
8287
+ var getMessageToolCalls = (message) => {
8288
+ const toolCalls = message.metadata?.toolCalls;
8289
+ return Array.isArray(toolCalls) ? toolCalls.filter((toolCall) => toolCall && typeof toolCall === "object" && typeof toolCall.name === "string") : [];
8290
+ };
8291
+ var createHTTPError = (provider, response) => new Error(`${provider} voice assistant model failed: HTTP ${response.status}`);
8292
+ var sleep4 = (ms) => new Promise((resolve2) => {
8293
+ setTimeout(resolve2, ms);
8294
+ });
8295
+ var errorMessage = (error) => error instanceof Error ? error.message : String(error);
8296
+ var defaultIsRateLimitError = (error) => /(\b429\b|rate limit|quota|too many requests)/i.test(errorMessage(error));
8297
+ var normalizeRouteOutput = (output) => {
8298
+ const result = {};
8299
+ if (typeof output.assistantText === "string") {
8300
+ result.assistantText = output.assistantText;
8301
+ }
8302
+ if (typeof output.complete === "boolean") {
8303
+ result.complete = output.complete;
8304
+ }
8305
+ if (output.result !== undefined) {
8306
+ result.result = output.result;
8307
+ }
8308
+ if (output.transfer && typeof output.transfer === "object") {
8309
+ const transfer = output.transfer;
8310
+ if (typeof transfer.target === "string") {
8311
+ result.transfer = {
8312
+ metadata: transfer.metadata && typeof transfer.metadata === "object" ? transfer.metadata : undefined,
8313
+ reason: typeof transfer.reason === "string" ? transfer.reason : undefined,
8314
+ target: transfer.target
8315
+ };
8316
+ }
8317
+ }
8318
+ if (output.escalate && typeof output.escalate === "object") {
8319
+ const escalate = output.escalate;
8320
+ if (typeof escalate.reason === "string") {
8321
+ result.escalate = {
8322
+ metadata: escalate.metadata && typeof escalate.metadata === "object" ? escalate.metadata : undefined,
8323
+ reason: escalate.reason
8324
+ };
8325
+ }
8326
+ }
8327
+ if (output.voicemail && typeof output.voicemail === "object") {
8328
+ const voicemail = output.voicemail;
8329
+ result.voicemail = {
8330
+ metadata: voicemail.metadata && typeof voicemail.metadata === "object" ? voicemail.metadata : undefined
8331
+ };
8332
+ }
8333
+ if (output.noAnswer && typeof output.noAnswer === "object") {
8334
+ const noAnswer = output.noAnswer;
8335
+ result.noAnswer = {
8336
+ metadata: noAnswer.metadata && typeof noAnswer.metadata === "object" ? noAnswer.metadata : undefined
8337
+ };
8338
+ }
8339
+ return result;
8340
+ };
8341
+ var createJSONVoiceAssistantModel = (options) => ({
8342
+ generate: async (input) => {
8343
+ const output = await options.generate(input);
8344
+ if ("assistantText" in output || "toolCalls" in output || "complete" in output || "transfer" in output || "escalate" in output) {
8345
+ return output;
8346
+ }
8347
+ return options.mapOutput?.(output) ?? normalizeRouteOutput(output);
8348
+ }
8349
+ });
8350
+ var createVoiceProviderRouter = (options) => {
8351
+ const providerIds = Object.keys(options.providers);
8352
+ const firstProvider = providerIds[0];
8353
+ const policy = typeof options.policy === "string" ? {
8354
+ strategy: options.policy
8355
+ } : options.policy;
8356
+ const strategy = policy?.strategy ?? "prefer-selected";
8357
+ const fallbackMode = policy?.fallbackMode ?? options.fallbackMode ?? "provider-error";
8358
+ const healthOptions = typeof options.providerHealth === "object" ? options.providerHealth : options.providerHealth ? {} : undefined;
8359
+ const healthState = new Map;
8360
+ const now = () => healthOptions?.now?.() ?? Date.now();
8361
+ const failureThreshold = Math.max(1, healthOptions?.failureThreshold ?? 1);
8362
+ const cooldownMs = Math.max(0, healthOptions?.cooldownMs ?? 30000);
8363
+ const rateLimitCooldownMs = Math.max(0, healthOptions?.rateLimitCooldownMs ?? 60000);
8364
+ const getProviderTimeoutMs = (provider) => {
8365
+ const timeoutMs = options.providerProfiles?.[provider]?.timeoutMs ?? options.timeoutMs;
8366
+ return typeof timeoutMs === "number" && Number.isFinite(timeoutMs) && timeoutMs > 0 ? timeoutMs : undefined;
8367
+ };
8368
+ const getHealth = (provider) => {
8369
+ const existing = healthState.get(provider);
8370
+ if (existing) {
8371
+ return existing;
8372
+ }
8373
+ const next = {
8374
+ consecutiveFailures: 0,
8375
+ provider,
8376
+ status: "healthy"
8377
+ };
8378
+ healthState.set(provider, next);
8379
+ return next;
8380
+ };
8381
+ const cloneHealth = (provider) => {
8382
+ if (!healthOptions) {
8383
+ return;
8384
+ }
8385
+ return {
8386
+ ...getHealth(provider)
8387
+ };
8388
+ };
8389
+ const getSuppressionRemainingMs = (provider) => {
8390
+ if (!healthOptions) {
8391
+ return;
8392
+ }
8393
+ const suppressedUntil = getHealth(provider).suppressedUntil;
8394
+ return typeof suppressedUntil === "number" ? Math.max(0, suppressedUntil - now()) : undefined;
8395
+ };
8396
+ const isSuppressed = (provider) => {
8397
+ if (!healthOptions) {
8398
+ return false;
8399
+ }
8400
+ const health = getHealth(provider);
8401
+ return typeof health.suppressedUntil === "number" && health.suppressedUntil > now();
8402
+ };
8403
+ const recordProviderSuccess = (provider) => {
8404
+ if (!healthOptions) {
8405
+ return;
8406
+ }
8407
+ const health = getHealth(provider);
8408
+ health.consecutiveFailures = 0;
8409
+ health.status = "healthy";
8410
+ health.suppressedUntil = undefined;
8411
+ return cloneHealth(provider);
8412
+ };
8413
+ const recordProviderError = (provider, isProviderError, rateLimited) => {
8414
+ if (!healthOptions || !isProviderError) {
8415
+ return cloneHealth(provider);
8416
+ }
8417
+ const currentTime = now();
8418
+ const health = getHealth(provider);
8419
+ health.consecutiveFailures += 1;
8420
+ health.lastFailureAt = currentTime;
8421
+ if (rateLimited) {
8422
+ health.lastRateLimitedAt = currentTime;
8423
+ }
8424
+ if (rateLimited || health.consecutiveFailures >= failureThreshold) {
8425
+ health.status = "suppressed";
8426
+ health.suppressedUntil = currentTime + (rateLimited ? rateLimitCooldownMs : cooldownMs);
8427
+ }
8428
+ return cloneHealth(provider);
8429
+ };
8430
+ const resolveAllowedProviders = async (input) => {
8431
+ const allowProviders = policy?.allowProviders ?? options.allowProviders;
8432
+ const allowed = typeof allowProviders === "function" ? await allowProviders(input) : allowProviders;
8433
+ return new Set(allowed ?? providerIds);
8434
+ };
8435
+ const sortProviders = (providers) => {
8436
+ if (strategy !== "prefer-cheapest" && strategy !== "prefer-fastest") {
8437
+ return providers;
8438
+ }
8439
+ return [...providers].sort((left, right) => {
8440
+ const leftProfile = options.providerProfiles?.[left];
8441
+ const rightProfile = options.providerProfiles?.[right];
8442
+ const leftValue = strategy === "prefer-cheapest" ? leftProfile?.cost ?? Number.MAX_SAFE_INTEGER : leftProfile?.latencyMs ?? Number.MAX_SAFE_INTEGER;
8443
+ const rightValue = strategy === "prefer-cheapest" ? rightProfile?.cost ?? Number.MAX_SAFE_INTEGER : rightProfile?.latencyMs ?? Number.MAX_SAFE_INTEGER;
8444
+ return leftValue - rightValue || (leftProfile?.priority ?? Number.MAX_SAFE_INTEGER) - (rightProfile?.priority ?? Number.MAX_SAFE_INTEGER);
8445
+ });
8446
+ };
8447
+ const resolveOrder = async (input) => {
8448
+ const selectedProvider = await options.selectProvider?.(input);
8449
+ const allowedProviders = await resolveAllowedProviders(input);
8450
+ const fallbackOrder = typeof options.fallback === "function" ? await options.fallback(input) : options.fallback;
8451
+ const rankedProviders = sortProviders([
8452
+ ...fallbackOrder ?? providerIds
8453
+ ]).filter((provider) => allowedProviders.has(provider));
8454
+ const healthyRankedProviders = healthOptions ? rankedProviders.filter((provider) => !isSuppressed(provider)) : rankedProviders;
8455
+ const candidateRankedProviders = healthyRankedProviders.length ? healthyRankedProviders : rankedProviders;
8456
+ const preferred = selectedProvider && allowedProviders.has(selectedProvider) && (!healthOptions || !isSuppressed(selectedProvider)) ? selectedProvider : candidateRankedProviders[0] ?? firstProvider;
8457
+ const seen = new Set;
8458
+ const order = [];
8459
+ const candidates = strategy === "ordered" ? candidateRankedProviders : [
8460
+ preferred,
8461
+ ...candidateRankedProviders,
8462
+ ...providerIds.filter((provider) => !healthOptions || !isSuppressed(provider))
8463
+ ];
8464
+ for (const provider of candidates) {
8465
+ if (!provider || seen.has(provider) || !allowedProviders.has(provider) || !options.providers[provider]) {
8466
+ continue;
8467
+ }
8468
+ seen.add(provider);
8469
+ order.push(provider);
8470
+ }
8471
+ return {
8472
+ order,
8473
+ selectedProvider: preferred
8474
+ };
8475
+ };
8476
+ const emit = async (event, input) => {
8477
+ await options.onProviderEvent?.(event, input);
8478
+ };
8479
+ const runProvider = async (provider, model, input) => {
8480
+ const timeoutMs = getProviderTimeoutMs(provider);
8481
+ if (!timeoutMs) {
8482
+ return model.generate(input);
8483
+ }
8484
+ let timeout;
8485
+ try {
8486
+ return await Promise.race([
8487
+ model.generate(input),
8488
+ new Promise((_, reject) => {
8489
+ timeout = setTimeout(() => reject(new VoiceProviderTimeoutError(provider, timeoutMs)), timeoutMs);
8490
+ })
8491
+ ]);
8492
+ } finally {
8493
+ if (timeout) {
8494
+ clearTimeout(timeout);
8495
+ }
8496
+ }
8497
+ };
8498
+ return {
8499
+ generate: async (input) => {
8500
+ const { order, selectedProvider } = await resolveOrder(input);
8501
+ if (!selectedProvider || order.length === 0) {
8502
+ throw new Error("Voice provider router has no available providers.");
8503
+ }
8504
+ let lastError;
8505
+ for (const [index, provider] of order.entries()) {
8506
+ const model = options.providers[provider];
8507
+ if (!model) {
8508
+ continue;
8509
+ }
8510
+ const startedAt = Date.now();
8511
+ try {
8512
+ const output = await runProvider(provider, model, input);
8513
+ const providerHealth = recordProviderSuccess(provider);
8514
+ await emit({
8515
+ at: Date.now(),
8516
+ attempt: index + 1,
8517
+ elapsedMs: Date.now() - startedAt,
8518
+ fallbackProvider: provider === selectedProvider ? undefined : provider,
8519
+ latencyBudgetMs: getProviderTimeoutMs(provider),
8520
+ provider,
8521
+ providerHealth,
8522
+ recovered: provider !== selectedProvider,
8523
+ selectedProvider,
8524
+ status: provider === selectedProvider ? "success" : "fallback"
8525
+ }, input);
8526
+ return output;
8527
+ } catch (error) {
8528
+ lastError = error;
8529
+ const hasNextProvider = index < order.length - 1;
8530
+ const isProviderError = options.isProviderError?.(error, provider) ?? true;
8531
+ const timedOut = options.isTimeoutError?.(error, provider) ?? error instanceof VoiceProviderTimeoutError;
8532
+ const rateLimited = options.isRateLimitError?.(error, provider) ?? defaultIsRateLimitError(error);
8533
+ const shouldFallback = fallbackMode === "provider-error" ? isProviderError : fallbackMode === "rate-limit" ? isProviderError && rateLimited : false;
8534
+ const providerHealth = recordProviderError(provider, isProviderError, rateLimited);
8535
+ const nextProvider = hasNextProvider ? order[index + 1] : undefined;
8536
+ await emit({
8537
+ at: Date.now(),
8538
+ attempt: index + 1,
8539
+ elapsedMs: Date.now() - startedAt,
8540
+ error: errorMessage(error),
8541
+ fallbackProvider: shouldFallback ? nextProvider : undefined,
8542
+ latencyBudgetMs: getProviderTimeoutMs(provider),
8543
+ provider,
8544
+ providerHealth,
8545
+ rateLimited,
8546
+ selectedProvider,
8547
+ suppressionRemainingMs: getSuppressionRemainingMs(provider),
8548
+ suppressedUntil: providerHealth?.suppressedUntil,
8549
+ status: "error",
8550
+ timedOut
8551
+ }, input);
8552
+ if (!hasNextProvider || !shouldFallback) {
8553
+ throw error;
8554
+ }
8555
+ }
8556
+ }
8557
+ throw lastError ?? new Error("Voice provider router did not run a provider.");
8558
+ }
8559
+ };
8560
+ };
8561
+ var messageToOpenAIInput = (message) => {
8562
+ if (message.role === "tool") {
8563
+ return [
8564
+ {
8565
+ call_id: message.toolCallId ?? message.name ?? crypto.randomUUID(),
8566
+ output: message.content,
8567
+ type: "function_call_output"
8568
+ }
8569
+ ];
8570
+ }
8571
+ const toolCalls = getMessageToolCalls(message);
8572
+ if (message.role === "assistant" && toolCalls.length) {
8573
+ return toolCalls.map((toolCall) => ({
8574
+ arguments: JSON.stringify(toolCall.args),
8575
+ call_id: toolCall.id ?? crypto.randomUUID(),
8576
+ name: toolCall.name,
8577
+ type: "function_call"
8578
+ }));
8579
+ }
8580
+ return [
8581
+ {
8582
+ content: message.content,
8583
+ role: message.role === "system" ? "developer" : message.role
8584
+ }
8585
+ ];
8586
+ };
8587
+ var messagesToOpenAIInput = (messages) => messages.flatMap(messageToOpenAIInput);
8588
+ var messageToAnthropicMessage = (message) => {
8589
+ if (message.role === "system") {
8590
+ return;
8591
+ }
8592
+ if (message.role === "tool") {
8593
+ if (!message.toolCallId) {
8594
+ return {
8595
+ content: `Tool result from ${message.name ?? "tool"}: ${message.content}`,
8596
+ role: "user"
8597
+ };
8598
+ }
8599
+ return {
8600
+ content: [
8601
+ {
8602
+ content: message.content,
8603
+ tool_use_id: message.toolCallId,
8604
+ type: "tool_result"
8605
+ }
8606
+ ],
8607
+ role: "user"
8608
+ };
8609
+ }
8610
+ const toolCalls = getMessageToolCalls(message);
8611
+ if (message.role === "assistant" && toolCalls.length) {
8612
+ return {
8613
+ content: [
8614
+ ...message.content ? [
8615
+ {
8616
+ text: message.content,
8617
+ type: "text"
8618
+ }
8619
+ ] : [],
8620
+ ...toolCalls.map((toolCall) => ({
8621
+ id: toolCall.id ?? crypto.randomUUID(),
8622
+ input: toolCall.args,
8623
+ name: toolCall.name,
8624
+ type: "tool_use"
8625
+ }))
8626
+ ],
8627
+ role: "assistant"
8628
+ };
8629
+ }
8630
+ return {
8631
+ content: message.content,
8632
+ role: message.role
8633
+ };
8634
+ };
8635
+ var toGeminiSchema = (schema) => {
8636
+ const next = {};
8637
+ for (const [key, value] of Object.entries(schema)) {
8638
+ if (key === "additionalProperties") {
8639
+ continue;
8640
+ }
8641
+ if (key === "type" && typeof value === "string") {
8642
+ next[key] = value.toUpperCase();
8643
+ continue;
8644
+ }
8645
+ if (Array.isArray(value)) {
8646
+ next[key] = value.map((item) => item && typeof item === "object" ? toGeminiSchema(item) : item);
8647
+ continue;
8648
+ }
8649
+ if (value && typeof value === "object") {
8650
+ next[key] = toGeminiSchema(value);
8651
+ continue;
8652
+ }
8653
+ next[key] = value;
8654
+ }
8655
+ return next;
8656
+ };
8657
+ var messageToGeminiContent = (message) => {
8658
+ if (message.role === "system") {
8659
+ return;
8660
+ }
8661
+ if (message.role === "tool") {
8662
+ return {
8663
+ parts: [
8664
+ {
8665
+ functionResponse: {
8666
+ id: message.toolCallId,
8667
+ name: message.name ?? "tool",
8668
+ response: {
8669
+ result: parseJSONValue(message.content)
8670
+ }
8671
+ }
8672
+ }
8673
+ ],
8674
+ role: "user"
8675
+ };
8676
+ }
8677
+ const toolCalls = getMessageToolCalls(message);
8678
+ if (message.role === "assistant" && toolCalls.length) {
8679
+ return {
8680
+ parts: [
8681
+ ...message.content ? [
8682
+ {
8683
+ text: message.content
8684
+ }
8685
+ ] : [],
8686
+ ...toolCalls.map((toolCall) => ({
8687
+ functionCall: {
8688
+ args: toolCall.args,
8689
+ id: toolCall.id,
8690
+ name: toolCall.name
8691
+ }
8692
+ }))
8693
+ ],
8694
+ role: "model"
8695
+ };
8696
+ }
8697
+ return {
8698
+ parts: [
8699
+ {
8700
+ text: message.content
8701
+ }
8702
+ ],
8703
+ role: message.role === "assistant" ? "model" : "user"
8704
+ };
8705
+ };
8706
+ var extractText = (response) => {
8707
+ if (typeof response.output_text === "string") {
8708
+ return response.output_text;
8709
+ }
8710
+ const output = Array.isArray(response.output) ? response.output : [];
8711
+ for (const item of output) {
8712
+ if (!item || typeof item !== "object") {
8713
+ continue;
8714
+ }
8715
+ const record = item;
8716
+ const content = Array.isArray(record.content) ? record.content : [];
8717
+ for (const contentItem of content) {
8718
+ if (!contentItem || typeof contentItem !== "object") {
8719
+ continue;
8720
+ }
8721
+ const contentRecord = contentItem;
8722
+ if (typeof contentRecord.text === "string") {
8723
+ return contentRecord.text;
8724
+ }
8725
+ }
8726
+ }
8727
+ return "";
8728
+ };
8729
+ var extractToolCalls = (response) => {
8730
+ const output = Array.isArray(response.output) ? response.output : [];
8731
+ const toolCalls = [];
8732
+ for (const item of output) {
8733
+ if (!item || typeof item !== "object") {
8734
+ continue;
8735
+ }
8736
+ const record = item;
8737
+ if (record.type !== "function_call" || typeof record.name !== "string") {
8738
+ continue;
8739
+ }
8740
+ const args = typeof record.arguments === "string" ? parseJSON(record.arguments) : {};
8741
+ toolCalls.push({
8742
+ args,
8743
+ id: typeof record.call_id === "string" ? record.call_id : typeof record.id === "string" ? record.id : undefined,
8744
+ name: record.name
8745
+ });
8746
+ }
8747
+ return toolCalls;
8748
+ };
8749
+ var createOpenAIVoiceAssistantModel = (options) => {
8750
+ const fetchImpl = options.fetch ?? globalThis.fetch;
8751
+ const baseUrl = options.baseUrl ?? "https://api.openai.com/v1";
8752
+ const model = options.model ?? "gpt-4.1-mini";
8753
+ return {
8754
+ generate: async (input) => {
8755
+ const response = await fetchImpl(`${baseUrl.replace(/\/$/, "")}/responses`, {
8756
+ body: JSON.stringify({
8757
+ input: messagesToOpenAIInput(input.messages),
8758
+ instructions: [
8759
+ input.system,
8760
+ "Return a JSON object with assistantText, complete, transfer, escalate, voicemail, noAnswer, and result when you are not calling tools."
8761
+ ].filter(Boolean).join(`
8762
+
8763
+ `),
8764
+ max_output_tokens: options.maxOutputTokens,
8765
+ model,
8766
+ temperature: options.temperature,
8767
+ text: {
8768
+ format: {
8769
+ name: "voice_route_result",
8770
+ schema: OUTPUT_SCHEMA,
8771
+ strict: false,
8772
+ type: "json_schema"
8773
+ }
8774
+ },
8775
+ tool_choice: input.tools.length ? "auto" : "none",
8776
+ tools: input.tools.map((tool) => ({
8777
+ description: tool.description,
8778
+ name: tool.name,
8779
+ parameters: tool.parameters ?? {
8780
+ additionalProperties: true,
8781
+ type: "object"
8782
+ },
8783
+ strict: false,
8784
+ type: "function"
8785
+ }))
8786
+ }),
8787
+ headers: {
8788
+ authorization: `Bearer ${options.apiKey}`,
8789
+ "content-type": "application/json"
8790
+ },
8791
+ method: "POST"
8792
+ });
8793
+ if (!response.ok) {
8794
+ throw createHTTPError("OpenAI", response);
8795
+ }
8796
+ const body = await response.json();
8797
+ if (body.usage && typeof body.usage === "object") {
8798
+ await options.onUsage?.(body.usage);
8799
+ }
8800
+ const toolCalls = extractToolCalls(body);
8801
+ if (toolCalls.length) {
8802
+ return {
8803
+ toolCalls
8804
+ };
8805
+ }
8806
+ return normalizeRouteOutput(parseJSON(extractText(body)));
8807
+ }
8808
+ };
8809
+ };
8810
+ var extractAnthropicText = (response) => {
8811
+ const content = Array.isArray(response.content) ? response.content : [];
8812
+ return content.map((item) => item && typeof item === "object" && item.type === "text" && typeof item.text === "string" ? item.text : "").filter(Boolean).join(`
8813
+ `);
8814
+ };
8815
+ var extractAnthropicToolCalls = (response) => {
8816
+ const content = Array.isArray(response.content) ? response.content : [];
8817
+ const toolCalls = [];
8818
+ for (const item of content) {
8819
+ if (!item || typeof item !== "object") {
8820
+ continue;
8821
+ }
8822
+ const record = item;
8823
+ if (record.type !== "tool_use" || typeof record.name !== "string") {
8824
+ continue;
8825
+ }
8826
+ toolCalls.push({
8827
+ args: record.input && typeof record.input === "object" ? record.input : {},
8828
+ id: typeof record.id === "string" ? record.id : undefined,
8829
+ name: record.name
8830
+ });
8831
+ }
8832
+ return toolCalls;
8833
+ };
8834
+ var createAnthropicVoiceAssistantModel = (options) => {
8835
+ const fetchImpl = options.fetch ?? globalThis.fetch;
8836
+ const baseUrl = options.baseUrl ?? "https://api.anthropic.com/v1";
8837
+ const model = options.model ?? "claude-sonnet-4-5";
8838
+ return {
8839
+ generate: async (input) => {
8840
+ const response = await fetchImpl(`${baseUrl.replace(/\/$/, "")}/messages`, {
8841
+ body: JSON.stringify({
8842
+ max_tokens: options.maxOutputTokens ?? 1024,
8843
+ messages: input.messages.map(messageToAnthropicMessage).filter(Boolean),
8844
+ model,
8845
+ system: [input.system, ROUTE_RESULT_INSTRUCTION].filter(Boolean).join(`
8846
+
8847
+ `),
8848
+ temperature: options.temperature,
8849
+ tool_choice: input.tools.length ? { type: "auto" } : { type: "none" },
8850
+ tools: input.tools.map((tool) => ({
8851
+ description: tool.description,
8852
+ input_schema: tool.parameters ?? {
8853
+ additionalProperties: true,
8854
+ type: "object"
8855
+ },
8856
+ name: tool.name
8857
+ }))
8858
+ }),
8859
+ headers: {
8860
+ "anthropic-version": options.version ?? "2023-06-01",
8861
+ "content-type": "application/json",
8862
+ "x-api-key": options.apiKey
8863
+ },
8864
+ method: "POST"
8865
+ });
8866
+ if (!response.ok) {
8867
+ throw createHTTPError("Anthropic", response);
8868
+ }
8869
+ const body = await response.json();
8870
+ if (body.usage && typeof body.usage === "object") {
8871
+ await options.onUsage?.(body.usage);
8872
+ }
8873
+ const toolCalls = extractAnthropicToolCalls(body);
8874
+ if (toolCalls.length) {
8875
+ return {
8876
+ assistantText: extractAnthropicText(body) || undefined,
8877
+ toolCalls
8878
+ };
8879
+ }
8880
+ return normalizeRouteOutput(parseJSON(extractAnthropicText(body)));
8881
+ }
8882
+ };
8883
+ };
8884
+ var extractGeminiCandidateParts = (response) => {
8885
+ const candidates = Array.isArray(response.candidates) ? response.candidates : [];
8886
+ const first = candidates[0];
8887
+ if (!first || typeof first !== "object") {
8888
+ return [];
8889
+ }
8890
+ const content = first.content;
8891
+ if (!content || typeof content !== "object") {
8892
+ return [];
8893
+ }
8894
+ const parts = content.parts;
8895
+ return Array.isArray(parts) ? parts : [];
8896
+ };
8897
+ var extractGeminiText = (response) => extractGeminiCandidateParts(response).map((part) => part && typeof part === "object" && typeof part.text === "string" ? part.text : "").filter(Boolean).join(`
8898
+ `);
8899
+ var extractGeminiToolCalls = (response) => {
8900
+ const toolCalls = [];
8901
+ for (const part of extractGeminiCandidateParts(response)) {
8902
+ if (!part || typeof part !== "object") {
8903
+ continue;
8904
+ }
8905
+ const functionCall = part.functionCall;
8906
+ if (!functionCall || typeof functionCall !== "object") {
8907
+ continue;
8908
+ }
8909
+ const record = functionCall;
8910
+ if (typeof record.name !== "string") {
8911
+ continue;
8912
+ }
8913
+ toolCalls.push({
8914
+ args: record.args && typeof record.args === "object" ? record.args : {},
8915
+ id: typeof record.id === "string" ? record.id : undefined,
8916
+ name: record.name
8917
+ });
8918
+ }
8919
+ return toolCalls;
8920
+ };
8921
+ var createGeminiVoiceAssistantModel = (options) => {
8922
+ const fetchImpl = options.fetch ?? globalThis.fetch;
8923
+ const baseUrl = options.baseUrl ?? "https://generativelanguage.googleapis.com/v1beta";
8924
+ const model = options.model ?? "gemini-2.5-flash";
8925
+ const maxRetries = Math.max(0, options.maxRetries ?? 2);
8926
+ return {
8927
+ generate: async (input) => {
8928
+ const endpoint = `${baseUrl.replace(/\/$/, "")}/models/${encodeURIComponent(model)}:generateContent?key=${encodeURIComponent(options.apiKey)}`;
8929
+ let response;
8930
+ for (let attempt = 0;attempt <= maxRetries; attempt += 1) {
8931
+ response = await fetchImpl(endpoint, {
8932
+ body: JSON.stringify({
8933
+ contents: input.messages.map(messageToGeminiContent).filter(Boolean),
8934
+ generationConfig: {
8935
+ maxOutputTokens: options.maxOutputTokens,
8936
+ ...input.tools.length ? {} : {
8937
+ responseMimeType: "application/json",
8938
+ responseSchema: toGeminiSchema(OUTPUT_SCHEMA)
8939
+ },
8940
+ temperature: options.temperature
8941
+ },
8942
+ systemInstruction: {
8943
+ parts: [
8944
+ {
8945
+ text: [input.system, ROUTE_RESULT_INSTRUCTION].filter(Boolean).join(`
8946
+
8947
+ `)
8948
+ }
8949
+ ]
8950
+ },
8951
+ tools: input.tools.length ? [
8952
+ {
8953
+ functionDeclarations: input.tools.map((tool) => ({
8954
+ description: tool.description,
8955
+ name: tool.name,
8956
+ parameters: toGeminiSchema(tool.parameters ?? {
8957
+ additionalProperties: true,
8958
+ type: "object"
8959
+ })
8960
+ }))
8961
+ }
8962
+ ] : undefined
8963
+ }),
8964
+ headers: {
8965
+ "content-type": "application/json"
8966
+ },
8967
+ method: "POST"
8968
+ });
8969
+ if (response.ok || response.status !== 429 && response.status < 500 || attempt === maxRetries) {
8970
+ break;
8971
+ }
8972
+ const retryAfter = Number(response.headers.get("retry-after"));
8973
+ await sleep4(Number.isFinite(retryAfter) && retryAfter > 0 ? retryAfter * 1000 : 500 * 2 ** attempt);
8974
+ }
8975
+ if (!response) {
8976
+ throw new Error("Gemini voice assistant model failed: no response");
8977
+ }
8978
+ if (!response.ok) {
8979
+ throw createHTTPError("Gemini", response);
8980
+ }
8981
+ const body = await response.json();
8982
+ if (body.usageMetadata && typeof body.usageMetadata === "object") {
8983
+ await options.onUsage?.(body.usageMetadata);
8984
+ }
8985
+ const toolCalls = extractGeminiToolCalls(body);
8986
+ if (toolCalls.length) {
8987
+ return {
8988
+ assistantText: extractGeminiText(body) || undefined,
8989
+ toolCalls
8990
+ };
8991
+ }
8992
+ return normalizeRouteOutput(parseJSON(extractGeminiText(body)));
8993
+ }
8994
+ };
8995
+ };
8996
+ // src/opsConsoleRoutes.ts
8997
+ import { Elysia as Elysia9 } from "elysia";
8998
+
8999
+ // src/handoffHealth.ts
9000
+ import { Elysia as Elysia6 } from "elysia";
9001
+ var escapeHtml8 = (value) => value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
9002
+ var getString5 = (value) => typeof value === "string" && value.length > 0 ? value : undefined;
9003
+ var isStatus = (value) => value === "delivered" || value === "failed" || value === "skipped";
9004
+ var increment3 = (record, key) => {
9005
+ record[key] = (record[key] ?? 0) + 1;
9006
+ };
9007
+ var normalizeDelivery = (adapterId, value) => {
9008
+ const record = value && typeof value === "object" ? value : {};
9009
+ return {
9010
+ adapterId: getString5(record.adapterId) ?? adapterId,
9011
+ adapterKind: getString5(record.adapterKind),
9012
+ deliveredAt: typeof record.deliveredAt === "number" ? record.deliveredAt : undefined,
9013
+ deliveredTo: getString5(record.deliveredTo),
9014
+ error: getString5(record.error),
9015
+ status: isStatus(record.status) ? record.status : "failed"
9016
+ };
9017
+ };
9018
+ var normalizeDeliveries = (payload) => {
9019
+ const deliveries = payload.deliveries;
9020
+ if (!deliveries || typeof deliveries !== "object") {
9021
+ return [];
9022
+ }
9023
+ return Object.entries(deliveries).map(([adapterId, value]) => normalizeDelivery(adapterId, value));
9024
+ };
9025
+ var resolveReplayHref = (event, replayHref) => {
9026
+ if (replayHref === false) {
9027
+ return;
9028
+ }
9029
+ if (typeof replayHref === "function") {
9030
+ return replayHref(event);
9031
+ }
9032
+ return `${replayHref ?? "/api/voice-sessions"}/${encodeURIComponent(event.sessionId)}/replay/htmx`;
9033
+ };
9034
+ var summarizeVoiceHandoffHealth = async (options = {}) => {
9035
+ const sourceEvents = options.events ?? await options.store?.list() ?? [];
9036
+ const search = options.q?.trim().toLowerCase();
9037
+ const byAction = {};
9038
+ const byAdapter = {};
9039
+ const byStatus = {
9040
+ delivered: 0,
9041
+ failed: 0,
9042
+ skipped: 0
9043
+ };
9044
+ const events = sourceEvents.filter((event) => event.type === "call.handoff").map((event) => {
9045
+ const status = isStatus(event.payload.status) ? event.payload.status : "failed";
9046
+ const deliveries = normalizeDeliveries(event.payload);
9047
+ const item = {
9048
+ action: getString5(event.payload.action),
9049
+ at: event.at,
9050
+ deliveries,
9051
+ reason: getString5(event.payload.reason),
9052
+ sessionId: event.sessionId,
9053
+ status,
9054
+ target: getString5(event.payload.target)
9055
+ };
9056
+ return {
9057
+ ...item,
9058
+ replayHref: resolveReplayHref(item, options.replayHref)
9059
+ };
9060
+ }).filter((event) => {
9061
+ if (options.status && options.status !== "all" && event.status !== options.status) {
9062
+ return false;
9063
+ }
9064
+ if (!search) {
9065
+ return true;
9066
+ }
9067
+ return [
9068
+ event.action,
9069
+ event.reason,
9070
+ event.sessionId,
9071
+ event.status,
9072
+ event.target,
9073
+ ...event.deliveries.flatMap((delivery) => [
9074
+ delivery.adapterId,
9075
+ delivery.adapterKind,
9076
+ delivery.deliveredTo,
9077
+ delivery.error,
9078
+ delivery.status
9079
+ ])
9080
+ ].some((value) => value?.toLowerCase().includes(search));
9081
+ }).sort((left, right) => right.at - left.at).slice(0, options.limit ?? 50);
9082
+ for (const event of events) {
9083
+ byStatus[event.status] += 1;
9084
+ if (event.action) {
9085
+ increment3(byAction, event.action);
9086
+ }
9087
+ for (const delivery of event.deliveries) {
9088
+ byAdapter[delivery.adapterId] ??= {
9089
+ delivered: 0,
9090
+ failed: 0,
9091
+ skipped: 0
9092
+ };
9093
+ byAdapter[delivery.adapterId][delivery.status] += 1;
9094
+ }
9095
+ }
9096
+ return {
9097
+ byAction,
9098
+ byAdapter,
9099
+ byStatus,
9100
+ events,
9101
+ failed: byStatus.failed,
9102
+ total: events.length
9103
+ };
9104
+ };
9105
+ var renderMetricGrid = (summary) => [
9106
+ '<section class="voice-handoff-health-grid">',
9107
+ `<article><span>Total</span><strong>${String(summary.total)}</strong></article>`,
9108
+ `<article><span>Delivered</span><strong>${String(summary.byStatus.delivered)}</strong></article>`,
9109
+ `<article><span>Failed</span><strong>${String(summary.byStatus.failed)}</strong></article>`,
9110
+ `<article><span>Skipped</span><strong>${String(summary.byStatus.skipped)}</strong></article>`,
9111
+ "</section>"
9112
+ ].join("");
9113
+ var renderActionSummary = (summary) => {
9114
+ const actions = Object.entries(summary.byAction).sort((left, right) => right[1] - left[1]);
9115
+ const adapters = Object.entries(summary.byAdapter).sort(([left], [right]) => left.localeCompare(right));
9116
+ return [
9117
+ '<section class="voice-handoff-health-columns">',
9118
+ "<article><h3>Actions</h3>",
9119
+ actions.length === 0 ? "<p>No handoff actions yet.</p>" : `<ul>${actions.map(([action, count]) => `<li>${escapeHtml8(action)}: ${String(count)}</li>`).join("")}</ul>`,
9120
+ "</article>",
9121
+ "<article><h3>Adapters</h3>",
9122
+ adapters.length === 0 ? "<p>No adapter deliveries yet.</p>" : `<ul>${adapters.map(([adapterId, counts]) => `<li>${escapeHtml8(adapterId)}: ${String(counts.delivered)} delivered / ${String(counts.failed)} failed / ${String(counts.skipped)} skipped</li>`).join("")}</ul>`,
9123
+ "</article>",
9124
+ "</section>"
9125
+ ].join("");
9126
+ };
9127
+ var renderVoiceHandoffHealthHTML = (summary) => [
9128
+ '<div class="voice-handoff-health">',
9129
+ renderMetricGrid(summary),
9130
+ renderActionSummary(summary),
9131
+ "<section>",
9132
+ "<h3>Recent Handoffs</h3>",
9133
+ summary.events.length === 0 ? '<p class="voice-handoff-health-empty">No handoffs found.</p>' : [
9134
+ '<div class="voice-handoff-health-events">',
9135
+ ...summary.events.map((event) => [
9136
+ `<article class="${escapeHtml8(event.status)}">`,
9137
+ '<div class="voice-handoff-health-event-header">',
9138
+ `<strong>${escapeHtml8(event.action ?? "handoff")}</strong>`,
9139
+ `<span>${escapeHtml8(event.status)}</span>`,
9140
+ "</div>",
9141
+ `<p><small>${escapeHtml8(event.sessionId)}</small></p>`,
9142
+ event.target ? `<p>Target: ${escapeHtml8(event.target)}</p>` : "",
9143
+ event.reason ? `<p>Reason: ${escapeHtml8(event.reason)}</p>` : "",
9144
+ event.deliveries.length ? `<ul>${event.deliveries.map((delivery) => [
9145
+ "<li>",
9146
+ `${escapeHtml8(delivery.adapterId)}: ${escapeHtml8(delivery.status)}`,
9147
+ delivery.deliveredTo ? ` to ${escapeHtml8(delivery.deliveredTo)}` : "",
9148
+ delivery.error ? ` (${escapeHtml8(delivery.error)})` : "",
9149
+ "</li>"
9150
+ ].join("")).join("")}</ul>` : "",
9151
+ event.replayHref ? `<p><a href="${escapeHtml8(event.replayHref)}">Open replay</a></p>` : "",
9152
+ "</article>"
9153
+ ].join("")),
9154
+ "</div>"
9155
+ ].join(""),
9156
+ "</section>",
9157
+ "</div>"
9158
+ ].join("");
9159
+ var createVoiceHandoffHealthJSONHandler = (options = {}) => async ({ query }) => summarizeVoiceHandoffHealth({
9160
+ ...options,
9161
+ limit: typeof query?.limit === "string" ? Number(query.limit) : options.limit,
9162
+ q: query?.q ?? options.q,
9163
+ status: query?.status === "delivered" || query?.status === "failed" || query?.status === "skipped" || query?.status === "all" ? query.status : options.status
9164
+ });
9165
+ var createVoiceHandoffHealthHTMLHandler = (options = {}) => async ({ query }) => {
9166
+ const summary = await summarizeVoiceHandoffHealth({
9167
+ ...options,
9168
+ limit: typeof query?.limit === "string" ? Number(query.limit) : options.limit,
9169
+ q: query?.q ?? options.q,
9170
+ status: query?.status === "delivered" || query?.status === "failed" || query?.status === "skipped" || query?.status === "all" ? query.status : options.status
9171
+ });
9172
+ const body = await (options.render?.(summary) ?? renderVoiceHandoffHealthHTML(summary));
9173
+ return new Response(body, {
9174
+ headers: {
9175
+ "Content-Type": "text/html; charset=utf-8",
9176
+ ...options.headers
9177
+ }
9178
+ });
9179
+ };
9180
+ var createVoiceHandoffHealthRoutes = (options = {}) => {
9181
+ const path = options.path ?? "/api/voice-handoffs";
9182
+ const htmlPath = options.htmlPath === undefined ? `${path}/htmx` : options.htmlPath;
9183
+ const routes = new Elysia6({
9184
+ name: options.name ?? "absolutejs-voice-handoff-health"
9185
+ }).get(path, createVoiceHandoffHealthJSONHandler(options));
9186
+ if (htmlPath) {
9187
+ routes.get(htmlPath, createVoiceHandoffHealthHTMLHandler(options));
9188
+ }
9189
+ return routes;
9190
+ };
9191
+
9192
+ // src/qualityRoutes.ts
9193
+ import { Elysia as Elysia7 } from "elysia";
9194
+ var DEFAULT_THRESHOLDS = {
9195
+ maxDuplicateTurnRate: 0,
9196
+ maxEmptyTurnRate: 0.02,
9197
+ maxHandoffFailureRate: 0,
9198
+ maxMissingAssistantReplyRate: 0.05,
9199
+ maxProviderAverageLatencyMs: 3000,
9200
+ maxProviderErrorRate: 0.05,
9201
+ maxProviderFallbackRate: 0.25,
9202
+ maxProviderTimeoutRate: 0.03
9203
+ };
9204
+ var getString6 = (value) => typeof value === "string" ? value : undefined;
9205
+ var getNumber3 = (value) => typeof value === "number" && Number.isFinite(value) ? value : undefined;
9206
+ var rate = (count, total) => count / Math.max(1, total);
9207
+ var roundMetric2 = (value) => Math.round(value * 1e4) / 1e4;
9208
+ var createMetric = (input) => ({
9209
+ ...input,
9210
+ actual: roundMetric2(input.actual),
9211
+ pass: input.actual <= input.threshold
9212
+ });
9213
+ var evaluateVoiceQuality = async (input) => {
9214
+ const events = filterVoiceTraceEvents(input.events ?? await input.store?.list() ?? []);
9215
+ const thresholds = {
9216
+ ...DEFAULT_THRESHOLDS,
9217
+ ...input.thresholds
9218
+ };
9219
+ const committedTurns = events.filter((event) => event.type === "turn.committed");
9220
+ const assistantReplies = events.filter((event) => event.type === "turn.assistant");
9221
+ const sessionIdsWithAssistantReply = new Set(assistantReplies.map((event) => event.sessionId));
9222
+ const sessionsWithTurns = new Set(committedTurns.map((event) => event.sessionId));
9223
+ const emptyTurns = committedTurns.filter((event) => !getString6(event.payload.text)?.trim());
9224
+ const turnTextsBySession = new Map;
9225
+ let duplicateTurns = 0;
9226
+ for (const turn of committedTurns) {
9227
+ const normalized = getString6(turn.payload.text)?.trim().toLowerCase();
9228
+ if (!normalized) {
9229
+ continue;
9230
+ }
9231
+ const seen = turnTextsBySession.get(turn.sessionId) ?? new Set;
9232
+ if (seen.has(normalized)) {
9233
+ duplicateTurns += 1;
9234
+ }
9235
+ seen.add(normalized);
9236
+ turnTextsBySession.set(turn.sessionId, seen);
9237
+ }
9238
+ const missingAssistantReplySessions = [...sessionsWithTurns].filter((sessionId) => !sessionIdsWithAssistantReply.has(sessionId)).length;
9239
+ const providerEvents = events.filter((event) => event.type === "session.error" && typeof event.payload.provider === "string" && typeof event.payload.providerStatus === "string");
9240
+ const providerErrors = providerEvents.filter((event) => event.payload.providerStatus === "error");
9241
+ const providerFallbacks = providerEvents.filter((event) => event.payload.providerStatus === "fallback");
9242
+ const providerTimeouts = providerEvents.filter((event) => event.payload.timedOut === true);
9243
+ const providerLatencies = providerEvents.map((event) => getNumber3(event.payload.elapsedMs)).filter((value) => value !== undefined);
9244
+ const averageProviderLatencyMs = providerLatencies.length > 0 ? providerLatencies.reduce((sum, value) => sum + value, 0) / providerLatencies.length : 0;
9245
+ const handoffHealth = await summarizeVoiceHandoffHealth({ events });
9246
+ const metrics = {
9247
+ duplicateTurnRate: createMetric({
9248
+ actual: rate(duplicateTurns, committedTurns.length),
9249
+ label: "Duplicate turn rate",
9250
+ threshold: thresholds.maxDuplicateTurnRate,
9251
+ unit: "rate"
9252
+ }),
9253
+ emptyTurnRate: createMetric({
9254
+ actual: rate(emptyTurns.length, committedTurns.length),
9255
+ label: "Empty turn rate",
9256
+ threshold: thresholds.maxEmptyTurnRate,
9257
+ unit: "rate"
9258
+ }),
9259
+ handoffFailureRate: createMetric({
9260
+ actual: rate(handoffHealth.failed, handoffHealth.total),
9261
+ label: "Handoff failure rate",
9262
+ threshold: thresholds.maxHandoffFailureRate,
9263
+ unit: "rate"
9264
+ }),
9265
+ missingAssistantReplyRate: createMetric({
9266
+ actual: rate(missingAssistantReplySessions, sessionsWithTurns.size),
9267
+ label: "Missing assistant reply rate",
9268
+ threshold: thresholds.maxMissingAssistantReplyRate,
9269
+ unit: "rate"
9270
+ }),
9271
+ providerAverageLatencyMs: createMetric({
9272
+ actual: averageProviderLatencyMs,
9273
+ label: "Average provider latency",
9274
+ threshold: thresholds.maxProviderAverageLatencyMs,
9275
+ unit: "ms"
9276
+ }),
9277
+ providerErrorRate: createMetric({
9278
+ actual: rate(providerErrors.length, providerEvents.length),
9279
+ label: "Provider error rate",
9280
+ threshold: thresholds.maxProviderErrorRate,
9281
+ unit: "rate"
9282
+ }),
9283
+ providerFallbackRate: createMetric({
9284
+ actual: rate(providerFallbacks.length, providerEvents.length),
9285
+ label: "Provider fallback rate",
9286
+ threshold: thresholds.maxProviderFallbackRate,
9287
+ unit: "rate"
9288
+ }),
9289
+ providerTimeoutRate: createMetric({
9290
+ actual: rate(providerTimeouts.length, providerEvents.length),
9291
+ label: "Provider timeout rate",
9292
+ threshold: thresholds.maxProviderTimeoutRate,
9293
+ unit: "rate"
9294
+ })
9295
+ };
9296
+ const status = Object.values(metrics).every((metric) => metric.pass) ? "pass" : "fail";
9297
+ return {
9298
+ checkedAt: Date.now(),
9299
+ eventCount: events.length,
9300
+ metrics,
9301
+ status,
9302
+ thresholds
9303
+ };
9304
+ };
9305
+ var escapeHtml9 = (value) => value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
9306
+ var formatMetricValue = (metric) => metric.unit === "rate" ? `${(metric.actual * 100).toFixed(2)}%` : metric.unit === "ms" ? `${Math.round(metric.actual)}ms` : String(metric.actual);
9307
+ var formatThreshold = (metric) => metric.unit === "rate" ? `${(metric.threshold * 100).toFixed(2)}%` : metric.unit === "ms" ? `${Math.round(metric.threshold)}ms` : String(metric.threshold);
9308
+ var renderVoiceQualityHTML = (report, options = {}) => {
9309
+ const rows = Object.entries(report.metrics).map(([key, metric]) => `<tr class="${metric.pass ? "pass" : "fail"}"><td>${escapeHtml9(metric.label)}</td><td>${escapeHtml9(formatMetricValue(metric))}</td><td>${escapeHtml9(formatThreshold(metric))}</td><td>${metric.pass ? "pass" : "fail"}</td><td><code>${escapeHtml9(key)}</code></td></tr>`).join("");
9310
+ const links = options.links?.length ? `<nav>${options.links.map((link) => `<a href="${escapeHtml9(link.href)}">${escapeHtml9(link.label)}</a>`).join("")}</nav>` : "";
9311
+ 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>`;
9312
+ };
9313
+ var createVoiceQualityRoutes = (options) => {
9314
+ const path = options.path ?? "/quality";
9315
+ const routes = new Elysia7({
9316
+ name: options.name ?? "absolutejs-voice-quality"
9317
+ });
9318
+ const getReport = () => evaluateVoiceQuality({
9319
+ events: options.events,
9320
+ store: options.store,
9321
+ thresholds: options.thresholds
9322
+ });
9323
+ routes.get(path, async () => {
9324
+ const report = await getReport();
9325
+ return new Response(renderVoiceQualityHTML(report, { links: options.links }), {
9326
+ headers: {
9327
+ "Content-Type": "text/html; charset=utf-8",
9328
+ ...options.headers
9329
+ }
9330
+ });
9331
+ });
9332
+ routes.get(`${path}/json`, async () => getReport());
9333
+ routes.get(`${path}/status`, async ({ set }) => {
9334
+ const report = await getReport();
9335
+ if (report.status === "fail") {
9336
+ set.status = 503;
9337
+ }
9338
+ return report;
9339
+ });
9340
+ return routes;
9341
+ };
9342
+
9343
+ // src/resilienceRoutes.ts
9344
+ import { Elysia as Elysia8 } from "elysia";
9345
+ var escapeHtml10 = (value) => value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
9346
+ var getString7 = (value) => typeof value === "string" ? value : undefined;
9347
+ var getNumber4 = (value) => typeof value === "number" && Number.isFinite(value) ? value : undefined;
9348
+ var getBoolean2 = (value) => value === true;
9349
+ var isProviderStatus2 = (value) => value === "error" || value === "fallback" || value === "success";
9350
+ var listVoiceRoutingEvents = (events) => {
9351
+ const routingEvents = [];
9352
+ for (const event of events) {
9353
+ if (event.type !== "session.error") {
9354
+ continue;
9355
+ }
9356
+ const provider = getString7(event.payload.provider);
9357
+ const providerStatus = isProviderStatus2(event.payload.providerStatus) ? event.payload.providerStatus : undefined;
9358
+ if (!provider || !providerStatus) {
9359
+ continue;
9360
+ }
9361
+ const kind = getString7(event.payload.kind);
9362
+ routingEvents.push({
9363
+ at: event.at,
9364
+ attempt: getNumber4(event.payload.attempt),
9365
+ elapsedMs: getNumber4(event.payload.elapsedMs),
9366
+ error: getString7(event.payload.error),
9367
+ fallbackProvider: getString7(event.payload.fallbackProvider),
9368
+ kind: kind === "stt" || kind === "tts" ? kind : "llm",
9369
+ latencyBudgetMs: getNumber4(event.payload.latencyBudgetMs),
9370
+ operation: getString7(event.payload.operation),
9371
+ provider,
9372
+ selectedProvider: getString7(event.payload.selectedProvider),
9373
+ sessionId: event.sessionId,
9374
+ status: providerStatus,
9375
+ timedOut: getBoolean2(event.payload.timedOut),
9376
+ turnId: event.turnId
9377
+ });
9378
+ }
9379
+ return routingEvents.sort((left, right) => right.at - left.at);
9380
+ };
9381
+ var summarizeRoutingEvents = (events) => {
9382
+ const byKind = new Map;
9383
+ let errors = 0;
9384
+ let fallbacks = 0;
9385
+ let timeouts = 0;
9386
+ for (const event of events) {
9387
+ byKind.set(event.kind, (byKind.get(event.kind) ?? 0) + 1);
9388
+ if (event.status === "error") {
9389
+ errors += 1;
9390
+ }
9391
+ if (event.status === "fallback") {
9392
+ fallbacks += 1;
9393
+ }
9394
+ if (event.timedOut) {
9395
+ timeouts += 1;
9396
+ }
9397
+ }
9398
+ return {
9399
+ byKind,
9400
+ errors,
9401
+ fallbacks,
9402
+ timeouts,
9403
+ total: events.length
9404
+ };
9405
+ };
9406
+ var renderProviderCards = (title, providers) => {
9407
+ if (providers.length === 0) {
9408
+ return `<p class="muted">No ${escapeHtml10(title)} provider health yet.</p>`;
9409
+ }
9410
+ return `<div class="provider-grid">${providers.map((provider) => `
9411
+ <article class="card provider ${escapeHtml10(provider.status)}">
9412
+ <div class="card-header">
9413
+ <strong>${escapeHtml10(provider.provider)}</strong>
9414
+ <span>${escapeHtml10(provider.status)}${provider.recommended ? " \xB7 recommended" : ""}</span>
9415
+ </div>
9416
+ <dl>
9417
+ <div><dt>Runs</dt><dd>${provider.runCount}</dd></div>
9418
+ <div><dt>Avg latency</dt><dd>${provider.averageElapsedMs ?? 0}ms</dd></div>
9419
+ <div><dt>Errors</dt><dd>${provider.errorCount}</dd></div>
9420
+ <div><dt>Timeouts</dt><dd>${provider.timeoutCount}</dd></div>
9421
+ <div><dt>Fallbacks</dt><dd>${provider.fallbackCount}</dd></div>
9422
+ </dl>
9423
+ ${provider.lastError ? `<p class="muted">${escapeHtml10(provider.lastError)}</p>` : ""}
9424
+ </article>
9425
+ `).join("")}</div>`;
9426
+ };
9427
+ var renderTimeline2 = (events) => {
9428
+ if (events.length === 0) {
9429
+ return '<p class="muted">No provider routing events yet. Run the app or simulate provider failover.</p>';
9430
+ }
9431
+ return `<div class="timeline">${events.slice(0, 40).map((event) => `
9432
+ <article class="card event ${escapeHtml10(event.status ?? "unknown")}">
9433
+ <div class="card-header">
9434
+ <strong>${escapeHtml10(event.kind.toUpperCase())} ${escapeHtml10(event.operation ?? "generate")}</strong>
9435
+ <span>${new Date(event.at).toLocaleString()}</span>
9436
+ </div>
9437
+ <p>
9438
+ <span class="pill">${escapeHtml10(event.status ?? "unknown")}</span>
9439
+ <span class="pill">provider: ${escapeHtml10(event.provider ?? "unknown")}</span>
9440
+ ${event.fallbackProvider ? `<span class="pill">fallback: ${escapeHtml10(event.fallbackProvider)}</span>` : ""}
9441
+ ${event.timedOut ? '<span class="pill danger">timed out</span>' : ""}
9442
+ </p>
9443
+ <dl>
9444
+ <div><dt>Attempt</dt><dd>${event.attempt ?? 0}</dd></div>
9445
+ <div><dt>Elapsed</dt><dd>${event.elapsedMs ?? 0}ms</dd></div>
9446
+ <div><dt>Budget</dt><dd>${event.latencyBudgetMs ?? 0}ms</dd></div>
9447
+ <div><dt>Session</dt><dd>${escapeHtml10(event.sessionId)}</dd></div>
9448
+ </dl>
9449
+ ${event.error ? `<p class="muted">${escapeHtml10(event.error)}</p>` : ""}
9450
+ </article>
9451
+ `).join("")}</div>`;
9452
+ };
9453
+ var renderSimulationControls = (kind, simulation) => {
9454
+ if (!simulation) {
9455
+ return "";
9456
+ }
9457
+ const configuredProviders = simulation.providers.filter((provider) => provider.configured !== false);
9458
+ if (configuredProviders.length === 0) {
9459
+ return `<p class="muted">No ${kind.toUpperCase()} providers are configured for simulation.</p>`;
9460
+ }
9461
+ const pathPrefix = simulation.pathPrefix ?? `/api/${kind}-simulate`;
9462
+ const failureProviders = simulation.failureProviders ?? configuredProviders.map(({ provider }) => provider);
9463
+ const canFail = (provider) => configuredProviders.some((entry) => entry.provider === provider) && (!simulation.fallbackRequiredProvider || configuredProviders.some((entry) => entry.provider === simulation.fallbackRequiredProvider));
9464
+ return `<div class="simulate-panel" data-sim-kind="${kind}" data-sim-prefix="${escapeHtml10(pathPrefix)}">
9465
+ <p class="muted">${escapeHtml10(simulation.failureMessage ?? `Simulate ${kind.toUpperCase()} provider failure without changing provider credentials.`)}</p>
9466
+ <div class="simulate-actions">
9467
+ ${failureProviders.map((provider) => `<button type="button" data-provider-fail="${escapeHtml10(provider)}"${canFail(provider) ? "" : " disabled"}>Simulate ${escapeHtml10(provider)} ${kind.toUpperCase()} failure</button>`).join("")}
9468
+ ${configuredProviders.map((provider) => `<button type="button" data-provider-recover="${escapeHtml10(provider.provider)}">Mark ${escapeHtml10(provider.provider)} recovered</button>`).join("")}
9469
+ </div>
9470
+ ${simulation.fallbackRequiredProvider && !configuredProviders.some((entry) => entry.provider === simulation.fallbackRequiredProvider) ? `<p class="muted">${escapeHtml10(simulation.fallbackRequiredMessage ?? `Configure ${simulation.fallbackRequiredProvider} to enable fallback simulation.`)}</p>` : ""}
9471
+ <pre class="simulate-output" hidden></pre>
9472
+ </div>`;
9473
+ };
9474
+ var renderVoiceResilienceHTML = (input) => {
9475
+ const summary = summarizeRoutingEvents(input.routingEvents);
9476
+ const kindCounts = [...summary.byKind.entries()].map(([kind, count]) => `<span class="pill">${escapeHtml10(kind)}: ${String(count)}</span>`).join("");
9477
+ const links = input.links?.length ? input.links.map((link) => `<a href="${escapeHtml10(link.href)}">${escapeHtml10(link.label)}</a>`).join(" \xB7 ") : "";
9478
+ return `<!doctype html>
9479
+ <html lang="en">
9480
+ <head>
9481
+ <meta charset="utf-8" />
9482
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
9483
+ <title>${escapeHtml10(input.title ?? "AbsoluteJS Voice Resilience")}</title>
9484
+ <style>
9485
+ :root { color-scheme: dark; }
9486
+ 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; }
9487
+ main { display: grid; gap: 16px; margin: 0 auto; max-width: 1180px; }
9488
+ section, .card { background: rgba(19, 22, 27, 0.92); border: 1px solid #27272a; border-radius: 20px; padding: 20px; }
9489
+ .hero { background: linear-gradient(135deg, rgba(14, 165, 233, 0.18), rgba(245, 158, 11, 0.12)); }
9490
+ .grid, .provider-grid { display: grid; gap: 14px; grid-template-columns: repeat(4, minmax(0, 1fr)); }
9491
+ .timeline { display: grid; gap: 12px; }
9492
+ .card-header { align-items: center; display: flex; gap: 12px; justify-content: space-between; }
9493
+ .card-header strong { font-size: 1.05rem; }
9494
+ .metric strong { display: block; font-size: 2rem; margin-top: 6px; }
9495
+ .muted, dt, span { color: #a1a1aa; }
9496
+ dl { display: grid; gap: 8px; grid-template-columns: repeat(4, minmax(0, 1fr)); }
9497
+ dl div { background: #0f1217; border: 1px solid #27272a; border-radius: 12px; padding: 10px; }
9498
+ dd { font-weight: 800; margin: 4px 0 0; }
9499
+ .pill { background: #0f1217; border: 1px solid #3f3f46; border-radius: 999px; color: #d4d4d8; display: inline-flex; margin: 3px 4px 3px 0; padding: 5px 9px; }
9500
+ .danger { border-color: rgba(239, 68, 68, 0.75); color: #fecaca; }
9501
+ .event.error { border-color: rgba(239, 68, 68, 0.7); }
9502
+ .event.fallback { border-color: rgba(245, 158, 11, 0.7); }
9503
+ .event.success, .provider.healthy { border-color: rgba(34, 197, 94, 0.5); }
9504
+ .provider.suppressed, .provider.degraded, .provider.rate-limited { border-color: rgba(239, 68, 68, 0.7); }
9505
+ .provider.recoverable { border-color: rgba(59, 130, 246, 0.7); }
9506
+ button { background: #f59e0b; border: 0; border-radius: 999px; color: #111827; cursor: pointer; font-weight: 800; padding: 10px 14px; }
9507
+ button:disabled { cursor: not-allowed; opacity: 0.45; }
9508
+ .simulate-actions { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 12px; }
9509
+ .simulate-output { background: #050505; border: 1px solid #27272a; border-radius: 14px; color: #d4d4d8; overflow: auto; padding: 12px; white-space: pre-wrap; }
9510
+ a { color: #f59e0b; }
9511
+ @media (max-width: 850px) { .grid, .provider-grid, dl { grid-template-columns: 1fr; } }
9512
+ </style>
9513
+ </head>
9514
+ <body>
9515
+ <main>
9516
+ <section class="hero">
9517
+ <h1>Provider routing and resilience</h1>
9518
+ <p>One view for the production reliability story: LLM failover, STT/TTS routing, latency budgets, timeouts, and fallback decisions.</p>
9519
+ ${links ? `<p>${links}</p>` : ""}
9520
+ <p>${kindCounts || '<span class="pill">No routing events yet</span>'}</p>
9521
+ </section>
9522
+ <section class="grid">
9523
+ <article class="card metric"><span>Total routing events</span><strong>${summary.total}</strong></article>
9524
+ <article class="card metric"><span>Fallbacks</span><strong>${summary.fallbacks}</strong></article>
9525
+ <article class="card metric"><span>Errors</span><strong>${summary.errors}</strong></article>
9526
+ <article class="card metric"><span>Timeouts</span><strong>${summary.timeouts}</strong></article>
9527
+ </section>
9528
+ <section>
9529
+ <h2>LLM provider health</h2>
9530
+ ${renderProviderCards("LLM", input.llmProviderHealth)}
9531
+ </section>
9532
+ <section>
9533
+ <h2>STT provider health</h2>
9534
+ ${renderSimulationControls("stt", input.sttSimulation)}
9535
+ ${renderProviderCards("STT", input.sttProviderHealth)}
9536
+ </section>
9537
+ <section>
9538
+ <h2>TTS provider health</h2>
9539
+ ${renderSimulationControls("tts", input.ttsSimulation)}
9540
+ ${renderProviderCards("TTS", input.ttsProviderHealth)}
9541
+ </section>
9542
+ <section>
9543
+ <h2>Routing timeline</h2>
9544
+ ${renderTimeline2(input.routingEvents)}
9545
+ </section>
9546
+ </main>
9547
+ <script>
9548
+ const showResult = (panel, result) => {
9549
+ const output = panel.querySelector(".simulate-output");
9550
+ if (!output) return;
9551
+ output.hidden = false;
9552
+ output.textContent = JSON.stringify(result, null, 2);
9553
+ };
9554
+ document.querySelectorAll("[data-sim-prefix]").forEach((panel) => {
9555
+ const prefix = panel.getAttribute("data-sim-prefix");
9556
+ panel.querySelectorAll("[data-provider-fail]").forEach((button) => {
9557
+ button.addEventListener("click", async () => {
9558
+ const provider = button.getAttribute("data-provider-fail");
9559
+ const response = await fetch(prefix + "/failure?provider=" + encodeURIComponent(provider || ""), { method: "POST" });
9560
+ showResult(panel, await response.json());
9561
+ if (response.ok) window.setTimeout(() => window.location.reload(), 450);
9562
+ });
9563
+ });
9564
+ panel.querySelectorAll("[data-provider-recover]").forEach((button) => {
9565
+ button.addEventListener("click", async () => {
9566
+ const provider = button.getAttribute("data-provider-recover");
9567
+ const response = await fetch(prefix + "/recovery?provider=" + encodeURIComponent(provider || ""), { method: "POST" });
9568
+ showResult(panel, await response.json());
9569
+ if (response.ok) window.setTimeout(() => window.location.reload(), 450);
9570
+ });
9571
+ });
9572
+ });
9573
+ </script>
9574
+ </body>
9575
+ </html>`;
9576
+ };
9577
+ var providerFromQuery = (value, providers) => typeof value === "string" && providers.some((provider) => provider.provider === value && provider.configured !== false) ? value : undefined;
9578
+ var registerSimulationRoutes = (routes, simulation, defaultPathPrefix) => {
9579
+ if (!simulation) {
9580
+ return routes;
9581
+ }
9582
+ const pathPrefix = simulation.pathPrefix ?? defaultPathPrefix;
9583
+ routes.post(`${pathPrefix}/failure`, async ({ query, set }) => {
9584
+ const provider = providerFromQuery(query.provider, simulation.providers);
9585
+ if (!provider) {
9586
+ set.status = 400;
9587
+ return {
9588
+ error: "Provider is not configured for simulation."
9589
+ };
9590
+ }
9591
+ if (simulation.failureProviders && !simulation.failureProviders.includes(provider)) {
9592
+ set.status = 400;
9593
+ return {
9594
+ error: `${provider} is not configured for failure simulation.`
9595
+ };
9596
+ }
9597
+ if (simulation.fallbackRequiredProvider && !simulation.providers.some((entry) => entry.provider === simulation.fallbackRequiredProvider && entry.configured !== false)) {
9598
+ set.status = 400;
9599
+ return {
9600
+ error: simulation.fallbackRequiredMessage ?? `Configure ${simulation.fallbackRequiredProvider} before simulating fallback.`
9601
+ };
9602
+ }
9603
+ return simulation.run(provider, "failure");
9604
+ });
9605
+ routes.post(`${pathPrefix}/recovery`, async ({ query, set }) => {
9606
+ const provider = providerFromQuery(query.provider, simulation.providers);
9607
+ if (!provider) {
9608
+ set.status = 400;
9609
+ return {
9610
+ error: "Provider is not configured for simulation."
9611
+ };
9612
+ }
9613
+ return simulation.run(provider, "recovery");
9614
+ });
9615
+ return routes;
9616
+ };
9617
+ var createVoiceResilienceRoutes = (options) => {
9618
+ const path = options.path ?? "/resilience";
9619
+ const routes = new Elysia8({
9620
+ name: options.name ?? "absolutejs-voice-resilience"
9621
+ }).get(path, async () => {
9622
+ const events = await options.store.list();
9623
+ const sttEvents = events.filter((event) => event.payload.kind === "stt");
9624
+ const ttsEvents = events.filter((event) => event.payload.kind === "tts");
9625
+ const data = {
9626
+ links: options.links,
9627
+ llmProviderHealth: await summarizeVoiceProviderHealth({
9628
+ events,
9629
+ providers: options.llmProviders ?? []
9630
+ }),
9631
+ routingEvents: listVoiceRoutingEvents(events),
9632
+ sttProviderHealth: await summarizeVoiceProviderHealth({
9633
+ events: sttEvents,
9634
+ providers: options.sttProviders ?? []
9635
+ }),
9636
+ sttSimulation: options.sttSimulation,
9637
+ title: options.title,
9638
+ ttsProviderHealth: await summarizeVoiceProviderHealth({
9639
+ events: ttsEvents,
9640
+ providers: options.ttsProviders ?? []
9641
+ }),
9642
+ ttsSimulation: options.ttsSimulation
9643
+ };
9644
+ const body = await (options.render ?? renderVoiceResilienceHTML)(data);
9645
+ return new Response(body, {
9646
+ headers: {
9647
+ "Content-Type": "text/html; charset=utf-8",
9648
+ ...options.headers
9649
+ }
9650
+ });
9651
+ });
9652
+ registerSimulationRoutes(routes, options.sttSimulation, "/api/stt-simulate");
9653
+ registerSimulationRoutes(routes, options.ttsSimulation, "/api/tts-simulate");
9654
+ return routes;
9655
+ };
9656
+
9657
+ // src/opsConsoleRoutes.ts
9658
+ var DEFAULT_LINKS = [
9659
+ {
9660
+ description: "Quality gates for CI, deploy checks, and production readiness.",
9661
+ href: "/quality",
9662
+ label: "Quality",
9663
+ statusHref: "/quality/status"
9664
+ },
9665
+ {
9666
+ description: "Provider health, fallback paths, and failure simulation.",
9667
+ href: "/resilience",
9668
+ label: "Resilience"
9669
+ },
9670
+ {
9671
+ description: "Redacted trace exports for debugging and support handoffs.",
9672
+ href: "/diagnostics",
9673
+ label: "Diagnostics"
9674
+ },
9675
+ {
9676
+ description: "Recent sessions with replay links.",
9677
+ href: "/sessions",
9678
+ label: "Sessions"
9679
+ },
9680
+ {
9681
+ description: "Transfer and webhook delivery health.",
9682
+ href: "/handoffs",
9683
+ label: "Handoffs"
9684
+ }
9685
+ ];
9686
+ var escapeHtml11 = (value) => value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
9687
+ var countProviderStatuses = (providers) => {
9688
+ const degradedStatuses = new Set(["degraded", "rate-limited", "suppressed"]);
9689
+ const healthy = providers.filter((provider) => provider.status === "healthy").length;
9690
+ const degraded = providers.filter((provider) => degradedStatuses.has(provider.status)).length;
9691
+ return {
9692
+ degraded,
9693
+ healthy,
9694
+ total: providers.length
9695
+ };
9696
+ };
9697
+ var buildVoiceOpsConsoleReport = async (options) => {
9698
+ const events = await options.store.list();
9699
+ const providers = [
9700
+ ...await summarizeVoiceProviderHealth({
9701
+ events,
9702
+ providers: options.llmProviders
9703
+ }),
9704
+ ...await summarizeVoiceProviderHealth({
9705
+ events,
9706
+ providers: options.sttProviders
9707
+ }),
9708
+ ...await summarizeVoiceProviderHealth({
9709
+ events,
9710
+ providers: options.ttsProviders
9711
+ })
9712
+ ];
9713
+ const handoffs = await summarizeVoiceHandoffHealth({ events });
9714
+ const sessions = await summarizeVoiceSessions({
9715
+ events,
9716
+ limit: 8,
9717
+ status: "all"
9718
+ });
9719
+ const quality = await evaluateVoiceQuality({ events });
9720
+ const routingEvents = listVoiceRoutingEvents(events).slice(0, 10);
9721
+ const trace = summarizeVoiceTrace(events);
9722
+ return {
9723
+ checkedAt: Date.now(),
9724
+ eventCount: events.length,
9725
+ handoffs: {
9726
+ failed: handoffs.failed,
9727
+ total: handoffs.total
9728
+ },
9729
+ links: options.links ?? DEFAULT_LINKS,
9730
+ providers: countProviderStatuses(providers),
9731
+ quality,
9732
+ recentRoutingEvents: routingEvents,
9733
+ recentSessions: sessions,
9734
+ sessions: {
9735
+ failed: sessions.filter((session) => session.status === "failed").length,
9736
+ healthy: sessions.filter((session) => session.status === "healthy").length,
9737
+ total: sessions.length
9738
+ },
9739
+ trace
9740
+ };
9741
+ };
9742
+ var renderMetricCard = (input) => `<article class="metric"><span>${escapeHtml11(input.label)}</span><strong>${escapeHtml11(String(input.value))}</strong>${input.status ? `<p class="${escapeHtml11(input.status)}">${escapeHtml11(input.status)}</p>` : ""}${input.href ? `<a href="${escapeHtml11(input.href)}">Open</a>` : ""}</article>`;
9743
+ var renderVoiceOpsConsoleHTML = (report, options = {}) => {
9744
+ const links = report.links.map((link) => `<article class="surface">
9745
+ <div><h2>${escapeHtml11(link.label)}</h2>${link.description ? `<p>${escapeHtml11(link.description)}</p>` : ""}</div>
9746
+ <p><a href="${escapeHtml11(link.href)}">Open ${escapeHtml11(link.label)}</a>${link.statusHref ? ` \xB7 <a href="${escapeHtml11(link.statusHref)}">Status</a>` : ""}</p>
9747
+ </article>`).join("");
9748
+ const sessions = report.recentSessions.length ? report.recentSessions.map((session) => `<tr><td>${escapeHtml11(session.sessionId)}</td><td>${escapeHtml11(session.status)}</td><td>${session.turnCount}</td><td>${session.errorCount}</td><td>${session.replayHref ? `<a href="${escapeHtml11(session.replayHref)}">Replay</a>` : ""}</td></tr>`).join("") : '<tr><td colspan="5">No sessions yet.</td></tr>';
9749
+ const routing = report.recentRoutingEvents.length ? report.recentRoutingEvents.map((event) => `<tr><td>${escapeHtml11(event.kind)}</td><td>${escapeHtml11(event.provider ?? "unknown")}</td><td>${escapeHtml11(event.status ?? "unknown")}</td><td>${event.elapsedMs ?? 0}ms</td><td>${escapeHtml11(event.sessionId)}</td></tr>`).join("") : '<tr><td colspan="5">No provider routing events yet.</td></tr>';
9750
+ const title = options.title ?? "AbsoluteJS Voice Ops Console";
9751
+ return `<!doctype html><html lang="en"><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /><title>${escapeHtml11(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>${escapeHtml11(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 ${escapeHtml11(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>`;
9752
+ };
9753
+ var createVoiceOpsConsoleRoutes = (options) => {
9754
+ const path = options.path ?? "/ops-console";
9755
+ const routes = new Elysia9({
9756
+ name: options.name ?? "absolutejs-voice-ops-console"
9757
+ });
9758
+ const getReport = () => buildVoiceOpsConsoleReport(options);
9759
+ routes.get(path, async () => {
9760
+ const report = await getReport();
9761
+ return new Response(renderVoiceOpsConsoleHTML(report, { title: options.title }), {
9762
+ headers: {
9763
+ "Content-Type": "text/html; charset=utf-8",
9764
+ ...options.headers
9765
+ }
9766
+ });
9767
+ });
9768
+ routes.get(`${path}/json`, async () => getReport());
9769
+ return routes;
9770
+ };
9771
+ // src/providerAdapters.ts
9772
+ class VoiceIOProviderTimeoutError extends Error {
9773
+ provider;
9774
+ timeoutMs;
9775
+ constructor(kind, provider, timeoutMs) {
9776
+ super(`Voice ${kind} provider ${provider} exceeded ${timeoutMs}ms latency budget.`);
9777
+ this.name = "VoiceIOProviderTimeoutError";
9778
+ this.provider = provider;
9779
+ this.timeoutMs = timeoutMs;
9780
+ }
9781
+ }
9782
+ var errorMessage2 = (error) => error instanceof Error ? error.message : String(error);
9783
+ var createEmitter = () => {
9784
+ const listeners = new Map;
9785
+ return {
9786
+ emit: async (event, payload) => {
9787
+ await Promise.all([...listeners.get(event) ?? []].map((handler) => Promise.resolve(handler(payload))));
9788
+ },
9789
+ on: (event, handler) => {
9790
+ const set = listeners.get(event) ?? new Set;
9791
+ set.add(handler);
9792
+ listeners.set(event, set);
9793
+ return () => {
9794
+ set.delete(handler);
9795
+ };
9796
+ }
9797
+ };
9798
+ };
9799
+ var getTimeoutMs = (options, provider) => {
9800
+ const timeoutMs = options.providerProfiles?.[provider]?.timeoutMs ?? options.timeoutMs;
9801
+ return typeof timeoutMs === "number" && Number.isFinite(timeoutMs) && timeoutMs > 0 ? timeoutMs : undefined;
9802
+ };
9803
+ var withTimeout = async (input) => {
9804
+ if (!input.timeoutMs) {
9805
+ return input.run();
9806
+ }
9807
+ let timeout;
9808
+ try {
9809
+ return await Promise.race([
9810
+ Promise.resolve(input.run()),
9811
+ new Promise((_, reject) => {
9812
+ timeout = setTimeout(() => reject(new VoiceIOProviderTimeoutError(input.kind, input.provider, input.timeoutMs)), input.timeoutMs);
9813
+ })
9814
+ ]);
9815
+ } finally {
9816
+ if (timeout) {
9817
+ clearTimeout(timeout);
9818
+ }
9819
+ }
9820
+ };
9821
+ var createResolver = (options) => {
9822
+ const providerIds = Object.keys(options.adapters);
9823
+ const firstProvider = providerIds[0];
9824
+ const healthOptions = typeof options.providerHealth === "object" ? options.providerHealth : options.providerHealth ? {} : undefined;
9825
+ const healthState = new Map;
9826
+ const now = () => healthOptions?.now?.() ?? Date.now();
9827
+ const failureThreshold = Math.max(1, healthOptions?.failureThreshold ?? 1);
9828
+ const cooldownMs = Math.max(0, healthOptions?.cooldownMs ?? 30000);
9829
+ const getHealth = (provider) => {
9830
+ const existing = healthState.get(provider);
9831
+ if (existing) {
9832
+ return existing;
9833
+ }
9834
+ const next = {
9835
+ consecutiveFailures: 0,
9836
+ provider,
9837
+ status: "healthy"
9838
+ };
9839
+ healthState.set(provider, next);
9840
+ return next;
9841
+ };
9842
+ const cloneHealth = (provider) => {
9843
+ if (!healthOptions) {
9844
+ return;
9845
+ }
9846
+ return {
9847
+ ...getHealth(provider)
9848
+ };
9849
+ };
9850
+ const getSuppressionRemainingMs = (provider) => {
9851
+ if (!healthOptions) {
9852
+ return;
9853
+ }
9854
+ const suppressedUntil = getHealth(provider).suppressedUntil;
9855
+ return typeof suppressedUntil === "number" ? Math.max(0, suppressedUntil - now()) : undefined;
9856
+ };
9857
+ const isSuppressed = (provider) => {
9858
+ if (!healthOptions) {
9859
+ return false;
9860
+ }
9861
+ const suppressedUntil = getHealth(provider).suppressedUntil;
9862
+ return typeof suppressedUntil === "number" && suppressedUntil > now();
9863
+ };
9864
+ const recordSuccess = (provider) => {
9865
+ if (!healthOptions) {
9866
+ return;
9867
+ }
9868
+ const health = getHealth(provider);
9869
+ health.consecutiveFailures = 0;
9870
+ health.status = "healthy";
9871
+ health.suppressedUntil = undefined;
9872
+ return cloneHealth(provider);
9873
+ };
9874
+ const recordError = (provider, isProviderError) => {
9875
+ if (!healthOptions || !isProviderError) {
9876
+ return cloneHealth(provider);
9877
+ }
9878
+ const health = getHealth(provider);
9879
+ health.consecutiveFailures += 1;
9880
+ health.lastFailureAt = now();
9881
+ if (health.consecutiveFailures >= failureThreshold) {
9882
+ health.status = "suppressed";
9883
+ health.suppressedUntil = now() + cooldownMs;
9884
+ }
9885
+ return cloneHealth(provider);
9886
+ };
9887
+ const resolveOrder = async (input) => {
9888
+ const selectedProvider = await options.selectProvider?.(input) ?? firstProvider;
9889
+ const fallbackOrder = typeof options.fallback === "function" ? await options.fallback(input) : options.fallback;
9890
+ const candidates = [selectedProvider, ...fallbackOrder ?? providerIds];
9891
+ const seen = new Set;
9892
+ const rankedOrder = candidates.filter((provider) => {
9893
+ if (!provider || seen.has(provider) || !options.adapters[provider]) {
9894
+ return false;
9895
+ }
9896
+ seen.add(provider);
9897
+ return true;
9898
+ });
9899
+ const healthyOrder = healthOptions ? rankedOrder.filter((provider) => !isSuppressed(provider)) : rankedOrder;
9900
+ const order = healthyOrder.length ? healthyOrder : rankedOrder;
9901
+ return {
9902
+ order,
9903
+ selectedProvider: selectedProvider && !isSuppressed(selectedProvider) ? selectedProvider : order[0]
9904
+ };
9905
+ };
9906
+ const emit = async (event, input) => {
9907
+ await options.onProviderEvent?.(event, input);
9908
+ };
9909
+ return {
9910
+ emit,
9911
+ getSuppressionRemainingMs,
9912
+ providerIds,
9913
+ recordError,
9914
+ recordSuccess,
9915
+ resolveOrder
9916
+ };
9917
+ };
9918
+ var createVoiceSTTProviderRouter = (options) => {
9919
+ const resolver = createResolver(options);
9920
+ return {
9921
+ kind: "stt",
9922
+ open: async (input) => {
9923
+ const { order, selectedProvider } = await resolver.resolveOrder(input);
9924
+ if (!selectedProvider || order.length === 0) {
9925
+ throw new Error("Voice STT provider router has no available providers.");
9926
+ }
9927
+ let lastError;
9928
+ for (const [index, provider] of order.entries()) {
9929
+ const adapter = options.adapters[provider];
9930
+ if (!adapter) {
9931
+ continue;
9932
+ }
9933
+ const startedAt = Date.now();
9934
+ try {
9935
+ const session = await withTimeout({
9936
+ kind: "stt",
9937
+ operation: "open",
9938
+ provider,
9939
+ run: () => adapter.open(input),
9940
+ timeoutMs: getTimeoutMs(options, provider)
9941
+ });
9942
+ const providerHealth = resolver.recordSuccess(provider);
9943
+ await resolver.emit({
9944
+ at: Date.now(),
9945
+ attempt: index + 1,
9946
+ elapsedMs: Date.now() - startedAt,
9947
+ fallbackProvider: provider === selectedProvider ? undefined : provider,
9948
+ kind: "stt",
9949
+ latencyBudgetMs: getTimeoutMs(options, provider),
9950
+ operation: "open",
9951
+ provider,
9952
+ providerHealth,
9953
+ selectedProvider,
9954
+ status: provider === selectedProvider ? "success" : "fallback"
9955
+ }, input);
9956
+ return session;
9957
+ } catch (error) {
9958
+ lastError = error;
9959
+ const hasNextProvider = index < order.length - 1;
9960
+ const shouldFallback = options.isProviderError?.(error, provider) ?? true;
9961
+ const providerHealth = resolver.recordError(provider, shouldFallback);
9962
+ await resolver.emit({
9963
+ at: Date.now(),
9964
+ attempt: index + 1,
9965
+ elapsedMs: Date.now() - startedAt,
9966
+ error: errorMessage2(error),
9967
+ fallbackProvider: shouldFallback ? order[index + 1] : undefined,
9968
+ kind: "stt",
9969
+ latencyBudgetMs: getTimeoutMs(options, provider),
9970
+ operation: "open",
9971
+ provider,
9972
+ providerHealth,
9973
+ selectedProvider,
9974
+ status: "error",
9975
+ suppressionRemainingMs: resolver.getSuppressionRemainingMs(provider),
9976
+ suppressedUntil: providerHealth?.suppressedUntil,
9977
+ timedOut: error instanceof VoiceIOProviderTimeoutError
9978
+ }, input);
9979
+ if (!hasNextProvider || !shouldFallback) {
9980
+ throw error;
9981
+ }
9982
+ }
9983
+ }
9984
+ throw lastError ?? new Error("Voice STT provider router did not open a provider.");
9985
+ }
9986
+ };
9987
+ };
9988
+ var createVoiceTTSProviderRouter = (options) => {
9989
+ const resolver = createResolver(options);
9990
+ return {
9991
+ kind: "tts",
9992
+ open: async (input) => {
9993
+ const { order, selectedProvider } = await resolver.resolveOrder(input);
9994
+ if (!selectedProvider || order.length === 0) {
9995
+ throw new Error("Voice TTS provider router has no available providers.");
9996
+ }
9997
+ const emitter = createEmitter();
9998
+ let activeSession;
9999
+ let activeProvider;
10000
+ let nextProviderIndex = 0;
10001
+ const attach = (session) => {
10002
+ session.on("audio", (event) => emitter.emit("audio", event));
10003
+ session.on("error", (event) => emitter.emit("error", event));
10004
+ session.on("close", (event) => emitter.emit("close", event));
10005
+ };
10006
+ const openProvider = async (provider, attempt) => {
10007
+ const adapter = options.adapters[provider];
10008
+ if (!adapter) {
10009
+ throw new Error(`Voice TTS provider ${provider} is not configured.`);
10010
+ }
10011
+ const startedAt = Date.now();
10012
+ const session = await withTimeout({
10013
+ kind: "tts",
10014
+ operation: "open",
10015
+ provider,
10016
+ run: () => adapter.open(input),
10017
+ timeoutMs: getTimeoutMs(options, provider)
10018
+ });
10019
+ attach(session);
10020
+ activeSession = session;
10021
+ activeProvider = provider;
10022
+ const providerHealth = resolver.recordSuccess(provider);
10023
+ await resolver.emit({
10024
+ at: Date.now(),
10025
+ attempt,
10026
+ elapsedMs: Date.now() - startedAt,
10027
+ fallbackProvider: provider === selectedProvider ? undefined : provider,
10028
+ kind: "tts",
10029
+ latencyBudgetMs: getTimeoutMs(options, provider),
10030
+ operation: "open",
10031
+ provider,
10032
+ providerHealth,
10033
+ selectedProvider,
10034
+ status: provider === selectedProvider ? "success" : "fallback"
10035
+ }, input);
10036
+ return session;
10037
+ };
10038
+ const failProvider = async (inputEvent) => {
10039
+ const shouldFallback = options.isProviderError?.(inputEvent.error, inputEvent.provider) ?? true;
10040
+ const providerHealth = resolver.recordError(inputEvent.provider, shouldFallback);
10041
+ await resolver.emit({
10042
+ at: Date.now(),
10043
+ attempt: inputEvent.attempt,
10044
+ elapsedMs: Date.now() - inputEvent.startedAt,
10045
+ error: errorMessage2(inputEvent.error),
10046
+ fallbackProvider: shouldFallback ? order[nextProviderIndex] : undefined,
10047
+ kind: "tts",
10048
+ latencyBudgetMs: getTimeoutMs(options, inputEvent.provider),
10049
+ operation: inputEvent.operation,
10050
+ provider: inputEvent.provider,
10051
+ providerHealth,
10052
+ selectedProvider,
10053
+ status: "error",
10054
+ suppressionRemainingMs: resolver.getSuppressionRemainingMs(inputEvent.provider),
10055
+ suppressedUntil: providerHealth?.suppressedUntil,
10056
+ timedOut: inputEvent.error instanceof VoiceIOProviderTimeoutError
10057
+ }, input);
10058
+ return shouldFallback;
10059
+ };
10060
+ for (const [index, provider] of order.entries()) {
10061
+ nextProviderIndex = index + 1;
10062
+ const startedAt = Date.now();
10063
+ try {
10064
+ await openProvider(provider, index + 1);
10065
+ break;
10066
+ } catch (error) {
10067
+ const shouldFallback = await failProvider({
10068
+ attempt: index + 1,
10069
+ error,
10070
+ operation: "open",
10071
+ provider,
10072
+ startedAt
10073
+ });
10074
+ if (!shouldFallback || index >= order.length - 1) {
10075
+ throw error;
10076
+ }
10077
+ }
10078
+ }
10079
+ if (!activeSession || !activeProvider) {
10080
+ throw new Error("Voice TTS provider router did not open a provider.");
10081
+ }
10082
+ const sendWithFallback = async (text) => {
10083
+ for (;; ) {
10084
+ const session = activeSession;
10085
+ const provider = activeProvider;
10086
+ if (!session || !provider) {
10087
+ throw new Error("Voice TTS provider router has no active provider.");
10088
+ }
10089
+ const startedAt = Date.now();
10090
+ try {
10091
+ await withTimeout({
10092
+ kind: "tts",
10093
+ operation: "send",
10094
+ provider,
10095
+ run: () => session.send(text),
10096
+ timeoutMs: getTimeoutMs(options, provider)
10097
+ });
10098
+ return;
10099
+ } catch (error) {
10100
+ const shouldFallback = await failProvider({
10101
+ attempt: nextProviderIndex,
10102
+ error,
10103
+ operation: "send",
10104
+ provider,
10105
+ startedAt
10106
+ });
10107
+ const nextProvider = order[nextProviderIndex];
10108
+ if (!shouldFallback || !nextProvider) {
10109
+ throw error;
10110
+ }
10111
+ nextProviderIndex += 1;
10112
+ await session.close("tts-provider-fallback").catch(() => {});
10113
+ await openProvider(nextProvider, nextProviderIndex);
10114
+ }
10115
+ }
10116
+ };
10117
+ return {
10118
+ close: async (reason) => {
10119
+ await activeSession?.close(reason);
10120
+ activeSession = undefined;
10121
+ activeProvider = undefined;
10122
+ await emitter.emit("close", {
10123
+ reason,
10124
+ type: "close"
10125
+ });
10126
+ },
10127
+ on: emitter.on,
10128
+ send: sendWithFallback
10129
+ };
10130
+ }
10131
+ };
7030
10132
  };
7031
- var createVoiceFileRuntimeStorage = (options) => ({
7032
- events: createVoiceFileIntegrationEventStore({
7033
- ...options,
7034
- directory: join(options.directory, "events")
7035
- }),
7036
- externalObjects: createVoiceFileExternalObjectMapStore({
7037
- ...options,
7038
- directory: join(options.directory, "external-objects")
7039
- }),
7040
- memories: createVoiceFileAssistantMemoryStore({
7041
- ...options,
7042
- directory: join(options.directory, "memories")
7043
- }),
7044
- reviews: createVoiceFileReviewStore({
7045
- ...options,
7046
- directory: join(options.directory, "reviews")
7047
- }),
7048
- session: createVoiceFileSessionStore({
7049
- ...options,
7050
- directory: join(options.directory, "sessions")
7051
- }),
7052
- tasks: createVoiceFileTaskStore({
7053
- ...options,
7054
- directory: join(options.directory, "tasks")
7055
- }),
7056
- traceDeliveries: createVoiceFileTraceSinkDeliveryStore({
7057
- ...options,
7058
- directory: join(options.directory, "trace-deliveries")
7059
- }),
7060
- traces: createVoiceFileTraceEventStore({
7061
- ...options,
7062
- directory: join(options.directory, "traces")
7063
- })
7064
- });
7065
- var createStoredVoiceCallReviewArtifact = (id, artifact) => withVoiceCallReviewId(id, artifact);
7066
- var createStoredVoiceOpsTask = (id, task) => withVoiceOpsTaskId(id, task);
7067
- var createStoredVoiceIntegrationEvent = (id, event) => withVoiceIntegrationEventId(id, event);
7068
- var createStoredVoiceExternalObjectMap = (mapping) => createVoiceExternalObjectMap({
7069
- at: mapping.at,
7070
- externalId: mapping.externalId,
7071
- provider: mapping.provider,
7072
- sinkId: mapping.sinkId,
7073
- sourceId: mapping.sourceId,
7074
- sourceType: mapping.sourceType
7075
- });
7076
10133
  // src/sqliteStore.ts
7077
10134
  import { Database } from "bun:sqlite";
7078
10135
  var normalizeTableNameSegment = (value) => value.trim().replace(/[^a-zA-Z0-9_]+/g, "_").replace(/^_+|_+$/g, "") || "voice";
@@ -7554,6 +10611,169 @@ var createVoiceMemoryStore = () => {
7554
10611
  };
7555
10612
  return { get, getOrCreate, list, remove, set };
7556
10613
  };
10614
+ // src/opsWebhook.ts
10615
+ import { Elysia as Elysia10 } from "elysia";
10616
+ var toHex5 = (bytes) => Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
10617
+ var signVoiceOpsWebhookBody = async (input) => {
10618
+ const encoder = new TextEncoder;
10619
+ const key = await crypto.subtle.importKey("raw", encoder.encode(input.secret), {
10620
+ hash: "SHA-256",
10621
+ name: "HMAC"
10622
+ }, false, ["sign"]);
10623
+ const signature = await crypto.subtle.sign("HMAC", key, encoder.encode(`${input.timestamp}.${input.body}`));
10624
+ return `sha256=${toHex5(new Uint8Array(signature))}`;
10625
+ };
10626
+ var timingSafeEqual = (left, right) => {
10627
+ const encoder = new TextEncoder;
10628
+ const leftBytes = encoder.encode(left);
10629
+ const rightBytes = encoder.encode(right);
10630
+ if (leftBytes.length !== rightBytes.length) {
10631
+ return false;
10632
+ }
10633
+ let diff = 0;
10634
+ for (let index = 0;index < leftBytes.length; index += 1) {
10635
+ diff |= leftBytes[index] ^ rightBytes[index];
10636
+ }
10637
+ return diff === 0;
10638
+ };
10639
+ var resolveWebhookLink = async (resolver, event) => {
10640
+ if (typeof resolver === "function") {
10641
+ return resolver({
10642
+ event
10643
+ });
10644
+ }
10645
+ return resolver;
10646
+ };
10647
+ var joinBaseUrl = (baseUrl, path) => `${baseUrl.replace(/\/+$/, "")}/${path.replace(/^\/+/, "")}`;
10648
+ var asString = (value) => typeof value === "string" && value.length > 0 ? value : undefined;
10649
+ var buildVoiceOpsWebhookEntity = (event) => ({
10650
+ disposition: asString(event.payload.disposition),
10651
+ outcome: asString(event.payload.outcome),
10652
+ priority: asString(event.payload.priority),
10653
+ queue: asString(event.payload.queue),
10654
+ reviewId: asString(event.payload.reviewId),
10655
+ scenarioId: asString(event.payload.scenarioId),
10656
+ sessionId: asString(event.payload.sessionId),
10657
+ status: asString(event.payload.status),
10658
+ target: asString(event.payload.target),
10659
+ taskId: asString(event.payload.taskId)
10660
+ });
10661
+ var createVoiceOpsWebhookEnvelope = async (input) => {
10662
+ const entity = buildVoiceOpsWebhookEntity(input.event);
10663
+ const replayHref = await resolveWebhookLink(input.replayHref, input.event) ?? (input.baseUrl && entity.sessionId ? joinBaseUrl(input.baseUrl, `/api/voice-sessions/${encodeURIComponent(entity.sessionId)}/replay`) : undefined);
10664
+ const links = {
10665
+ event: await resolveWebhookLink(input.eventHref, input.event),
10666
+ replay: replayHref,
10667
+ review: await resolveWebhookLink(input.reviewHref, input.event),
10668
+ task: await resolveWebhookLink(input.taskHref, input.event)
10669
+ };
10670
+ return {
10671
+ entity,
10672
+ event: {
10673
+ createdAt: input.event.createdAt,
10674
+ id: input.event.id,
10675
+ payload: input.event.payload,
10676
+ type: input.event.type
10677
+ },
10678
+ links: links.event || links.replay || links.review || links.task ? links : undefined,
10679
+ schemaVersion: 1,
10680
+ source: "absolutejs-voice"
10681
+ };
10682
+ };
10683
+ var createVoiceOpsWebhookSink = (options) => createVoiceIntegrationHTTPSink({
10684
+ ...options,
10685
+ body: ({ event }) => createVoiceOpsWebhookEnvelope({
10686
+ baseUrl: options.baseUrl,
10687
+ event,
10688
+ eventHref: options.eventHref,
10689
+ replayHref: options.replayHref,
10690
+ reviewHref: options.reviewHref,
10691
+ taskHref: options.taskHref
10692
+ }),
10693
+ kind: options.kind ?? "ops-webhook"
10694
+ });
10695
+ var verifyVoiceOpsWebhookSignature = async (input) => {
10696
+ if (!input.secret) {
10697
+ return {
10698
+ ok: false,
10699
+ reason: "missing-secret"
10700
+ };
10701
+ }
10702
+ if (!input.signature) {
10703
+ return {
10704
+ ok: false,
10705
+ reason: "missing-signature"
10706
+ };
10707
+ }
10708
+ if (!input.signature.startsWith("sha256=")) {
10709
+ return {
10710
+ ok: false,
10711
+ reason: "unsupported-algorithm"
10712
+ };
10713
+ }
10714
+ if (!input.timestamp) {
10715
+ return {
10716
+ ok: false,
10717
+ reason: "missing-timestamp"
10718
+ };
10719
+ }
10720
+ const timestampMs = Number(input.timestamp);
10721
+ const toleranceMs = Math.max(0, input.toleranceMs ?? 5 * 60 * 1000);
10722
+ if (!Number.isFinite(timestampMs) || toleranceMs > 0 && Math.abs((input.now ?? Date.now()) - timestampMs) > toleranceMs) {
10723
+ return {
10724
+ ok: false,
10725
+ reason: "stale-timestamp"
10726
+ };
10727
+ }
10728
+ const expected = await signVoiceOpsWebhookBody({
10729
+ body: input.body,
10730
+ secret: input.secret,
10731
+ timestamp: input.timestamp
10732
+ });
10733
+ if (!timingSafeEqual(expected, input.signature)) {
10734
+ return {
10735
+ ok: false,
10736
+ reason: "invalid-signature"
10737
+ };
10738
+ }
10739
+ return {
10740
+ ok: true
10741
+ };
10742
+ };
10743
+ var createVoiceOpsWebhookReceiverRoutes = (options = {}) => {
10744
+ const path = options.path ?? "/api/voice-ops/webhook";
10745
+ return new Elysia10().post(path, async ({ body, request, set }) => {
10746
+ const bodyText = typeof body === "string" ? body : JSON.stringify(body);
10747
+ if (options.signingSecret) {
10748
+ const verification = await verifyVoiceOpsWebhookSignature({
10749
+ body: bodyText,
10750
+ secret: options.signingSecret,
10751
+ signature: request.headers.get("x-absolutejs-signature"),
10752
+ timestamp: request.headers.get("x-absolutejs-timestamp"),
10753
+ toleranceMs: options.toleranceMs
10754
+ });
10755
+ if (!verification.ok) {
10756
+ set.status = 401;
10757
+ return {
10758
+ ok: false,
10759
+ reason: verification.reason
10760
+ };
10761
+ }
10762
+ }
10763
+ const envelope = JSON.parse(bodyText);
10764
+ await options.onEnvelope?.({
10765
+ envelope,
10766
+ request
10767
+ });
10768
+ return {
10769
+ eventId: envelope.event?.id,
10770
+ ok: true,
10771
+ type: envelope.event?.type
10772
+ };
10773
+ }, {
10774
+ parse: "text"
10775
+ });
10776
+ };
7557
10777
  // src/queue.ts
7558
10778
  var releaseLeaseScript = `
7559
10779
  if redis.call("GET", KEYS[1]) == ARGV[1] then
@@ -7625,6 +10845,8 @@ var shouldDeadLetterSinkEvent = (event, sinks, maxFailures) => typeof maxFailure
7625
10845
  var shouldDeadLetterTask = (task, maxFailures) => typeof maxFailures === "number" && maxFailures > 0 && (task.processingAttempts ?? 0) >= maxFailures;
7626
10846
  var shouldProcessTraceDeliveryStatus = (status, allowed) => allowed.includes(status);
7627
10847
  var shouldDeadLetterTraceDelivery = (delivery, maxFailures) => typeof maxFailures === "number" && maxFailures > 0 && (delivery.deliveryAttempts ?? 0) >= maxFailures;
10848
+ var shouldProcessHandoffDeliveryStatus = (status, allowed) => allowed.includes(status);
10849
+ var shouldDeadLetterHandoffDelivery = (delivery, maxFailures) => typeof maxFailures === "number" && maxFailures > 0 && (delivery.deliveryAttempts ?? 0) >= maxFailures;
7628
10850
  var summarizeVoiceIntegrationEvents = (events, input = {}) => {
7629
10851
  const buildSummary = async () => {
7630
10852
  const deadLetterIds = new Set(input.deadLetters ? (await input.deadLetters.list()).map((event) => event.id) : []);
@@ -7706,6 +10928,48 @@ var summarizeVoiceTraceSinkDeliveries = (deliveries, input = {}) => {
7706
10928
  };
7707
10929
  return buildSummary();
7708
10930
  };
10931
+ var summarizeVoiceHandoffDeliveries = (deliveries, input = {}) => {
10932
+ const buildSummary = async () => {
10933
+ const deadLetterIds = new Set(input.deadLetters ? (await input.deadLetters.list()).map((delivery) => delivery.id) : []);
10934
+ const byAction = new Map;
10935
+ const summary = {
10936
+ byAction: [],
10937
+ deadLettered: 0,
10938
+ delivered: 0,
10939
+ failed: 0,
10940
+ pending: 0,
10941
+ retryEligible: 0,
10942
+ skipped: 0,
10943
+ total: deliveries.length
10944
+ };
10945
+ for (const delivery of deliveries) {
10946
+ byAction.set(delivery.action, (byAction.get(delivery.action) ?? 0) + 1);
10947
+ if (deadLetterIds.has(delivery.id)) {
10948
+ summary.deadLettered += 1;
10949
+ }
10950
+ switch (delivery.deliveryStatus) {
10951
+ case "delivered":
10952
+ summary.delivered += 1;
10953
+ break;
10954
+ case "failed":
10955
+ summary.failed += 1;
10956
+ if ((delivery.deliveryAttempts ?? 0) > 0) {
10957
+ summary.retryEligible += 1;
10958
+ }
10959
+ break;
10960
+ case "skipped":
10961
+ summary.skipped += 1;
10962
+ break;
10963
+ case "pending":
10964
+ summary.pending += 1;
10965
+ break;
10966
+ }
10967
+ }
10968
+ summary.byAction = [...byAction.entries()].sort((left, right) => right[1] - left[1]);
10969
+ return summary;
10970
+ };
10971
+ return buildSummary();
10972
+ };
7709
10973
  var summarizeVoiceOpsTaskQueue = (tasks, input = {}) => {
7710
10974
  const buildSummary = async () => {
7711
10975
  const deadLetterIds = new Set(input.deadLetters ? (await input.deadLetters.list()).map((task) => task.id) : []);
@@ -8135,6 +11399,108 @@ var createVoiceTraceSinkDeliveryWorkerLoop = (options) => {
8135
11399
  tick
8136
11400
  };
8137
11401
  };
11402
+ var createVoiceHandoffDeliveryWorker = (options) => {
11403
+ const allowedStatuses = options.statuses ?? ["pending", "failed"];
11404
+ const leaseMs = Math.max(1, options.leaseMs ?? 30000);
11405
+ return {
11406
+ drain: async () => {
11407
+ const result = {
11408
+ alreadyProcessed: 0,
11409
+ attempted: 0,
11410
+ deadLettered: 0,
11411
+ delivered: 0,
11412
+ failed: 0,
11413
+ skipped: 0
11414
+ };
11415
+ const deliveries = [...await options.deliveries.list()].sort((left, right) => left.createdAt - right.createdAt);
11416
+ for (const delivery of deliveries) {
11417
+ if (!shouldProcessHandoffDeliveryStatus(delivery.deliveryStatus, allowedStatuses)) {
11418
+ continue;
11419
+ }
11420
+ if (shouldDeadLetterHandoffDelivery(delivery, options.maxFailures)) {
11421
+ await options.deadLetters?.set(delivery.id, delivery);
11422
+ await options.onDeadLetter?.(delivery);
11423
+ result.deadLettered += 1;
11424
+ continue;
11425
+ }
11426
+ const claimed = await options.leases.claim({
11427
+ leaseMs,
11428
+ taskId: delivery.id,
11429
+ workerId: options.workerId
11430
+ });
11431
+ if (!claimed) {
11432
+ continue;
11433
+ }
11434
+ try {
11435
+ const idempotencyKey = `${delivery.id}:handoff`;
11436
+ if (options.idempotency && await options.idempotency.has(idempotencyKey)) {
11437
+ result.alreadyProcessed += 1;
11438
+ continue;
11439
+ }
11440
+ result.attempted += 1;
11441
+ const updatedDelivery = await deliverVoiceHandoffDelivery({
11442
+ adapters: options.adapters,
11443
+ api: options.api,
11444
+ delivery,
11445
+ failMode: options.failMode
11446
+ });
11447
+ await options.deliveries.set(updatedDelivery.id, updatedDelivery);
11448
+ if (updatedDelivery.deliveryStatus === "delivered" || updatedDelivery.deliveryStatus === "skipped") {
11449
+ await options.idempotency?.set(idempotencyKey, {
11450
+ ttlSeconds: options.idempotencyTtlSeconds
11451
+ });
11452
+ }
11453
+ if (updatedDelivery.deliveryStatus === "delivered") {
11454
+ result.delivered += 1;
11455
+ } else if (updatedDelivery.deliveryStatus === "skipped") {
11456
+ result.skipped += 1;
11457
+ } else if (updatedDelivery.deliveryStatus === "failed") {
11458
+ result.failed += 1;
11459
+ if (shouldDeadLetterHandoffDelivery(updatedDelivery, options.maxFailures)) {
11460
+ await options.deadLetters?.set(updatedDelivery.id, updatedDelivery);
11461
+ await options.onDeadLetter?.(updatedDelivery);
11462
+ result.deadLettered += 1;
11463
+ }
11464
+ }
11465
+ } finally {
11466
+ await options.leases.release({
11467
+ taskId: delivery.id,
11468
+ workerId: options.workerId
11469
+ });
11470
+ }
11471
+ }
11472
+ return result;
11473
+ }
11474
+ };
11475
+ };
11476
+ var createVoiceHandoffDeliveryWorkerLoop = (options) => {
11477
+ const pollIntervalMs = Math.max(1, options.pollIntervalMs ?? 1000);
11478
+ let timer;
11479
+ let running = false;
11480
+ const tick = async () => options.worker.drain();
11481
+ return {
11482
+ isRunning: () => running,
11483
+ start: () => {
11484
+ if (timer) {
11485
+ return;
11486
+ }
11487
+ running = true;
11488
+ timer = setInterval(() => {
11489
+ tick().catch((error) => {
11490
+ options.onError?.(error);
11491
+ });
11492
+ }, pollIntervalMs);
11493
+ },
11494
+ stop: () => {
11495
+ if (timer) {
11496
+ clearInterval(timer);
11497
+ timer = undefined;
11498
+ }
11499
+ running = false;
11500
+ },
11501
+ tick
11502
+ };
11503
+ };
8138
11504
  var createVoiceOpsTaskWorker = (options) => {
8139
11505
  const leaseMs = Math.max(1, options.leaseMs ?? 30000);
8140
11506
  const getTask = async (taskId) => {
@@ -8270,10 +11636,10 @@ var createVoiceOpsTaskProcessorWorker = (options) => ({
8270
11636
  result.completed += 1;
8271
11637
  } catch (error) {
8272
11638
  await options.onError?.(error, task);
8273
- const errorMessage = error instanceof Error ? error.message : String(error);
11639
+ const errorMessage3 = error instanceof Error ? error.message : String(error);
8274
11640
  const failedTask = failVoiceOpsTask(task, {
8275
11641
  actor: task.claimedBy ?? "ops-worker",
8276
- error: errorMessage
11642
+ error: errorMessage3
8277
11643
  });
8278
11644
  if (shouldDeadLetterTask(failedTask, options.maxFailures)) {
8279
11645
  const deadLetterTask = deadLetterVoiceOpsTask(failedTask, {
@@ -9086,7 +12452,7 @@ var createVoiceSTTRoutingCorrectionHandler = (mode = "generic") => {
9086
12452
  import { Buffer as Buffer2 } from "buffer";
9087
12453
  var TWILIO_MULAW_SAMPLE_RATE = 8000;
9088
12454
  var VOICE_PCM_SAMPLE_RATE = 16000;
9089
- var escapeXml = (value) => value.replaceAll("&", "&amp;").replaceAll('"', "&quot;").replaceAll("'", "&apos;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
12455
+ var escapeXml2 = (value) => value.replaceAll("&", "&amp;").replaceAll('"', "&quot;").replaceAll("'", "&apos;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
9090
12456
  var normalizeOnTurn2 = (handler) => {
9091
12457
  if (handler.length > 1) {
9092
12458
  const directHandler = handler;
@@ -9282,8 +12648,8 @@ var createTwilioSocketAdapter = (socket, getState) => ({
9282
12648
  }
9283
12649
  });
9284
12650
  var createTwilioVoiceResponse = (options) => {
9285
- const parameters = Object.entries(options.parameters ?? {}).filter((entry) => entry[1] !== undefined).map(([name, value]) => `<Parameter name="${escapeXml(name)}" value="${escapeXml(String(value))}" />`).join("");
9286
- 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>`;
12651
+ const parameters = Object.entries(options.parameters ?? {}).filter((entry) => entry[1] !== undefined).map(([name, value]) => `<Parameter name="${escapeXml2(name)}" value="${escapeXml2(String(value))}" />`).join("");
12652
+ 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>`;
9287
12653
  };
9288
12654
  var createTwilioMediaStreamBridge = (socket, options) => {
9289
12655
  const runtimePreset = resolveVoiceRuntimePreset(options.preset);
@@ -9519,15 +12885,22 @@ export {
9519
12885
  withVoiceOpsTaskId,
9520
12886
  withVoiceIntegrationEventId,
9521
12887
  voice,
12888
+ verifyVoiceOpsWebhookSignature,
9522
12889
  transcodeTwilioInboundPayloadToPCM16,
9523
12890
  transcodePCMToTwilioOutboundPayload,
9524
12891
  summarizeVoiceTraceSinkDeliveries,
9525
12892
  summarizeVoiceTrace,
12893
+ summarizeVoiceSessions,
12894
+ summarizeVoiceSessionReplay,
12895
+ summarizeVoiceProviderHealth,
9526
12896
  summarizeVoiceOpsTasks,
9527
12897
  summarizeVoiceOpsTaskQueue,
9528
12898
  summarizeVoiceOpsTaskAnalytics,
9529
12899
  summarizeVoiceIntegrationEvents,
12900
+ summarizeVoiceHandoffHealth,
12901
+ summarizeVoiceHandoffDeliveries,
9530
12902
  summarizeVoiceAssistantRuns,
12903
+ summarizeVoiceAssistantHealth,
9531
12904
  startVoiceOpsTask,
9532
12905
  shapeTelephonyAssistantText,
9533
12906
  selectVoiceTraceEventsForPrune,
@@ -9539,6 +12912,7 @@ export {
9539
12912
  resolveVoiceOpsTaskAssignment,
9540
12913
  resolveVoiceOpsTaskAgeBucket,
9541
12914
  resolveVoiceOpsPreset,
12915
+ resolveVoiceDiagnosticsTraceFilter,
9542
12916
  resolveVoiceAssistantMemoryNamespace,
9543
12917
  resolveTurnDetectionConfig,
9544
12918
  resolveAudioConditioningConfig,
@@ -9546,8 +12920,15 @@ export {
9546
12920
  reopenVoiceOpsTask,
9547
12921
  renderVoiceTraceMarkdown,
9548
12922
  renderVoiceTraceHTML,
12923
+ renderVoiceSessionsHTML,
12924
+ renderVoiceResilienceHTML,
12925
+ renderVoiceQualityHTML,
12926
+ renderVoiceProviderHealthHTML,
12927
+ renderVoiceOpsConsoleHTML,
12928
+ renderVoiceHandoffHealthHTML,
9549
12929
  renderVoiceCallReviewMarkdown,
9550
12930
  renderVoiceCallReviewHTML,
12931
+ renderVoiceAssistantHealthHTML,
9551
12932
  redactVoiceTraceText,
9552
12933
  redactVoiceTraceEvents,
9553
12934
  redactVoiceTraceEvent,
@@ -9555,6 +12936,7 @@ export {
9555
12936
  pruneVoiceTraceEvents,
9556
12937
  matchesVoiceOpsTaskAssignmentRule,
9557
12938
  markVoiceOpsTaskSLABreached,
12939
+ listVoiceRoutingEvents,
9558
12940
  listVoiceOpsTasks,
9559
12941
  isVoiceOpsTaskOverdue,
9560
12942
  heartbeatVoiceOpsTask,
@@ -9563,17 +12945,22 @@ export {
9563
12945
  failVoiceOpsTask,
9564
12946
  exportVoiceTrace,
9565
12947
  evaluateVoiceTrace,
12948
+ evaluateVoiceQuality,
9566
12949
  encodeTwilioMulawBase64,
9567
12950
  deliverVoiceTraceEventsToSinks,
9568
12951
  deliverVoiceIntegrationEventToSinks,
9569
12952
  deliverVoiceIntegrationEvent,
12953
+ deliverVoiceHandoffDelivery,
12954
+ deliverVoiceHandoff,
9570
12955
  decodeTwilioMulawBase64,
9571
12956
  deadLetterVoiceOpsTask,
9572
12957
  createVoiceZendeskTicketUpdateSink,
9573
12958
  createVoiceZendeskTicketSyncSinks,
9574
12959
  createVoiceZendeskTicketSink,
12960
+ createVoiceWebhookHandoffAdapter,
9575
12961
  createVoiceWebhookDeliveryWorkerLoop,
9576
12962
  createVoiceWebhookDeliveryWorker,
12963
+ createVoiceTwilioRedirectHandoffAdapter,
9577
12964
  createVoiceTraceSinkStore,
9578
12965
  createVoiceTraceSinkDeliveryWorkerLoop,
9579
12966
  createVoiceTraceSinkDeliveryWorker,
@@ -9585,9 +12972,17 @@ export {
9585
12972
  createVoiceTaskUpdatedEvent,
9586
12973
  createVoiceTaskSLABreachedEvent,
9587
12974
  createVoiceTaskCreatedEvent,
12975
+ createVoiceTTSProviderRouter,
12976
+ createVoiceSessionsJSONHandler,
12977
+ createVoiceSessionsHTMLHandler,
12978
+ createVoiceSessionReplayRoutes,
12979
+ createVoiceSessionReplayJSONHandler,
12980
+ createVoiceSessionReplayHTMLHandler,
9588
12981
  createVoiceSessionRecord,
12982
+ createVoiceSessionListRoutes,
9589
12983
  createVoiceSession,
9590
12984
  createVoiceSTTRoutingCorrectionHandler,
12985
+ createVoiceSTTProviderRouter,
9591
12986
  createVoiceSQLiteTraceSinkDeliveryStore,
9592
12987
  createVoiceSQLiteTraceEventStore,
9593
12988
  createVoiceSQLiteTaskStore,
@@ -9598,8 +12993,14 @@ export {
9598
12993
  createVoiceSQLiteExternalObjectMapStore,
9599
12994
  createVoiceS3ReviewStore,
9600
12995
  createVoiceReviewSavedEvent,
12996
+ createVoiceResilienceRoutes,
9601
12997
  createVoiceRedisTaskLeaseCoordinator,
9602
12998
  createVoiceRedisIdempotencyStore,
12999
+ createVoiceQualityRoutes,
13000
+ createVoiceProviderRouter,
13001
+ createVoiceProviderHealthRoutes,
13002
+ createVoiceProviderHealthJSONHandler,
13003
+ createVoiceProviderHealthHTMLHandler,
9603
13004
  createVoicePostgresTraceSinkDeliveryStore,
9604
13005
  createVoicePostgresTraceEventStore,
9605
13006
  createVoicePostgresTaskStore,
@@ -9608,13 +13009,18 @@ export {
9608
13009
  createVoicePostgresReviewStore,
9609
13010
  createVoicePostgresIntegrationEventStore,
9610
13011
  createVoicePostgresExternalObjectMapStore,
13012
+ createVoiceOpsWebhookSink,
13013
+ createVoiceOpsWebhookReceiverRoutes,
13014
+ createVoiceOpsWebhookEnvelope,
9611
13015
  createVoiceOpsTaskWorker,
9612
13016
  createVoiceOpsTaskProcessorWorkerLoop,
9613
13017
  createVoiceOpsTaskProcessorWorker,
9614
13018
  createVoiceOpsRuntime,
13019
+ createVoiceOpsConsoleRoutes,
9615
13020
  createVoiceMemoryTraceSinkDeliveryStore,
9616
13021
  createVoiceMemoryTraceEventStore,
9617
13022
  createVoiceMemoryStore,
13023
+ createVoiceMemoryHandoffDeliveryStore,
9618
13024
  createVoiceMemoryAssistantMemoryStore,
9619
13025
  createVoiceLinearIssueUpdateSink,
9620
13026
  createVoiceLinearIssueSyncSinks,
@@ -9627,6 +13033,12 @@ export {
9627
13033
  createVoiceHubSpotTaskSyncSinks,
9628
13034
  createVoiceHubSpotTaskSink,
9629
13035
  createVoiceHelpdeskTicketSink,
13036
+ createVoiceHandoffHealthRoutes,
13037
+ createVoiceHandoffHealthJSONHandler,
13038
+ createVoiceHandoffHealthHTMLHandler,
13039
+ createVoiceHandoffDeliveryWorkerLoop,
13040
+ createVoiceHandoffDeliveryWorker,
13041
+ createVoiceHandoffDeliveryRecord,
9630
13042
  createVoiceFileTraceSinkDeliveryStore,
9631
13043
  createVoiceFileTraceEventStore,
9632
13044
  createVoiceFileTaskStore,
@@ -9639,6 +13051,7 @@ export {
9639
13051
  createVoiceExternalObjectMapId,
9640
13052
  createVoiceExternalObjectMap,
9641
13053
  createVoiceExperiment,
13054
+ createVoiceDiagnosticsRoutes,
9642
13055
  createVoiceCallReviewRecorder,
9643
13056
  createVoiceCallReviewFromSession,
9644
13057
  createVoiceCallReviewFromLiveTelephonyReport,
@@ -9646,6 +13059,9 @@ export {
9646
13059
  createVoiceCRMActivitySink,
9647
13060
  createVoiceAssistantMemoryRecord,
9648
13061
  createVoiceAssistantMemoryHandle,
13062
+ createVoiceAssistantHealthRoutes,
13063
+ createVoiceAssistantHealthJSONHandler,
13064
+ createVoiceAssistantHealthHTMLHandler,
9649
13065
  createVoiceAssistant,
9650
13066
  createVoiceAgentTool,
9651
13067
  createVoiceAgentSquad,
@@ -9658,18 +13074,25 @@ export {
9658
13074
  createStoredVoiceCallReviewArtifact,
9659
13075
  createRiskyTurnCorrectionHandler,
9660
13076
  createPhraseHintCorrectionHandler,
13077
+ createOpenAIVoiceAssistantModel,
13078
+ createJSONVoiceAssistantModel,
9661
13079
  createId,
13080
+ createGeminiVoiceAssistantModel,
9662
13081
  createDomainPhraseHints,
9663
13082
  createDomainLexicon,
13083
+ createAnthropicVoiceAssistantModel,
9664
13084
  conditionAudioChunk,
9665
13085
  completeVoiceOpsTask,
9666
13086
  claimVoiceOpsTask,
9667
13087
  buildVoiceTraceReplay,
9668
13088
  buildVoiceOpsTaskFromSLABreach,
9669
13089
  buildVoiceOpsTaskFromReview,
13090
+ buildVoiceOpsConsoleReport,
13091
+ buildVoiceDiagnosticsMarkdown,
9670
13092
  assignVoiceOpsTask,
9671
13093
  applyVoiceOpsTaskPolicy,
9672
13094
  applyVoiceOpsTaskAssignmentRule,
13095
+ applyVoiceHandoffDeliveryResult,
9673
13096
  applyRiskTieredPhraseHintCorrections,
9674
13097
  applyPhraseHintCorrections,
9675
13098
  TURN_PROFILE_DEFAULTS