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

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 (41) 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/assistant.d.ts +20 -0
  6. package/dist/assistantHealth.d.ts +81 -0
  7. package/dist/assistantMemory.d.ts +63 -0
  8. package/dist/client/actions.d.ts +22 -0
  9. package/dist/client/connection.d.ts +3 -0
  10. package/dist/client/htmxBootstrap.js +44 -2
  11. package/dist/client/index.d.ts +2 -0
  12. package/dist/client/index.js +125 -2
  13. package/dist/client/providerStatus.d.ts +19 -0
  14. package/dist/fileStore.d.ts +5 -2
  15. package/dist/handoff.d.ts +54 -0
  16. package/dist/handoffHealth.d.ts +94 -0
  17. package/dist/index.d.ts +20 -4
  18. package/dist/index.js +2509 -21
  19. package/dist/modelAdapters.d.ts +93 -0
  20. package/dist/opsWebhook.d.ts +126 -0
  21. package/dist/providerHealth.d.ts +78 -0
  22. package/dist/queue.d.ts +52 -0
  23. package/dist/react/index.d.ts +1 -0
  24. package/dist/react/index.js +148 -2
  25. package/dist/react/useVoiceController.d.ts +2 -0
  26. package/dist/react/useVoiceProviderStatus.d.ts +8 -0
  27. package/dist/react/useVoiceStream.d.ts +2 -0
  28. package/dist/sessionReplay.d.ts +175 -0
  29. package/dist/svelte/createVoiceProviderStatus.d.ts +8 -0
  30. package/dist/svelte/index.d.ts +1 -0
  31. package/dist/svelte/index.js +127 -2
  32. package/dist/testing/index.d.ts +1 -0
  33. package/dist/testing/index.js +1310 -7
  34. package/dist/testing/providerSimulator.d.ts +44 -0
  35. package/dist/trace.d.ts +1 -1
  36. package/dist/types.d.ts +84 -2
  37. package/dist/vue/index.d.ts +1 -0
  38. package/dist/vue/index.js +161 -2
  39. package/dist/vue/useVoiceProviderStatus.d.ts +9 -0
  40. package/dist/vue/useVoiceStream.d.ts +2 -0
  41. 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",
@@ -5594,6 +6026,112 @@ var resolveVoiceOutcomeRecipe = (name, options = {}) => {
5594
6026
  };
5595
6027
  };
5596
6028
 
6029
+ // src/assistantMemory.ts
6030
+ var createMemoryId = (input) => `${input.assistantId}:${input.namespace}:${input.key}`;
6031
+ var createVoiceAssistantMemoryRecord = (input) => {
6032
+ const now = Date.now();
6033
+ return {
6034
+ ...input,
6035
+ createdAt: input.createdAt ?? input.updatedAt ?? now,
6036
+ updatedAt: input.updatedAt ?? now
6037
+ };
6038
+ };
6039
+ var createVoiceMemoryAssistantMemoryStore = () => {
6040
+ const records = new Map;
6041
+ return {
6042
+ delete: async (input) => {
6043
+ records.delete(createMemoryId(input));
6044
+ },
6045
+ get: async (input) => records.get(createMemoryId(input)),
6046
+ list: async (input) => [...records.values()].filter((record) => record.assistantId === input.assistantId && (input.namespace === undefined || record.namespace === input.namespace)).sort((left, right) => right.updatedAt - left.updatedAt),
6047
+ set: async (input) => {
6048
+ const id = createMemoryId(input);
6049
+ const existing = records.get(id);
6050
+ const record = createVoiceAssistantMemoryRecord({
6051
+ ...input,
6052
+ createdAt: input.createdAt ?? existing?.createdAt,
6053
+ updatedAt: input.updatedAt
6054
+ });
6055
+ records.set(id, record);
6056
+ return record;
6057
+ }
6058
+ };
6059
+ };
6060
+ var resolveVoiceAssistantMemoryNamespace = async (input) => typeof input.memory.namespace === "function" ? await input.memory.namespace(input) : input.memory.namespace;
6061
+ var createVoiceAssistantMemoryHandle = async (input) => {
6062
+ const namespace = await resolveVoiceAssistantMemoryNamespace({
6063
+ assistantId: input.assistantId,
6064
+ context: input.context,
6065
+ memory: input.memory,
6066
+ session: input.session
6067
+ });
6068
+ const trace = async (event) => {
6069
+ await input.trace?.append({
6070
+ at: Date.now(),
6071
+ payload: {
6072
+ assistantId: input.assistantId,
6073
+ namespace,
6074
+ ...event
6075
+ },
6076
+ scenarioId: input.session.scenarioId,
6077
+ sessionId: input.session.id,
6078
+ type: "assistant.memory"
6079
+ });
6080
+ };
6081
+ return {
6082
+ delete: async (key) => {
6083
+ await input.memory.store.delete({
6084
+ assistantId: input.assistantId,
6085
+ key,
6086
+ namespace
6087
+ });
6088
+ await trace({
6089
+ action: "delete",
6090
+ key
6091
+ });
6092
+ },
6093
+ get: async (key) => {
6094
+ const record = await input.memory.store.get({
6095
+ assistantId: input.assistantId,
6096
+ key,
6097
+ namespace
6098
+ });
6099
+ await trace({
6100
+ action: "get",
6101
+ found: Boolean(record),
6102
+ key
6103
+ });
6104
+ return record?.value;
6105
+ },
6106
+ list: async () => {
6107
+ const records = await input.memory.store.list({
6108
+ assistantId: input.assistantId,
6109
+ namespace
6110
+ });
6111
+ await trace({
6112
+ action: "list",
6113
+ count: records.length
6114
+ });
6115
+ return records;
6116
+ },
6117
+ namespace,
6118
+ set: async (key, value, metadata) => {
6119
+ const record = await input.memory.store.set({
6120
+ assistantId: input.assistantId,
6121
+ key,
6122
+ metadata,
6123
+ namespace,
6124
+ value
6125
+ });
6126
+ await trace({
6127
+ action: "set",
6128
+ key
6129
+ });
6130
+ return record;
6131
+ }
6132
+ };
6133
+ };
6134
+
5597
6135
  // src/assistant.ts
5598
6136
  var hashString = (value) => {
5599
6137
  let hash = 2166136261;
@@ -5742,12 +6280,35 @@ var createVoiceAssistant = (options) => {
5742
6280
  });
5743
6281
  }
