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

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 (40) 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 +40 -0
  16. package/dist/handoffHealth.d.ts +94 -0
  17. package/dist/index.d.ts +18 -2
  18. package/dist/index.js +2379 -138
  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/react/index.d.ts +1 -0
  23. package/dist/react/index.js +148 -2
  24. package/dist/react/useVoiceController.d.ts +2 -0
  25. package/dist/react/useVoiceProviderStatus.d.ts +8 -0
  26. package/dist/react/useVoiceStream.d.ts +2 -0
  27. package/dist/sessionReplay.d.ts +175 -0
  28. package/dist/svelte/createVoiceProviderStatus.d.ts +8 -0
  29. package/dist/svelte/index.d.ts +1 -0
  30. package/dist/svelte/index.js +127 -2
  31. package/dist/testing/index.d.ts +1 -0
  32. package/dist/testing/index.js +1216 -7
  33. package/dist/testing/providerSimulator.d.ts +44 -0
  34. package/dist/trace.d.ts +1 -1
  35. package/dist/types.d.ts +54 -2
  36. package/dist/vue/index.d.ts +1 -0
  37. package/dist/vue/index.js +161 -2
  38. package/dist/vue/useVoiceProviderStatus.d.ts +9 -0
  39. package/dist/vue/useVoiceStream.d.ts +2 -0
  40. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -2992,6 +2992,214 @@ 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 defaultWebhookBody = (input) => ({
3023
+ action: input.action,
3024
+ metadata: input.metadata,
3025
+ reason: input.reason,
3026
+ result: input.result,
3027
+ session: {
3028
+ id: input.session.id,
3029
+ scenarioId: input.session.scenarioId,
3030
+ status: input.session.status
3031
+ },
3032
+ source: "absolutejs-voice",
3033
+ target: input.target
3034
+ });
3035
+ var deliverVoiceHandoff = async (input) => {
3036
+ if (!input.config || input.config.adapters.length === 0) {
3037
+ return;
3038
+ }
3039
+ const deliveries = {};
3040
+ for (const adapter of input.config.adapters) {
3041
+ if (adapter.actions && !adapter.actions.includes(input.handoff.action)) {
3042
+ deliveries[adapter.id] = createSkippedDelivery(adapter);
3043
+ continue;
3044
+ }
3045
+ try {
3046
+ const result = await adapter.handoff(input.handoff);
3047
+ deliveries[adapter.id] = {
3048
+ ...result,
3049
+ adapterId: adapter.id,
3050
+ adapterKind: adapter.kind
3051
+ };
3052
+ } catch (error) {
3053
+ deliveries[adapter.id] = {
3054
+ adapterId: adapter.id,
3055
+ adapterKind: adapter.kind,
3056
+ error: toErrorMessage2(error),
3057
+ status: "failed"
3058
+ };
3059
+ if (input.config.failMode === "throw") {
3060
+ throw error;
3061
+ }
3062
+ }
3063
+ }
3064
+ return {
3065
+ action: input.handoff.action,
3066
+ deliveries,
3067
+ status: aggregateHandoffStatus(deliveries)
3068
+ };
3069
+ };
3070
+ var createVoiceWebhookHandoffAdapter = (options) => ({
3071
+ actions: options.actions,
3072
+ handoff: async (input) => {
3073
+ const fetchImpl = options.fetch ?? globalThis.fetch;
3074
+ if (typeof fetchImpl !== "function") {
3075
+ return {
3076
+ deliveredTo: options.url,
3077
+ error: "Handoff delivery failed: fetch is not available in this runtime.",
3078
+ status: "failed"
3079
+ };
3080
+ }
3081
+ const body = JSON.stringify(await options.body?.(input) ?? defaultWebhookBody(input));
3082
+ const headers = {
3083
+ "content-type": "application/json",
3084
+ ...options.headers
3085
+ };
3086
+ if (options.signingSecret) {
3087
+ const timestamp = String(Date.now());
3088
+ headers["x-absolutejs-timestamp"] = timestamp;
3089
+ headers["x-absolutejs-signature"] = await signHandoffBody({
3090
+ body,
3091
+ secret: options.signingSecret,
3092
+ timestamp
3093
+ });
3094
+ }
3095
+ const controller = options.timeoutMs && options.timeoutMs > 0 ? new AbortController : undefined;
3096
+ const timeout = controller && options.timeoutMs ? setTimeout(() => controller.abort(), options.timeoutMs) : undefined;
3097
+ try {
3098
+ const response = await fetchImpl(options.url, {
3099
+ body,
3100
+ headers,
3101
+ method: options.method ?? "POST",
3102
+ signal: controller?.signal
3103
+ });
3104
+ if (!response.ok) {
3105
+ return {
3106
+ deliveredTo: options.url,
3107
+ error: `Handoff delivery failed with response ${response.status}.`,
3108
+ status: "failed"
3109
+ };
3110
+ }
3111
+ return {
3112
+ deliveredAt: Date.now(),
3113
+ deliveredTo: options.url,
3114
+ status: "delivered"
3115
+ };
3116
+ } finally {
3117
+ if (timeout) {
3118
+ clearTimeout(timeout);
3119
+ }
3120
+ }
3121
+ },
3122
+ id: options.id,
3123
+ kind: options.kind ?? "webhook"
3124
+ });
3125
+ var escapeXml = (value) => value.replaceAll("&", "&amp;").replaceAll('"', "&quot;").replaceAll("'", "&apos;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
3126
+ var defaultTwilioTransferTwiML = (input) => {
3127
+ if (!input.target) {
3128
+ return "<Response><Hangup /></Response>";
3129
+ }
3130
+ return `<Response><Dial>${escapeXml(input.target)}</Dial></Response>`;
3131
+ };
3132
+ var resolveTwilioCallSid = async (resolver, input) => {
3133
+ if (typeof resolver === "function") {
3134
+ return resolver(input);
3135
+ }
3136
+ if (typeof resolver === "string" && resolver.length > 0) {
3137
+ return resolver;
3138
+ }
3139
+ const metadataSid = typeof input.metadata?.callSid === "string" ? input.metadata.callSid : undefined;
3140
+ const sessionMetadata = input.session.metadata && typeof input.session.metadata === "object" ? input.session.metadata : undefined;
3141
+ const sessionSid = typeof sessionMetadata?.callSid === "string" ? sessionMetadata.callSid : undefined;
3142
+ return metadataSid ?? sessionSid;
3143
+ };
3144
+ var createVoiceTwilioRedirectHandoffAdapter = (options) => ({
3145
+ actions: options.actions ?? ["transfer"],
3146
+ handoff: async (input) => {
3147
+ const fetchImpl = options.fetch ?? globalThis.fetch;
3148
+ const callSid = await resolveTwilioCallSid(options.callSid, input);
3149
+ if (!callSid) {
3150
+ return {
3151
+ error: "Twilio handoff requires a callSid.",
3152
+ status: "failed"
3153
+ };
3154
+ }
3155
+ if (typeof fetchImpl !== "function") {
3156
+ return {
3157
+ error: "Twilio handoff failed: fetch is not available in this runtime.",
3158
+ status: "failed"
3159
+ };
3160
+ }
3161
+ const url = `https://api.twilio.com/2010-04-01/Accounts/${encodeURIComponent(options.accountSid)}/Calls/${encodeURIComponent(callSid)}.json`;
3162
+ const body = new URLSearchParams({
3163
+ Twiml: await (options.buildTwiML?.(input) ?? defaultTwilioTransferTwiML(input))
3164
+ });
3165
+ const auth = btoa(`${options.accountSid}:${options.authToken}`);
3166
+ const controller = options.timeoutMs && options.timeoutMs > 0 ? new AbortController : undefined;
3167
+ const timeout = controller && options.timeoutMs ? setTimeout(() => controller.abort(), options.timeoutMs) : undefined;
3168
+ try {
3169
+ const response = await fetchImpl(url, {
3170
+ body,
3171
+ headers: {
3172
+ authorization: `Basic ${auth}`,
3173
+ "content-type": "application/x-www-form-urlencoded"
3174
+ },
3175
+ method: "POST",
3176
+ signal: controller?.signal
3177
+ });
3178
+ if (!response.ok) {
3179
+ return {
3180
+ deliveredTo: url,
3181
+ error: `Twilio handoff failed with response ${response.status}.`,
3182
+ status: "failed"
3183
+ };
3184
+ }
3185
+ return {
3186
+ deliveredAt: Date.now(),
3187
+ deliveredTo: url,
3188
+ metadata: {
3189
+ callSid
3190
+ },
3191
+ status: "delivered"
3192
+ };
3193
+ } finally {
3194
+ if (timeout) {
3195
+ clearTimeout(timeout);
3196
+ }
3197
+ }
3198
+ },
3199
+ id: options.id ?? "twilio-redirect",
3200
+ kind: "twilio-redirect"
3201
+ });
3202
+
2995
3203
  // src/turnDetection.ts
2996
3204
  var DEFAULT_SILENCE_MS = 700;
2997
3205
  var DEFAULT_SPEECH_THRESHOLD = 0.015;
@@ -3288,6 +3496,7 @@ var pushCallLifecycleEvent = (session, input) => {
3288
3496
  }
3289
3497
  return lifecycle;
3290
3498
  };
3499
+ var getLatestCallLifecycleEvent = (session) => session.call?.events.at(-1);
3291
3500
  var createVoiceSession = (options) => {
3292
3501
  const logger = resolveLogger(options.logger);
3293
3502
  const reconnect = {
@@ -3388,6 +3597,45 @@ var createVoiceSession = (options) => {
3388
3597
  });
3389
3598
  }
3390
3599
  };
3600
+ const sendCallLifecycle = async (session) => {
3601
+ const event = getLatestCallLifecycleEvent(session);
3602
+ if (!event) {
3603
+ return;
3604
+ }
3605
+ await send({
3606
+ event,
3607
+ sessionId: options.id,
3608
+ type: "call_lifecycle"
3609
+ });
3610
+ };
3611
+ const runHandoff = async (input) => {
3612
+ const result = await deliverVoiceHandoff({
3613
+ config: options.handoff,
3614
+ handoff: {
3615
+ action: input.action,
3616
+ api,
3617
+ context: options.context,
3618
+ metadata: input.metadata,
3619
+ reason: input.reason,
3620
+ result: input.result,
3621
+ session: input.session,
3622
+ target: input.target
3623
+ }
3624
+ });
3625
+ if (!result) {
3626
+ return;
3627
+ }
3628
+ await appendTrace({
3629
+ metadata: input.metadata,
3630
+ payload: {
3631
+ ...result,
3632
+ reason: input.reason,
3633
+ target: input.target
3634
+ },
3635
+ session: input.session,
3636
+ type: "call.handoff"
3637
+ });
3638
+ };
3391
3639
  const readSession = async () => options.store.getOrCreate(options.id);