5744
6282
  const onTurn = async (input) => {
6283
+ const memory = options.memory ? await createVoiceAssistantMemoryHandle({
6284
+ assistantId: options.id,
6285
+ context: input.context,
6286
+ memory: options.memory,
6287
+ session: input.session,
6288
+ trace: options.trace
6289
+ }) : undefined;
5745
6290
  const guardrailInput = {
5746
6291
  ...input,
5747
- assistantId: options.id
6292
+ assistantId: options.id,
6293
+ memory
5748
6294
  };
6295
+ if (memory) {
6296
+ await options.memoryLifecycle?.beforeTurn?.({
6297
+ ...input,
6298
+ assistantId: options.id,
6299
+ memory
6300
+ });
6301
+ }
5749
6302
  const blocked = await options.guardrails?.beforeTurn?.(guardrailInput);
5750
6303
  if (blocked) {
6304
+ if (memory) {
6305
+ await options.memoryLifecycle?.afterTurn?.({
6306
+ ...input,
6307
+ assistantId: options.id,
6308
+ memory,
6309
+ result: blocked
6310
+ });
6311
+ }
5751
6312
  await appendAssistantTrace({
5752
6313
  assistantId: options.id,
5753
6314
  event: {
@@ -5797,6 +6358,14 @@ var createVoiceAssistant = (options) => {
5797
6358
  result
5798
6359
  });
5799
6360
  const finalResult = guarded ?? result;
6361
+ if (memory) {
6362
+ await options.memoryLifecycle?.afterTurn?.({
6363
+ ...input,
6364
+ assistantId: options.id,
6365
+ memory,
6366
+ result: finalResult
6367
+ });
6368
+ }
5800
6369
  if (guarded) {
5801
6370
  await appendAssistantTrace({
5802
6371
  assistantId: options.id,
@@ -5864,6 +6433,12 @@ var summarizeVoiceAssistantRuns = async (input) => {
5864
6433
  escalationCount: 0,
5865
6434
  experiments: {},
5866
6435
  guardrailCount: 0,
6436
+ memory: {
6437
+ deletes: 0,
6438
+ gets: 0,
6439
+ lists: 0,
6440
+ sets: 0
6441
+ },
5867
6442
  outcomes: {},
5868
6443
  runCount: 0,
5869
6444
  sessionIds: new Set,
@@ -5919,6 +6494,24 @@ var summarizeVoiceAssistantRuns = async (input) => {
5919
6494
  const summary = getSummary(assistantId);
5920
6495
  summary.guardrailCount += 1;
5921
6496
  }
6497
+ for (const event of events.filter((event2) => event2.type === "assistant.memory")) {
6498
+ const assistantId = typeof event.payload.assistantId === "string" ? event.payload.assistantId : "unknown";
6499
+ const summary = getSummary(assistantId);
6500
+ switch (event.payload.action) {
6501
+ case "delete":
6502
+ summary.memory.deletes += 1;
6503
+ break;
6504
+ case "get":
6505
+ summary.memory.gets += 1;
6506
+ break;
6507
+ case "list":
6508
+ summary.memory.lists += 1;
6509
+ break;
6510
+ case "set":
6511
+ summary.memory.sets += 1;
6512
+ break;
6513
+ }
6514
+ }
5922
6515
  const assistants = [...byAssistant.values()].map(({ elapsedCount, elapsedTotal, sessionIds, ...summary }) => ({
5923
6516
  ...summary,
5924
6517
  averageElapsedMs: elapsedCount > 0 ? Math.round(elapsedTotal / elapsedCount) : undefined,
@@ -5929,9 +6522,302 @@ var summarizeVoiceAssistantRuns = async (input) => {
5929
6522
  totalRuns: assistantRuns.length
5930
6523
  };
5931
6524
  };
5932
- // src/fileStore.ts
5933
- import { mkdir, readFile, readdir, rename, rm, writeFile } from "fs/promises";
5934
- import { join } from "path";
6525
+ // src/assistantHealth.ts
6526
+ import { Elysia as Elysia3 } from "elysia";
6527
+
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
+ };
6557
+ entries.set(provider, entry);
6558
+ return entry;
6559
+ };
6560
+ for (const provider of providers) {
6561
+ getEntry(provider);
6562
+ }
6563
+ const hasProviderRouterEvents = events.some((event) => event.type === "session.error" && isAllowedProvider(event.payload.provider) && isProviderStatus(event.payload.providerStatus));
6564
+ for (const event of events) {
6565
+ if (event.type === "assistant.run") {
6566
+ if (hasProviderRouterEvents) {
6567
+ continue;
6568
+ }
6569
+ const provider2 = event.payload.variantId;
6570
+ if (!isAllowedProvider(provider2)) {
6571
+ continue;
6572
+ }
6573
+ const entry2 = getEntry(provider2);
6574
+ entry2.runCount += 1;
6575
+ const elapsedMs = getNumber(event.payload.elapsedMs);
6576
+ if (elapsedMs !== undefined) {
6577
+ entry2.elapsedCount += 1;
6578
+ entry2.elapsedTotal += elapsedMs;
6579
+ }
6580
+ continue;
6581
+ }
6582
+ if (event.type !== "session.error") {
6583
+ continue;
6584
+ }
6585
+ const provider = event.payload.provider;
6586
+ if (!isAllowedProvider(provider)) {
6587
+ continue;
6588
+ }
6589
+ const providerStatus = isProviderStatus(event.payload.providerStatus) ? event.payload.providerStatus : undefined;
6590
+ const applyProviderHealth = () => {
6591
+ const entry2 = getEntry(provider);
6592
+ const providerHealth = event.payload.providerHealth;
6593
+ if (providerHealth && typeof providerHealth === "object") {
6594
+ const suppressedUntil2 = getNumber(providerHealth.suppressedUntil);
6595
+ if (suppressedUntil2 !== undefined) {
6596
+ entry2.suppressedUntil = suppressedUntil2;
6597
+ }
6598
+ }
6599
+ const suppressedUntil = getNumber(event.payload.suppressedUntil);
6600
+ if (suppressedUntil !== undefined) {
6601
+ entry2.suppressedUntil = suppressedUntil;
6602
+ }
6603
+ const suppressionRemainingMs = getNumber(event.payload.suppressionRemainingMs);
6604
+ if (suppressionRemainingMs !== undefined) {
6605
+ entry2.suppressionRemainingMs = suppressionRemainingMs;
6606
+ }
6607
+ return entry2;
6608
+ };
6609
+ if (providerStatus === "success" || providerStatus === "fallback") {
6610
+ const entry2 = applyProviderHealth();
6611
+ entry2.runCount += 1;
6612
+ entry2.lastSuccessAt = event.at;
6613
+ if (providerStatus === "success") {
6614
+ entry2.lastError = undefined;
6615
+ entry2.rateLimited = false;
6616
+ entry2.suppressedUntil = undefined;
6617
+ entry2.suppressionRemainingMs = undefined;
6618
+ }
6619
+ const elapsedMs = getNumber(event.payload.elapsedMs);
6620
+ if (elapsedMs !== undefined) {
6621
+ entry2.elapsedCount += 1;
6622
+ entry2.elapsedTotal += elapsedMs;
6623
+ }
6624
+ const selectedProvider = event.payload.selectedProvider;
6625
+ if (providerStatus === "fallback" && isAllowedProvider(selectedProvider) && selectedProvider !== provider) {
6626
+ getEntry(selectedProvider).fallbackCount += 1;
6627
+ }
6628
+ continue;
6629
+ }
6630
+ const entry = applyProviderHealth();
6631
+ entry.errorCount += 1;
6632
+ entry.lastError = getString(event.payload.error);
6633
+ entry.lastErrorAt = event.at;
6634
+ entry.rateLimited ||= event.payload.rateLimited === true;
6635
+ }
6636
+ const summaries = [...entries.values()].map((entry) => {
6637
+ const hadSuppression = typeof entry.suppressedUntil === "number" || typeof entry.suppressionRemainingMs === "number";
6638
+ const suppressionRemainingMs = typeof entry.suppressedUntil === "number" ? Math.max(0, entry.suppressedUntil - now) : entry.suppressionRemainingMs;
6639
+ const activeSuppression = typeof suppressionRemainingMs === "number" && suppressionRemainingMs > 0;
6640
+ const recoverable = hadSuppression && !activeSuppression;
6641
+ const averageElapsedMs = entry.elapsedCount > 0 ? Math.round(entry.elapsedTotal / entry.elapsedCount) : undefined;
6642
+ 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";
6643
+ return {
6644
+ averageElapsedMs,
6645
+ errorCount: entry.errorCount,
6646
+ fallbackCount: entry.fallbackCount,
6647
+ lastError: entry.lastError,
6648
+ lastErrorAt: entry.lastErrorAt,
6649
+ lastSuccessAt: entry.lastSuccessAt,
6650
+ provider: entry.provider,
6651
+ rateLimited: entry.rateLimited,
6652
+ recommended: false,
6653
+ runCount: entry.runCount,
6654
+ status,
6655
+ suppressionRemainingMs: activeSuppression ? suppressionRemainingMs : undefined,
6656
+ suppressedUntil: entry.suppressedUntil
6657
+ };
6658
+ });
6659
+ const recommended = summaries.filter((entry) => entry.status === "healthy").sort((left, right) => (left.averageElapsedMs ?? Number.MAX_SAFE_INTEGER) - (right.averageElapsedMs ?? Number.MAX_SAFE_INTEGER))[0];
6660
+ if (recommended) {
6661
+ recommended.recommended = true;
6662
+ }
6663
+ return summaries;
6664
+ };
6665
+ var escapeHtml3 = (value) => value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
6666
+ var renderVoiceProviderHealthHTML = (providers) => providers.length === 0 ? '<p class="voice-provider-empty">No provider status yet.</p>' : [
6667
+ '<div class="voice-provider-health">',
6668
+ ...providers.map((provider) => {
6669
+ const suppressionSeconds = typeof provider.suppressionRemainingMs === "number" ? Math.ceil(provider.suppressionRemainingMs / 1000) : undefined;
6670
+ return [
6671
+ `<article class="voice-provider-card ${escapeHtml3(provider.status)}">`,
6672
+ '<div class="voice-provider-card-header">',
6673
+ `<strong>${escapeHtml3(provider.provider)}</strong>`,
6674
+ `<span>${escapeHtml3(provider.status)}${provider.recommended ? " \xB7 recommended" : ""}</span>`,
6675
+ "</div>",
6676
+ "<dl>",
6677
+ `<div><dt>Runs</dt><dd>${String(provider.runCount)}</dd></div>`,
6678
+ `<div><dt>Avg latency</dt><dd>${String(provider.averageElapsedMs ?? 0)}ms</dd></div>`,
6679
+ `<div><dt>Errors</dt><dd>${String(provider.errorCount)}</dd></div>`,
6680
+ `<div><dt>Fallbacks</dt><dd>${String(provider.fallbackCount)}</dd></div>`,
6681
+ "</dl>",
6682
+ suppressionSeconds ? `<p>Temporarily suppressed for ${String(suppressionSeconds)}s.</p>` : "",
6683
+ provider.lastError ? `<p>${escapeHtml3(provider.lastError)}</p>` : "",
6684
+ "</article>"
6685
+ ].join("");
6686
+ }),
6687
+ "</div>"
6688
+ ].join("");
6689
+ var createVoiceProviderHealthJSONHandler = (options) => async () => summarizeVoiceProviderHealth(options);
6690
+ var createVoiceProviderHealthHTMLHandler = (options) => async () => {
6691
+ const providers = await summarizeVoiceProviderHealth(options);
6692
+ const render = options.render ?? renderVoiceProviderHealthHTML;
6693
+ const body = await render(providers);
6694
+ return new Response(body, {
6695
+ headers: {
6696
+ "Content-Type": "text/html; charset=utf-8",
6697
+ ...options.headers
6698
+ }
6699
+ });
6700
+ };
6701
+ var createVoiceProviderHealthRoutes = (options) => {
6702
+ const path = options.path ?? "/api/provider-status";
6703
+ const htmlPath = options.htmlPath === undefined ? `${path}/htmx` : options.htmlPath;
6704
+ const routes = new Elysia2({
6705
+ name: options.name ?? "absolutejs-voice-provider-health"
6706
+ }).get(path, createVoiceProviderHealthJSONHandler(options));
6707
+ if (htmlPath) {
6708
+ routes.get(htmlPath, createVoiceProviderHealthHTMLHandler(options));
6709
+ }
6710
+ return routes;
6711
+ };
6712
+
6713
+ // src/assistantHealth.ts
6714
+ var escapeHtml4 = (value) => value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
6715
+ var renderCountMap = (values) => {
6716
+ const entries = Object.entries(values).sort((left, right) => right[1] - left[1]);
6717
+ if (entries.length === 0) {
6718
+ return '<p class="voice-assistant-health-empty">No data yet.</p>';
6719
+ }
6720
+ return [
6721
+ '<div class="voice-assistant-health-metrics">',
6722
+ ...entries.map(([label, value]) => `<div><span>${escapeHtml4(label)}</span><strong>${String(value)}</strong></div>`),
6723
+ "</div>"
6724
+ ].join("");
6725
+ };
6726
+ var getString2 = (value) => typeof value === "string" ? value : undefined;
6727
+ 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) => {
6728
+ const failure = {
6729
+ at: event.at,
6730
+ assistantId: getString2(event.payload.assistantId),
6731
+ error: getString2(event.payload.error),
6732
+ provider: getString2(event.payload.provider),
6733
+ rateLimited: event.payload.rateLimited === true ? true : undefined,
6734
+ sessionId: event.sessionId,
6735
+ status: getString2(event.payload.providerStatus),
6736
+ turnId: event.turnId,
6737
+ type: event.type
6738
+ };
6739
+ const href = replayHref === false ? undefined : typeof replayHref === "function" ? replayHref(failure) : `${replayHref ?? "/api/voice-sessions"}/${encodeURIComponent(event.sessionId)}/replay/htmx`;
6740
+ return {
6741
+ ...failure,
6742
+ replayHref: href
6743
+ };
6744
+ });
6745
+ var summarizeVoiceAssistantHealth = async (options) => {
6746
+ const events = options.events ?? await options.store?.list() ?? [];
6747
+ return {
6748
+ assistantRuns: await summarizeVoiceAssistantRuns({ events }),
6749
+ providerHealth: await summarizeVoiceProviderHealth({
6750
+ events,
6751
+ providers: options.providers
6752
+ }),
6753
+ recentFailures: getRecentFailures(events, options.maxFailures ?? 8, options.replayHref)
6754
+ };
6755
+ };
6756
+ var renderVoiceAssistantHealthHTML = (summary) => {
6757
+ const assistant = summary.assistantRuns.assistants[0];
6758
+ const failures = summary.recentFailures;
6759
+ return [
6760
+ '<div class="voice-assistant-health">',
6761
+ '<section class="voice-assistant-health-grid">',
6762
+ `<article><span>Runs</span><strong>${String(assistant?.runCount ?? 0)}</strong></article>`,
6763
+ `<article><span>Sessions</span><strong>${String(assistant?.sessions ?? 0)}</strong></article>`,
6764
+ `<article><span>Guardrails</span><strong>${String(assistant?.guardrailCount ?? 0)}</strong></article>`,
6765
+ `<article><span>Avg latency</span><strong>${String(assistant?.averageElapsedMs ?? 0)}ms</strong></article>`,
6766
+ "</section>",
6767
+ "<section>",
6768
+ "<h3>Provider Health</h3>",
6769
+ renderVoiceProviderHealthHTML(summary.providerHealth),
6770
+ "</section>",
6771
+ '<section class="voice-assistant-health-columns">',
6772
+ `<article><h3>Outcomes</h3>${renderCountMap(assistant?.outcomes ?? {})}</article>`,
6773
+ `<article><h3>Variants</h3>${renderCountMap(assistant?.variants ?? {})}</article>`,
6774
+ `<article><h3>Tools</h3>${renderCountMap(assistant?.toolCalls ?? {})}</article>`,
6775
+ `<article><h3>Artifact Plans</h3>${renderCountMap(assistant?.artifactPlans ?? {})}</article>`,
6776
+ "</section>",
6777
+ "<section>",
6778
+ "<h3>Recent Failures</h3>",
6779
+ failures.length === 0 ? '<p class="voice-assistant-health-empty">No failures yet.</p>' : [
6780
+ '<div class="voice-assistant-health-failures">',
6781
+ ...failures.map((failure) => [
6782
+ "<article>",
6783
+ `<strong>${escapeHtml4(failure.provider ?? failure.assistantId ?? failure.type)}</strong>`,
6784
+ `<span>${escapeHtml4(failure.status ?? (failure.rateLimited ? "rate-limited" : "error"))}</span>`,
6785
+ failure.error ? `<p>${escapeHtml4(failure.error)}</p>` : "",
6786
+ `<small>${escapeHtml4(failure.sessionId)}${failure.turnId ? ` / ${escapeHtml4(failure.turnId)}` : ""}</small>`,
6787
+ failure.replayHref ? `<p><a href="${escapeHtml4(failure.replayHref)}">Open replay</a></p>` : "",
6788
+ "</article>"
6789
+ ].join("")),
6790
+ "</div>"
6791
+ ].join(""),
6792
+ "</section>",
6793
+ "</div>"
6794
+ ].join("");
6795
+ };
6796
+ var createVoiceAssistantHealthJSONHandler = (options) => async () => summarizeVoiceAssistantHealth(options);
6797
+ var createVoiceAssistantHealthHTMLHandler = (options) => async () => {
6798
+ const summary = await summarizeVoiceAssistantHealth(options);
6799
+ const render = options.render ?? renderVoiceAssistantHealthHTML;
6800
+ const body = await render(summary);
6801
+ return new Response(body, {
6802
+ headers: {
6803
+ "Content-Type": "text/html; charset=utf-8",
6804
+ ...options.headers
6805
+ }
6806
+ });
6807
+ };
6808
+ var createVoiceAssistantHealthRoutes = (options) => {
6809
+ const path = options.path ?? "/api/assistant-health";
6810
+ const htmlPath = options.htmlPath === undefined ? `${path}/htmx` : options.htmlPath;
6811
+ const routes = new Elysia3({
6812
+ name: options.name ?? "absolutejs-voice-assistant-health"
6813
+ }).get(path, createVoiceAssistantHealthJSONHandler(options));
6814
+ if (htmlPath) {
6815
+ routes.get(htmlPath, createVoiceAssistantHealthHTMLHandler(options));
6816
+ }
6817
+ return routes;
6818
+ };
6819
+ // src/sessionReplay.ts
6820
+ import { Elysia as Elysia4 } from "elysia";
5935
6821
 
5936
6822
  // src/trace.ts
5937
6823
  var createVoiceTraceEventId = (event) => [
@@ -6035,7 +6921,7 @@ var sleep3 = async (delayMs) => {
6035
6921
  }
6036
6922
  await new Promise((resolve2) => setTimeout(resolve2, delayMs));
6037
6923
  };
6038
- var toHex3 = (bytes) => Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
6924
+ var toHex4 = (bytes) => Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
6039
6925
  var signVoiceTraceSinkBody = async (input) => {
6040
6926
  const encoder = new TextEncoder;
6041
6927
  const key = await crypto.subtle.importKey("raw", encoder.encode(input.secret), {
@@ -6044,7 +6930,7 @@ var signVoiceTraceSinkBody = async (input) => {
6044
6930
  }, false, ["sign"]);
6045
6931
  const payload = encoder.encode(`${input.timestamp}.${input.body}`);
6046
6932
  const signature = await crypto.subtle.sign("HMAC", key, payload);
6047
- return `sha256=${toHex3(new Uint8Array(signature))}`;
6933
+ return `sha256=${toHex4(new Uint8Array(signature))}`;
6048
6934
  };
6049
6935
  var createVoiceTraceSinkDeliveryError = (input) => {
6050
6936
  if (input.response) {
@@ -6265,7 +7151,7 @@ var exportVoiceTrace = async (input) => {
6265
7151
  };
6266
7152
  };
6267
7153
  var toNumber = (value) => typeof value === "number" && Number.isFinite(value) ? value : 0;
6268
- var escapeHtml3 = (value) => value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
7154
+ var escapeHtml5 = (value) => value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
6269
7155
  var formatTraceValue = (value) => {
6270
7156
  if (value === undefined || value === null) {
6271
7157
  return "";
@@ -6543,10 +7429,10 @@ var renderVoiceTraceHTML = (events, options = {}) => {
6543
7429
  const offset = summary.startedAt === undefined ? event.at : Math.max(0, event.at - summary.startedAt);
6544
7430
  return [
6545
7431
  "<tr>",
6546
- `<td>${escapeHtml3(String(offset))}</td>`,
6547
- `<td>${escapeHtml3(event.type)}</td>`,
6548
- `<td>${escapeHtml3(event.turnId ?? "")}</td>`,
6549
- `<td><code>${escapeHtml3(JSON.stringify(event.payload))}</code></td>`,
7432
+ `<td>${escapeHtml5(String(offset))}</td>`,
7433
+ `<td>${escapeHtml5(event.type)}</td>`,
7434
+ `<td>${escapeHtml5(event.turnId ?? "")}</td>`,
7435
+ `<td><code>${escapeHtml5(JSON.stringify(event.payload))}</code></td>`,
6550
7436
  "</tr>"
6551
7437
  ].join("");
6552
7438
  }).join(`
@@ -6557,7 +7443,7 @@ var renderVoiceTraceHTML = (events, options = {}) => {
6557
7443
  "<head>",
6558
7444
  '<meta charset="utf-8" />',
6559
7445
  '<meta name="viewport" content="width=device-width, initial-scale=1" />',
6560
- `<title>${escapeHtml3(options.title ?? "Voice Trace")}</title>`,
7446
+ `<title>${escapeHtml5(options.title ?? "Voice Trace")}</title>`,
6561
7447
  "<style>",
6562
7448
  "body{font-family:ui-sans-serif,system-ui,sans-serif;margin:2rem;line-height:1.45;background:#f8f7f2;color:#181713}",
6563
7449
  "main{max-width:1100px;margin:auto}",
@@ -6571,7 +7457,7 @@ var renderVoiceTraceHTML = (events, options = {}) => {
6571
7457
  "</style>",
6572
7458
  "</head>",
6573
7459
  "<body><main>",
6574
- `<h1>${escapeHtml3(options.title ?? `Voice Trace ${summary.sessionId ?? ""}`.trim())}</h1>`,
7460
+ `<h1>${escapeHtml5(options.title ?? `Voice Trace ${summary.sessionId ?? ""}`.trim())}</h1>`,
6575
7461
  `<p class="${evaluation.pass ? "pass" : "fail"}">QA: ${evaluation.pass ? "pass" : "fail"}</p>`,
6576
7462
  '<section class="summary">',
6577
7463
  `<div class="card"><strong>Events</strong><br>${summary.eventCount}</div>`,
@@ -6585,7 +7471,7 @@ var renderVoiceTraceHTML = (events, options = {}) => {
6585
7471
  eventRows,
6586
7472
  "</tbody></table>",
6587
7473
  "<h2>Markdown Export</h2>",
6588
- `<pre>${escapeHtml3(markdown)}</pre>`,
7474
+ `<pre>${escapeHtml5(markdown)}</pre>`,
6589
7475
  "</main></body></html>"
6590
7476
  ].join(`
6591
7477
  `);
@@ -6597,7 +7483,250 @@ var buildVoiceTraceReplay = (events, options = {}) => ({
6597
7483
  summary: summarizeVoiceTrace(options.redact ? redactVoiceTraceEvents(events, options.redact) : events)
6598
7484
  });
6599
7485
 
7486
+ // src/sessionReplay.ts
7487
+ var getString3 = (value) => typeof value === "string" ? value : undefined;
7488
+ var escapeHtml6 = (value) => value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
7489
+ var increment2 = (record, key) => {
7490
+ record[key] = (record[key] ?? 0) + 1;
7491
+ };
7492
+ var buildReplayTurns = (events) => {
7493
+ const turns = new Map;
7494
+ const getTurn = (turnId) => {
7495
+ const existing = turns.get(turnId);
7496
+ if (existing) {
7497
+ return existing;
7498
+ }
7499
+ const turn = {
7500
+ assistantReplies: [],
7501
+ errors: [],
7502
+ id: turnId,
7503
+ modelCalls: [],
7504
+ tools: [],
7505
+ transcripts: []
7506
+ };
7507
+ turns.set(turnId, turn);
7508
+ return turn;
7509
+ };
7510
+ for (const event of events) {
7511
+ const turnId = event.turnId ?? "session";
7512
+ const turn = getTurn(turnId);
7513
+ switch (event.type) {
7514
+ case "turn.transcript":
7515
+ turn.transcripts.push({
7516
+ isFinal: event.payload.isFinal === true,
7517
+ text: getString3(event.payload.text)
7518
+ });
7519
+ break;
7520
+ case "turn.committed":
7521
+ turn.committedText = getString3(event.payload.text);
7522
+ break;
7523
+ case "turn.assistant": {
7524
+ const text = getString3(event.payload.text);
7525
+ if (text) {
7526
+ turn.assistantReplies.push(text);
7527
+ }
7528
+ break;
7529
+ }
7530
+ case "agent.model":
7531
+ case "assistant.run":
7532
+ turn.modelCalls.push(event.payload);
7533
+ break;
7534
+ case "agent.tool":
7535
+ turn.tools.push(event.payload);
7536
+ break;
7537
+ case "session.error":
7538
+ turn.errors.push(event.payload);
7539
+ break;
7540
+ }
7541
+ }
7542
+ return [...turns.values()];
7543
+ };
7544
+ var summarizeVoiceSessionReplay = async (options) => {
7545
+ const sourceEvents = options.events ?? await options.store?.list({ sessionId: options.sessionId }) ?? [];
7546
+ const events = filterVoiceTraceEvents(sourceEvents, {
7547
+ sessionId: options.sessionId
7548
+ });
7549
+ const replay = buildVoiceTraceReplay(events, {
7550
+ evaluation: options.evaluation,
7551
+ redact: options.redact,
7552
+ title: options.title ?? `Voice Session ${options.sessionId}`
7553
+ });
7554
+ const startedAt = replay.summary.startedAt;
7555
+ return {
7556
+ evaluation: replay.evaluation,
7557
+ events,
7558
+ html: replay.html,
7559
+ markdown: replay.markdown,
7560
+ sessionId: options.sessionId,
7561
+ summary: replay.summary,
7562
+ timeline: events.map((event) => ({
7563
+ at: event.at,
7564
+ offsetMs: startedAt === undefined ? undefined : Math.max(0, event.at - startedAt),
7565
+ payload: event.payload,
7566
+ turnId: event.turnId,
7567
+ type: event.type
7568
+ })),
7569
+ turns: buildReplayTurns(events)
7570
+ };
7571
+ };
7572
+ var summarizeVoiceSessions = async (options = {}) => {
7573
+ const events = options.events ?? await options.store?.list() ?? [];
7574
+ const grouped = new Map;
7575
+ for (const event of events) {
7576
+ grouped.set(event.sessionId, [...grouped.get(event.sessionId) ?? [], event]);
7577
+ }
7578
+ const sessions = [...grouped.entries()].map(([sessionId, sessionEvents]) => {
7579
+ const sorted = filterVoiceTraceEvents(sessionEvents);
7580
+ const summary = buildVoiceTraceReplay(sorted, {
7581
+ evaluation: {
7582
+ requireAssistantReply: false,
7583
+ requireCompletedCall: false,
7584
+ requireTranscript: false,
7585
+ requireTurn: false
7586
+ }
7587
+ }).summary;
7588
+ const providerErrors = {};
7589
+ const providers = new Set;
7590
+ let latestOutcome;
7591
+ let errorCount = 0;
7592
+ for (const event of sorted) {
7593
+ const provider = getString3(event.payload.provider);
7594
+ if (provider) {
7595
+ providers.add(provider);
7596
+ }
7597
+ if (event.type === "session.error" && (event.payload.providerStatus === "error" || typeof event.payload.error === "string")) {
7598
+ errorCount += 1;
7599
+ increment2(providerErrors, provider ?? "unknown");
7600
+ }
7601
+ const outcome = getString3(event.payload.outcome);
7602
+ if (outcome) {
7603
+ latestOutcome = outcome;
7604
+ }
7605
+ }
7606
+ const item = {
7607
+ endedAt: summary.endedAt,
7608
+ errorCount,
7609
+ eventCount: summary.eventCount,
7610
+ latestOutcome,
7611
+ providerErrors,
7612
+ providers: [...providers].sort(),
7613
+ sessionId,
7614
+ startedAt: summary.startedAt,
7615
+ status: errorCount > 0 ? "failed" : "healthy",
7616
+ transcriptCount: summary.transcriptCount,
7617
+ turnCount: summary.turnCount
7618
+ };
7619
+ const replayHref = options.replayHref === false ? "" : typeof options.replayHref === "function" ? options.replayHref(item) : `${options.replayHref ?? "/api/voice-sessions"}/${encodeURIComponent(sessionId)}/replay/htmx`;
7620
+ return {
7621
+ ...item,
7622
+ replayHref
7623
+ };
7624
+ });
7625
+ const search = options.q?.trim().toLowerCase();
7626
+ return sessions.filter((session) => {
7627
+ if (options.status && options.status !== "all" && session.status !== options.status) {
7628
+ return false;
7629
+ }
7630
+ if (options.provider && !session.providers.includes(options.provider)) {
7631
+ return false;
7632
+ }
7633
+ if (!search) {
7634
+ return true;
7635
+ }
7636
+ return [
7637
+ session.sessionId,
7638
+ session.latestOutcome,
7639
+ session.status,
7640
+ ...session.providers
7641
+ ].some((value) => value?.toLowerCase().includes(search));
7642
+ }).sort((left, right) => (right.endedAt ?? right.startedAt ?? 0) - (left.endedAt ?? left.startedAt ?? 0)).slice(0, options.limit ?? 50);
7643
+ };
7644
+ var renderVoiceSessionsHTML = (sessions) => sessions.length === 0 ? '<p class="voice-sessions-empty">No voice sessions found.</p>' : [
7645
+ '<div class="voice-sessions-list">',
7646
+ ...sessions.map((session) => [
7647
+ `<article class="voice-session-card ${escapeHtml6(session.status)}">`,
7648
+ '<div class="voice-session-card-header">',
7649
+ `<strong>${escapeHtml6(session.sessionId)}</strong>`,
7650
+ `<span>${escapeHtml6(session.status)}</span>`,
7651
+ "</div>",
7652
+ "<dl>",
7653
+ `<div><dt>Events</dt><dd>${String(session.eventCount)}</dd></div>`,
7654
+ `<div><dt>Turns</dt><dd>${String(session.turnCount)}</dd></div>`,
7655
+ `<div><dt>Transcripts</dt><dd>${String(session.transcriptCount)}</dd></div>`,
7656
+ `<div><dt>Errors</dt><dd>${String(session.errorCount)}</dd></div>`,
7657
+ "</dl>",
7658
+ session.latestOutcome ? `<p>Outcome: ${escapeHtml6(session.latestOutcome)}</p>` : "",
7659
+ session.providers.length ? `<p>Providers: ${session.providers.map(escapeHtml6).join(", ")}</p>` : "",
7660
+ session.replayHref ? `<p><a href="${escapeHtml6(session.replayHref)}">Open replay</a></p>` : "",
7661
+ "</article>"
7662
+ ].join("")),
7663
+ "</div>"
7664
+ ].join("");
7665
+ var createVoiceSessionsJSONHandler = (options = {}) => async ({ query }) => summarizeVoiceSessions({
7666
+ ...options,
7667
+ limit: typeof query?.limit === "string" ? Number(query.limit) : options.limit,
7668
+ provider: query?.provider ?? options.provider,
7669
+ q: query?.q ?? options.q,
7670
+ status: query?.status === "failed" || query?.status === "healthy" || query?.status === "all" ? query.status : options.status
7671
+ });
7672
+ var createVoiceSessionsHTMLHandler = (options = {}) => async ({ query }) => {
7673
+ const sessions = await summarizeVoiceSessions({
7674
+ ...options,
7675
+ limit: typeof query?.limit === "string" ? Number(query.limit) : options.limit,
7676
+ provider: query?.provider ?? options.provider,
7677
+ q: query?.q ?? options.q,
7678
+ status: query?.status === "failed" || query?.status === "healthy" || query?.status === "all" ? query.status : options.status
7679
+ });
7680
+ const body = await (options.render?.(sessions) ?? renderVoiceSessionsHTML(sessions));
7681
+ return new Response(body, {
7682
+ headers: {
7683
+ "Content-Type": "text/html; charset=utf-8",
7684
+ ...options.headers
7685
+ }
7686
+ });
7687
+ };
7688
+ var createVoiceSessionListRoutes = (options = {}) => {
7689
+ const path = options.path ?? "/api/voice-sessions";
7690
+ const htmlPath = options.htmlPath === undefined ? `${path}/htmx` : options.htmlPath;
7691
+ const routes = new Elysia4({
7692
+ name: options.name ?? "absolutejs-voice-session-list"
7693
+ }).get(path, createVoiceSessionsJSONHandler(options));
7694
+ if (htmlPath) {
7695
+ routes.get(htmlPath, createVoiceSessionsHTMLHandler(options));
7696
+ }
7697
+ return routes;
7698
+ };
7699
+ var createVoiceSessionReplayJSONHandler = (options) => async ({ params }) => summarizeVoiceSessionReplay({
7700
+ ...options,
7701
+ sessionId: params.sessionId ?? ""
7702
+ });
7703
+ var createVoiceSessionReplayHTMLHandler = (options) => async ({ params }) => {
7704
+ const replay = await summarizeVoiceSessionReplay({
7705
+ ...options,
7706
+ sessionId: params.sessionId ?? ""
7707
+ });
7708
+ const body = await (options.render?.(replay) ?? replay.html);
7709
+ return new Response(body, {
7710
+ headers: {
7711
+ "Content-Type": "text/html; charset=utf-8",
7712
+ ...options.headers
7713
+ }
7714
+ });
7715
+ };
7716
+ var createVoiceSessionReplayRoutes = (options) => {
7717
+ const path = options.path ?? "/api/voice-sessions/:sessionId/replay";
7718
+ const htmlPath = options.htmlPath === undefined ? `${path}/htmx` : options.htmlPath;
7719
+ const routes = new Elysia4({
7720
+ name: options.name ?? "absolutejs-voice-session-replay"
7721
+ }).get(path, createVoiceSessionReplayJSONHandler(options));
7722
+ if (htmlPath) {
7723
+ routes.get(htmlPath, createVoiceSessionReplayHTMLHandler(options));
7724
+ }
7725
+ return routes;
7726
+ };
6600
7727
  // src/fileStore.ts
7728
+ import { mkdir, readFile, readdir, rename, rm, writeFile } from "fs/promises";
7729
+ import { join } from "path";
6601
7730
  var listJsonFiles = async (directory) => {
6602
7731
  try {
6603
7732
  const entries = await readdir(directory, {
@@ -6613,6 +7742,7 @@ var listJsonFiles = async (directory) => {
6613
7742
  };
6614
7743
  var encodeStoreId = (id) => `${encodeURIComponent(id)}.json`;
6615
7744
  var resolveFilePath = (directory, id) => join(directory, encodeStoreId(id));
7745
+ var createMemoryStoreId = (input) => `${input.assistantId}:${input.namespace}:${input.key}`;
6616
7746
  var readJsonFile = async (path) => JSON.parse(await readFile(path, "utf8"));
6617
7747
  var writeJsonFile = async (path, value, options) => {
6618
7748
  await mkdir(options.directory, {
@@ -6832,6 +7962,40 @@ var createVoiceFileTraceSinkDeliveryStore = (options) => {
6832
7962
  };
6833
7963
  return { get, list, remove, set };
6834
7964
  };
7965
+ var createVoiceFileAssistantMemoryStore = (options) => {
7966
+ const get = async (input) => {
7967
+ const path = resolveFilePath(options.directory, createMemoryStoreId(input));
7968
+ try {
7969
+ return await readJsonFile(path);
7970
+ } catch (error) {
7971
+ if (error.code === "ENOENT") {
7972
+ return;
7973
+ }
7974
+ throw error;
7975
+ }
7976
+ };
7977
+ const list = async (input) => {
7978
+ const files = await listJsonFiles(options.directory);
7979
+ const records = await Promise.all(files.map((file) => readJsonFile(file)));
7980
+ return records.filter((record) => record.assistantId === input.assistantId && (input.namespace === undefined || record.namespace === input.namespace)).sort((left, right) => right.updatedAt - left.updatedAt);
7981
+ };
7982
+ const set = async (input) => {
7983
+ const existing = await get(input);
7984
+ const record = createVoiceAssistantMemoryRecord({
7985
+ ...input,
7986
+ createdAt: input.createdAt ?? existing?.createdAt,
7987
+ updatedAt: input.updatedAt
7988
+ });
7989
+ await writeJsonFile(resolveFilePath(options.directory, createMemoryStoreId(record)), record, options);
7990
+ return record;
7991
+ };
7992
+ const remove = async (input) => {
7993
+ await rm(resolveFilePath(options.directory, createMemoryStoreId(input)), {
7994
+ force: true
7995
+ });
7996
+ };
7997
+ return { delete: remove, get, list, set };
7998
+ };
6835
7999
  var createVoiceFileRuntimeStorage = (options) => ({
6836
8000
  events: createVoiceFileIntegrationEventStore({
6837
8001
  ...options,
@@ -6841,6 +8005,10 @@ var createVoiceFileRuntimeStorage = (options) => ({
6841
8005
  ...options,
6842
8006
  directory: join(options.directory, "external-objects")
6843
8007
  }),
8008
+ memories: createVoiceFileAssistantMemoryStore({
8009
+ ...options,
8010
+ directory: join(options.directory, "memories")
8011
+ }),
6844
8012
  reviews: createVoiceFileReviewStore({
6845
8013
  ...options,
6846
8014
  directory: join(options.directory, "reviews")
@@ -6873,6 +8041,777 @@ var createStoredVoiceExternalObjectMap = (mapping) => createVoiceExternalObjectM
6873
8041
  sourceId: mapping.sourceId,
6874
8042
  sourceType: mapping.sourceType
6875
8043
  });
8044
+ // src/modelAdapters.ts
8045
+ var OUTPUT_SCHEMA = {
8046
+ additionalProperties: false,
8047
+ properties: {
8048
+ assistantText: {
8049
+ type: "string"
8050
+ },
8051
+ complete: {
8052
+ type: "boolean"
8053
+ },
8054
+ escalate: {
8055
+ additionalProperties: false,
8056
+ properties: {
8057
+ metadata: {
8058
+ additionalProperties: true,
8059
+ type: "object"
8060
+ },
8061
+ reason: {
8062
+ type: "string"
8063
+ }
8064
+ },
8065
+ required: ["reason"],
8066
+ type: "object"
8067
+ },
8068
+ noAnswer: {
8069
+ additionalProperties: false,
8070
+ properties: {
8071
+ metadata: {
8072
+ additionalProperties: true,
8073
+ type: "object"
8074
+ }
8075
+ },
8076
+ type: "object"
8077
+ },
8078
+ result: {
8079
+ additionalProperties: true,
8080
+ type: "object"
8081
+ },
8082
+ transfer: {
8083
+ additionalProperties: false,
8084
+ properties: {
8085
+ metadata: {
8086
+ additionalProperties: true,
8087
+ type: "object"
8088
+ },
8089
+ reason: {
8090
+ type: "string"
8091
+ },
8092
+ target: {
8093
+ type: "string"
8094
+ }
8095
+ },
8096
+ required: ["target"],
8097
+ type: "object"
8098
+ },
8099
+ voicemail: {
8100
+ additionalProperties: false,
8101
+ properties: {
8102
+ metadata: {
8103
+ additionalProperties: true,
8104
+ type: "object"
8105
+ }
8106
+ },
8107
+ type: "object"
8108
+ }
8109
+ },
8110
+ type: "object"
8111
+ };
8112
+ 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.";
8113
+ var stripJSONCodeFence = (value) => {
8114
+ const trimmed = value.trim();
8115
+ const match = trimmed.match(/^```(?:json)?\s*([\s\S]*?)\s*```$/i);
8116
+ return match?.[1]?.trim() ?? value;
8117
+ };
8118
+ var parseJSON = (value) => {
8119
+ try {
8120
+ const parsed = JSON.parse(stripJSONCodeFence(value));
8121
+ return parsed && typeof parsed === "object" ? parsed : {};
8122
+ } catch {
8123
+ return {
8124
+ assistantText: value
8125
+ };
8126
+ }
8127
+ };
8128
+ var parseJSONValue = (value) => {
8129
+ try {
8130
+ return JSON.parse(value);
8131
+ } catch {
8132
+ return value;
8133
+ }
8134
+ };
8135
+ var getMessageToolCalls = (message) => {
8136
+ const toolCalls = message.metadata?.toolCalls;
8137
+ return Array.isArray(toolCalls) ? toolCalls.filter((toolCall) => toolCall && typeof toolCall === "object" && typeof toolCall.name === "string") : [];
8138
+ };
8139
+ var createHTTPError = (provider, response) => new Error(`${provider} voice assistant model failed: HTTP ${response.status}`);
8140
+ var sleep4 = (ms) => new Promise((resolve2) => {
8141
+ setTimeout(resolve2, ms);
8142
+ });
8143
+ var errorMessage = (error) => error instanceof Error ? error.message : String(error);
8144
+ var defaultIsRateLimitError = (error) => /(\b429\b|rate limit|quota|too many requests)/i.test(errorMessage(error));
8145
+ var normalizeRouteOutput = (output) => {
8146
+ const result = {};
8147
+ if (typeof output.assistantText === "string") {
8148
+ result.assistantText = output.assistantText;
8149
+ }
8150
+ if (typeof output.complete === "boolean") {
8151
+ result.complete = output.complete;
8152
+ }
8153
+ if (output.result !== undefined) {
8154
+ result.result = output.result;
8155
+ }
8156
+ if (output.transfer && typeof output.transfer === "object") {
8157
+ const transfer = output.transfer;
8158
+ if (typeof transfer.target === "string") {
8159
+ result.transfer = {
8160
+ metadata: transfer.metadata && typeof transfer.metadata === "object" ? transfer.metadata : undefined,
8161
+ reason: typeof transfer.reason === "string" ? transfer.reason : undefined,
8162
+ target: transfer.target
8163
+ };
8164
+ }
8165
+ }
8166
+ if (output.escalate && typeof output.escalate === "object") {
8167
+ const escalate = output.escalate;
8168
+ if (typeof escalate.reason === "string") {
8169
+ result.escalate = {
8170
+ metadata: escalate.metadata && typeof escalate.metadata === "object" ? escalate.metadata : undefined,
8171
+ reason: escalate.reason
8172
+ };
8173
+ }
8174
+ }
8175
+ if (output.voicemail && typeof output.voicemail === "object") {
8176
+ const voicemail = output.voicemail;
8177
+ result.voicemail = {
8178
+ metadata: voicemail.metadata && typeof voicemail.metadata === "object" ? voicemail.metadata : undefined
8179
+ };
8180
+ }
8181
+ if (output.noAnswer && typeof output.noAnswer === "object") {
8182
+ const noAnswer = output.noAnswer;
8183
+ result.noAnswer = {
8184
+ metadata: noAnswer.metadata && typeof noAnswer.metadata === "object" ? noAnswer.metadata : undefined
8185
+ };
8186
+ }
8187
+ return result;
8188
+ };
8189
+ var createJSONVoiceAssistantModel = (options) => ({
8190
+ generate: async (input) => {
8191
+ const output = await options.generate(input);
8192
+ if ("assistantText" in output || "toolCalls" in output || "complete" in output || "transfer" in output || "escalate" in output) {
8193
+ return output;
8194
+ }
8195
+ return options.mapOutput?.(output) ?? normalizeRouteOutput(output);
8196
+ }
8197
+ });
8198
+ var createVoiceProviderRouter = (options) => {
8199
+ const providerIds = Object.keys(options.providers);
8200
+ const firstProvider = providerIds[0];
8201
+ const policy = typeof options.policy === "string" ? {
8202
+ strategy: options.policy
8203
+ } : options.policy;
8204
+ const strategy = policy?.strategy ?? "prefer-selected";
8205
+ const fallbackMode = policy?.fallbackMode ?? options.fallbackMode ?? "provider-error";
8206
+ const healthOptions = typeof options.providerHealth === "object" ? options.providerHealth : options.providerHealth ? {} : undefined;
8207
+ const healthState = new Map;
8208
+ const now = () => healthOptions?.now?.() ?? Date.now();
8209
+ const failureThreshold = Math.max(1, healthOptions?.failureThreshold ?? 1);
8210
+ const cooldownMs = Math.max(0, healthOptions?.cooldownMs ?? 30000);
8211
+ const rateLimitCooldownMs = Math.max(0, healthOptions?.rateLimitCooldownMs ?? 60000);
8212
+ const getHealth = (provider) => {
8213
+ const existing = healthState.get(provider);
8214
+ if (existing) {
8215
+ return existing;
8216
+ }
8217
+ const next = {
8218
+ consecutiveFailures: 0,
8219
+ provider,
8220
+ status: "healthy"
8221
+ };
8222
+ healthState.set(provider, next);
8223
+ return next;
8224
+ };
8225
+ const cloneHealth = (provider) => {
8226
+ if (!healthOptions) {
8227
+ return;
8228
+ }
8229
+ return {
8230
+ ...getHealth(provider)
8231
+ };
8232
+ };
8233
+ const getSuppressionRemainingMs = (provider) => {
8234
+ if (!healthOptions) {
8235
+ return;
8236
+ }
8237
+ const suppressedUntil = getHealth(provider).suppressedUntil;
8238
+ return typeof suppressedUntil === "number" ? Math.max(0, suppressedUntil - now()) : undefined;
8239
+ };
8240
+ const isSuppressed = (provider) => {
8241
+ if (!healthOptions) {
8242
+ return false;
8243
+ }
8244
+ const health = getHealth(provider);
8245
+ return typeof health.suppressedUntil === "number" && health.suppressedUntil > now();
8246
+ };
8247
+ const recordProviderSuccess = (provider) => {
8248
+ if (!healthOptions) {
8249
+ return;
8250
+ }
8251
+ const health = getHealth(provider);
8252
+ health.consecutiveFailures = 0;
8253
+ health.status = "healthy";
8254
+ health.suppressedUntil = undefined;
8255
+ return cloneHealth(provider);
8256
+ };
8257
+ const recordProviderError = (provider, isProviderError, rateLimited) => {
8258
+ if (!healthOptions || !isProviderError) {
8259
+ return cloneHealth(provider);
8260
+ }
8261
+ const currentTime = now();
8262
+ const health = getHealth(provider);
8263
+ health.consecutiveFailures += 1;
8264
+ health.lastFailureAt = currentTime;
8265
+ if (rateLimited) {
8266
+ health.lastRateLimitedAt = currentTime;
8267
+ }
8268
+ if (rateLimited || health.consecutiveFailures >= failureThreshold) {
8269
+ health.status = "suppressed";
8270
+ health.suppressedUntil = currentTime + (rateLimited ? rateLimitCooldownMs : cooldownMs);
8271
+ }
8272
+ return cloneHealth(provider);
8273
+ };
8274
+ const resolveAllowedProviders = async (input) => {
8275
+ const allowProviders = policy?.allowProviders ?? options.allowProviders;
8276
+ const allowed = typeof allowProviders === "function" ? await allowProviders(input) : allowProviders;
8277
+ return new Set(allowed ?? providerIds);
8278
+ };
8279
+ const sortProviders = (providers) => {
8280
+ if (strategy !== "prefer-cheapest" && strategy !== "prefer-fastest") {
8281
+ return providers;
8282
+ }
8283
+ return [...providers].sort((left, right) => {
8284
+ const leftProfile = options.providerProfiles?.[left];
8285
+ const rightProfile = options.providerProfiles?.[right];
8286
+ const leftValue = strategy === "prefer-cheapest" ? leftProfile?.cost ?? Number.MAX_SAFE_INTEGER : leftProfile?.latencyMs ?? Number.MAX_SAFE_INTEGER;
8287
+ const rightValue = strategy === "prefer-cheapest" ? rightProfile?.cost ?? Number.MAX_SAFE_INTEGER : rightProfile?.latencyMs ?? Number.MAX_SAFE_INTEGER;
8288
+ return leftValue - rightValue || (leftProfile?.priority ?? Number.MAX_SAFE_INTEGER) - (rightProfile?.priority ?? Number.MAX_SAFE_INTEGER);
8289
+ });
8290
+ };
8291
+ const resolveOrder = async (input) => {
8292
+ const selectedProvider = await options.selectProvider?.(input);
8293
+ const allowedProviders = await resolveAllowedProviders(input);
8294
+ const fallbackOrder = typeof options.fallback === "function" ? await options.fallback(input) : options.fallback;
8295
+ const rankedProviders = sortProviders([
8296
+ ...fallbackOrder ?? providerIds
8297
+ ]).filter((provider) => allowedProviders.has(provider));
8298
+ const healthyRankedProviders = healthOptions ? rankedProviders.filter((provider) => !isSuppressed(provider)) : rankedProviders;
8299
+ const candidateRankedProviders = healthyRankedProviders.length ? healthyRankedProviders : rankedProviders;
8300
+ const preferred = selectedProvider && allowedProviders.has(selectedProvider) && (!healthOptions || !isSuppressed(selectedProvider)) ? selectedProvider : candidateRankedProviders[0] ?? firstProvider;
8301
+ const seen = new Set;
8302
+ const order = [];
8303
+ const candidates = strategy === "ordered" ? candidateRankedProviders : [
8304
+ preferred,
8305
+ ...candidateRankedProviders,
8306
+ ...providerIds.filter((provider) => !healthOptions || !isSuppressed(provider))
8307
+ ];
8308
+ for (const provider of candidates) {
8309
+ if (!provider || seen.has(provider) || !allowedProviders.has(provider) || !options.providers[provider]) {
8310
+ continue;
8311
+ }
8312
+ seen.add(provider);
8313
+ order.push(provider);
8314
+ }
8315
+ return {
8316
+ order,
8317
+ selectedProvider: preferred
8318
+ };
8319
+ };
8320
+ const emit = async (event, input) => {
8321
+ await options.onProviderEvent?.(event, input);
8322
+ };
8323
+ return {
8324
+ generate: async (input) => {
8325
+ const { order, selectedProvider } = await resolveOrder(input);
8326
+ if (!selectedProvider || order.length === 0) {
8327
+ throw new Error("Voice provider router has no available providers.");
8328
+ }
8329
+ let lastError;
8330
+ for (const [index, provider] of order.entries()) {
8331
+ const model = options.providers[provider];
8332
+ if (!model) {
8333
+ continue;
8334
+ }
8335
+ const startedAt = Date.now();
8336
+ try {
8337
+ const output = await model.generate(input);
8338
+ const providerHealth = recordProviderSuccess(provider);
8339
+ await emit({
8340
+ at: Date.now(),
8341
+ elapsedMs: Date.now() - startedAt,
8342
+ fallbackProvider: provider === selectedProvider ? undefined : provider,
8343
+ provider,
8344
+ providerHealth,
8345
+ recovered: provider !== selectedProvider,
8346
+ selectedProvider,
8347
+ status: provider === selectedProvider ? "success" : "fallback"
8348
+ }, input);
8349
+ return output;
8350
+ } catch (error) {
8351
+ lastError = error;
8352
+ const hasNextProvider = index < order.length - 1;
8353
+ const isProviderError = options.isProviderError?.(error, provider) ?? true;
8354
+ const rateLimited = options.isRateLimitError?.(error, provider) ?? defaultIsRateLimitError(error);
8355
+ const shouldFallback = fallbackMode === "provider-error" ? isProviderError : fallbackMode === "rate-limit" ? isProviderError && rateLimited : false;
8356
+ const providerHealth = recordProviderError(provider, isProviderError, rateLimited);
8357
+ const nextProvider = hasNextProvider ? order[index + 1] : undefined;
8358
+ await emit({
8359
+ at: Date.now(),
8360
+ elapsedMs: Date.now() - startedAt,
8361
+ error: errorMessage(error),
8362
+ fallbackProvider: shouldFallback ? nextProvider : undefined,
8363
+ provider,
8364
+ providerHealth,
8365
+ rateLimited,
8366
+ selectedProvider,
8367
+ suppressionRemainingMs: getSuppressionRemainingMs(provider),
8368
+ suppressedUntil: providerHealth?.suppressedUntil,
8369
+ status: "error"
8370
+ }, input);
8371
+ if (!hasNextProvider || !shouldFallback) {
8372
+ throw error;
8373
+ }
8374
+ }
8375
+ }
8376
+ throw lastError ?? new Error("Voice provider router did not run a provider.");
8377
+ }
8378
+ };
8379
+ };
8380
+ var messageToOpenAIInput = (message) => {
8381
+ if (message.role === "tool") {
8382
+ return [
8383
+ {
8384
+ call_id: message.toolCallId ?? message.name ?? crypto.randomUUID(),
8385
+ output: message.content,
8386
+ type: "function_call_output"
8387
+ }
8388
+ ];
8389
+ }
8390
+ const toolCalls = getMessageToolCalls(message);
8391
+ if (message.role === "assistant" && toolCalls.length) {
8392
+ return toolCalls.map((toolCall) => ({
8393
+ arguments: JSON.stringify(toolCall.args),
8394
+ call_id: toolCall.id ?? crypto.randomUUID(),
8395
+ name: toolCall.name,
8396
+ type: "function_call"
8397
+ }));
8398
+ }
8399
+ return [
8400
+ {
8401
+ content: message.content,
8402
+ role: message.role === "system" ? "developer" : message.role
8403
+ }
8404
+ ];
8405
+ };
8406
+ var messagesToOpenAIInput = (messages) => messages.flatMap(messageToOpenAIInput);
8407
+ var messageToAnthropicMessage = (message) => {
8408
+ if (message.role === "system") {
8409
+ return;
8410
+ }
8411
+ if (message.role === "tool") {
8412
+ if (!message.toolCallId) {
8413
+ return {
8414
+ content: `Tool result from ${message.name ?? "tool"}: ${message.content}`,
8415
+ role: "user"
8416
+ };
8417
+ }
8418
+ return {
8419
+ content: [
8420
+ {
8421
+ content: message.content,
8422
+ tool_use_id: message.toolCallId,
8423
+ type: "tool_result"
8424
+ }
8425
+ ],
8426
+ role: "user"
8427
+ };
8428
+ }
8429
+ const toolCalls = getMessageToolCalls(message);
8430
+ if (message.role === "assistant" && toolCalls.length) {
8431
+ return {
8432
+ content: [
8433
+ ...message.content ? [
8434
+ {
8435
+ text: message.content,
8436
+ type: "text"
8437
+ }
8438
+ ] : [],
8439
+ ...toolCalls.map((toolCall) => ({
8440
+ id: toolCall.id ?? crypto.randomUUID(),
8441
+ input: toolCall.args,
8442
+ name: toolCall.name,
8443
+ type: "tool_use"
8444
+ }))
8445
+ ],
8446
+ role: "assistant"
8447
+ };
8448
+ }
8449
+ return {
8450
+ content: message.content,
8451
+ role: message.role
8452
+ };
8453
+ };
8454
+ var toGeminiSchema = (schema) => {
8455
+ const next = {};
8456
+ for (const [key, value] of Object.entries(schema)) {
8457
+ if (key === "additionalProperties") {
8458
+ continue;
8459
+ }
8460
+ if (key === "type" && typeof value === "string") {
8461
+ next[key] = value.toUpperCase();
8462
+ continue;
8463
+ }
8464
+ if (Array.isArray(value)) {
8465
+ next[key] = value.map((item) => item && typeof item === "object" ? toGeminiSchema(item) : item);
8466
+ continue;
8467
+ }
8468
+ if (value && typeof value === "object") {
8469
+ next[key] = toGeminiSchema(value);
8470
+ continue;
8471
+ }
8472
+ next[key] = value;
8473
+ }
8474
+ return next;
8475
+ };
8476
+ var messageToGeminiContent = (message) => {
8477
+ if (message.role === "system") {
8478
+ return;
8479
+ }
8480
+ if (message.role === "tool") {
8481
+ return {
8482
+ parts: [
8483
+ {
8484
+ functionResponse: {
8485
+ id: message.toolCallId,
8486
+ name: message.name ?? "tool",
8487
+ response: {
8488
+ result: parseJSONValue(message.content)
8489
+ }
8490
+ }
8491
+ }
8492
+ ],
8493
+ role: "user"
8494
+ };
8495
+ }
8496
+ const toolCalls = getMessageToolCalls(message);
8497
+ if (message.role === "assistant" && toolCalls.length) {
8498
+ return {
8499
+ parts: [
8500
+ ...message.content ? [
8501
+ {
8502
+ text: message.content
8503
+ }
8504
+ ] : [],
8505
+ ...toolCalls.map((toolCall) => ({
8506
+ functionCall: {
8507
+ args: toolCall.args,
8508
+ id: toolCall.id,
8509
+ name: toolCall.name
8510
+ }
8511
+ }))
8512
+ ],
8513
+ role: "model"
8514
+ };
8515
+ }
8516
+ return {
8517
+ parts: [
8518
+ {
8519
+ text: message.content
8520
+ }
8521
+ ],
8522
+ role: message.role === "assistant" ? "model" : "user"
8523
+ };
8524
+ };
8525
+ var extractText = (response) => {
8526
+ if (typeof response.output_text === "string") {
8527
+ return response.output_text;
8528
+ }
8529
+ const output = Array.isArray(response.output) ? response.output : [];
8530
+ for (const item of output) {
8531
+ if (!item || typeof item !== "object") {
8532
+ continue;
8533
+ }
8534
+ const record = item;
8535
+ const content = Array.isArray(record.content) ? record.content : [];
8536
+ for (const contentItem of content) {
8537
+ if (!contentItem || typeof contentItem !== "object") {
8538
+ continue;
8539
+ }
8540
+ const contentRecord = contentItem;
8541
+ if (typeof contentRecord.text === "string") {
8542
+ return contentRecord.text;
8543
+ }
8544
+ }
8545
+ }
8546
+ return "";
8547
+ };
8548
+ var extractToolCalls = (response) => {
8549
+ const output = Array.isArray(response.output) ? response.output : [];
8550
+ const toolCalls = [];
8551
+ for (const item of output) {
8552
+ if (!item || typeof item !== "object") {
8553
+ continue;
8554
+ }
8555
+ const record = item;
8556
+ if (record.type !== "function_call" || typeof record.name !== "string") {
8557
+ continue;
8558
+ }
8559
+ const args = typeof record.arguments === "string" ? parseJSON(record.arguments) : {};
8560
+ toolCalls.push({
8561
+ args,
8562
+ id: typeof record.call_id === "string" ? record.call_id : typeof record.id === "string" ? record.id : undefined,
8563
+ name: record.name
8564
+ });
8565
+ }
8566
+ return toolCalls;
8567
+ };
8568
+ var createOpenAIVoiceAssistantModel = (options) => {
8569
+ const fetchImpl = options.fetch ?? globalThis.fetch;
8570
+ const baseUrl = options.baseUrl ?? "https://api.openai.com/v1";
8571
+ const model = options.model ?? "gpt-4.1-mini";
8572
+ return {
8573
+ generate: async (input) => {
8574
+ const response = await fetchImpl(`${baseUrl.replace(/\/$/, "")}/responses`, {
8575
+ body: JSON.stringify({
8576
+ input: messagesToOpenAIInput(input.messages),
8577
+ instructions: [
8578
+ input.system,
8579
+ "Return a JSON object with assistantText, complete, transfer, escalate, voicemail, noAnswer, and result when you are not calling tools."
8580
+ ].filter(Boolean).join(`
8581
+
8582
+ `),
8583
+ max_output_tokens: options.maxOutputTokens,
8584
+ model,
8585
+ temperature: options.temperature,
8586
+ text: {
8587
+ format: {
8588
+ name: "voice_route_result",
8589
+ schema: OUTPUT_SCHEMA,
8590
+ strict: false,
8591
+ type: "json_schema"
8592
+ }
8593
+ },
8594
+ tool_choice: input.tools.length ? "auto" : "none",
8595
+ tools: input.tools.map((tool) => ({
8596
+ description: tool.description,
8597
+ name: tool.name,
8598
+ parameters: tool.parameters ?? {
8599
+ additionalProperties: true,
8600
+ type: "object"
8601
+ },
8602
+ strict: false,
8603
+ type: "function"
8604
+ }))
8605
+ }),
8606
+ headers: {
8607
+ authorization: `Bearer ${options.apiKey}`,
8608
+ "content-type": "application/json"
8609
+ },
8610
+ method: "POST"
8611
+ });
8612
+ if (!response.ok) {
8613
+ throw createHTTPError("OpenAI", response);
8614
+ }
8615
+ const body = await response.json();
8616
+ if (body.usage && typeof body.usage === "object") {
8617
+ await options.onUsage?.(body.usage);
8618
+ }
8619
+ const toolCalls = extractToolCalls(body);
8620
+ if (toolCalls.length) {
8621
+ return {
8622
+ toolCalls
8623
+ };
8624
+ }
8625
+ return normalizeRouteOutput(parseJSON(extractText(body)));
8626
+ }
8627
+ };
8628
+ };
8629
+ var extractAnthropicText = (response) => {
8630
+ const content = Array.isArray(response.content) ? response.content : [];
8631
+ return content.map((item) => item && typeof item === "object" && item.type === "text" && typeof item.text === "string" ? item.text : "").filter(Boolean).join(`
8632
+ `);
8633
+ };
8634
+ var extractAnthropicToolCalls = (response) => {
8635
+ const content = Array.isArray(response.content) ? response.content : [];
8636
+ const toolCalls = [];
8637
+ for (const item of content) {
8638
+ if (!item || typeof item !== "object") {
8639
+ continue;
8640
+ }
8641
+ const record = item;
8642
+ if (record.type !== "tool_use" || typeof record.name !== "string") {
8643
+ continue;
8644
+ }
8645
+ toolCalls.push({
8646
+ args: record.input && typeof record.input === "object" ? record.input : {},
8647
+ id: typeof record.id === "string" ? record.id : undefined,
8648
+ name: record.name
8649
+ });
8650
+ }
8651
+ return toolCalls;
8652
+ };
8653
+ var createAnthropicVoiceAssistantModel = (options) => {
8654
+ const fetchImpl = options.fetch ?? globalThis.fetch;
8655
+ const baseUrl = options.baseUrl ?? "https://api.anthropic.com/v1";
8656
+ const model = options.model ?? "claude-sonnet-4-5";
8657
+ return {
8658
+ generate: async (input) => {
8659
+ const response = await fetchImpl(`${baseUrl.replace(/\/$/, "")}/messages`, {
8660
+ body: JSON.stringify({
8661
+ max_tokens: options.maxOutputTokens ?? 1024,
8662
+ messages: input.messages.map(messageToAnthropicMessage).filter(Boolean),
8663
+ model,
8664
+ system: [input.system, ROUTE_RESULT_INSTRUCTION].filter(Boolean).join(`
8665
+
8666
+ `),
8667
+ temperature: options.temperature,
8668
+ tool_choice: input.tools.length ? { type: "auto" } : { type: "none" },
8669
+ tools: input.tools.map((tool) => ({
8670
+ description: tool.description,
8671
+ input_schema: tool.parameters ?? {
8672
+ additionalProperties: true,
8673
+ type: "object"
8674
+ },
8675
+ name: tool.name
8676
+ }))
8677
+ }),
8678
+ headers: {
8679
+ "anthropic-version": options.version ?? "2023-06-01",
8680
+ "content-type": "application/json",
8681
+ "x-api-key": options.apiKey
8682
+ },
8683
+ method: "POST"
8684
+ });
8685
+ if (!response.ok) {
8686
+ throw createHTTPError("Anthropic", response);
8687
+ }
8688
+ const body = await response.json();
8689
+ if (body.usage && typeof body.usage === "object") {
8690
+ await options.onUsage?.(body.usage);
8691
+ }
8692
+ const toolCalls = extractAnthropicToolCalls(body);
8693
+ if (toolCalls.length) {
8694
+ return {
8695
+ assistantText: extractAnthropicText(body) || undefined,
8696
+ toolCalls
8697
+ };
8698
+ }
8699
+ return normalizeRouteOutput(parseJSON(extractAnthropicText(body)));
8700
+ }
8701
+ };
8702
+ };
8703
+ var extractGeminiCandidateParts = (response) => {
8704
+ const candidates = Array.isArray(response.candidates) ? response.candidates : [];
8705
+ const first = candidates[0];
8706
+ if (!first || typeof first !== "object") {
8707
+ return [];
8708
+ }
8709
+ const content = first.content;
8710
+ if (!content || typeof content !== "object") {
8711
+ return [];
8712
+ }
8713
+ const parts = content.parts;
8714
+ return Array.isArray(parts) ? parts : [];
8715
+ };
8716
+ var extractGeminiText = (response) => extractGeminiCandidateParts(response).map((part) => part && typeof part === "object" && typeof part.text === "string" ? part.text : "").filter(Boolean).join(`
8717
+ `);
8718
+ var extractGeminiToolCalls = (response) => {
8719
+ const toolCalls = [];
8720
+ for (const part of extractGeminiCandidateParts(response)) {
8721
+ if (!part || typeof part !== "object") {
8722
+ continue;
8723
+ }
8724
+ const functionCall = part.functionCall;
8725
+ if (!functionCall || typeof functionCall !== "object") {
8726
+ continue;
8727
+ }
8728
+ const record = functionCall;
8729
+ if (typeof record.name !== "string") {
8730
+ continue;
8731
+ }
8732
+ toolCalls.push({
8733
+ args: record.args && typeof record.args === "object" ? record.args : {},
8734
+ id: typeof record.id === "string" ? record.id : undefined,
8735
+ name: record.name
8736
+ });
8737
+ }
8738
+ return toolCalls;
8739
+ };
8740
+ var createGeminiVoiceAssistantModel = (options) => {
8741
+ const fetchImpl = options.fetch ?? globalThis.fetch;
8742
+ const baseUrl = options.baseUrl ?? "https://generativelanguage.googleapis.com/v1beta";
8743
+ const model = options.model ?? "gemini-2.5-flash";
8744
+ const maxRetries = Math.max(0, options.maxRetries ?? 2);
8745
+ return {
8746
+ generate: async (input) => {
8747
+ const endpoint = `${baseUrl.replace(/\/$/, "")}/models/${encodeURIComponent(model)}:generateContent?key=${encodeURIComponent(options.apiKey)}`;
8748
+ let response;
8749
+ for (let attempt = 0;attempt <= maxRetries; attempt += 1) {
8750
+ response = await fetchImpl(endpoint, {
8751
+ body: JSON.stringify({
8752
+ contents: input.messages.map(messageToGeminiContent).filter(Boolean),
8753
+ generationConfig: {
8754
+ maxOutputTokens: options.maxOutputTokens,
8755
+ ...input.tools.length ? {} : {
8756
+ responseMimeType: "application/json",
8757
+ responseSchema: toGeminiSchema(OUTPUT_SCHEMA)
8758
+ },
8759
+ temperature: options.temperature
8760
+ },
8761
+ systemInstruction: {
8762
+ parts: [
8763
+ {
8764
+ text: [input.system, ROUTE_RESULT_INSTRUCTION].filter(Boolean).join(`
8765
+
8766
+ `)
8767
+ }
8768
+ ]
8769
+ },
8770
+ tools: input.tools.length ? [
8771
+ {
8772
+ functionDeclarations: input.tools.map((tool) => ({
8773
+ description: tool.description,
8774
+ name: tool.name,
8775
+ parameters: toGeminiSchema(tool.parameters ?? {
8776
+ additionalProperties: true,
8777
+ type: "object"
8778
+ })
8779
+ }))
8780
+ }
8781
+ ] : undefined
8782
+ }),
8783
+ headers: {
8784
+ "content-type": "application/json"
8785
+ },
8786
+ method: "POST"
8787
+ });
8788
+ if (response.ok || response.status !== 429 && response.status < 500 || attempt === maxRetries) {
8789
+ break;
8790
+ }
8791
+ const retryAfter = Number(response.headers.get("retry-after"));
8792
+ await sleep4(Number.isFinite(retryAfter) && retryAfter > 0 ? retryAfter * 1000 : 500 * 2 ** attempt);
8793
+ }
8794
+ if (!response) {
8795
+ throw new Error("Gemini voice assistant model failed: no response");
8796
+ }
8797
+ if (!response.ok) {
8798
+ throw createHTTPError("Gemini", response);
8799
+ }
8800
+ const body = await response.json();
8801
+ if (body.usageMetadata && typeof body.usageMetadata === "object") {
8802
+ await options.onUsage?.(body.usageMetadata);
8803
+ }
8804
+ const toolCalls = extractGeminiToolCalls(body);
8805
+ if (toolCalls.length) {
8806
+ return {
8807
+ assistantText: extractGeminiText(body) || undefined,
8808
+ toolCalls
8809
+ };
8810
+ }
8811
+ return normalizeRouteOutput(parseJSON(extractGeminiText(body)));
8812
+ }
8813
+ };
8814
+ };
6876
8815
  // src/sqliteStore.ts
6877
8816
  import { Database } from "bun:sqlite";
6878
8817
  var normalizeTableNameSegment = (value) => value.trim().replace(/[^a-zA-Z0-9_]+/g, "_").replace(/^_+|_+$/g, "") || "voice";
@@ -7354,6 +9293,361 @@ var createVoiceMemoryStore = () => {
7354
9293
  };
7355
9294
  return { get, getOrCreate, list, remove, set };
7356
9295
  };
9296
+ // src/opsWebhook.ts
9297
+ import { Elysia as Elysia5 } from "elysia";
9298
+ var toHex5 = (bytes) => Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
9299
+ var signVoiceOpsWebhookBody = async (input) => {
9300
+ const encoder = new TextEncoder;
9301
+ const key = await crypto.subtle.importKey("raw", encoder.encode(input.secret), {
9302
+ hash: "SHA-256",
9303
+ name: "HMAC"
9304
+ }, false, ["sign"]);
9305
+ const signature = await crypto.subtle.sign("HMAC", key, encoder.encode(`${input.timestamp}.${input.body}`));
9306
+ return `sha256=${toHex5(new Uint8Array(signature))}`;
9307
+ };
9308
+ var timingSafeEqual = (left, right) => {
9309
+ const encoder = new TextEncoder;
9310
+ const leftBytes = encoder.encode(left);
9311
+ const rightBytes = encoder.encode(right);
9312
+ if (leftBytes.length !== rightBytes.length) {
9313
+ return false;
9314
+ }
9315
+ let diff = 0;
9316
+ for (let index = 0;index < leftBytes.length; index += 1) {
9317
+ diff |= leftBytes[index] ^ rightBytes[index];
9318
+ }
9319
+ return diff === 0;
9320
+ };
9321
+ var resolveWebhookLink = async (resolver, event) => {
9322
+ if (typeof resolver === "function") {
9323
+ return resolver({
9324
+ event
9325
+ });
9326
+ }
9327
+ return resolver;
9328
+ };
9329
+ var joinBaseUrl = (baseUrl, path) => `${baseUrl.replace(/\/+$/, "")}/${path.replace(/^\/+/, "")}`;
9330
+ var asString = (value) => typeof value === "string" && value.length > 0 ? value : undefined;
9331
+ var buildVoiceOpsWebhookEntity = (event) => ({
9332
+ disposition: asString(event.payload.disposition),
9333
+ outcome: asString(event.payload.outcome),
9334
+ priority: asString(event.payload.priority),
9335
+ queue: asString(event.payload.queue),
9336
+ reviewId: asString(event.payload.reviewId),
9337
+ scenarioId: asString(event.payload.scenarioId),
9338
+ sessionId: asString(event.payload.sessionId),
9339
+ status: asString(event.payload.status),
9340
+ target: asString(event.payload.target),
9341
+ taskId: asString(event.payload.taskId)
9342
+ });
9343
+ var createVoiceOpsWebhookEnvelope = async (input) => {
9344
+ const entity = buildVoiceOpsWebhookEntity(input.event);
9345
+ const replayHref = await resolveWebhookLink(input.replayHref, input.event) ?? (input.baseUrl && entity.sessionId ? joinBaseUrl(input.baseUrl, `/api/voice-sessions/${encodeURIComponent(entity.sessionId)}/replay`) : undefined);
9346
+ const links = {
9347
+ event: await resolveWebhookLink(input.eventHref, input.event),
9348
+ replay: replayHref,
9349
+ review: await resolveWebhookLink(input.reviewHref, input.event),
9350
+ task: await resolveWebhookLink(input.taskHref, input.event)
9351
+ };
9352
+ return {
9353
+ entity,
9354
+ event: {
9355
+ createdAt: input.event.createdAt,
9356
+ id: input.event.id,
9357
+ payload: input.event.payload,
9358
+ type: input.event.type
9359
+ },
9360
+ links: links.event || links.replay || links.review || links.task ? links : undefined,
9361
+ schemaVersion: 1,
9362
+ source: "absolutejs-voice"
9363
+ };
9364
+ };
9365
+ var createVoiceOpsWebhookSink = (options) => createVoiceIntegrationHTTPSink({
9366
+ ...options,
9367
+ body: ({ event }) => createVoiceOpsWebhookEnvelope({
9368
+ baseUrl: options.baseUrl,
9369
+ event,
9370
+ eventHref: options.eventHref,
9371
+ replayHref: options.replayHref,
9372
+ reviewHref: options.reviewHref,
9373
+ taskHref: options.taskHref
9374
+ }),
9375
+ kind: options.kind ?? "ops-webhook"
9376
+ });
9377
+ var verifyVoiceOpsWebhookSignature = async (input) => {
9378
+ if (!input.secret) {
9379
+ return {
9380
+ ok: false,
9381
+ reason: "missing-secret"
9382
+ };
9383
+ }
9384
+ if (!input.signature) {
9385
+ return {
9386
+ ok: false,
9387
+ reason: "missing-signature"
9388
+ };
9389
+ }
9390
+ if (!input.signature.startsWith("sha256=")) {
9391
+ return {
9392
+ ok: false,
9393
+ reason: "unsupported-algorithm"
9394
+ };
9395
+ }
9396
+ if (!input.timestamp) {
9397
+ return {
9398
+ ok: false,
9399
+ reason: "missing-timestamp"
9400
+ };
9401
+ }
9402
+ const timestampMs = Number(input.timestamp);
9403
+ const toleranceMs = Math.max(0, input.toleranceMs ?? 5 * 60 * 1000);
9404
+ if (!Number.isFinite(timestampMs) || toleranceMs > 0 && Math.abs((input.now ?? Date.now()) - timestampMs) > toleranceMs) {
9405
+ return {
9406
+ ok: false,
9407
+ reason: "stale-timestamp"
9408
+ };
9409
+ }
9410
+ const expected = await signVoiceOpsWebhookBody({
9411
+ body: input.body,
9412
+ secret: input.secret,
9413
+ timestamp: input.timestamp
9414
+ });
9415
+ if (!timingSafeEqual(expected, input.signature)) {
9416
+ return {
9417
+ ok: false,
9418
+ reason: "invalid-signature"
9419
+ };
9420
+ }
9421
+ return {
9422
+ ok: true
9423
+ };
9424
+ };
9425
+ var createVoiceOpsWebhookReceiverRoutes = (options = {}) => {
9426
+ const path = options.path ?? "/api/voice-ops/webhook";
9427
+ return new Elysia5().post(path, async ({ body, request, set }) => {
9428
+ const bodyText = typeof body === "string" ? body : JSON.stringify(body);
9429
+ if (options.signingSecret) {
9430
+ const verification = await verifyVoiceOpsWebhookSignature({
9431
+ body: bodyText,
9432
+ secret: options.signingSecret,
9433
+ signature: request.headers.get("x-absolutejs-signature"),
9434
+ timestamp: request.headers.get("x-absolutejs-timestamp"),
9435
+ toleranceMs: options.toleranceMs
9436
+ });
9437
+ if (!verification.ok) {
9438
+ set.status = 401;
9439
+ return {
9440
+ ok: false,
9441
+ reason: verification.reason
9442
+ };
9443
+ }
9444
+ }
9445
+ const envelope = JSON.parse(bodyText);
9446
+ await options.onEnvelope?.({
9447
+ envelope,
9448
+ request
9449
+ });
9450
+ return {
9451
+ eventId: envelope.event?.id,
9452
+ ok: true,
9453
+ type: envelope.event?.type
9454
+ };
9455
+ }, {
9456
+ parse: "text"
9457
+ });
9458
+ };
9459
+ // src/handoffHealth.ts
9460
+ import { Elysia as Elysia6 } from "elysia";
9461
+ var escapeHtml7 = (value) => value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
9462
+ var getString4 = (value) => typeof value === "string" && value.length > 0 ? value : undefined;
9463
+ var isStatus = (value) => value === "delivered" || value === "failed" || value === "skipped";
9464
+ var increment3 = (record, key) => {
9465
+ record[key] = (record[key] ?? 0) + 1;
9466
+ };
9467
+ var normalizeDelivery = (adapterId, value) => {
9468
+ const record = value && typeof value === "object" ? value : {};
9469
+ return {
9470
+ adapterId: getString4(record.adapterId) ?? adapterId,
9471
+ adapterKind: getString4(record.adapterKind),
9472
+ deliveredAt: typeof record.deliveredAt === "number" ? record.deliveredAt : undefined,
9473
+ deliveredTo: getString4(record.deliveredTo),
9474
+ error: getString4(record.error),
9475
+ status: isStatus(record.status) ? record.status : "failed"
9476
+ };
9477
+ };
9478
+ var normalizeDeliveries = (payload) => {
9479
+ const deliveries = payload.deliveries;
9480
+ if (!deliveries || typeof deliveries !== "object") {
9481
+ return [];
9482
+ }
9483
+ return Object.entries(deliveries).map(([adapterId, value]) => normalizeDelivery(adapterId, value));
9484
+ };
9485
+ var resolveReplayHref = (event, replayHref) => {
9486
+ if (replayHref === false) {
9487
+ return;
9488
+ }
9489
+ if (typeof replayHref === "function") {
9490
+ return replayHref(event);
9491
+ }
9492
+ return `${replayHref ?? "/api/voice-sessions"}/${encodeURIComponent(event.sessionId)}/replay/htmx`;
9493
+ };
9494
+ var summarizeVoiceHandoffHealth = async (options = {}) => {
9495
+ const sourceEvents = options.events ?? await options.store?.list() ?? [];
9496
+ const search = options.q?.trim().toLowerCase();
9497
+ const byAction = {};
9498
+ const byAdapter = {};
9499
+ const byStatus = {
9500
+ delivered: 0,
9501
+ failed: 0,
9502
+ skipped: 0
9503
+ };
9504
+ const events = sourceEvents.filter((event) => event.type === "call.handoff").map((event) => {
9505
+ const status = isStatus(event.payload.status) ? event.payload.status : "failed";
9506
+ const deliveries = normalizeDeliveries(event.payload);
9507
+ const item = {
9508
+ action: getString4(event.payload.action),
9509
+ at: event.at,
9510
+ deliveries,
9511
+ reason: getString4(event.payload.reason),
9512
+ sessionId: event.sessionId,
9513
+ status,
9514
+ target: getString4(event.payload.target)
9515
+ };
9516
+ return {
9517
+ ...item,
9518
+ replayHref: resolveReplayHref(item, options.replayHref)
9519
+ };
9520
+ }).filter((event) => {
9521
+ if (options.status && options.status !== "all" && event.status !== options.status) {
9522
+ return false;
9523
+ }
9524
+ if (!search) {
9525
+ return true;
9526
+ }
9527
+ return [
9528
+ event.action,
9529
+ event.reason,
9530
+ event.sessionId,
9531
+ event.status,
9532
+ event.target,
9533
+ ...event.deliveries.flatMap((delivery) => [
9534
+ delivery.adapterId,
9535
+ delivery.adapterKind,
9536
+ delivery.deliveredTo,
9537
+ delivery.error,
9538
+ delivery.status
9539
+ ])
9540
+ ].some((value) => value?.toLowerCase().includes(search));
9541
+ }).sort((left, right) => right.at - left.at).slice(0, options.limit ?? 50);
9542
+ for (const event of events) {
9543
+ byStatus[event.status] += 1;
9544
+ if (event.action) {
9545
+ increment3(byAction, event.action);
9546
+ }
9547
+ for (const delivery of event.deliveries) {
9548
+ byAdapter[delivery.adapterId] ??= {
9549
+ delivered: 0,
9550
+ failed: 0,
9551
+ skipped: 0
9552
+ };
9553
+ byAdapter[delivery.adapterId][delivery.status] += 1;
9554
+ }
9555
+ }
9556
+ return {
9557
+ byAction,
9558
+ byAdapter,
9559
+ byStatus,
9560
+ events,
9561
+ failed: byStatus.failed,
9562
+ total: events.length
9563
+ };
9564
+ };
9565
+ var renderMetricGrid = (summary) => [
9566
+ '<section class="voice-handoff-health-grid">',
9567
+ `<article><span>Total</span><strong>${String(summary.total)}</strong></article>`,
9568
+ `<article><span>Delivered</span><strong>${String(summary.byStatus.delivered)}</strong></article>`,
9569
+ `<article><span>Failed</span><strong>${String(summary.byStatus.failed)}</strong></article>`,
9570
+ `<article><span>Skipped</span><strong>${String(summary.byStatus.skipped)}</strong></article>`,
9571
+ "</section>"
9572
+ ].join("");
9573
+ var renderActionSummary = (summary) => {
9574
+ const actions = Object.entries(summary.byAction).sort((left, right) => right[1] - left[1]);
9575
+ const adapters = Object.entries(summary.byAdapter).sort(([left], [right]) => left.localeCompare(right));
9576
+ return [
9577
+ '<section class="voice-handoff-health-columns">',
9578
+ "<article><h3>Actions</h3>",
9579
+ actions.length === 0 ? "<p>No handoff actions yet.</p>" : `<ul>${actions.map(([action, count]) => `<li>${escapeHtml7(action)}: ${String(count)}</li>`).join("")}</ul>`,
9580
+ "</article>",
9581
+ "<article><h3>Adapters</h3>",
9582
+ adapters.length === 0 ? "<p>No adapter deliveries yet.</p>" : `<ul>${adapters.map(([adapterId, counts]) => `<li>${escapeHtml7(adapterId)}: ${String(counts.delivered)} delivered / ${String(counts.failed)} failed / ${String(counts.skipped)} skipped</li>`).join("")}</ul>`,
9583
+ "</article>",
9584
+ "</section>"
9585
+ ].join("");
9586
+ };
9587
+ var renderVoiceHandoffHealthHTML = (summary) => [
9588
+ '<div class="voice-handoff-health">',
9589
+ renderMetricGrid(summary),
9590
+ renderActionSummary(summary),
9591
+ "<section>",
9592
+ "<h3>Recent Handoffs</h3>",
9593
+ summary.events.length === 0 ? '<p class="voice-handoff-health-empty">No handoffs found.</p>' : [
9594
+ '<div class="voice-handoff-health-events">',
9595
+ ...summary.events.map((event) => [
9596
+ `<article class="${escapeHtml7(event.status)}">`,
9597
+ '<div class="voice-handoff-health-event-header">',
9598
+ `<strong>${escapeHtml7(event.action ?? "handoff")}</strong>`,
9599
+ `<span>${escapeHtml7(event.status)}</span>`,
9600
+ "</div>",
9601
+ `<p><small>${escapeHtml7(event.sessionId)}</small></p>`,
9602
+ event.target ? `<p>Target: ${escapeHtml7(event.target)}</p>` : "",
9603
+ event.reason ? `<p>Reason: ${escapeHtml7(event.reason)}</p>` : "",
9604
+ event.deliveries.length ? `<ul>${event.deliveries.map((delivery) => [
9605
+ "<li>",
9606
+ `${escapeHtml7(delivery.adapterId)}: ${escapeHtml7(delivery.status)}`,
9607
+ delivery.deliveredTo ? ` to ${escapeHtml7(delivery.deliveredTo)}` : "",
9608
+ delivery.error ? ` (${escapeHtml7(delivery.error)})` : "",
9609
+ "</li>"
9610
+ ].join("")).join("")}</ul>` : "",
9611
+ event.replayHref ? `<p><a href="${escapeHtml7(event.replayHref)}">Open replay</a></p>` : "",
9612
+ "</article>"
9613
+ ].join("")),
9614
+ "</div>"
9615
+ ].join(""),
9616
+ "</section>",
9617
+ "</div>"
9618
+ ].join("");
9619
+ var createVoiceHandoffHealthJSONHandler = (options = {}) => async ({ query }) => summarizeVoiceHandoffHealth({
9620
+ ...options,
9621
+ limit: typeof query?.limit === "string" ? Number(query.limit) : options.limit,
9622
+ q: query?.q ?? options.q,
9623
+ status: query?.status === "delivered" || query?.status === "failed" || query?.status === "skipped" || query?.status === "all" ? query.status : options.status
9624
+ });
9625
+ var createVoiceHandoffHealthHTMLHandler = (options = {}) => async ({ query }) => {
9626
+ const summary = await summarizeVoiceHandoffHealth({
9627
+ ...options,
9628
+ limit: typeof query?.limit === "string" ? Number(query.limit) : options.limit,
9629
+ q: query?.q ?? options.q,
9630
+ status: query?.status === "delivered" || query?.status === "failed" || query?.status === "skipped" || query?.status === "all" ? query.status : options.status
9631
+ });
9632
+ const body = await (options.render?.(summary) ?? renderVoiceHandoffHealthHTML(summary));
9633
+ return new Response(body, {
9634
+ headers: {
9635
+ "Content-Type": "text/html; charset=utf-8",
9636
+ ...options.headers
9637
+ }
9638
+ });
9639
+ };
9640
+ var createVoiceHandoffHealthRoutes = (options = {}) => {
9641
+ const path = options.path ?? "/api/voice-handoffs";
9642
+ const htmlPath = options.htmlPath === undefined ? `${path}/htmx` : options.htmlPath;
9643
+ const routes = new Elysia6({
9644
+ name: options.name ?? "absolutejs-voice-handoff-health"
9645
+ }).get(path, createVoiceHandoffHealthJSONHandler(options));
9646
+ if (htmlPath) {
9647
+ routes.get(htmlPath, createVoiceHandoffHealthHTMLHandler(options));
9648
+ }
9649
+ return routes;
9650
+ };
7357
9651
  // src/queue.ts
7358
9652
  var releaseLeaseScript = `
7359
9653
  if redis.call("GET", KEYS[1]) == ARGV[1] then
@@ -7425,6 +9719,8 @@ var shouldDeadLetterSinkEvent = (event, sinks, maxFailures) => typeof maxFailure
7425
9719
  var shouldDeadLetterTask = (task, maxFailures) => typeof maxFailures === "number" && maxFailures > 0 && (task.processingAttempts ?? 0) >= maxFailures;
7426
9720
  var shouldProcessTraceDeliveryStatus = (status, allowed) => allowed.includes(status);
7427
9721
  var shouldDeadLetterTraceDelivery = (delivery, maxFailures) => typeof maxFailures === "number" && maxFailures > 0 && (delivery.deliveryAttempts ?? 0) >= maxFailures;
9722
+ var shouldProcessHandoffDeliveryStatus = (status, allowed) => allowed.includes(status);
9723
+ var shouldDeadLetterHandoffDelivery = (delivery, maxFailures) => typeof maxFailures === "number" && maxFailures > 0 && (delivery.deliveryAttempts ?? 0) >= maxFailures;
7428
9724
  var summarizeVoiceIntegrationEvents = (events, input = {}) => {
7429
9725
  const buildSummary = async () => {
7430
9726
  const deadLetterIds = new Set(input.deadLetters ? (await input.deadLetters.list()).map((event) => event.id) : []);
@@ -7506,6 +9802,48 @@ var summarizeVoiceTraceSinkDeliveries = (deliveries, input = {}) => {
7506
9802
  };
7507
9803
  return buildSummary();
7508
9804
  };
9805
+ var summarizeVoiceHandoffDeliveries = (deliveries, input = {}) => {
9806
+ const buildSummary = async () => {
9807
+ const deadLetterIds = new Set(input.deadLetters ? (await input.deadLetters.list()).map((delivery) => delivery.id) : []);
9808
+ const byAction = new Map;
9809
+ const summary = {
9810
+ byAction: [],
9811
+ deadLettered: 0,
9812
+ delivered: 0,
9813
+ failed: 0,
9814
+ pending: 0,
9815
+ retryEligible: 0,
9816
+ skipped: 0,
9817
+ total: deliveries.length
9818
+ };
9819
+ for (const delivery of deliveries) {
9820
+ byAction.set(delivery.action, (byAction.get(delivery.action) ?? 0) + 1);
9821
+ if (deadLetterIds.has(delivery.id)) {
9822
+ summary.deadLettered += 1;
9823
+ }
9824
+ switch (delivery.deliveryStatus) {
9825
+ case "delivered":
9826
+ summary.delivered += 1;
9827
+ break;
9828
+ case "failed":
9829
+ summary.failed += 1;
9830
+ if ((delivery.deliveryAttempts ?? 0) > 0) {
9831
+ summary.retryEligible += 1;
9832
+ }
9833
+ break;
9834
+ case "skipped":
9835
+ summary.skipped += 1;
9836
+ break;
9837
+ case "pending":
9838
+ summary.pending += 1;
9839
+ break;
9840
+ }
9841
+ }
9842
+ summary.byAction = [...byAction.entries()].sort((left, right) => right[1] - left[1]);
9843
+ return summary;
9844
+ };
9845
+ return buildSummary();
9846
+ };
7509
9847
  var summarizeVoiceOpsTaskQueue = (tasks, input = {}) => {
7510
9848
  const buildSummary = async () => {
7511
9849
  const deadLetterIds = new Set(input.deadLetters ? (await input.deadLetters.list()).map((task) => task.id) : []);
@@ -7935,6 +10273,108 @@ var createVoiceTraceSinkDeliveryWorkerLoop = (options) => {
7935
10273
  tick
7936
10274
  };
7937
10275
  };
10276
+ var createVoiceHandoffDeliveryWorker = (options) => {
10277
+ const allowedStatuses = options.statuses ?? ["pending", "failed"];
10278
+ const leaseMs = Math.max(1, options.leaseMs ?? 30000);
10279
+ return {
10280
+ drain: async () => {
10281
+ const result = {
10282
+ alreadyProcessed: 0,
10283
+ attempted: 0,
10284
+ deadLettered: 0,
10285
+ delivered: 0,
10286
+ failed: 0,
10287
+ skipped: 0
10288
+ };
10289
+ const deliveries = [...await options.deliveries.list()].sort((left, right) => left.createdAt - right.createdAt);
10290
+ for (const delivery of deliveries) {
10291
+ if (!shouldProcessHandoffDeliveryStatus(delivery.deliveryStatus, allowedStatuses)) {
10292
+ continue;
10293
+ }
10294
+ if (shouldDeadLetterHandoffDelivery(delivery, options.maxFailures)) {
10295
+ await options.deadLetters?.set(delivery.id, delivery);
10296
+ await options.onDeadLetter?.(delivery);
10297
+ result.deadLettered += 1;
10298
+ continue;
10299
+ }
10300
+ const claimed = await options.leases.claim({
10301
+ leaseMs,
10302
+ taskId: delivery.id,
10303
+ workerId: options.workerId
10304
+ });
10305
+ if (!claimed) {
10306
+ continue;
10307
+ }
10308
+ try {
10309
+ const idempotencyKey = `${delivery.id}:handoff`;
10310
+ if (options.idempotency && await options.idempotency.has(idempotencyKey)) {
10311
+ result.alreadyProcessed += 1;
10312
+ continue;
10313
+ }
10314
+ result.attempted += 1;
10315
+ const updatedDelivery = await deliverVoiceHandoffDelivery({
10316
+ adapters: options.adapters,
10317
+ api: options.api,
10318
+ delivery,
10319
+ failMode: options.failMode
10320
+ });
10321
+ await options.deliveries.set(updatedDelivery.id, updatedDelivery);
10322
+ if (updatedDelivery.deliveryStatus === "delivered" || updatedDelivery.deliveryStatus === "skipped") {
10323
+ await options.idempotency?.set(idempotencyKey, {
10324
+ ttlSeconds: options.idempotencyTtlSeconds
10325
+ });
10326
+ }
10327
+ if (updatedDelivery.deliveryStatus === "delivered") {
10328
+ result.delivered += 1;
10329
+ } else if (updatedDelivery.deliveryStatus === "skipped") {
10330
+ result.skipped += 1;
10331
+ } else if (updatedDelivery.deliveryStatus === "failed") {
10332
+ result.failed += 1;
10333
+ if (shouldDeadLetterHandoffDelivery(updatedDelivery, options.maxFailures)) {
10334
+ await options.deadLetters?.set(updatedDelivery.id, updatedDelivery);
10335
+ await options.onDeadLetter?.(updatedDelivery);
10336
+ result.deadLettered += 1;
10337
+ }
10338
+ }
10339
+ } finally {
10340
+ await options.leases.release({
10341
+ taskId: delivery.id,
10342
+ workerId: options.workerId
10343
+ });
10344
+ }
10345
+ }
10346
+ return result;
10347
+ }
10348
+ };
10349
+ };
10350
+ var createVoiceHandoffDeliveryWorkerLoop = (options) => {
10351
+ const pollIntervalMs = Math.max(1, options.pollIntervalMs ?? 1000);
10352
+ let timer;
10353
+ let running = false;
10354
+ const tick = async () => options.worker.drain();
10355
+ return {
10356
+ isRunning: () => running,
10357
+ start: () => {
10358
+ if (timer) {
10359
+ return;
10360
+ }
10361
+ running = true;
10362
+ timer = setInterval(() => {
10363
+ tick().catch((error) => {
10364
+ options.onError?.(error);
10365
+ });
10366
+ }, pollIntervalMs);
10367
+ },
10368
+ stop: () => {
10369
+ if (timer) {
10370
+ clearInterval(timer);
10371
+ timer = undefined;
10372
+ }
10373
+ running = false;
10374
+ },
10375
+ tick
10376
+ };
10377
+ };
7938
10378
  var createVoiceOpsTaskWorker = (options) => {
7939
10379
  const leaseMs = Math.max(1, options.leaseMs ?? 30000);
7940
10380
  const getTask = async (taskId) => {
@@ -8070,10 +10510,10 @@ var createVoiceOpsTaskProcessorWorker = (options) => ({
8070
10510
  result.completed += 1;
8071
10511
  } catch (error) {
8072
10512
  await options.onError?.(error, task);
8073
- const errorMessage = error instanceof Error ? error.message : String(error);
10513
+ const errorMessage2 = error instanceof Error ? error.message : String(error);
8074
10514
  const failedTask = failVoiceOpsTask(task, {
8075
10515
  actor: task.claimedBy ?? "ops-worker",
8076
- error: errorMessage
10516
+ error: errorMessage2
8077
10517
  });
8078
10518
  if (shouldDeadLetterTask(failedTask, options.maxFailures)) {
8079
10519
  const deadLetterTask = deadLetterVoiceOpsTask(failedTask, {
@@ -8886,7 +11326,7 @@ var createVoiceSTTRoutingCorrectionHandler = (mode = "generic") => {
8886
11326
  import { Buffer as Buffer2 } from "buffer";
8887
11327
  var TWILIO_MULAW_SAMPLE_RATE = 8000;
8888
11328
  var VOICE_PCM_SAMPLE_RATE = 16000;
8889
- var escapeXml = (value) => value.replaceAll("&", "&amp;").replaceAll('"', "&quot;").replaceAll("'", "&apos;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
11329
+ var escapeXml2 = (value) => value.replaceAll("&", "&amp;").replaceAll('"', "&quot;").replaceAll("'", "&apos;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
8890
11330
  var normalizeOnTurn2 = (handler) => {
8891
11331
  if (handler.length > 1) {
8892
11332
  const directHandler = handler;
@@ -9082,8 +11522,8 @@ var createTwilioSocketAdapter = (socket, getState) => ({
9082
11522
  }
9083
11523
  });
9084
11524
  var createTwilioVoiceResponse = (options) => {
9085
- const parameters = Object.entries(options.parameters ?? {}).filter((entry) => entry[1] !== undefined).map(([name, value]) => `<Parameter name="${escapeXml(name)}" value="${escapeXml(String(value))}" />`).join("");
9086
- 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>`;
11525
+ const parameters = Object.entries(options.parameters ?? {}).filter((entry) => entry[1] !== undefined).map(([name, value]) => `<Parameter name="${escapeXml2(name)}" value="${escapeXml2(String(value))}" />`).join("");
11526
+ 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>`;
9087
11527
  };
9088
11528
  var createTwilioMediaStreamBridge = (socket, options) => {
9089
11529
  const runtimePreset = resolveVoiceRuntimePreset(options.preset);
@@ -9319,15 +11759,22 @@ export {
9319
11759
  withVoiceOpsTaskId,
9320
11760
  withVoiceIntegrationEventId,
9321
11761
  voice,
11762
+ verifyVoiceOpsWebhookSignature,
9322
11763
  transcodeTwilioInboundPayloadToPCM16,
9323
11764
  transcodePCMToTwilioOutboundPayload,
9324
11765
  summarizeVoiceTraceSinkDeliveries,
9325
11766
  summarizeVoiceTrace,
11767
+ summarizeVoiceSessions,
11768
+ summarizeVoiceSessionReplay,
11769
+ summarizeVoiceProviderHealth,
9326
11770
  summarizeVoiceOpsTasks,
9327
11771
  summarizeVoiceOpsTaskQueue,
9328
11772
  summarizeVoiceOpsTaskAnalytics,
9329
11773
  summarizeVoiceIntegrationEvents,
11774
+ summarizeVoiceHandoffHealth,
11775
+ summarizeVoiceHandoffDeliveries,
9330
11776
  summarizeVoiceAssistantRuns,
11777
+ summarizeVoiceAssistantHealth,
9331
11778
  startVoiceOpsTask,
9332
11779
  shapeTelephonyAssistantText,
9333
11780
  selectVoiceTraceEventsForPrune,
@@ -9339,14 +11786,19 @@ export {
9339
11786
  resolveVoiceOpsTaskAssignment,
9340
11787
  resolveVoiceOpsTaskAgeBucket,
9341
11788
  resolveVoiceOpsPreset,
11789
+ resolveVoiceAssistantMemoryNamespace,
9342
11790
  resolveTurnDetectionConfig,
9343
11791
  resolveAudioConditioningConfig,
9344
11792
  requeueVoiceOpsTask,
9345
11793
  reopenVoiceOpsTask,
9346
11794
  renderVoiceTraceMarkdown,
9347
11795
  renderVoiceTraceHTML,
11796
+ renderVoiceSessionsHTML,
11797
+ renderVoiceProviderHealthHTML,
11798
+ renderVoiceHandoffHealthHTML,
9348
11799
  renderVoiceCallReviewMarkdown,
9349
11800
  renderVoiceCallReviewHTML,
11801
+ renderVoiceAssistantHealthHTML,
9350
11802
  redactVoiceTraceText,
9351
11803
  redactVoiceTraceEvents,
9352
11804
  redactVoiceTraceEvent,
@@ -9366,13 +11818,17 @@ export {
9366
11818
  deliverVoiceTraceEventsToSinks,
9367
11819
  deliverVoiceIntegrationEventToSinks,
9368
11820
  deliverVoiceIntegrationEvent,
11821
+ deliverVoiceHandoffDelivery,
11822
+ deliverVoiceHandoff,
9369
11823
  decodeTwilioMulawBase64,
9370
11824
  deadLetterVoiceOpsTask,
9371
11825
  createVoiceZendeskTicketUpdateSink,
9372
11826
  createVoiceZendeskTicketSyncSinks,
9373
11827
  createVoiceZendeskTicketSink,
11828
+ createVoiceWebhookHandoffAdapter,
9374
11829
  createVoiceWebhookDeliveryWorkerLoop,
9375
11830
  createVoiceWebhookDeliveryWorker,
11831
+ createVoiceTwilioRedirectHandoffAdapter,
9376
11832
  createVoiceTraceSinkStore,
9377
11833
  createVoiceTraceSinkDeliveryWorkerLoop,
9378
11834
  createVoiceTraceSinkDeliveryWorker,
@@ -9384,7 +11840,13 @@ export {
9384
11840
  createVoiceTaskUpdatedEvent,
9385
11841
  createVoiceTaskSLABreachedEvent,
9386
11842
  createVoiceTaskCreatedEvent,
11843
+ createVoiceSessionsJSONHandler,
11844
+ createVoiceSessionsHTMLHandler,
11845
+ createVoiceSessionReplayRoutes,
11846
+ createVoiceSessionReplayJSONHandler,
11847
+ createVoiceSessionReplayHTMLHandler,
9387
11848
  createVoiceSessionRecord,
11849
+ createVoiceSessionListRoutes,
9388
11850
  createVoiceSession,
9389
11851
  createVoiceSTTRoutingCorrectionHandler,
9390
11852
  createVoiceSQLiteTraceSinkDeliveryStore,
@@ -9399,6 +11861,10 @@ export {
9399
11861
  createVoiceReviewSavedEvent,
9400
11862
  createVoiceRedisTaskLeaseCoordinator,
9401
11863
  createVoiceRedisIdempotencyStore,
11864
+ createVoiceProviderRouter,
11865
+ createVoiceProviderHealthRoutes,
11866
+ createVoiceProviderHealthJSONHandler,
11867
+ createVoiceProviderHealthHTMLHandler,
9402
11868
  createVoicePostgresTraceSinkDeliveryStore,
9403
11869
  createVoicePostgresTraceEventStore,
9404
11870
  createVoicePostgresTaskStore,
@@ -9407,6 +11873,9 @@ export {
9407
11873
  createVoicePostgresReviewStore,
9408
11874
  createVoicePostgresIntegrationEventStore,
9409
11875
  createVoicePostgresExternalObjectMapStore,
11876
+ createVoiceOpsWebhookSink,
11877
+ createVoiceOpsWebhookReceiverRoutes,
11878
+ createVoiceOpsWebhookEnvelope,
9410
11879
  createVoiceOpsTaskWorker,
9411
11880
  createVoiceOpsTaskProcessorWorkerLoop,
9412
11881
  createVoiceOpsTaskProcessorWorker,
@@ -9414,6 +11883,8 @@ export {
9414
11883
  createVoiceMemoryTraceSinkDeliveryStore,
9415
11884
  createVoiceMemoryTraceEventStore,
9416
11885
  createVoiceMemoryStore,
11886
+ createVoiceMemoryHandoffDeliveryStore,
11887
+ createVoiceMemoryAssistantMemoryStore,
9417
11888
  createVoiceLinearIssueUpdateSink,
9418
11889
  createVoiceLinearIssueSyncSinks,
9419
11890
  createVoiceLinearIssueSink,
@@ -9425,6 +11896,12 @@ export {
9425
11896
  createVoiceHubSpotTaskSyncSinks,
9426
11897
  createVoiceHubSpotTaskSink,
9427
11898
  createVoiceHelpdeskTicketSink,
11899
+ createVoiceHandoffHealthRoutes,
11900
+ createVoiceHandoffHealthJSONHandler,
11901
+ createVoiceHandoffHealthHTMLHandler,
11902
+ createVoiceHandoffDeliveryWorkerLoop,
11903
+ createVoiceHandoffDeliveryWorker,
11904
+ createVoiceHandoffDeliveryRecord,
9428
11905
  createVoiceFileTraceSinkDeliveryStore,
9429
11906
  createVoiceFileTraceEventStore,
9430
11907
  createVoiceFileTaskStore,
@@ -9433,6 +11910,7 @@ export {
9433
11910
  createVoiceFileReviewStore,
9434
11911
  createVoiceFileIntegrationEventStore,
9435
11912
  createVoiceFileExternalObjectMapStore,
11913
+ createVoiceFileAssistantMemoryStore,
9436
11914
  createVoiceExternalObjectMapId,
9437
11915
  createVoiceExternalObjectMap,
9438
11916
  createVoiceExperiment,
@@ -9441,6 +11919,11 @@ export {
9441
11919
  createVoiceCallReviewFromLiveTelephonyReport,
9442
11920
  createVoiceCallCompletedEvent,
9443
11921
  createVoiceCRMActivitySink,
11922
+ createVoiceAssistantMemoryRecord,
11923
+ createVoiceAssistantMemoryHandle,
11924
+ createVoiceAssistantHealthRoutes,
11925
+ createVoiceAssistantHealthJSONHandler,
11926
+ createVoiceAssistantHealthHTMLHandler,
9444
11927
  createVoiceAssistant,
9445
11928
  createVoiceAgentTool,
9446
11929
  createVoiceAgentSquad,
@@ -9453,9 +11936,13 @@ export {
9453
11936
  createStoredVoiceCallReviewArtifact,
9454
11937
  createRiskyTurnCorrectionHandler,
9455
11938
  createPhraseHintCorrectionHandler,
11939
+ createOpenAIVoiceAssistantModel,
11940
+ createJSONVoiceAssistantModel,
9456
11941
  createId,
11942
+ createGeminiVoiceAssistantModel,
9457
11943
  createDomainPhraseHints,
9458
11944
  createDomainLexicon,
11945
+ createAnthropicVoiceAssistantModel,
9459
11946
  conditionAudioChunk,
9460
11947
  completeVoiceOpsTask,
9461
11948
  claimVoiceOpsTask,
@@ -9465,6 +11952,7 @@ export {
9465
11952
  assignVoiceOpsTask,
9466
11953
  applyVoiceOpsTaskPolicy,
9467
11954
  applyVoiceOpsTaskAssignmentRule,
11955
+ applyVoiceHandoffDeliveryResult,
9468
11956
  applyRiskTieredPhraseHintCorrections,
9469
11957
  applyPhraseHintCorrections,
9470
11958
  TURN_PROFILE_DEFAULTS