3392
3640
  const writeSession = async (mutate) => {
3393
3641
  const session = await options.store.getOrCreate(options.id);
@@ -3578,6 +3826,7 @@ var createVoiceSession = (options) => {
3578
3826
  await appendTrace({
3579
3827
  payload: {
3580
3828
  disposition,
3829
+ metadata: input.metadata,
3581
3830
  reason: input.reason,
3582
3831
  target: input.target,
3583
3832
  type: "end"
@@ -3585,6 +3834,7 @@ var createVoiceSession = (options) => {
3585
3834
  session,
3586
3835
  type: "call.lifecycle"
3587
3836
  });
3837
+ await sendCallLifecycle(session);
3588
3838
  await send({
3589
3839
  sessionId: options.id,
3590
3840
  type: "complete"
@@ -3664,6 +3914,15 @@ var createVoiceSession = (options) => {
3664
3914
  session,
3665
3915
  type: "call.lifecycle"
3666
3916
  });
3917
+ await sendCallLifecycle(session);
3918
+ await runHandoff({
3919
+ action: "transfer",
3920
+ metadata: input.metadata,
3921
+ reason: input.reason,
3922
+ result: input.result,
3923
+ session,
3924
+ target: input.target
3925
+ });
3667
3926
  await completeInternal(input.result, {
3668
3927
  disposition: "transferred",
3669
3928
  invokeOnComplete: false,
@@ -3689,6 +3948,14 @@ var createVoiceSession = (options) => {
3689
3948
  session,
3690
3949
  type: "call.lifecycle"
3691
3950
  });
3951
+ await sendCallLifecycle(session);
3952
+ await runHandoff({
3953
+ action: "escalate",
3954
+ metadata: input.metadata,
3955
+ reason: input.reason,
3956
+ result: input.result,
3957
+ session
3958
+ });
3692
3959
  await completeInternal(input.result, {
3693
3960
  disposition: "escalated",
3694
3961
  invokeOnComplete: false,
@@ -3711,6 +3978,13 @@ var createVoiceSession = (options) => {
3711
3978
  session,
3712
3979
  type: "call.lifecycle"
3713
3980
  });
3981
+ await sendCallLifecycle(session);
3982
+ await runHandoff({
3983
+ action: "no-answer",
3984
+ metadata: input?.metadata,
3985
+ result: input?.result,
3986
+ session
3987
+ });
3714
3988
  await completeInternal(input?.result, {
3715
3989
  disposition: "no-answer",
3716
3990
  invokeOnComplete: false,
@@ -3732,6 +4006,13 @@ var createVoiceSession = (options) => {
3732
4006
  session,
3733
4007
  type: "call.lifecycle"
3734
4008
  });
4009
+ await sendCallLifecycle(session);
4010
+ await runHandoff({
4011
+ action: "voicemail",
4012
+ metadata: input?.metadata,
4013
+ result: input?.result,
4014
+ session
4015
+ });
3735
4016
  await completeInternal(input?.result, {
3736
4017
  disposition: "voicemail",
3737
4018
  invokeOnComplete: false,
@@ -4518,6 +4799,7 @@ var createVoiceSession = (options) => {
4518
4799
  session,
4519
4800
  type: "call.lifecycle"
4520
4801
  });
4802
+ await sendCallLifecycle(session);
4521
4803
  }
4522
4804
  await send({
4523
4805
  sessionId: options.id,
@@ -4755,6 +5037,14 @@ var isVoiceClientMessage = (value) => {
4755
5037
  return false;
4756
5038
  }
4757
5039
  switch (value.type) {
5040
+ case "call_control":
5041
+ if (!("action" in value)) {
5042
+ return false;
5043
+ }
5044
+ if (value.action !== "complete" && value.action !== "escalate" && value.action !== "no-answer" && value.action !== "transfer" && value.action !== "voicemail") {
5045
+ return false;
5046
+ }
5047
+ 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
5048
  case "close":
4759
5049
  return true;
4760
5050
  case "end_turn":
@@ -4895,6 +5185,7 @@ var voice = (config) => {
4895
5185
  audioConditioning: sessionOptions.audioConditioning,
4896
5186
  context,
4897
5187
  id: sessionId,
5188
+ handoff: config.handoff,
4898
5189
  languageStrategy: config.languageStrategy,
4899
5190
  lexicon,
4900
5191
  logger: sessionOptions.logger,
@@ -5006,6 +5297,42 @@ var voice = (config) => {
5006
5297
  await current.close(message.reason);
5007
5298
  runtime.activeSessions.delete(sessionState.sessionId);
5008
5299
  }
5300
+ if (message.type === "call_control" && current) {
5301
+ if (message.action === "transfer") {
5302
+ if (message.target) {
5303
+ await current.transfer({
5304
+ metadata: message.metadata,
5305
+ reason: message.reason,
5306
+ target: message.target
5307
+ });
5308
+ } else {
5309
+ ws.send(JSON.stringify({
5310
+ message: "call_control transfer requires target",
5311
+ recoverable: true,
5312
+ type: "error"
5313
+ }));
5314
+ }
5315
+ }
5316
+ if (message.action === "escalate") {
5317
+ await current.escalate({
5318
+ metadata: message.metadata,
5319
+ reason: message.reason ?? "client-requested-escalation"
5320
+ });
5321
+ }
5322
+ if (message.action === "voicemail") {
5323
+ await current.markVoicemail({
5324
+ metadata: message.metadata
5325
+ });
5326
+ }
5327
+ if (message.action === "no-answer") {
5328
+ await current.markNoAnswer({
5329
+ metadata: message.metadata
5330
+ });
5331
+ }
5332
+ if (message.action === "complete") {
5333
+ await current.complete();
5334
+ }
5335
+ }
5009
5336
  if (message.type === "start" && message.sessionId && message.sessionId !== sessionState.sessionId) {
5010
5337
  const currentSession = runtime.activeSessions.get(sessionState.sessionId);
5011
5338
  if (currentSession) {
@@ -5054,7 +5381,7 @@ var voice = (config) => {
5054
5381
  };
5055
5382
  // src/agent.ts
5056
5383
  var normalizeText3 = (value) => typeof value === "string" ? value.trim() : "";
5057
- var toErrorMessage2 = (error) => error instanceof Error ? error.message : String(error);
5384
+ var toErrorMessage3 = (error) => error instanceof Error ? error.message : String(error);
5058
5385
  var createHistoryMessages = (session, turn) => {
5059
5386
  const messages = [];
5060
5387
  for (const previousTurn of session.turns) {
@@ -5150,6 +5477,17 @@ var createVoiceAgent = (options) => {
5150
5477
  if (output.assistantText?.trim()) {
5151
5478
  messages.push({
5152
5479
  content: output.assistantText,
5480
+ metadata: output.toolCalls?.length ? {
5481
+ toolCalls: output.toolCalls
5482
+ } : undefined,
5483
+ role: "assistant"
5484
+ });
5485
+ } else if (output.toolCalls?.length) {
5486
+ messages.push({
5487
+ content: "",
5488
+ metadata: {
5489
+ toolCalls: output.toolCalls
5490
+ },
5153
5491
  role: "assistant"
5154
5492
  });
5155
5493
  }
@@ -5224,7 +5562,7 @@ var createVoiceAgent = (options) => {
5224
5562
  toolCallId: toolCall.id
5225
5563
  });
5226
5564
  } catch (error) {
5227
- const errorMessage = toErrorMessage2(error);
5565
+ const errorMessage = toErrorMessage3(error);
5228
5566
  toolResults.push({
5229
5567
  error: errorMessage,
5230
5568
  status: "error",
@@ -5594,6 +5932,112 @@ var resolveVoiceOutcomeRecipe = (name, options = {}) => {
5594
5932
  };
5595
5933
  };
5596
5934
 
5935
+ // src/assistantMemory.ts
5936
+ var createMemoryId = (input) => `${input.assistantId}:${input.namespace}:${input.key}`;
5937
+ var createVoiceAssistantMemoryRecord = (input) => {
5938
+ const now = Date.now();
5939
+ return {
5940
+ ...input,
5941
+ createdAt: input.createdAt ?? input.updatedAt ?? now,
5942
+ updatedAt: input.updatedAt ?? now
5943
+ };
5944
+ };
5945
+ var createVoiceMemoryAssistantMemoryStore = () => {
5946
+ const records = new Map;
5947
+ return {
5948
+ delete: async (input) => {
5949
+ records.delete(createMemoryId(input));
5950
+ },
5951
+ get: async (input) => records.get(createMemoryId(input)),
5952
+ 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),
5953
+ set: async (input) => {
5954
+ const id = createMemoryId(input);
5955
+ const existing = records.get(id);
5956
+ const record = createVoiceAssistantMemoryRecord({
5957
+ ...input,
5958
+ createdAt: input.createdAt ?? existing?.createdAt,
5959
+ updatedAt: input.updatedAt
5960
+ });
5961
+ records.set(id, record);
5962
+ return record;
5963
+ }
5964
+ };
5965
+ };
5966
+ var resolveVoiceAssistantMemoryNamespace = async (input) => typeof input.memory.namespace === "function" ? await input.memory.namespace(input) : input.memory.namespace;
5967
+ var createVoiceAssistantMemoryHandle = async (input) => {
5968
+ const namespace = await resolveVoiceAssistantMemoryNamespace({
5969
+ assistantId: input.assistantId,
5970
+ context: input.context,
5971
+ memory: input.memory,
5972
+ session: input.session
5973
+ });
5974
+ const trace = async (event) => {
5975
+ await input.trace?.append({
5976
+ at: Date.now(),
5977
+ payload: {
5978
+ assistantId: input.assistantId,
5979
+ namespace,
5980
+ ...event
5981
+ },
5982
+ scenarioId: input.session.scenarioId,
5983
+ sessionId: input.session.id,
5984
+ type: "assistant.memory"
5985
+ });
5986
+ };
5987
+ return {
5988
+ delete: async (key) => {
5989
+ await input.memory.store.delete({
5990
+ assistantId: input.assistantId,
5991
+ key,
5992
+ namespace
5993
+ });
5994
+ await trace({
5995
+ action: "delete",
5996
+ key
5997
+ });
5998
+ },
5999
+ get: async (key) => {
6000
+ const record = await input.memory.store.get({
6001
+ assistantId: input.assistantId,
6002
+ key,
6003
+ namespace
6004
+ });
6005
+ await trace({
6006
+ action: "get",
6007
+ found: Boolean(record),
6008
+ key
6009
+ });
6010
+ return record?.value;
6011
+ },
6012
+ list: async () => {
6013
+ const records = await input.memory.store.list({
6014
+ assistantId: input.assistantId,
6015
+ namespace
6016
+ });
6017
+ await trace({
6018
+ action: "list",
6019
+ count: records.length
6020
+ });
6021
+ return records;
6022
+ },
6023
+ namespace,
6024
+ set: async (key, value, metadata) => {
6025
+ const record = await input.memory.store.set({
6026
+ assistantId: input.assistantId,
6027
+ key,
6028
+ metadata,
6029
+ namespace,
6030
+ value
6031
+ });
6032
+ await trace({
6033
+ action: "set",
6034
+ key
6035
+ });
6036
+ return record;
6037
+ }
6038
+ };
6039
+ };
6040
+
5597
6041
  // src/assistant.ts
5598
6042
  var hashString = (value) => {
5599
6043
  let hash = 2166136261;
@@ -5742,12 +6186,35 @@ var createVoiceAssistant = (options) => {
5742
6186
  });
5743
6187
  }
5744
6188
  const onTurn = async (input) => {
6189
+ const memory = options.memory ? await createVoiceAssistantMemoryHandle({
6190
+ assistantId: options.id,
6191
+ context: input.context,
6192
+ memory: options.memory,
6193
+ session: input.session,
6194
+ trace: options.trace
6195
+ }) : undefined;
5745
6196
  const guardrailInput = {
5746
6197
  ...input,
5747
- assistantId: options.id
6198
+ assistantId: options.id,
6199
+ memory
5748
6200
  };
6201
+ if (memory) {
6202
+ await options.memoryLifecycle?.beforeTurn?.({
6203
+ ...input,
6204
+ assistantId: options.id,
6205
+ memory
6206
+ });
6207
+ }
5749
6208
  const blocked = await options.guardrails?.beforeTurn?.(guardrailInput);
5750
6209
  if (blocked) {
6210
+ if (memory) {
6211
+ await options.memoryLifecycle?.afterTurn?.({
6212
+ ...input,
6213
+ assistantId: options.id,
6214
+ memory,
6215
+ result: blocked
6216
+ });
6217
+ }
5751
6218
  await appendAssistantTrace({
5752
6219
  assistantId: options.id,
5753
6220
  event: {
@@ -5797,6 +6264,14 @@ var createVoiceAssistant = (options) => {
5797
6264
  result
5798
6265
  });
5799
6266
  const finalResult = guarded ?? result;
6267
+ if (memory) {
6268
+ await options.memoryLifecycle?.afterTurn?.({
6269
+ ...input,
6270
+ assistantId: options.id,
6271
+ memory,
6272
+ result: finalResult
6273
+ });
6274
+ }
5800
6275
  if (guarded) {
5801
6276
  await appendAssistantTrace({
5802
6277
  assistantId: options.id,
@@ -5864,6 +6339,12 @@ var summarizeVoiceAssistantRuns = async (input) => {
5864
6339
  escalationCount: 0,
5865
6340
  experiments: {},
5866
6341
  guardrailCount: 0,
6342
+ memory: {
6343
+ deletes: 0,
6344
+ gets: 0,
6345
+ lists: 0,
6346
+ sets: 0
6347
+ },
5867
6348
  outcomes: {},
5868
6349
  runCount: 0,
5869
6350
  sessionIds: new Set,
@@ -5919,6 +6400,24 @@ var summarizeVoiceAssistantRuns = async (input) => {
5919
6400
  const summary = getSummary(assistantId);
5920
6401
  summary.guardrailCount += 1;
5921
6402
  }
6403
+ for (const event of events.filter((event2) => event2.type === "assistant.memory")) {
6404
+ const assistantId = typeof event.payload.assistantId === "string" ? event.payload.assistantId : "unknown";
6405
+ const summary = getSummary(assistantId);
6406
+ switch (event.payload.action) {
6407
+ case "delete":
6408
+ summary.memory.deletes += 1;
6409
+ break;
6410
+ case "get":
6411
+ summary.memory.gets += 1;
6412
+ break;
6413
+ case "list":
6414
+ summary.memory.lists += 1;
6415
+ break;
6416
+ case "set":
6417
+ summary.memory.sets += 1;
6418
+ break;
6419
+ }
6420
+ }
5922
6421
  const assistants = [...byAssistant.values()].map(({ elapsedCount, elapsedTotal, sessionIds, ...summary }) => ({
5923
6422
  ...summary,
5924
6423
  averageElapsedMs: elapsedCount > 0 ? Math.round(elapsedTotal / elapsedCount) : undefined,
@@ -5929,38 +6428,331 @@ var summarizeVoiceAssistantRuns = async (input) => {
5929
6428
  totalRuns: assistantRuns.length
5930
6429
  };
5931
6430
  };
5932
- // src/fileStore.ts
5933
- import { mkdir, readFile, readdir, rename, rm, writeFile } from "fs/promises";
5934
- import { join } from "path";
6431
+ // src/assistantHealth.ts
6432
+ import { Elysia as Elysia3 } from "elysia";
5935
6433
 
5936
- // src/trace.ts
5937
- var createVoiceTraceEventId = (event) => [
5938
- event.sessionId,
5939
- event.turnId ?? "session",
5940
- event.type,
5941
- String(event.at ?? Date.now()),
5942
- crypto.randomUUID()
5943
- ].map(encodeURIComponent).join(":");
5944
- var createVoiceTraceEvent = (event) => ({
5945
- ...event,
5946
- at: event.at,
5947
- id: event.id ?? createVoiceTraceEventId({
5948
- at: event.at,
5949
- sessionId: event.sessionId,
5950
- turnId: event.turnId,
5951
- type: event.type
5952
- })
5953
- });
5954
- var createVoiceTraceSinkDeliveryId = (events) => {
5955
- const firstEvent = events[0];
5956
- return [
5957
- firstEvent?.sessionId ?? "trace",
5958
- firstEvent?.traceId ?? "sink",
5959
- String(firstEvent?.at ?? Date.now()),
5960
- crypto.randomUUID()
5961
- ].map(encodeURIComponent).join(":");
5962
- };
5963
- var createVoiceTraceSinkDeliveryRecord = (input) => {
6434
+ // src/providerHealth.ts
6435
+ import { Elysia as Elysia2 } from "elysia";
6436
+ var getString = (value) => typeof value === "string" ? value : undefined;
6437
+ var getNumber = (value) => typeof value === "number" && Number.isFinite(value) ? value : undefined;
6438
+ var isProviderStatus = (value) => value === "success" || value === "fallback" || value === "error";
6439
+ var summarizeVoiceProviderHealth = async (input) => {
6440
+ const options = Array.isArray(input) ? { events: input } : input;
6441
+ const events = options.events ?? await options.store?.list() ?? [];
6442
+ const providers = options.providers ?? [];
6443
+ const providerSet = new Set(providers);
6444
+ const now = options.now ?? Date.now();
6445
+ const entries = new Map;
6446
+ const isAllowedProvider = (value) => typeof value === "string" && (providerSet.size === 0 || providerSet.has(value));
6447
+ const getEntry = (provider) => {
6448
+ const existing = entries.get(provider);
6449
+ if (existing) {
6450
+ return existing;
6451
+ }
6452
+ const entry = {
6453
+ elapsedCount: 0,
6454
+ elapsedTotal: 0,
6455
+ errorCount: 0,
6456
+ fallbackCount: 0,
6457
+ provider,
6458
+ rateLimited: false,
6459
+ recommended: false,
6460
+ runCount: 0,
6461
+ status: "idle"
6462
+ };
6463
+ entries.set(provider, entry);
6464
+ return entry;
6465
+ };
6466
+ for (const provider of providers) {
6467
+ getEntry(provider);
6468
+ }
6469
+ const hasProviderRouterEvents = events.some((event) => event.type === "session.error" && isAllowedProvider(event.payload.provider) && isProviderStatus(event.payload.providerStatus));
6470
+ for (const event of events) {
6471
+ if (event.type === "assistant.run") {
6472
+ if (hasProviderRouterEvents) {
6473
+ continue;
6474
+ }
6475
+ const provider2 = event.payload.variantId;
6476
+ if (!isAllowedProvider(provider2)) {
6477
+ continue;
6478
+ }
6479
+ const entry2 = getEntry(provider2);
6480
+ entry2.runCount += 1;
6481
+ const elapsedMs = getNumber(event.payload.elapsedMs);
6482
+ if (elapsedMs !== undefined) {
6483
+ entry2.elapsedCount += 1;
6484
+ entry2.elapsedTotal += elapsedMs;
6485
+ }
6486
+ continue;
6487
+ }
6488
+ if (event.type !== "session.error") {
6489
+ continue;
6490
+ }
6491
+ const provider = event.payload.provider;
6492
+ if (!isAllowedProvider(provider)) {
6493
+ continue;
6494
+ }
6495
+ const providerStatus = isProviderStatus(event.payload.providerStatus) ? event.payload.providerStatus : undefined;
6496
+ const applyProviderHealth = () => {
6497
+ const entry2 = getEntry(provider);
6498
+ const providerHealth = event.payload.providerHealth;
6499
+ if (providerHealth && typeof providerHealth === "object") {
6500
+ const suppressedUntil2 = getNumber(providerHealth.suppressedUntil);
6501
+ if (suppressedUntil2 !== undefined) {
6502
+ entry2.suppressedUntil = suppressedUntil2;
6503
+ }
6504
+ }
6505
+ const suppressedUntil = getNumber(event.payload.suppressedUntil);
6506
+ if (suppressedUntil !== undefined) {
6507
+ entry2.suppressedUntil = suppressedUntil;
6508
+ }
6509
+ const suppressionRemainingMs = getNumber(event.payload.suppressionRemainingMs);
6510
+ if (suppressionRemainingMs !== undefined) {
6511
+ entry2.suppressionRemainingMs = suppressionRemainingMs;
6512
+ }
6513
+ return entry2;
6514
+ };
6515
+ if (providerStatus === "success" || providerStatus === "fallback") {
6516
+ const entry2 = applyProviderHealth();
6517
+ entry2.runCount += 1;
6518
+ entry2.lastSuccessAt = event.at;
6519
+ if (providerStatus === "success") {
6520
+ entry2.lastError = undefined;
6521
+ entry2.rateLimited = false;
6522
+ entry2.suppressedUntil = undefined;
6523
+ entry2.suppressionRemainingMs = undefined;
6524
+ }
6525
+ const elapsedMs = getNumber(event.payload.elapsedMs);
6526
+ if (elapsedMs !== undefined) {
6527
+ entry2.elapsedCount += 1;
6528
+ entry2.elapsedTotal += elapsedMs;
6529
+ }
6530
+ const selectedProvider = event.payload.selectedProvider;
6531
+ if (providerStatus === "fallback" && isAllowedProvider(selectedProvider) && selectedProvider !== provider) {
6532
+ getEntry(selectedProvider).fallbackCount += 1;
6533
+ }
6534
+ continue;
6535
+ }
6536
+ const entry = applyProviderHealth();
6537
+ entry.errorCount += 1;
6538
+ entry.lastError = getString(event.payload.error);
6539
+ entry.lastErrorAt = event.at;
6540
+ entry.rateLimited ||= event.payload.rateLimited === true;
6541
+ }
6542
+ const summaries = [...entries.values()].map((entry) => {
6543
+ const hadSuppression = typeof entry.suppressedUntil === "number" || typeof entry.suppressionRemainingMs === "number";
6544
+ const suppressionRemainingMs = typeof entry.suppressedUntil === "number" ? Math.max(0, entry.suppressedUntil - now) : entry.suppressionRemainingMs;
6545
+ const activeSuppression = typeof suppressionRemainingMs === "number" && suppressionRemainingMs > 0;
6546
+ const recoverable = hadSuppression && !activeSuppression;
6547
+ const averageElapsedMs = entry.elapsedCount > 0 ? Math.round(entry.elapsedTotal / entry.elapsedCount) : undefined;
6548
+ 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";
6549
+ return {
6550
+ averageElapsedMs,
6551
+ errorCount: entry.errorCount,
6552
+ fallbackCount: entry.fallbackCount,
6553
+ lastError: entry.lastError,
6554
+ lastErrorAt: entry.lastErrorAt,
6555
+ lastSuccessAt: entry.lastSuccessAt,
6556
+ provider: entry.provider,
6557
+ rateLimited: entry.rateLimited,
6558
+ recommended: false,
6559
+ runCount: entry.runCount,
6560
+ status,
6561
+ suppressionRemainingMs: activeSuppression ? suppressionRemainingMs : undefined,
6562
+ suppressedUntil: entry.suppressedUntil
6563
+ };
6564
+ });
6565
+ const recommended = summaries.filter((entry) => entry.status === "healthy").sort((left, right) => (left.averageElapsedMs ?? Number.MAX_SAFE_INTEGER) - (right.averageElapsedMs ?? Number.MAX_SAFE_INTEGER))[0];
6566
+ if (recommended) {
6567
+ recommended.recommended = true;
6568
+ }
6569
+ return summaries;
6570
+ };
6571
+ var escapeHtml3 = (value) => value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
6572
+ var renderVoiceProviderHealthHTML = (providers) => providers.length === 0 ? '<p class="voice-provider-empty">No provider status yet.</p>' : [
6573
+ '<div class="voice-provider-health">',
6574
+ ...providers.map((provider) => {
6575
+ const suppressionSeconds = typeof provider.suppressionRemainingMs === "number" ? Math.ceil(provider.suppressionRemainingMs / 1000) : undefined;
6576
+ return [
6577
+ `<article class="voice-provider-card ${escapeHtml3(provider.status)}">`,
6578
+ '<div class="voice-provider-card-header">',
6579
+ `<strong>${escapeHtml3(provider.provider)}</strong>`,
6580
+ `<span>${escapeHtml3(provider.status)}${provider.recommended ? " \xB7 recommended" : ""}</span>`,
6581
+ "</div>",
6582
+ "<dl>",
6583
+ `<div><dt>Runs</dt><dd>${String(provider.runCount)}</dd></div>`,
6584
+ `<div><dt>Avg latency</dt><dd>${String(provider.averageElapsedMs ?? 0)}ms</dd></div>`,
6585
+ `<div><dt>Errors</dt><dd>${String(provider.errorCount)}</dd></div>`,
6586
+ `<div><dt>Fallbacks</dt><dd>${String(provider.fallbackCount)}</dd></div>`,
6587
+ "</dl>",
6588
+ suppressionSeconds ? `<p>Temporarily suppressed for ${String(suppressionSeconds)}s.</p>` : "",
6589
+ provider.lastError ? `<p>${escapeHtml3(provider.lastError)}</p>` : "",
6590
+ "</article>"
6591
+ ].join("");
6592
+ }),
6593
+ "</div>"
6594
+ ].join("");
6595
+ var createVoiceProviderHealthJSONHandler = (options) => async () => summarizeVoiceProviderHealth(options);
6596
+ var createVoiceProviderHealthHTMLHandler = (options) => async () => {
6597
+ const providers = await summarizeVoiceProviderHealth(options);
6598
+ const render = options.render ?? renderVoiceProviderHealthHTML;
6599
+ const body = await render(providers);
6600
+ return new Response(body, {
6601
+ headers: {
6602
+ "Content-Type": "text/html; charset=utf-8",
6603
+ ...options.headers
6604
+ }
6605
+ });
6606
+ };
6607
+ var createVoiceProviderHealthRoutes = (options) => {
6608
+ const path = options.path ?? "/api/provider-status";
6609
+ const htmlPath = options.htmlPath === undefined ? `${path}/htmx` : options.htmlPath;
6610
+ const routes = new Elysia2({
6611
+ name: options.name ?? "absolutejs-voice-provider-health"
6612
+ }).get(path, createVoiceProviderHealthJSONHandler(options));
6613
+ if (htmlPath) {
6614
+ routes.get(htmlPath, createVoiceProviderHealthHTMLHandler(options));
6615
+ }
6616
+ return routes;
6617
+ };
6618
+
6619
+ // src/assistantHealth.ts
6620
+ var escapeHtml4 = (value) => value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
6621
+ var renderCountMap = (values) => {
6622
+ const entries = Object.entries(values).sort((left, right) => right[1] - left[1]);
6623
+ if (entries.length === 0) {
6624
+ return '<p class="voice-assistant-health-empty">No data yet.</p>';
6625
+ }
6626
+ return [
6627
+ '<div class="voice-assistant-health-metrics">',
6628
+ ...entries.map(([label, value]) => `<div><span>${escapeHtml4(label)}</span><strong>${String(value)}</strong></div>`),
6629
+ "</div>"
6630
+ ].join("");
6631
+ };
6632
+ var getString2 = (value) => typeof value === "string" ? value : undefined;
6633
+ 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) => {
6634
+ const failure = {
6635
+ at: event.at,
6636
+ assistantId: getString2(event.payload.assistantId),
6637
+ error: getString2(event.payload.error),
6638
+ provider: getString2(event.payload.provider),
6639
+ rateLimited: event.payload.rateLimited === true ? true : undefined,
6640
+ sessionId: event.sessionId,
6641
+ status: getString2(event.payload.providerStatus),
6642
+ turnId: event.turnId,
6643
+ type: event.type
6644
+ };
6645
+ const href = replayHref === false ? undefined : typeof replayHref === "function" ? replayHref(failure) : `${replayHref ?? "/api/voice-sessions"}/${encodeURIComponent(event.sessionId)}/replay/htmx`;
6646
+ return {
6647
+ ...failure,
6648
+ replayHref: href
6649
+ };
6650
+ });
6651
+ var summarizeVoiceAssistantHealth = async (options) => {
6652
+ const events = options.events ?? await options.store?.list() ?? [];
6653
+ return {
6654
+ assistantRuns: await summarizeVoiceAssistantRuns({ events }),
6655
+ providerHealth: await summarizeVoiceProviderHealth({
6656
+ events,
6657
+ providers: options.providers
6658
+ }),
6659
+ recentFailures: getRecentFailures(events, options.maxFailures ?? 8, options.replayHref)
6660
+ };
6661
+ };
6662
+ var renderVoiceAssistantHealthHTML = (summary) => {
6663
+ const assistant = summary.assistantRuns.assistants[0];
6664
+ const failures = summary.recentFailures;
6665
+ return [
6666
+ '<div class="voice-assistant-health">',
6667
+ '<section class="voice-assistant-health-grid">',
6668
+ `<article><span>Runs</span><strong>${String(assistant?.runCount ?? 0)}</strong></article>`,
6669
+ `<article><span>Sessions</span><strong>${String(assistant?.sessions ?? 0)}</strong></article>`,
6670
+ `<article><span>Guardrails</span><strong>${String(assistant?.guardrailCount ?? 0)}</strong></article>`,
6671
+ `<article><span>Avg latency</span><strong>${String(assistant?.averageElapsedMs ?? 0)}ms</strong></article>`,
6672
+ "</section>",
6673
+ "<section>",
6674
+ "<h3>Provider Health</h3>",
6675
+ renderVoiceProviderHealthHTML(summary.providerHealth),
6676
+ "</section>",
6677
+ '<section class="voice-assistant-health-columns">',
6678
+ `<article><h3>Outcomes</h3>${renderCountMap(assistant?.outcomes ?? {})}</article>`,
6679
+ `<article><h3>Variants</h3>${renderCountMap(assistant?.variants ?? {})}</article>`,
6680
+ `<article><h3>Tools</h3>${renderCountMap(assistant?.toolCalls ?? {})}</article>`,
6681
+ `<article><h3>Artifact Plans</h3>${renderCountMap(assistant?.artifactPlans ?? {})}</article>`,
6682
+ "</section>",
6683
+ "<section>",
6684
+ "<h3>Recent Failures</h3>",
6685
+ failures.length === 0 ? '<p class="voice-assistant-health-empty">No failures yet.</p>' : [
6686
+ '<div class="voice-assistant-health-failures">',
6687
+ ...failures.map((failure) => [
6688
+ "<article>",
6689
+ `<strong>${escapeHtml4(failure.provider ?? failure.assistantId ?? failure.type)}</strong>`,
6690
+ `<span>${escapeHtml4(failure.status ?? (failure.rateLimited ? "rate-limited" : "error"))}</span>`,
6691
+ failure.error ? `<p>${escapeHtml4(failure.error)}</p>` : "",
6692
+ `<small>${escapeHtml4(failure.sessionId)}${failure.turnId ? ` / ${escapeHtml4(failure.turnId)}` : ""}</small>`,
6693
+ failure.replayHref ? `<p><a href="${escapeHtml4(failure.replayHref)}">Open replay</a></p>` : "",
6694
+ "</article>"
6695
+ ].join("")),
6696
+ "</div>"
6697
+ ].join(""),
6698
+ "</section>",
6699
+ "</div>"
6700
+ ].join("");
6701
+ };
6702
+ var createVoiceAssistantHealthJSONHandler = (options) => async () => summarizeVoiceAssistantHealth(options);
6703
+ var createVoiceAssistantHealthHTMLHandler = (options) => async () => {
6704
+ const summary = await summarizeVoiceAssistantHealth(options);
6705
+ const render = options.render ?? renderVoiceAssistantHealthHTML;
6706
+ const body = await render(summary);
6707
+ return new Response(body, {
6708
+ headers: {
6709
+ "Content-Type": "text/html; charset=utf-8",
6710
+ ...options.headers
6711
+ }
6712
+ });
6713
+ };
6714
+ var createVoiceAssistantHealthRoutes = (options) => {
6715
+ const path = options.path ?? "/api/assistant-health";
6716
+ const htmlPath = options.htmlPath === undefined ? `${path}/htmx` : options.htmlPath;
6717
+ const routes = new Elysia3({
6718
+ name: options.name ?? "absolutejs-voice-assistant-health"
6719
+ }).get(path, createVoiceAssistantHealthJSONHandler(options));
6720
+ if (htmlPath) {
6721
+ routes.get(htmlPath, createVoiceAssistantHealthHTMLHandler(options));
6722
+ }
6723
+ return routes;
6724
+ };
6725
+ // src/sessionReplay.ts
6726
+ import { Elysia as Elysia4 } from "elysia";
6727
+
6728
+ // src/trace.ts
6729
+ var createVoiceTraceEventId = (event) => [
6730
+ event.sessionId,
6731
+ event.turnId ?? "session",
6732
+ event.type,
6733
+ String(event.at ?? Date.now()),
6734
+ crypto.randomUUID()
6735
+ ].map(encodeURIComponent).join(":");
6736
+ var createVoiceTraceEvent = (event) => ({
6737
+ ...event,
6738
+ at: event.at,
6739
+ id: event.id ?? createVoiceTraceEventId({
6740
+ at: event.at,
6741
+ sessionId: event.sessionId,
6742
+ turnId: event.turnId,
6743
+ type: event.type
6744
+ })
6745
+ });
6746
+ var createVoiceTraceSinkDeliveryId = (events) => {
6747
+ const firstEvent = events[0];
6748
+ return [
6749
+ firstEvent?.sessionId ?? "trace",
6750
+ firstEvent?.traceId ?? "sink",
6751
+ String(firstEvent?.at ?? Date.now()),
6752
+ crypto.randomUUID()
6753
+ ].map(encodeURIComponent).join(":");
6754
+ };
6755
+ var createVoiceTraceSinkDeliveryRecord = (input) => {
5964
6756
  const createdAt = input.createdAt ?? Date.now();
5965
6757
  return {
5966
6758
  createdAt,
@@ -6035,7 +6827,7 @@ var sleep3 = async (delayMs) => {
6035
6827
  }
6036
6828
  await new Promise((resolve2) => setTimeout(resolve2, delayMs));
6037
6829
  };
6038
- var toHex3 = (bytes) => Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
6830
+ var toHex4 = (bytes) => Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
6039
6831
  var signVoiceTraceSinkBody = async (input) => {
6040
6832
  const encoder = new TextEncoder;
6041
6833
  const key = await crypto.subtle.importKey("raw", encoder.encode(input.secret), {
@@ -6044,7 +6836,7 @@ var signVoiceTraceSinkBody = async (input) => {
6044
6836
  }, false, ["sign"]);
6045
6837
  const payload = encoder.encode(`${input.timestamp}.${input.body}`);
6046
6838
  const signature = await crypto.subtle.sign("HMAC", key, payload);
6047
- return `sha256=${toHex3(new Uint8Array(signature))}`;
6839
+ return `sha256=${toHex4(new Uint8Array(signature))}`;
6048
6840
  };
6049
6841
  var createVoiceTraceSinkDeliveryError = (input) => {
6050
6842
  if (input.response) {
@@ -6265,7 +7057,7 @@ var exportVoiceTrace = async (input) => {
6265
7057
  };
6266
7058
  };
6267
7059
  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;");
7060
+ var escapeHtml5 = (value) => value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
6269
7061
  var formatTraceValue = (value) => {
6270
7062
  if (value === undefined || value === null) {
6271
7063
  return "";
@@ -6543,10 +7335,10 @@ var renderVoiceTraceHTML = (events, options = {}) => {
6543
7335
  const offset = summary.startedAt === undefined ? event.at : Math.max(0, event.at - summary.startedAt);
6544
7336
  return [
6545
7337
  "<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>`,
7338
+ `<td>${escapeHtml5(String(offset))}</td>`,
7339
+ `<td>${escapeHtml5(event.type)}</td>`,
7340
+ `<td>${escapeHtml5(event.turnId ?? "")}</td>`,
7341
+ `<td><code>${escapeHtml5(JSON.stringify(event.payload))}</code></td>`,
6550
7342
  "</tr>"
6551
7343
  ].join("");
6552
7344
  }).join(`
@@ -6557,7 +7349,7 @@ var renderVoiceTraceHTML = (events, options = {}) => {
6557
7349
  "<head>",
6558
7350
  '<meta charset="utf-8" />',
6559
7351
  '<meta name="viewport" content="width=device-width, initial-scale=1" />',
6560
- `<title>${escapeHtml3(options.title ?? "Voice Trace")}</title>`,
7352
+ `<title>${escapeHtml5(options.title ?? "Voice Trace")}</title>`,
6561
7353
  "<style>",
6562
7354
  "body{font-family:ui-sans-serif,system-ui,sans-serif;margin:2rem;line-height:1.45;background:#f8f7f2;color:#181713}",
6563
7355
  "main{max-width:1100px;margin:auto}",
@@ -6571,7 +7363,7 @@ var renderVoiceTraceHTML = (events, options = {}) => {
6571
7363
  "</style>",
6572
7364
  "</head>",
6573
7365
  "<body><main>",
6574
- `<h1>${escapeHtml3(options.title ?? `Voice Trace ${summary.sessionId ?? ""}`.trim())}</h1>`,
7366
+ `<h1>${escapeHtml5(options.title ?? `Voice Trace ${summary.sessionId ?? ""}`.trim())}</h1>`,
6575
7367
  `<p class="${evaluation.pass ? "pass" : "fail"}">QA: ${evaluation.pass ? "pass" : "fail"}</p>`,
6576
7368
  '<section class="summary">',
6577
7369
  `<div class="card"><strong>Events</strong><br>${summary.eventCount}</div>`,
@@ -6585,7 +7377,7 @@ var renderVoiceTraceHTML = (events, options = {}) => {
6585
7377
  eventRows,
6586
7378
  "</tbody></table>",
6587
7379
  "<h2>Markdown Export</h2>",
6588
- `<pre>${escapeHtml3(markdown)}</pre>`,
7380
+ `<pre>${escapeHtml5(markdown)}</pre>`,
6589
7381
  "</main></body></html>"
6590
7382
  ].join(`
6591
7383
  `);
@@ -6597,7 +7389,250 @@ var buildVoiceTraceReplay = (events, options = {}) => ({
6597
7389
  summary: summarizeVoiceTrace(options.redact ? redactVoiceTraceEvents(events, options.redact) : events)
6598
7390
  });
6599
7391
 
7392
+ // src/sessionReplay.ts
7393
+ var getString3 = (value) => typeof value === "string" ? value : undefined;
7394
+ var escapeHtml6 = (value) => value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
7395
+ var increment2 = (record, key) => {
7396
+ record[key] = (record[key] ?? 0) + 1;
7397
+ };
7398
+ var buildReplayTurns = (events) => {
7399
+ const turns = new Map;
7400
+ const getTurn = (turnId) => {
7401
+ const existing = turns.get(turnId);
7402
+ if (existing) {
7403
+ return existing;
7404
+ }
7405
+ const turn = {
7406
+ assistantReplies: [],
7407
+ errors: [],
7408
+ id: turnId,
7409
+ modelCalls: [],
7410
+ tools: [],
7411
+ transcripts: []
7412
+ };
7413
+ turns.set(turnId, turn);
7414
+ return turn;
7415
+ };
7416
+ for (const event of events) {
7417
+ const turnId = event.turnId ?? "session";
7418
+ const turn = getTurn(turnId);
7419
+ switch (event.type) {
7420
+ case "turn.transcript":
7421
+ turn.transcripts.push({
7422
+ isFinal: event.payload.isFinal === true,
7423
+ text: getString3(event.payload.text)
7424
+ });
7425
+ break;
7426
+ case "turn.committed":
7427
+ turn.committedText = getString3(event.payload.text);
7428
+ break;
7429
+ case "turn.assistant": {
7430
+ const text = getString3(event.payload.text);
7431
+ if (text) {
7432
+ turn.assistantReplies.push(text);
7433
+ }
7434
+ break;
7435
+ }
7436
+ case "agent.model":
7437
+ case "assistant.run":
7438
+ turn.modelCalls.push(event.payload);
7439
+ break;
7440
+ case "agent.tool":
7441
+ turn.tools.push(event.payload);
7442
+ break;
7443
+ case "session.error":
7444
+ turn.errors.push(event.payload);
7445
+ break;
7446
+ }
7447
+ }
7448
+ return [...turns.values()];
7449
+ };
7450
+ var summarizeVoiceSessionReplay = async (options) => {
7451
+ const sourceEvents = options.events ?? await options.store?.list({ sessionId: options.sessionId }) ?? [];
7452
+ const events = filterVoiceTraceEvents(sourceEvents, {
7453
+ sessionId: options.sessionId
7454
+ });
7455
+ const replay = buildVoiceTraceReplay(events, {
7456
+ evaluation: options.evaluation,
7457
+ redact: options.redact,
7458
+ title: options.title ?? `Voice Session ${options.sessionId}`
7459
+ });
7460
+ const startedAt = replay.summary.startedAt;
7461
+ return {
7462
+ evaluation: replay.evaluation,
7463
+ events,
7464
+ html: replay.html,
7465
+ markdown: replay.markdown,
7466
+ sessionId: options.sessionId,
7467
+ summary: replay.summary,
7468
+ timeline: events.map((event) => ({
7469
+ at: event.at,
7470
+ offsetMs: startedAt === undefined ? undefined : Math.max(0, event.at - startedAt),
7471
+ payload: event.payload,
7472
+ turnId: event.turnId,
7473
+ type: event.type
7474
+ })),
7475
+ turns: buildReplayTurns(events)
7476
+ };
7477
+ };
7478
+ var summarizeVoiceSessions = async (options = {}) => {
7479
+ const events = options.events ?? await options.store?.list() ?? [];
7480
+ const grouped = new Map;
7481
+ for (const event of events) {
7482
+ grouped.set(event.sessionId, [...grouped.get(event.sessionId) ?? [], event]);
7483
+ }
7484
+ const sessions = [...grouped.entries()].map(([sessionId, sessionEvents]) => {
7485
+ const sorted = filterVoiceTraceEvents(sessionEvents);
7486
+ const summary = buildVoiceTraceReplay(sorted, {
7487
+ evaluation: {
7488
+ requireAssistantReply: false,
7489
+ requireCompletedCall: false,
7490
+ requireTranscript: false,
7491
+ requireTurn: false
7492
+ }
7493
+ }).summary;
7494
+ const providerErrors = {};
7495
+ const providers = new Set;
7496
+ let latestOutcome;
7497
+ let errorCount = 0;
7498
+ for (const event of sorted) {
7499
+ const provider = getString3(event.payload.provider);
7500
+ if (provider) {
7501
+ providers.add(provider);
7502
+ }
7503
+ if (event.type === "session.error" && (event.payload.providerStatus === "error" || typeof event.payload.error === "string")) {
7504
+ errorCount += 1;
7505
+ increment2(providerErrors, provider ?? "unknown");
7506
+ }
7507
+ const outcome = getString3(event.payload.outcome);
7508
+ if (outcome) {
7509
+ latestOutcome = outcome;
7510
+ }
7511
+ }
7512
+ const item = {
7513
+ endedAt: summary.endedAt,
7514
+ errorCount,
7515
+ eventCount: summary.eventCount,
7516
+ latestOutcome,
7517
+ providerErrors,
7518
+ providers: [...providers].sort(),
7519
+ sessionId,
7520
+ startedAt: summary.startedAt,
7521
+ status: errorCount > 0 ? "failed" : "healthy",
7522
+ transcriptCount: summary.transcriptCount,
7523
+ turnCount: summary.turnCount
7524
+ };
7525
+ const replayHref = options.replayHref === false ? "" : typeof options.replayHref === "function" ? options.replayHref(item) : `${options.replayHref ?? "/api/voice-sessions"}/${encodeURIComponent(sessionId)}/replay/htmx`;
7526
+ return {
7527
+ ...item,
7528
+ replayHref
7529
+ };
7530
+ });
7531
+ const search = options.q?.trim().toLowerCase();
7532
+ return sessions.filter((session) => {
7533
+ if (options.status && options.status !== "all" && session.status !== options.status) {
7534
+ return false;
7535
+ }
7536
+ if (options.provider && !session.providers.includes(options.provider)) {
7537
+ return false;
7538
+ }
7539
+ if (!search) {
7540
+ return true;
7541
+ }
7542
+ return [
7543
+ session.sessionId,
7544
+ session.latestOutcome,
7545
+ session.status,
7546
+ ...session.providers
7547
+ ].some((value) => value?.toLowerCase().includes(search));
7548
+ }).sort((left, right) => (right.endedAt ?? right.startedAt ?? 0) - (left.endedAt ?? left.startedAt ?? 0)).slice(0, options.limit ?? 50);
7549
+ };
7550
+ var renderVoiceSessionsHTML = (sessions) => sessions.length === 0 ? '<p class="voice-sessions-empty">No voice sessions found.</p>' : [
7551
+ '<div class="voice-sessions-list">',
7552
+ ...sessions.map((session) => [
7553
+ `<article class="voice-session-card ${escapeHtml6(session.status)}">`,
7554
+ '<div class="voice-session-card-header">',
7555
+ `<strong>${escapeHtml6(session.sessionId)}</strong>`,
7556
+ `<span>${escapeHtml6(session.status)}</span>`,
7557
+ "</div>",
7558
+ "<dl>",
7559
+ `<div><dt>Events</dt><dd>${String(session.eventCount)}</dd></div>`,
7560
+ `<div><dt>Turns</dt><dd>${String(session.turnCount)}</dd></div>`,
7561
+ `<div><dt>Transcripts</dt><dd>${String(session.transcriptCount)}</dd></div>`,
7562
+ `<div><dt>Errors</dt><dd>${String(session.errorCount)}</dd></div>`,
7563
+ "</dl>",
7564
+ session.latestOutcome ? `<p>Outcome: ${escapeHtml6(session.latestOutcome)}</p>` : "",
7565
+ session.providers.length ? `<p>Providers: ${session.providers.map(escapeHtml6).join(", ")}</p>` : "",
7566
+ session.replayHref ? `<p><a href="${escapeHtml6(session.replayHref)}">Open replay</a></p>` : "",
7567
+ "</article>"
7568
+ ].join("")),
7569
+ "</div>"
7570
+ ].join("");
7571
+ var createVoiceSessionsJSONHandler = (options = {}) => async ({ query }) => summarizeVoiceSessions({
7572
+ ...options,
7573
+ limit: typeof query?.limit === "string" ? Number(query.limit) : options.limit,
7574
+ provider: query?.provider ?? options.provider,
7575
+ q: query?.q ?? options.q,
7576
+ status: query?.status === "failed" || query?.status === "healthy" || query?.status === "all" ? query.status : options.status
7577
+ });
7578
+ var createVoiceSessionsHTMLHandler = (options = {}) => async ({ query }) => {
7579
+ const sessions = await summarizeVoiceSessions({
7580
+ ...options,
7581
+ limit: typeof query?.limit === "string" ? Number(query.limit) : options.limit,
7582
+ provider: query?.provider ?? options.provider,
7583
+ q: query?.q ?? options.q,
7584
+ status: query?.status === "failed" || query?.status === "healthy" || query?.status === "all" ? query.status : options.status
7585
+ });
7586
+ const body = await (options.render?.(sessions) ?? renderVoiceSessionsHTML(sessions));
7587
+ return new Response(body, {
7588
+ headers: {
7589
+ "Content-Type": "text/html; charset=utf-8",
7590
+ ...options.headers
7591
+ }
7592
+ });
7593
+ };
7594
+ var createVoiceSessionListRoutes = (options = {}) => {
7595
+ const path = options.path ?? "/api/voice-sessions";
7596
+ const htmlPath = options.htmlPath === undefined ? `${path}/htmx` : options.htmlPath;
7597
+ const routes = new Elysia4({
7598
+ name: options.name ?? "absolutejs-voice-session-list"
7599
+ }).get(path, createVoiceSessionsJSONHandler(options));
7600
+ if (htmlPath) {
7601
+ routes.get(htmlPath, createVoiceSessionsHTMLHandler(options));
7602
+ }
7603
+ return routes;
7604
+ };
7605
+ var createVoiceSessionReplayJSONHandler = (options) => async ({ params }) => summarizeVoiceSessionReplay({
7606
+ ...options,
7607
+ sessionId: params.sessionId ?? ""
7608
+ });
7609
+ var createVoiceSessionReplayHTMLHandler = (options) => async ({ params }) => {
7610
+ const replay = await summarizeVoiceSessionReplay({
7611
+ ...options,
7612
+ sessionId: params.sessionId ?? ""
7613
+ });
7614
+ const body = await (options.render?.(replay) ?? replay.html);
7615
+ return new Response(body, {
7616
+ headers: {
7617
+ "Content-Type": "text/html; charset=utf-8",
7618
+ ...options.headers
7619
+ }
7620
+ });
7621
+ };
7622
+ var createVoiceSessionReplayRoutes = (options) => {
7623
+ const path = options.path ?? "/api/voice-sessions/:sessionId/replay";
7624
+ const htmlPath = options.htmlPath === undefined ? `${path}/htmx` : options.htmlPath;
7625
+ const routes = new Elysia4({
7626
+ name: options.name ?? "absolutejs-voice-session-replay"
7627
+ }).get(path, createVoiceSessionReplayJSONHandler(options));
7628
+ if (htmlPath) {
7629
+ routes.get(htmlPath, createVoiceSessionReplayHTMLHandler(options));
7630
+ }
7631
+ return routes;
7632
+ };
6600
7633
  // src/fileStore.ts
7634
+ import { mkdir, readFile, readdir, rename, rm, writeFile } from "fs/promises";
7635
+ import { join } from "path";
6601
7636
  var listJsonFiles = async (directory) => {
6602
7637
  try {
6603
7638
  const entries = await readdir(directory, {
@@ -6613,6 +7648,7 @@ var listJsonFiles = async (directory) => {
6613
7648
  };
6614
7649
  var encodeStoreId = (id) => `${encodeURIComponent(id)}.json`;
6615
7650
  var resolveFilePath = (directory, id) => join(directory, encodeStoreId(id));
7651
+ var createMemoryStoreId = (input) => `${input.assistantId}:${input.namespace}:${input.key}`;
6616
7652
  var readJsonFile = async (path) => JSON.parse(await readFile(path, "utf8"));
6617
7653
  var writeJsonFile = async (path, value, options) => {
6618
7654
  await mkdir(options.directory, {
@@ -6773,106 +7809,915 @@ var createVoiceFileExternalObjectMapStore = (options) => {
6773
7809
  };
6774
7810
  return { find, get, list, remove, set };
6775
7811
  };
6776
- var createVoiceFileTraceEventStore = (options) => {
6777
- const append = async (event) => {
6778
- const stored = createVoiceTraceEvent(event);
6779
- await writeJsonFile(resolveFilePath(options.directory, stored.id), stored, options);
6780
- return stored;
6781
- };
6782
- const get = async (id) => {
6783
- const path = resolveFilePath(options.directory, id);
6784
- try {
6785
- return await readJsonFile(path);
6786
- } catch (error) {
6787
- if (error.code === "ENOENT") {
6788
- return;
7812
+ var createVoiceFileTraceEventStore = (options) => {
7813
+ const append = async (event) => {
7814
+ const stored = createVoiceTraceEvent(event);
7815
+ await writeJsonFile(resolveFilePath(options.directory, stored.id), stored, options);
7816
+ return stored;
7817
+ };
7818
+ const get = async (id) => {
7819
+ const path = resolveFilePath(options.directory, id);
7820
+ try {
7821
+ return await readJsonFile(path);
7822
+ } catch (error) {
7823
+ if (error.code === "ENOENT") {
7824
+ return;
7825
+ }
7826
+ throw error;
7827
+ }
7828
+ };
7829
+ const list = async (filter = {}) => {
7830
+ const files = await listJsonFiles(options.directory);
7831
+ const events = await Promise.all(files.map((file) => readJsonFile(file)));
7832
+ return filterVoiceTraceEvents(events, filter);
7833
+ };
7834
+ const remove = async (id) => {
7835
+ await rm(resolveFilePath(options.directory, id), {
7836
+ force: true
7837
+ });
7838
+ };
7839
+ return { append, get, list, remove };
7840
+ };
7841
+ var createVoiceFileTraceSinkDeliveryStore = (options) => {
7842
+ const get = async (id) => {
7843
+ const path = resolveFilePath(options.directory, id);
7844
+ try {
7845
+ return await readJsonFile(path);
7846
+ } catch (error) {
7847
+ if (error.code === "ENOENT") {
7848
+ return;
7849
+ }
7850
+ throw error;
7851
+ }
7852
+ };
7853
+ const list = async () => {
7854
+ const files = await listJsonFiles(options.directory);
7855
+ const deliveries = await Promise.all(files.map((file) => readJsonFile(file)));
7856
+ return deliveries.sort((left, right) => left.createdAt - right.createdAt || left.id.localeCompare(right.id));
7857
+ };
7858
+ const set = async (id, delivery) => {
7859
+ await writeJsonFile(resolveFilePath(options.directory, id), {
7860
+ ...delivery,
7861
+ id
7862
+ }, options);
7863
+ };
7864
+ const remove = async (id) => {
7865
+ await rm(resolveFilePath(options.directory, id), {
7866
+ force: true
7867
+ });
7868
+ };
7869
+ return { get, list, remove, set };
7870
+ };
7871
+ var createVoiceFileAssistantMemoryStore = (options) => {
7872
+ const get = async (input) => {
7873
+ const path = resolveFilePath(options.directory, createMemoryStoreId(input));
7874
+ try {
7875
+ return await readJsonFile(path);
7876
+ } catch (error) {
7877
+ if (error.code === "ENOENT") {
7878
+ return;
7879
+ }
7880
+ throw error;
7881
+ }
7882
+ };
7883
+ const list = async (input) => {
7884
+ const files = await listJsonFiles(options.directory);
7885
+ const records = await Promise.all(files.map((file) => readJsonFile(file)));
7886
+ return records.filter((record) => record.assistantId === input.assistantId && (input.namespace === undefined || record.namespace === input.namespace)).sort((left, right) => right.updatedAt - left.updatedAt);
7887
+ };
7888
+ const set = async (input) => {
7889
+ const existing = await get(input);
7890
+ const record = createVoiceAssistantMemoryRecord({
7891
+ ...input,
7892
+ createdAt: input.createdAt ?? existing?.createdAt,
7893
+ updatedAt: input.updatedAt
7894
+ });
7895
+ await writeJsonFile(resolveFilePath(options.directory, createMemoryStoreId(record)), record, options);
7896
+ return record;
7897
+ };
7898
+ const remove = async (input) => {
7899
+ await rm(resolveFilePath(options.directory, createMemoryStoreId(input)), {
7900
+ force: true
7901
+ });
7902
+ };
7903
+ return { delete: remove, get, list, set };
7904
+ };
7905
+ var createVoiceFileRuntimeStorage = (options) => ({
7906
+ events: createVoiceFileIntegrationEventStore({
7907
+ ...options,
7908
+ directory: join(options.directory, "events")
7909
+ }),
7910
+ externalObjects: createVoiceFileExternalObjectMapStore({
7911
+ ...options,
7912
+ directory: join(options.directory, "external-objects")
7913
+ }),
7914
+ memories: createVoiceFileAssistantMemoryStore({
7915
+ ...options,
7916
+ directory: join(options.directory, "memories")
7917
+ }),
7918
+ reviews: createVoiceFileReviewStore({
7919
+ ...options,
7920
+ directory: join(options.directory, "reviews")
7921
+ }),
7922
+ session: createVoiceFileSessionStore({
7923
+ ...options,
7924
+ directory: join(options.directory, "sessions")
7925
+ }),
7926
+ tasks: createVoiceFileTaskStore({
7927
+ ...options,
7928
+ directory: join(options.directory, "tasks")
7929
+ }),
7930
+ traceDeliveries: createVoiceFileTraceSinkDeliveryStore({
7931
+ ...options,
7932
+ directory: join(options.directory, "trace-deliveries")
7933
+ }),
7934
+ traces: createVoiceFileTraceEventStore({
7935
+ ...options,
7936
+ directory: join(options.directory, "traces")
7937
+ })
7938
+ });
7939
+ var createStoredVoiceCallReviewArtifact = (id, artifact) => withVoiceCallReviewId(id, artifact);
7940
+ var createStoredVoiceOpsTask = (id, task) => withVoiceOpsTaskId(id, task);
7941
+ var createStoredVoiceIntegrationEvent = (id, event) => withVoiceIntegrationEventId(id, event);
7942
+ var createStoredVoiceExternalObjectMap = (mapping) => createVoiceExternalObjectMap({
7943
+ at: mapping.at,
7944
+ externalId: mapping.externalId,
7945
+ provider: mapping.provider,
7946
+ sinkId: mapping.sinkId,
7947
+ sourceId: mapping.sourceId,
7948
+ sourceType: mapping.sourceType
7949
+ });
7950
+ // src/modelAdapters.ts
7951
+ var OUTPUT_SCHEMA = {
7952
+ additionalProperties: false,
7953
+ properties: {
7954
+ assistantText: {
7955
+ type: "string"
7956
+ },
7957
+ complete: {
7958
+ type: "boolean"
7959
+ },
7960
+ escalate: {
7961
+ additionalProperties: false,
7962
+ properties: {
7963
+ metadata: {
7964
+ additionalProperties: true,
7965
+ type: "object"
7966
+ },
7967
+ reason: {
7968
+ type: "string"
7969
+ }
7970
+ },
7971
+ required: ["reason"],
7972
+ type: "object"
7973
+ },
7974
+ noAnswer: {
7975
+ additionalProperties: false,
7976
+ properties: {
7977
+ metadata: {
7978
+ additionalProperties: true,
7979
+ type: "object"
7980
+ }
7981
+ },
7982
+ type: "object"
7983
+ },
7984
+ result: {
7985
+ additionalProperties: true,
7986
+ type: "object"
7987
+ },
7988
+ transfer: {
7989
+ additionalProperties: false,
7990
+ properties: {
7991
+ metadata: {
7992
+ additionalProperties: true,
7993
+ type: "object"
7994
+ },
7995
+ reason: {
7996
+ type: "string"
7997
+ },
7998
+ target: {
7999
+ type: "string"
8000
+ }
8001
+ },
8002
+ required: ["target"],
8003
+ type: "object"
8004
+ },
8005
+ voicemail: {
8006
+ additionalProperties: false,
8007
+ properties: {
8008
+ metadata: {
8009
+ additionalProperties: true,
8010
+ type: "object"
8011
+ }
8012
+ },
8013
+ type: "object"
8014
+ }
8015
+ },
8016
+ type: "object"
8017
+ };
8018
+ 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.";
8019
+ var stripJSONCodeFence = (value) => {
8020
+ const trimmed = value.trim();
8021
+ const match = trimmed.match(/^```(?:json)?\s*([\s\S]*?)\s*```$/i);
8022
+ return match?.[1]?.trim() ?? value;
8023
+ };
8024
+ var parseJSON = (value) => {
8025
+ try {
8026
+ const parsed = JSON.parse(stripJSONCodeFence(value));
8027
+ return parsed && typeof parsed === "object" ? parsed : {};
8028
+ } catch {
8029
+ return {
8030
+ assistantText: value
8031
+ };
8032
+ }
8033
+ };
8034
+ var parseJSONValue = (value) => {
8035
+ try {
8036
+ return JSON.parse(value);
8037
+ } catch {
8038
+ return value;
8039
+ }
8040
+ };
8041
+ var getMessageToolCalls = (message) => {
8042
+ const toolCalls = message.metadata?.toolCalls;
8043
+ return Array.isArray(toolCalls) ? toolCalls.filter((toolCall) => toolCall && typeof toolCall === "object" && typeof toolCall.name === "string") : [];
8044
+ };
8045
+ var createHTTPError = (provider, response) => new Error(`${provider} voice assistant model failed: HTTP ${response.status}`);
8046
+ var sleep4 = (ms) => new Promise((resolve2) => {
8047
+ setTimeout(resolve2, ms);
8048
+ });
8049
+ var errorMessage = (error) => error instanceof Error ? error.message : String(error);
8050
+ var defaultIsRateLimitError = (error) => /(\b429\b|rate limit|quota|too many requests)/i.test(errorMessage(error));
8051
+ var normalizeRouteOutput = (output) => {
8052
+ const result = {};
8053
+ if (typeof output.assistantText === "string") {
8054
+ result.assistantText = output.assistantText;
8055
+ }
8056
+ if (typeof output.complete === "boolean") {
8057
+ result.complete = output.complete;
8058
+ }
8059
+ if (output.result !== undefined) {
8060
+ result.result = output.result;
8061
+ }
8062
+ if (output.transfer && typeof output.transfer === "object") {
8063
+ const transfer = output.transfer;
8064
+ if (typeof transfer.target === "string") {
8065
+ result.transfer = {
8066
+ metadata: transfer.metadata && typeof transfer.metadata === "object" ? transfer.metadata : undefined,
8067
+ reason: typeof transfer.reason === "string" ? transfer.reason : undefined,
8068
+ target: transfer.target
8069
+ };
8070
+ }
8071
+ }
8072
+ if (output.escalate && typeof output.escalate === "object") {
8073
+ const escalate = output.escalate;
8074
+ if (typeof escalate.reason === "string") {
8075
+ result.escalate = {
8076
+ metadata: escalate.metadata && typeof escalate.metadata === "object" ? escalate.metadata : undefined,
8077
+ reason: escalate.reason
8078
+ };
8079
+ }
8080
+ }
8081
+ if (output.voicemail && typeof output.voicemail === "object") {
8082
+ const voicemail = output.voicemail;
8083
+ result.voicemail = {
8084
+ metadata: voicemail.metadata && typeof voicemail.metadata === "object" ? voicemail.metadata : undefined
8085
+ };
8086
+ }
8087
+ if (output.noAnswer && typeof output.noAnswer === "object") {
8088
+ const noAnswer = output.noAnswer;
8089
+ result.noAnswer = {
8090
+ metadata: noAnswer.metadata && typeof noAnswer.metadata === "object" ? noAnswer.metadata : undefined
8091
+ };
8092
+ }
8093
+ return result;
8094
+ };
8095
+ var createJSONVoiceAssistantModel = (options) => ({
8096
+ generate: async (input) => {
8097
+ const output = await options.generate(input);
8098
+ if ("assistantText" in output || "toolCalls" in output || "complete" in output || "transfer" in output || "escalate" in output) {
8099
+ return output;
8100
+ }
8101
+ return options.mapOutput?.(output) ?? normalizeRouteOutput(output);
8102
+ }
8103
+ });
8104
+ var createVoiceProviderRouter = (options) => {
8105
+ const providerIds = Object.keys(options.providers);
8106
+ const firstProvider = providerIds[0];
8107
+ const policy = typeof options.policy === "string" ? {
8108
+ strategy: options.policy
8109
+ } : options.policy;
8110
+ const strategy = policy?.strategy ?? "prefer-selected";
8111
+ const fallbackMode = policy?.fallbackMode ?? options.fallbackMode ?? "provider-error";
8112
+ const healthOptions = typeof options.providerHealth === "object" ? options.providerHealth : options.providerHealth ? {} : undefined;
8113
+ const healthState = new Map;
8114
+ const now = () => healthOptions?.now?.() ?? Date.now();
8115
+ const failureThreshold = Math.max(1, healthOptions?.failureThreshold ?? 1);
8116
+ const cooldownMs = Math.max(0, healthOptions?.cooldownMs ?? 30000);
8117
+ const rateLimitCooldownMs = Math.max(0, healthOptions?.rateLimitCooldownMs ?? 60000);
8118
+ const getHealth = (provider) => {
8119
+ const existing = healthState.get(provider);
8120
+ if (existing) {
8121
+ return existing;
8122
+ }
8123
+ const next = {
8124
+ consecutiveFailures: 0,
8125
+ provider,
8126
+ status: "healthy"
8127
+ };
8128
+ healthState.set(provider, next);
8129
+ return next;
8130
+ };
8131
+ const cloneHealth = (provider) => {
8132
+ if (!healthOptions) {
8133
+ return;
8134
+ }
8135
+ return {
8136
+ ...getHealth(provider)
8137
+ };
8138
+ };
8139
+ const getSuppressionRemainingMs = (provider) => {
8140
+ if (!healthOptions) {
8141
+ return;
8142
+ }
8143
+ const suppressedUntil = getHealth(provider).suppressedUntil;
8144
+ return typeof suppressedUntil === "number" ? Math.max(0, suppressedUntil - now()) : undefined;
8145
+ };
8146
+ const isSuppressed = (provider) => {
8147
+ if (!healthOptions) {
8148
+ return false;
8149
+ }
8150
+ const health = getHealth(provider);
8151
+ return typeof health.suppressedUntil === "number" && health.suppressedUntil > now();
8152
+ };
8153
+ const recordProviderSuccess = (provider) => {
8154
+ if (!healthOptions) {
8155
+ return;
8156
+ }
8157
+ const health = getHealth(provider);
8158
+ health.consecutiveFailures = 0;
8159
+ health.status = "healthy";
8160
+ health.suppressedUntil = undefined;
8161
+ return cloneHealth(provider);
8162
+ };
8163
+ const recordProviderError = (provider, isProviderError, rateLimited) => {
8164
+ if (!healthOptions || !isProviderError) {
8165
+ return cloneHealth(provider);
8166
+ }
8167
+ const currentTime = now();
8168
+ const health = getHealth(provider);
8169
+ health.consecutiveFailures += 1;
8170
+ health.lastFailureAt = currentTime;
8171
+ if (rateLimited) {
8172
+ health.lastRateLimitedAt = currentTime;
8173
+ }
8174
+ if (rateLimited || health.consecutiveFailures >= failureThreshold) {
8175
+ health.status = "suppressed";
8176
+ health.suppressedUntil = currentTime + (rateLimited ? rateLimitCooldownMs : cooldownMs);
8177
+ }
8178
+ return cloneHealth(provider);
8179
+ };
8180
+ const resolveAllowedProviders = async (input) => {
8181
+ const allowProviders = policy?.allowProviders ?? options.allowProviders;
8182
+ const allowed = typeof allowProviders === "function" ? await allowProviders(input) : allowProviders;
8183
+ return new Set(allowed ?? providerIds);
8184
+ };
8185
+ const sortProviders = (providers) => {
8186
+ if (strategy !== "prefer-cheapest" && strategy !== "prefer-fastest") {
8187
+ return providers;
8188
+ }
8189
+ return [...providers].sort((left, right) => {
8190
+ const leftProfile = options.providerProfiles?.[left];
8191
+ const rightProfile = options.providerProfiles?.[right];
8192
+ const leftValue = strategy === "prefer-cheapest" ? leftProfile?.cost ?? Number.MAX_SAFE_INTEGER : leftProfile?.latencyMs ?? Number.MAX_SAFE_INTEGER;
8193
+ const rightValue = strategy === "prefer-cheapest" ? rightProfile?.cost ?? Number.MAX_SAFE_INTEGER : rightProfile?.latencyMs ?? Number.MAX_SAFE_INTEGER;
8194
+ return leftValue - rightValue || (leftProfile?.priority ?? Number.MAX_SAFE_INTEGER) - (rightProfile?.priority ?? Number.MAX_SAFE_INTEGER);
8195
+ });
8196
+ };
8197
+ const resolveOrder = async (input) => {
8198
+ const selectedProvider = await options.selectProvider?.(input);
8199
+ const allowedProviders = await resolveAllowedProviders(input);
8200
+ const fallbackOrder = typeof options.fallback === "function" ? await options.fallback(input) : options.fallback;
8201
+ const rankedProviders = sortProviders([
8202
+ ...fallbackOrder ?? providerIds
8203
+ ]).filter((provider) => allowedProviders.has(provider));
8204
+ const healthyRankedProviders = healthOptions ? rankedProviders.filter((provider) => !isSuppressed(provider)) : rankedProviders;
8205
+ const candidateRankedProviders = healthyRankedProviders.length ? healthyRankedProviders : rankedProviders;
8206
+ const preferred = selectedProvider && allowedProviders.has(selectedProvider) && (!healthOptions || !isSuppressed(selectedProvider)) ? selectedProvider : candidateRankedProviders[0] ?? firstProvider;
8207
+ const seen = new Set;
8208
+ const order = [];
8209
+ const candidates = strategy === "ordered" ? candidateRankedProviders : [
8210
+ preferred,
8211
+ ...candidateRankedProviders,
8212
+ ...providerIds.filter((provider) => !healthOptions || !isSuppressed(provider))
8213
+ ];
8214
+ for (const provider of candidates) {
8215
+ if (!provider || seen.has(provider) || !allowedProviders.has(provider) || !options.providers[provider]) {
8216
+ continue;
8217
+ }
8218
+ seen.add(provider);
8219
+ order.push(provider);
8220
+ }
8221
+ return {
8222
+ order,
8223
+ selectedProvider: preferred
8224
+ };
8225
+ };
8226
+ const emit = async (event, input) => {
8227
+ await options.onProviderEvent?.(event, input);
8228
+ };
8229
+ return {
8230
+ generate: async (input) => {
8231
+ const { order, selectedProvider } = await resolveOrder(input);
8232
+ if (!selectedProvider || order.length === 0) {
8233
+ throw new Error("Voice provider router has no available providers.");
8234
+ }
8235
+ let lastError;
8236
+ for (const [index, provider] of order.entries()) {
8237
+ const model = options.providers[provider];
8238
+ if (!model) {
8239
+ continue;
8240
+ }
8241
+ const startedAt = Date.now();
8242
+ try {
8243
+ const output = await model.generate(input);
8244
+ const providerHealth = recordProviderSuccess(provider);
8245
+ await emit({
8246
+ at: Date.now(),
8247
+ elapsedMs: Date.now() - startedAt,
8248
+ fallbackProvider: provider === selectedProvider ? undefined : provider,
8249
+ provider,
8250
+ providerHealth,
8251
+ recovered: provider !== selectedProvider,
8252
+ selectedProvider,
8253
+ status: provider === selectedProvider ? "success" : "fallback"
8254
+ }, input);
8255
+ return output;
8256
+ } catch (error) {
8257
+ lastError = error;
8258
+ const hasNextProvider = index < order.length - 1;
8259
+ const isProviderError = options.isProviderError?.(error, provider) ?? true;
8260
+ const rateLimited = options.isRateLimitError?.(error, provider) ?? defaultIsRateLimitError(error);
8261
+ const shouldFallback = fallbackMode === "provider-error" ? isProviderError : fallbackMode === "rate-limit" ? isProviderError && rateLimited : false;
8262
+ const providerHealth = recordProviderError(provider, isProviderError, rateLimited);
8263
+ const nextProvider = hasNextProvider ? order[index + 1] : undefined;
8264
+ await emit({
8265
+ at: Date.now(),
8266
+ elapsedMs: Date.now() - startedAt,
8267
+ error: errorMessage(error),
8268
+ fallbackProvider: shouldFallback ? nextProvider : undefined,
8269
+ provider,
8270
+ providerHealth,
8271
+ rateLimited,
8272
+ selectedProvider,
8273
+ suppressionRemainingMs: getSuppressionRemainingMs(provider),
8274
+ suppressedUntil: providerHealth?.suppressedUntil,
8275
+ status: "error"
8276
+ }, input);
8277
+ if (!hasNextProvider || !shouldFallback) {
8278
+ throw error;
8279
+ }
8280
+ }
8281
+ }
8282
+ throw lastError ?? new Error("Voice provider router did not run a provider.");
8283
+ }
8284
+ };
8285
+ };
8286
+ var messageToOpenAIInput = (message) => {
8287
+ if (message.role === "tool") {
8288
+ return [
8289
+ {
8290
+ call_id: message.toolCallId ?? message.name ?? crypto.randomUUID(),
8291
+ output: message.content,
8292
+ type: "function_call_output"
8293
+ }
8294
+ ];
8295
+ }
8296
+ const toolCalls = getMessageToolCalls(message);
8297
+ if (message.role === "assistant" && toolCalls.length) {
8298
+ return toolCalls.map((toolCall) => ({
8299
+ arguments: JSON.stringify(toolCall.args),
8300
+ call_id: toolCall.id ?? crypto.randomUUID(),
8301
+ name: toolCall.name,
8302
+ type: "function_call"
8303
+ }));
8304
+ }
8305
+ return [
8306
+ {
8307
+ content: message.content,
8308
+ role: message.role === "system" ? "developer" : message.role
8309
+ }
8310
+ ];
8311
+ };
8312
+ var messagesToOpenAIInput = (messages) => messages.flatMap(messageToOpenAIInput);
8313
+ var messageToAnthropicMessage = (message) => {
8314
+ if (message.role === "system") {
8315
+ return;
8316
+ }
8317
+ if (message.role === "tool") {
8318
+ if (!message.toolCallId) {
8319
+ return {
8320
+ content: `Tool result from ${message.name ?? "tool"}: ${message.content}`,
8321
+ role: "user"
8322
+ };
8323
+ }
8324
+ return {
8325
+ content: [
8326
+ {
8327
+ content: message.content,
8328
+ tool_use_id: message.toolCallId,
8329
+ type: "tool_result"
8330
+ }
8331
+ ],
8332
+ role: "user"
8333
+ };
8334
+ }
8335
+ const toolCalls = getMessageToolCalls(message);
8336
+ if (message.role === "assistant" && toolCalls.length) {
8337
+ return {
8338
+ content: [
8339
+ ...message.content ? [
8340
+ {
8341
+ text: message.content,
8342
+ type: "text"
8343
+ }
8344
+ ] : [],
8345
+ ...toolCalls.map((toolCall) => ({
8346
+ id: toolCall.id ?? crypto.randomUUID(),
8347
+ input: toolCall.args,
8348
+ name: toolCall.name,
8349
+ type: "tool_use"
8350
+ }))
8351
+ ],
8352
+ role: "assistant"
8353
+ };
8354
+ }
8355
+ return {
8356
+ content: message.content,
8357
+ role: message.role
8358
+ };
8359
+ };
8360
+ var toGeminiSchema = (schema) => {
8361
+ const next = {};
8362
+ for (const [key, value] of Object.entries(schema)) {
8363
+ if (key === "additionalProperties") {
8364
+ continue;
8365
+ }
8366
+ if (key === "type" && typeof value === "string") {
8367
+ next[key] = value.toUpperCase();
8368
+ continue;
8369
+ }
8370
+ if (Array.isArray(value)) {
8371
+ next[key] = value.map((item) => item && typeof item === "object" ? toGeminiSchema(item) : item);
8372
+ continue;
8373
+ }
8374
+ if (value && typeof value === "object") {
8375
+ next[key] = toGeminiSchema(value);
8376
+ continue;
8377
+ }
8378
+ next[key] = value;
8379
+ }
8380
+ return next;
8381
+ };
8382
+ var messageToGeminiContent = (message) => {
8383
+ if (message.role === "system") {
8384
+ return;
8385
+ }
8386
+ if (message.role === "tool") {
8387
+ return {
8388
+ parts: [
8389
+ {
8390
+ functionResponse: {
8391
+ id: message.toolCallId,
8392
+ name: message.name ?? "tool",
8393
+ response: {
8394
+ result: parseJSONValue(message.content)
8395
+ }
8396
+ }
8397
+ }
8398
+ ],
8399
+ role: "user"
8400
+ };
8401
+ }
8402
+ const toolCalls = getMessageToolCalls(message);
8403
+ if (message.role === "assistant" && toolCalls.length) {
8404
+ return {
8405
+ parts: [
8406
+ ...message.content ? [
8407
+ {
8408
+ text: message.content
8409
+ }
8410
+ ] : [],
8411
+ ...toolCalls.map((toolCall) => ({
8412
+ functionCall: {
8413
+ args: toolCall.args,
8414
+ id: toolCall.id,
8415
+ name: toolCall.name
8416
+ }
8417
+ }))
8418
+ ],
8419
+ role: "model"
8420
+ };
8421
+ }
8422
+ return {
8423
+ parts: [
8424
+ {
8425
+ text: message.content
8426
+ }
8427
+ ],
8428
+ role: message.role === "assistant" ? "model" : "user"
8429
+ };
8430
+ };
8431
+ var extractText = (response) => {
8432
+ if (typeof response.output_text === "string") {
8433
+ return response.output_text;
8434
+ }
8435
+ const output = Array.isArray(response.output) ? response.output : [];
8436
+ for (const item of output) {
8437
+ if (!item || typeof item !== "object") {
8438
+ continue;
8439
+ }
8440
+ const record = item;
8441
+ const content = Array.isArray(record.content) ? record.content : [];
8442
+ for (const contentItem of content) {
8443
+ if (!contentItem || typeof contentItem !== "object") {
8444
+ continue;
8445
+ }
8446
+ const contentRecord = contentItem;
8447
+ if (typeof contentRecord.text === "string") {
8448
+ return contentRecord.text;
8449
+ }
8450
+ }
8451
+ }
8452
+ return "";
8453
+ };
8454
+ var extractToolCalls = (response) => {
8455
+ const output = Array.isArray(response.output) ? response.output : [];
8456
+ const toolCalls = [];
8457
+ for (const item of output) {
8458
+ if (!item || typeof item !== "object") {
8459
+ continue;
8460
+ }
8461
+ const record = item;
8462
+ if (record.type !== "function_call" || typeof record.name !== "string") {
8463
+ continue;
8464
+ }
8465
+ const args = typeof record.arguments === "string" ? parseJSON(record.arguments) : {};
8466
+ toolCalls.push({
8467
+ args,
8468
+ id: typeof record.call_id === "string" ? record.call_id : typeof record.id === "string" ? record.id : undefined,
8469
+ name: record.name
8470
+ });
8471
+ }
8472
+ return toolCalls;
8473
+ };
8474
+ var createOpenAIVoiceAssistantModel = (options) => {
8475
+ const fetchImpl = options.fetch ?? globalThis.fetch;
8476
+ const baseUrl = options.baseUrl ?? "https://api.openai.com/v1";
8477
+ const model = options.model ?? "gpt-4.1-mini";
8478
+ return {
8479
+ generate: async (input) => {
8480
+ const response = await fetchImpl(`${baseUrl.replace(/\/$/, "")}/responses`, {
8481
+ body: JSON.stringify({
8482
+ input: messagesToOpenAIInput(input.messages),
8483
+ instructions: [
8484
+ input.system,
8485
+ "Return a JSON object with assistantText, complete, transfer, escalate, voicemail, noAnswer, and result when you are not calling tools."
8486
+ ].filter(Boolean).join(`
8487
+
8488
+ `),
8489
+ max_output_tokens: options.maxOutputTokens,
8490
+ model,
8491
+ temperature: options.temperature,
8492
+ text: {
8493
+ format: {
8494
+ name: "voice_route_result",
8495
+ schema: OUTPUT_SCHEMA,
8496
+ strict: false,
8497
+ type: "json_schema"
8498
+ }
8499
+ },
8500
+ tool_choice: input.tools.length ? "auto" : "none",
8501
+ tools: input.tools.map((tool) => ({
8502
+ description: tool.description,
8503
+ name: tool.name,
8504
+ parameters: tool.parameters ?? {
8505
+ additionalProperties: true,
8506
+ type: "object"
8507
+ },
8508
+ strict: false,
8509
+ type: "function"
8510
+ }))
8511
+ }),
8512
+ headers: {
8513
+ authorization: `Bearer ${options.apiKey}`,
8514
+ "content-type": "application/json"
8515
+ },
8516
+ method: "POST"
8517
+ });
8518
+ if (!response.ok) {
8519
+ throw createHTTPError("OpenAI", response);
6789
8520
  }
6790
- throw error;
8521
+ const body = await response.json();
8522
+ if (body.usage && typeof body.usage === "object") {
8523
+ await options.onUsage?.(body.usage);
8524
+ }
8525
+ const toolCalls = extractToolCalls(body);
8526
+ if (toolCalls.length) {
8527
+ return {
8528
+ toolCalls
8529
+ };
8530
+ }
8531
+ return normalizeRouteOutput(parseJSON(extractText(body)));
6791
8532
  }
6792
8533
  };
6793
- const list = async (filter = {}) => {
6794
- const files = await listJsonFiles(options.directory);
6795
- const events = await Promise.all(files.map((file) => readJsonFile(file)));
6796
- return filterVoiceTraceEvents(events, filter);
6797
- };
6798
- const remove = async (id) => {
6799
- await rm(resolveFilePath(options.directory, id), {
6800
- force: true
8534
+ };
8535
+ var extractAnthropicText = (response) => {
8536
+ const content = Array.isArray(response.content) ? response.content : [];
8537
+ return content.map((item) => item && typeof item === "object" && item.type === "text" && typeof item.text === "string" ? item.text : "").filter(Boolean).join(`
8538
+ `);
8539
+ };
8540
+ var extractAnthropicToolCalls = (response) => {
8541
+ const content = Array.isArray(response.content) ? response.content : [];
8542
+ const toolCalls = [];
8543
+ for (const item of content) {
8544
+ if (!item || typeof item !== "object") {
8545
+ continue;
8546
+ }
8547
+ const record = item;
8548
+ if (record.type !== "tool_use" || typeof record.name !== "string") {
8549
+ continue;
8550
+ }
8551
+ toolCalls.push({
8552
+ args: record.input && typeof record.input === "object" ? record.input : {},
8553
+ id: typeof record.id === "string" ? record.id : undefined,
8554
+ name: record.name
6801
8555
  });
6802
- };
6803
- return { append, get, list, remove };
8556
+ }
8557
+ return toolCalls;
6804
8558
  };
6805
- var createVoiceFileTraceSinkDeliveryStore = (options) => {
6806
- const get = async (id) => {
6807
- const path = resolveFilePath(options.directory, id);
6808
- try {
6809
- return await readJsonFile(path);
6810
- } catch (error) {
6811
- if (error.code === "ENOENT") {
6812
- return;
8559
+ var createAnthropicVoiceAssistantModel = (options) => {
8560
+ const fetchImpl = options.fetch ?? globalThis.fetch;
8561
+ const baseUrl = options.baseUrl ?? "https://api.anthropic.com/v1";
8562
+ const model = options.model ?? "claude-sonnet-4-5";
8563
+ return {
8564
+ generate: async (input) => {
8565
+ const response = await fetchImpl(`${baseUrl.replace(/\/$/, "")}/messages`, {
8566
+ body: JSON.stringify({
8567
+ max_tokens: options.maxOutputTokens ?? 1024,
8568
+ messages: input.messages.map(messageToAnthropicMessage).filter(Boolean),
8569
+ model,
8570
+ system: [input.system, ROUTE_RESULT_INSTRUCTION].filter(Boolean).join(`
8571
+
8572
+ `),
8573
+ temperature: options.temperature,
8574
+ tool_choice: input.tools.length ? { type: "auto" } : { type: "none" },
8575
+ tools: input.tools.map((tool) => ({
8576
+ description: tool.description,
8577
+ input_schema: tool.parameters ?? {
8578
+ additionalProperties: true,
8579
+ type: "object"
8580
+ },
8581
+ name: tool.name
8582
+ }))
8583
+ }),
8584
+ headers: {
8585
+ "anthropic-version": options.version ?? "2023-06-01",
8586
+ "content-type": "application/json",
8587
+ "x-api-key": options.apiKey
8588
+ },
8589
+ method: "POST"
8590
+ });
8591
+ if (!response.ok) {
8592
+ throw createHTTPError("Anthropic", response);
6813
8593
  }
6814
- throw error;
8594
+ const body = await response.json();
8595
+ if (body.usage && typeof body.usage === "object") {
8596
+ await options.onUsage?.(body.usage);
8597
+ }
8598
+ const toolCalls = extractAnthropicToolCalls(body);
8599
+ if (toolCalls.length) {
8600
+ return {
8601
+ assistantText: extractAnthropicText(body) || undefined,
8602
+ toolCalls
8603
+ };
8604
+ }
8605
+ return normalizeRouteOutput(parseJSON(extractAnthropicText(body)));
6815
8606
  }
6816
8607
  };
6817
- const list = async () => {
6818
- const files = await listJsonFiles(options.directory);
6819
- const deliveries = await Promise.all(files.map((file) => readJsonFile(file)));
6820
- return deliveries.sort((left, right) => left.createdAt - right.createdAt || left.id.localeCompare(right.id));
6821
- };
6822
- const set = async (id, delivery) => {
6823
- await writeJsonFile(resolveFilePath(options.directory, id), {
6824
- ...delivery,
6825
- id
6826
- }, options);
6827
- };
6828
- const remove = async (id) => {
6829
- await rm(resolveFilePath(options.directory, id), {
6830
- force: true
8608
+ };
8609
+ var extractGeminiCandidateParts = (response) => {
8610
+ const candidates = Array.isArray(response.candidates) ? response.candidates : [];
8611
+ const first = candidates[0];
8612
+ if (!first || typeof first !== "object") {
8613
+ return [];
8614
+ }
8615
+ const content = first.content;
8616
+ if (!content || typeof content !== "object") {
8617
+ return [];
8618
+ }
8619
+ const parts = content.parts;
8620
+ return Array.isArray(parts) ? parts : [];
8621
+ };
8622
+ var extractGeminiText = (response) => extractGeminiCandidateParts(response).map((part) => part && typeof part === "object" && typeof part.text === "string" ? part.text : "").filter(Boolean).join(`
8623
+ `);
8624
+ var extractGeminiToolCalls = (response) => {
8625
+ const toolCalls = [];
8626
+ for (const part of extractGeminiCandidateParts(response)) {
8627
+ if (!part || typeof part !== "object") {
8628
+ continue;
8629
+ }
8630
+ const functionCall = part.functionCall;
8631
+ if (!functionCall || typeof functionCall !== "object") {
8632
+ continue;
8633
+ }
8634
+ const record = functionCall;
8635
+ if (typeof record.name !== "string") {
8636
+ continue;
8637
+ }
8638
+ toolCalls.push({
8639
+ args: record.args && typeof record.args === "object" ? record.args : {},
8640
+ id: typeof record.id === "string" ? record.id : undefined,
8641
+ name: record.name
6831
8642
  });
8643
+ }
8644
+ return toolCalls;
8645
+ };
8646
+ var createGeminiVoiceAssistantModel = (options) => {
8647
+ const fetchImpl = options.fetch ?? globalThis.fetch;
8648
+ const baseUrl = options.baseUrl ?? "https://generativelanguage.googleapis.com/v1beta";
8649
+ const model = options.model ?? "gemini-2.5-flash";
8650
+ const maxRetries = Math.max(0, options.maxRetries ?? 2);
8651
+ return {
8652
+ generate: async (input) => {
8653
+ const endpoint = `${baseUrl.replace(/\/$/, "")}/models/${encodeURIComponent(model)}:generateContent?key=${encodeURIComponent(options.apiKey)}`;
8654
+ let response;
8655
+ for (let attempt = 0;attempt <= maxRetries; attempt += 1) {
8656
+ response = await fetchImpl(endpoint, {
8657
+ body: JSON.stringify({
8658
+ contents: input.messages.map(messageToGeminiContent).filter(Boolean),
8659
+ generationConfig: {
8660
+ maxOutputTokens: options.maxOutputTokens,
8661
+ ...input.tools.length ? {} : {
8662
+ responseMimeType: "application/json",
8663
+ responseSchema: toGeminiSchema(OUTPUT_SCHEMA)
8664
+ },
8665
+ temperature: options.temperature
8666
+ },
8667
+ systemInstruction: {
8668
+ parts: [
8669
+ {
8670
+ text: [input.system, ROUTE_RESULT_INSTRUCTION].filter(Boolean).join(`
8671
+
8672
+ `)
8673
+ }
8674
+ ]
8675
+ },
8676
+ tools: input.tools.length ? [
8677
+ {
8678
+ functionDeclarations: input.tools.map((tool) => ({
8679
+ description: tool.description,
8680
+ name: tool.name,
8681
+ parameters: toGeminiSchema(tool.parameters ?? {
8682
+ additionalProperties: true,
8683
+ type: "object"
8684
+ })
8685
+ }))
8686
+ }
8687
+ ] : undefined
8688
+ }),
8689
+ headers: {
8690
+ "content-type": "application/json"
8691
+ },
8692
+ method: "POST"
8693
+ });
8694
+ if (response.ok || response.status !== 429 && response.status < 500 || attempt === maxRetries) {
8695
+ break;
8696
+ }
8697
+ const retryAfter = Number(response.headers.get("retry-after"));
8698
+ await sleep4(Number.isFinite(retryAfter) && retryAfter > 0 ? retryAfter * 1000 : 500 * 2 ** attempt);
8699
+ }
8700
+ if (!response) {
8701
+ throw new Error("Gemini voice assistant model failed: no response");
8702
+ }
8703
+ if (!response.ok) {
8704
+ throw createHTTPError("Gemini", response);
8705
+ }
8706
+ const body = await response.json();
8707
+ if (body.usageMetadata && typeof body.usageMetadata === "object") {
8708
+ await options.onUsage?.(body.usageMetadata);
8709
+ }
8710
+ const toolCalls = extractGeminiToolCalls(body);
8711
+ if (toolCalls.length) {
8712
+ return {
8713
+ assistantText: extractGeminiText(body) || undefined,
8714
+ toolCalls
8715
+ };
8716
+ }
8717
+ return normalizeRouteOutput(parseJSON(extractGeminiText(body)));
8718
+ }
6832
8719
  };
6833
- return { get, list, remove, set };
6834
8720
  };
6835
- var createVoiceFileRuntimeStorage = (options) => ({
6836
- events: createVoiceFileIntegrationEventStore({
6837
- ...options,
6838
- directory: join(options.directory, "events")
6839
- }),
6840
- externalObjects: createVoiceFileExternalObjectMapStore({
6841
- ...options,
6842
- directory: join(options.directory, "external-objects")
6843
- }),
6844
- reviews: createVoiceFileReviewStore({
6845
- ...options,
6846
- directory: join(options.directory, "reviews")
6847
- }),
6848
- session: createVoiceFileSessionStore({
6849
- ...options,
6850
- directory: join(options.directory, "sessions")
6851
- }),
6852
- tasks: createVoiceFileTaskStore({
6853
- ...options,
6854
- directory: join(options.directory, "tasks")
6855
- }),
6856
- traceDeliveries: createVoiceFileTraceSinkDeliveryStore({
6857
- ...options,
6858
- directory: join(options.directory, "trace-deliveries")
6859
- }),
6860
- traces: createVoiceFileTraceEventStore({
6861
- ...options,
6862
- directory: join(options.directory, "traces")
6863
- })
6864
- });
6865
- var createStoredVoiceCallReviewArtifact = (id, artifact) => withVoiceCallReviewId(id, artifact);
6866
- var createStoredVoiceOpsTask = (id, task) => withVoiceOpsTaskId(id, task);
6867
- var createStoredVoiceIntegrationEvent = (id, event) => withVoiceIntegrationEventId(id, event);
6868
- var createStoredVoiceExternalObjectMap = (mapping) => createVoiceExternalObjectMap({
6869
- at: mapping.at,
6870
- externalId: mapping.externalId,
6871
- provider: mapping.provider,
6872
- sinkId: mapping.sinkId,
6873
- sourceId: mapping.sourceId,
6874
- sourceType: mapping.sourceType
6875
- });
6876
8721
  // src/sqliteStore.ts
6877
8722
  import { Database } from "bun:sqlite";
6878
8723
  var normalizeTableNameSegment = (value) => value.trim().replace(/[^a-zA-Z0-9_]+/g, "_").replace(/^_+|_+$/g, "") || "voice";
@@ -7354,6 +9199,361 @@ var createVoiceMemoryStore = () => {
7354
9199
  };
7355
9200
  return { get, getOrCreate, list, remove, set };
7356
9201
  };
9202
+ // src/opsWebhook.ts
9203
+ import { Elysia as Elysia5 } from "elysia";
9204
+ var toHex5 = (bytes) => Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
9205
+ var signVoiceOpsWebhookBody = async (input) => {
9206
+ const encoder = new TextEncoder;
9207
+ const key = await crypto.subtle.importKey("raw", encoder.encode(input.secret), {
9208
+ hash: "SHA-256",
9209
+ name: "HMAC"
9210
+ }, false, ["sign"]);
9211
+ const signature = await crypto.subtle.sign("HMAC", key, encoder.encode(`${input.timestamp}.${input.body}`));
9212
+ return `sha256=${toHex5(new Uint8Array(signature))}`;
9213
+ };
9214
+ var timingSafeEqual = (left, right) => {
9215
+ const encoder = new TextEncoder;
9216
+ const leftBytes = encoder.encode(left);
9217
+ const rightBytes = encoder.encode(right);
9218
+ if (leftBytes.length !== rightBytes.length) {
9219
+ return false;
9220
+ }
9221
+ let diff = 0;
9222
+ for (let index = 0;index < leftBytes.length; index += 1) {
9223
+ diff |= leftBytes[index] ^ rightBytes[index];
9224
+ }
9225
+ return diff === 0;
9226
+ };
9227
+ var resolveWebhookLink = async (resolver, event) => {
9228
+ if (typeof resolver === "function") {
9229
+ return resolver({
9230
+ event
9231
+ });
9232
+ }
9233
+ return resolver;
9234
+ };
9235
+ var joinBaseUrl = (baseUrl, path) => `${baseUrl.replace(/\/+$/, "")}/${path.replace(/^\/+/, "")}`;
9236
+ var asString = (value) => typeof value === "string" && value.length > 0 ? value : undefined;
9237
+ var buildVoiceOpsWebhookEntity = (event) => ({
9238
+ disposition: asString(event.payload.disposition),
9239
+ outcome: asString(event.payload.outcome),
9240
+ priority: asString(event.payload.priority),
9241
+ queue: asString(event.payload.queue),
9242
+ reviewId: asString(event.payload.reviewId),
9243
+ scenarioId: asString(event.payload.scenarioId),
9244
+ sessionId: asString(event.payload.sessionId),
9245
+ status: asString(event.payload.status),
9246
+ target: asString(event.payload.target),
9247
+ taskId: asString(event.payload.taskId)
9248
+ });
9249
+ var createVoiceOpsWebhookEnvelope = async (input) => {
9250
+ const entity = buildVoiceOpsWebhookEntity(input.event);
9251
+ const replayHref = await resolveWebhookLink(input.replayHref, input.event) ?? (input.baseUrl && entity.sessionId ? joinBaseUrl(input.baseUrl, `/api/voice-sessions/${encodeURIComponent(entity.sessionId)}/replay`) : undefined);
9252
+ const links = {
9253
+ event: await resolveWebhookLink(input.eventHref, input.event),
9254
+ replay: replayHref,
9255
+ review: await resolveWebhookLink(input.reviewHref, input.event),
9256
+ task: await resolveWebhookLink(input.taskHref, input.event)
9257
+ };
9258
+ return {
9259
+ entity,
9260
+ event: {
9261
+ createdAt: input.event.createdAt,
9262
+ id: input.event.id,
9263
+ payload: input.event.payload,
9264
+ type: input.event.type
9265
+ },
9266
+ links: links.event || links.replay || links.review || links.task ? links : undefined,
9267
+ schemaVersion: 1,
9268
+ source: "absolutejs-voice"
9269
+ };
9270
+ };
9271
+ var createVoiceOpsWebhookSink = (options) => createVoiceIntegrationHTTPSink({
9272
+ ...options,
9273
+ body: ({ event }) => createVoiceOpsWebhookEnvelope({
9274
+ baseUrl: options.baseUrl,
9275
+ event,
9276
+ eventHref: options.eventHref,
9277
+ replayHref: options.replayHref,
9278
+ reviewHref: options.reviewHref,
9279
+ taskHref: options.taskHref
9280
+ }),
9281
+ kind: options.kind ?? "ops-webhook"
9282
+ });
9283
+ var verifyVoiceOpsWebhookSignature = async (input) => {
9284
+ if (!input.secret) {
9285
+ return {
9286
+ ok: false,
9287
+ reason: "missing-secret"
9288
+ };
9289
+ }
9290
+ if (!input.signature) {
9291
+ return {
9292
+ ok: false,
9293
+ reason: "missing-signature"
9294
+ };
9295
+ }
9296
+ if (!input.signature.startsWith("sha256=")) {
9297
+ return {
9298
+ ok: false,
9299
+ reason: "unsupported-algorithm"
9300
+ };
9301
+ }
9302
+ if (!input.timestamp) {
9303
+ return {
9304
+ ok: false,
9305
+ reason: "missing-timestamp"
9306
+ };
9307
+ }
9308
+ const timestampMs = Number(input.timestamp);
9309
+ const toleranceMs = Math.max(0, input.toleranceMs ?? 5 * 60 * 1000);
9310
+ if (!Number.isFinite(timestampMs) || toleranceMs > 0 && Math.abs((input.now ?? Date.now()) - timestampMs) > toleranceMs) {
9311
+ return {
9312
+ ok: false,
9313
+ reason: "stale-timestamp"
9314
+ };
9315
+ }
9316
+ const expected = await signVoiceOpsWebhookBody({
9317
+ body: input.body,
9318
+ secret: input.secret,
9319
+ timestamp: input.timestamp
9320
+ });
9321
+ if (!timingSafeEqual(expected, input.signature)) {
9322
+ return {
9323
+ ok: false,
9324
+ reason: "invalid-signature"
9325
+ };
9326
+ }
9327
+ return {
9328
+ ok: true
9329
+ };
9330
+ };
9331
+ var createVoiceOpsWebhookReceiverRoutes = (options = {}) => {
9332
+ const path = options.path ?? "/api/voice-ops/webhook";
9333
+ return new Elysia5().post(path, async ({ body, request, set }) => {
9334
+ const bodyText = typeof body === "string" ? body : JSON.stringify(body);
9335
+ if (options.signingSecret) {
9336
+ const verification = await verifyVoiceOpsWebhookSignature({
9337
+ body: bodyText,
9338
+ secret: options.signingSecret,
9339
+ signature: request.headers.get("x-absolutejs-signature"),
9340
+ timestamp: request.headers.get("x-absolutejs-timestamp"),
9341
+ toleranceMs: options.toleranceMs
9342
+ });
9343
+ if (!verification.ok) {
9344
+ set.status = 401;
9345
+ return {
9346
+ ok: false,
9347
+ reason: verification.reason
9348
+ };
9349
+ }
9350
+ }
9351
+ const envelope = JSON.parse(bodyText);
9352
+ await options.onEnvelope?.({
9353
+ envelope,
9354
+ request
9355
+ });
9356
+ return {
9357
+ eventId: envelope.event?.id,
9358
+ ok: true,
9359
+ type: envelope.event?.type
9360
+ };
9361
+ }, {
9362
+ parse: "text"
9363
+ });
9364
+ };
9365
+ // src/handoffHealth.ts
9366
+ import { Elysia as Elysia6 } from "elysia";
9367
+ var escapeHtml7 = (value) => value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
9368
+ var getString4 = (value) => typeof value === "string" && value.length > 0 ? value : undefined;
9369
+ var isStatus = (value) => value === "delivered" || value === "failed" || value === "skipped";
9370
+ var increment3 = (record, key) => {
9371
+ record[key] = (record[key] ?? 0) + 1;
9372
+ };
9373
+ var normalizeDelivery = (adapterId, value) => {
9374
+ const record = value && typeof value === "object" ? value : {};
9375
+ return {
9376
+ adapterId: getString4(record.adapterId) ?? adapterId,
9377
+ adapterKind: getString4(record.adapterKind),
9378
+ deliveredAt: typeof record.deliveredAt === "number" ? record.deliveredAt : undefined,
9379
+ deliveredTo: getString4(record.deliveredTo),
9380
+ error: getString4(record.error),
9381
+ status: isStatus(record.status) ? record.status : "failed"
9382
+ };
9383
+ };
9384
+ var normalizeDeliveries = (payload) => {
9385
+ const deliveries = payload.deliveries;
9386
+ if (!deliveries || typeof deliveries !== "object") {
9387
+ return [];
9388
+ }
9389
+ return Object.entries(deliveries).map(([adapterId, value]) => normalizeDelivery(adapterId, value));
9390
+ };
9391
+ var resolveReplayHref = (event, replayHref) => {
9392
+ if (replayHref === false) {
9393
+ return;
9394
+ }
9395
+ if (typeof replayHref === "function") {
9396
+ return replayHref(event);
9397
+ }
9398
+ return `${replayHref ?? "/api/voice-sessions"}/${encodeURIComponent(event.sessionId)}/replay/htmx`;
9399
+ };
9400
+ var summarizeVoiceHandoffHealth = async (options = {}) => {
9401
+ const sourceEvents = options.events ?? await options.store?.list() ?? [];
9402
+ const search = options.q?.trim().toLowerCase();
9403
+ const byAction = {};
9404
+ const byAdapter = {};
9405
+ const byStatus = {
9406
+ delivered: 0,
9407
+ failed: 0,
9408
+ skipped: 0
9409
+ };
9410
+ const events = sourceEvents.filter((event) => event.type === "call.handoff").map((event) => {
9411
+ const status = isStatus(event.payload.status) ? event.payload.status : "failed";
9412
+ const deliveries = normalizeDeliveries(event.payload);
9413
+ const item = {
9414
+ action: getString4(event.payload.action),
9415
+ at: event.at,
9416
+ deliveries,
9417
+ reason: getString4(event.payload.reason),
9418
+ sessionId: event.sessionId,
9419
+ status,
9420
+ target: getString4(event.payload.target)
9421
+ };
9422
+ return {
9423
+ ...item,
9424
+ replayHref: resolveReplayHref(item, options.replayHref)
9425
+ };
9426
+ }).filter((event) => {
9427
+ if (options.status && options.status !== "all" && event.status !== options.status) {
9428
+ return false;
9429
+ }
9430
+ if (!search) {
9431
+ return true;
9432
+ }
9433
+ return [
9434
+ event.action,
9435
+ event.reason,
9436
+ event.sessionId,
9437
+ event.status,
9438
+ event.target,
9439
+ ...event.deliveries.flatMap((delivery) => [
9440
+ delivery.adapterId,
9441
+ delivery.adapterKind,
9442
+ delivery.deliveredTo,
9443
+ delivery.error,
9444
+ delivery.status
9445
+ ])
9446
+ ].some((value) => value?.toLowerCase().includes(search));
9447
+ }).sort((left, right) => right.at - left.at).slice(0, options.limit ?? 50);
9448
+ for (const event of events) {
9449
+ byStatus[event.status] += 1;
9450
+ if (event.action) {
9451
+ increment3(byAction, event.action);
9452
+ }
9453
+ for (const delivery of event.deliveries) {
9454
+ byAdapter[delivery.adapterId] ??= {
9455
+ delivered: 0,
9456
+ failed: 0,
9457
+ skipped: 0
9458
+ };
9459
+ byAdapter[delivery.adapterId][delivery.status] += 1;
9460
+ }
9461
+ }
9462
+ return {
9463
+ byAction,
9464
+ byAdapter,
9465
+ byStatus,
9466
+ events,
9467
+ failed: byStatus.failed,
9468
+ total: events.length
9469
+ };
9470
+ };
9471
+ var renderMetricGrid = (summary) => [
9472
+ '<section class="voice-handoff-health-grid">',
9473
+ `<article><span>Total</span><strong>${String(summary.total)}</strong></article>`,
9474
+ `<article><span>Delivered</span><strong>${String(summary.byStatus.delivered)}</strong></article>`,
9475
+ `<article><span>Failed</span><strong>${String(summary.byStatus.failed)}</strong></article>`,
9476
+ `<article><span>Skipped</span><strong>${String(summary.byStatus.skipped)}</strong></article>`,
9477
+ "</section>"
9478
+ ].join("");
9479
+ var renderActionSummary = (summary) => {
9480
+ const actions = Object.entries(summary.byAction).sort((left, right) => right[1] - left[1]);
9481
+ const adapters = Object.entries(summary.byAdapter).sort(([left], [right]) => left.localeCompare(right));
9482
+ return [
9483
+ '<section class="voice-handoff-health-columns">',
9484
+ "<article><h3>Actions</h3>",
9485
+ actions.length === 0 ? "<p>No handoff actions yet.</p>" : `<ul>${actions.map(([action, count]) => `<li>${escapeHtml7(action)}: ${String(count)}</li>`).join("")}</ul>`,
9486
+ "</article>",
9487
+ "<article><h3>Adapters</h3>",
9488
+ 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>`,
9489
+ "</article>",
9490
+ "</section>"
9491
+ ].join("");
9492
+ };
9493
+ var renderVoiceHandoffHealthHTML = (summary) => [
9494
+ '<div class="voice-handoff-health">',
9495
+ renderMetricGrid(summary),
9496
+ renderActionSummary(summary),
9497
+ "<section>",
9498
+ "<h3>Recent Handoffs</h3>",
9499
+ summary.events.length === 0 ? '<p class="voice-handoff-health-empty">No handoffs found.</p>' : [
9500
+ '<div class="voice-handoff-health-events">',
9501
+ ...summary.events.map((event) => [
9502
+ `<article class="${escapeHtml7(event.status)}">`,
9503
+ '<div class="voice-handoff-health-event-header">',
9504
+ `<strong>${escapeHtml7(event.action ?? "handoff")}</strong>`,
9505
+ `<span>${escapeHtml7(event.status)}</span>`,
9506
+ "</div>",
9507
+ `<p><small>${escapeHtml7(event.sessionId)}</small></p>`,
9508
+ event.target ? `<p>Target: ${escapeHtml7(event.target)}</p>` : "",
9509
+ event.reason ? `<p>Reason: ${escapeHtml7(event.reason)}</p>` : "",
9510
+ event.deliveries.length ? `<ul>${event.deliveries.map((delivery) => [
9511
+ "<li>",
9512
+ `${escapeHtml7(delivery.adapterId)}: ${escapeHtml7(delivery.status)}`,
9513
+ delivery.deliveredTo ? ` to ${escapeHtml7(delivery.deliveredTo)}` : "",
9514
+ delivery.error ? ` (${escapeHtml7(delivery.error)})` : "",
9515
+ "</li>"
9516
+ ].join("")).join("")}</ul>` : "",
9517
+ event.replayHref ? `<p><a href="${escapeHtml7(event.replayHref)}">Open replay</a></p>` : "",
9518
+ "</article>"
9519
+ ].join("")),
9520
+ "</div>"
9521
+ ].join(""),
9522
+ "</section>",
9523
+ "</div>"
9524
+ ].join("");
9525
+ var createVoiceHandoffHealthJSONHandler = (options = {}) => async ({ query }) => summarizeVoiceHandoffHealth({
9526
+ ...options,
9527
+ limit: typeof query?.limit === "string" ? Number(query.limit) : options.limit,
9528
+ q: query?.q ?? options.q,
9529
+ status: query?.status === "delivered" || query?.status === "failed" || query?.status === "skipped" || query?.status === "all" ? query.status : options.status
9530
+ });
9531
+ var createVoiceHandoffHealthHTMLHandler = (options = {}) => async ({ query }) => {
9532
+ const summary = await summarizeVoiceHandoffHealth({
9533
+ ...options,
9534
+ limit: typeof query?.limit === "string" ? Number(query.limit) : options.limit,
9535
+ q: query?.q ?? options.q,
9536
+ status: query?.status === "delivered" || query?.status === "failed" || query?.status === "skipped" || query?.status === "all" ? query.status : options.status
9537
+ });
9538
+ const body = await (options.render?.(summary) ?? renderVoiceHandoffHealthHTML(summary));
9539
+ return new Response(body, {
9540
+ headers: {
9541
+ "Content-Type": "text/html; charset=utf-8",
9542
+ ...options.headers
9543
+ }
9544
+ });
9545
+ };
9546
+ var createVoiceHandoffHealthRoutes = (options = {}) => {
9547
+ const path = options.path ?? "/api/voice-handoffs";
9548
+ const htmlPath = options.htmlPath === undefined ? `${path}/htmx` : options.htmlPath;
9549
+ const routes = new Elysia6({
9550
+ name: options.name ?? "absolutejs-voice-handoff-health"
9551
+ }).get(path, createVoiceHandoffHealthJSONHandler(options));
9552
+ if (htmlPath) {
9553
+ routes.get(htmlPath, createVoiceHandoffHealthHTMLHandler(options));
9554
+ }
9555
+ return routes;
9556
+ };
7357
9557
  // src/queue.ts
7358
9558
  var releaseLeaseScript = `
7359
9559
  if redis.call("GET", KEYS[1]) == ARGV[1] then
@@ -8070,10 +10270,10 @@ var createVoiceOpsTaskProcessorWorker = (options) => ({
8070
10270
  result.completed += 1;
8071
10271
  } catch (error) {
8072
10272
  await options.onError?.(error, task);
8073
- const errorMessage = error instanceof Error ? error.message : String(error);
10273
+ const errorMessage2 = error instanceof Error ? error.message : String(error);
8074
10274
  const failedTask = failVoiceOpsTask(task, {
8075
10275
  actor: task.claimedBy ?? "ops-worker",
8076
- error: errorMessage
10276
+ error: errorMessage2
8077
10277
  });
8078
10278
  if (shouldDeadLetterTask(failedTask, options.maxFailures)) {
8079
10279
  const deadLetterTask = deadLetterVoiceOpsTask(failedTask, {
@@ -8886,7 +11086,7 @@ var createVoiceSTTRoutingCorrectionHandler = (mode = "generic") => {
8886
11086
  import { Buffer as Buffer2 } from "buffer";
8887
11087
  var TWILIO_MULAW_SAMPLE_RATE = 8000;
8888
11088
  var VOICE_PCM_SAMPLE_RATE = 16000;
8889
- var escapeXml = (value) => value.replaceAll("&", "&amp;").replaceAll('"', "&quot;").replaceAll("'", "&apos;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
11089
+ var escapeXml2 = (value) => value.replaceAll("&", "&amp;").replaceAll('"', "&quot;").replaceAll("'", "&apos;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
8890
11090
  var normalizeOnTurn2 = (handler) => {
8891
11091
  if (handler.length > 1) {
8892
11092
  const directHandler = handler;
@@ -9082,8 +11282,8 @@ var createTwilioSocketAdapter = (socket, getState) => ({
9082
11282
  }
9083
11283
  });
9084
11284
  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>`;
11285
+ const parameters = Object.entries(options.parameters ?? {}).filter((entry) => entry[1] !== undefined).map(([name, value]) => `<Parameter name="${escapeXml2(name)}" value="${escapeXml2(String(value))}" />`).join("");
11286
+ 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
11287
  };
9088
11288
  var createTwilioMediaStreamBridge = (socket, options) => {
9089
11289
  const runtimePreset = resolveVoiceRuntimePreset(options.preset);
@@ -9319,15 +11519,21 @@ export {
9319
11519
  withVoiceOpsTaskId,
9320
11520
  withVoiceIntegrationEventId,
9321
11521
  voice,
11522
+ verifyVoiceOpsWebhookSignature,
9322
11523
  transcodeTwilioInboundPayloadToPCM16,
9323
11524
  transcodePCMToTwilioOutboundPayload,
9324
11525
  summarizeVoiceTraceSinkDeliveries,
9325
11526
  summarizeVoiceTrace,
11527
+ summarizeVoiceSessions,
11528
+ summarizeVoiceSessionReplay,
11529
+ summarizeVoiceProviderHealth,
9326
11530
  summarizeVoiceOpsTasks,
9327
11531
  summarizeVoiceOpsTaskQueue,
9328
11532
  summarizeVoiceOpsTaskAnalytics,
9329
11533
  summarizeVoiceIntegrationEvents,
11534
+ summarizeVoiceHandoffHealth,
9330
11535
  summarizeVoiceAssistantRuns,
11536
+ summarizeVoiceAssistantHealth,
9331
11537
  startVoiceOpsTask,
9332
11538
  shapeTelephonyAssistantText,
9333
11539
  selectVoiceTraceEventsForPrune,
@@ -9339,14 +11545,19 @@ export {
9339
11545
  resolveVoiceOpsTaskAssignment,
9340
11546
  resolveVoiceOpsTaskAgeBucket,
9341
11547
  resolveVoiceOpsPreset,
11548
+ resolveVoiceAssistantMemoryNamespace,
9342
11549
  resolveTurnDetectionConfig,
9343
11550
  resolveAudioConditioningConfig,
9344
11551
  requeueVoiceOpsTask,
9345
11552
  reopenVoiceOpsTask,
9346
11553
  renderVoiceTraceMarkdown,
9347
11554
  renderVoiceTraceHTML,
11555
+ renderVoiceSessionsHTML,
11556
+ renderVoiceProviderHealthHTML,
11557
+ renderVoiceHandoffHealthHTML,
9348
11558
  renderVoiceCallReviewMarkdown,
9349
11559
  renderVoiceCallReviewHTML,
11560
+ renderVoiceAssistantHealthHTML,
9350
11561
  redactVoiceTraceText,
9351
11562
  redactVoiceTraceEvents,
9352
11563
  redactVoiceTraceEvent,
@@ -9366,13 +11577,16 @@ export {
9366
11577
  deliverVoiceTraceEventsToSinks,
9367
11578
  deliverVoiceIntegrationEventToSinks,
9368
11579
  deliverVoiceIntegrationEvent,
11580
+ deliverVoiceHandoff,
9369
11581
  decodeTwilioMulawBase64,
9370
11582
  deadLetterVoiceOpsTask,
9371
11583
  createVoiceZendeskTicketUpdateSink,
9372
11584
  createVoiceZendeskTicketSyncSinks,
9373
11585
  createVoiceZendeskTicketSink,
11586
+ createVoiceWebhookHandoffAdapter,
9374
11587
  createVoiceWebhookDeliveryWorkerLoop,
9375
11588
  createVoiceWebhookDeliveryWorker,
11589
+ createVoiceTwilioRedirectHandoffAdapter,
9376
11590
  createVoiceTraceSinkStore,
9377
11591
  createVoiceTraceSinkDeliveryWorkerLoop,
9378
11592
  createVoiceTraceSinkDeliveryWorker,
@@ -9384,7 +11598,13 @@ export {
9384
11598
  createVoiceTaskUpdatedEvent,
9385
11599
  createVoiceTaskSLABreachedEvent,
9386
11600
  createVoiceTaskCreatedEvent,
11601
+ createVoiceSessionsJSONHandler,
11602
+ createVoiceSessionsHTMLHandler,
11603
+ createVoiceSessionReplayRoutes,
11604
+ createVoiceSessionReplayJSONHandler,
11605
+ createVoiceSessionReplayHTMLHandler,
9387
11606
  createVoiceSessionRecord,
11607
+ createVoiceSessionListRoutes,
9388
11608
  createVoiceSession,
9389
11609
  createVoiceSTTRoutingCorrectionHandler,
9390
11610
  createVoiceSQLiteTraceSinkDeliveryStore,
@@ -9399,6 +11619,10 @@ export {
9399
11619
  createVoiceReviewSavedEvent,
9400
11620
  createVoiceRedisTaskLeaseCoordinator,
9401
11621
  createVoiceRedisIdempotencyStore,
11622
+ createVoiceProviderRouter,
11623
+ createVoiceProviderHealthRoutes,
11624
+ createVoiceProviderHealthJSONHandler,
11625
+ createVoiceProviderHealthHTMLHandler,
9402
11626
  createVoicePostgresTraceSinkDeliveryStore,
9403
11627
  createVoicePostgresTraceEventStore,
9404
11628
  createVoicePostgresTaskStore,
@@ -9407,6 +11631,9 @@ export {
9407
11631
  createVoicePostgresReviewStore,
9408
11632
  createVoicePostgresIntegrationEventStore,
9409
11633
  createVoicePostgresExternalObjectMapStore,
11634
+ createVoiceOpsWebhookSink,
11635
+ createVoiceOpsWebhookReceiverRoutes,
11636
+ createVoiceOpsWebhookEnvelope,
9410
11637
  createVoiceOpsTaskWorker,
9411
11638
  createVoiceOpsTaskProcessorWorkerLoop,
9412
11639
  createVoiceOpsTaskProcessorWorker,
@@ -9414,6 +11641,7 @@ export {
9414
11641
  createVoiceMemoryTraceSinkDeliveryStore,
9415
11642
  createVoiceMemoryTraceEventStore,
9416
11643
  createVoiceMemoryStore,
11644
+ createVoiceMemoryAssistantMemoryStore,
9417
11645
  createVoiceLinearIssueUpdateSink,
9418
11646
  createVoiceLinearIssueSyncSinks,
9419
11647
  createVoiceLinearIssueSink,
@@ -9425,6 +11653,9 @@ export {
9425
11653
  createVoiceHubSpotTaskSyncSinks,
9426
11654
  createVoiceHubSpotTaskSink,
9427
11655
  createVoiceHelpdeskTicketSink,
11656
+ createVoiceHandoffHealthRoutes,
11657
+ createVoiceHandoffHealthJSONHandler,
11658
+ createVoiceHandoffHealthHTMLHandler,
9428
11659
  createVoiceFileTraceSinkDeliveryStore,
9429
11660
  createVoiceFileTraceEventStore,
9430
11661
  createVoiceFileTaskStore,
@@ -9433,6 +11664,7 @@ export {
9433
11664
  createVoiceFileReviewStore,
9434
11665
  createVoiceFileIntegrationEventStore,
9435
11666
  createVoiceFileExternalObjectMapStore,
11667
+ createVoiceFileAssistantMemoryStore,
9436
11668
  createVoiceExternalObjectMapId,
9437
11669
  createVoiceExternalObjectMap,
9438
11670
  createVoiceExperiment,
@@ -9441,6 +11673,11 @@ export {
9441
11673
  createVoiceCallReviewFromLiveTelephonyReport,
9442
11674
  createVoiceCallCompletedEvent,
9443
11675
  createVoiceCRMActivitySink,
11676
+ createVoiceAssistantMemoryRecord,
11677
+ createVoiceAssistantMemoryHandle,
11678
+ createVoiceAssistantHealthRoutes,
11679
+ createVoiceAssistantHealthJSONHandler,
11680
+ createVoiceAssistantHealthHTMLHandler,
9444
11681
  createVoiceAssistant,
9445
11682
  createVoiceAgentTool,
9446
11683
  createVoiceAgentSquad,
@@ -9453,9 +11690,13 @@ export {
9453
11690
  createStoredVoiceCallReviewArtifact,
9454
11691
  createRiskyTurnCorrectionHandler,
9455
11692
  createPhraseHintCorrectionHandler,
11693
+ createOpenAIVoiceAssistantModel,
11694
+ createJSONVoiceAssistantModel,
9456
11695
  createId,
11696
+ createGeminiVoiceAssistantModel,
9457
11697
  createDomainPhraseHints,
9458
11698
  createDomainLexicon,
11699
+ createAnthropicVoiceAssistantModel,
9459
11700
  conditionAudioChunk,
9460
11701
  completeVoiceOpsTask,
9461
11702
  claimVoiceOpsTask,