@absolutejs/voice 0.0.22-beta.2 → 0.0.22-beta.20

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.
package/dist/index.js CHANGED
@@ -5150,6 +5150,17 @@ var createVoiceAgent = (options) => {
5150
5150
  if (output.assistantText?.trim()) {
5151
5151
  messages.push({
5152
5152
  content: output.assistantText,
5153
+ metadata: output.toolCalls?.length ? {
5154
+ toolCalls: output.toolCalls
5155
+ } : undefined,
5156
+ role: "assistant"
5157
+ });
5158
+ } else if (output.toolCalls?.length) {
5159
+ messages.push({
5160
+ content: "",
5161
+ metadata: {
5162
+ toolCalls: output.toolCalls
5163
+ },
5153
5164
  role: "assistant"
5154
5165
  });
5155
5166
  }
@@ -5594,6 +5605,112 @@ var resolveVoiceOutcomeRecipe = (name, options = {}) => {
5594
5605
  };
5595
5606
  };
5596
5607
 
5608
+ // src/assistantMemory.ts
5609
+ var createMemoryId = (input) => `${input.assistantId}:${input.namespace}:${input.key}`;
5610
+ var createVoiceAssistantMemoryRecord = (input) => {
5611
+ const now = Date.now();
5612
+ return {
5613
+ ...input,
5614
+ createdAt: input.createdAt ?? input.updatedAt ?? now,
5615
+ updatedAt: input.updatedAt ?? now
5616
+ };
5617
+ };
5618
+ var createVoiceMemoryAssistantMemoryStore = () => {
5619
+ const records = new Map;
5620
+ return {
5621
+ delete: async (input) => {
5622
+ records.delete(createMemoryId(input));
5623
+ },
5624
+ get: async (input) => records.get(createMemoryId(input)),
5625
+ 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),
5626
+ set: async (input) => {
5627
+ const id = createMemoryId(input);
5628
+ const existing = records.get(id);
5629
+ const record = createVoiceAssistantMemoryRecord({
5630
+ ...input,
5631
+ createdAt: input.createdAt ?? existing?.createdAt,
5632
+ updatedAt: input.updatedAt
5633
+ });
5634
+ records.set(id, record);
5635
+ return record;
5636
+ }
5637
+ };
5638
+ };
5639
+ var resolveVoiceAssistantMemoryNamespace = async (input) => typeof input.memory.namespace === "function" ? await input.memory.namespace(input) : input.memory.namespace;
5640
+ var createVoiceAssistantMemoryHandle = async (input) => {
5641
+ const namespace = await resolveVoiceAssistantMemoryNamespace({
5642
+ assistantId: input.assistantId,
5643
+ context: input.context,
5644
+ memory: input.memory,
5645
+ session: input.session
5646
+ });
5647
+ const trace = async (event) => {
5648
+ await input.trace?.append({
5649
+ at: Date.now(),
5650
+ payload: {
5651
+ assistantId: input.assistantId,
5652
+ namespace,
5653
+ ...event
5654
+ },
5655
+ scenarioId: input.session.scenarioId,
5656
+ sessionId: input.session.id,
5657
+ type: "assistant.memory"
5658
+ });
5659
+ };
5660
+ return {
5661
+ delete: async (key) => {
5662
+ await input.memory.store.delete({
5663
+ assistantId: input.assistantId,
5664
+ key,
5665
+ namespace
5666
+ });
5667
+ await trace({
5668
+ action: "delete",
5669
+ key
5670
+ });
5671
+ },
5672
+ get: async (key) => {
5673
+ const record = await input.memory.store.get({
5674
+ assistantId: input.assistantId,
5675
+ key,
5676
+ namespace
5677
+ });
5678
+ await trace({
5679
+ action: "get",
5680
+ found: Boolean(record),
5681
+ key
5682
+ });
5683
+ return record?.value;
5684
+ },
5685
+ list: async () => {
5686
+ const records = await input.memory.store.list({
5687
+ assistantId: input.assistantId,
5688
+ namespace
5689
+ });
5690
+ await trace({
5691
+ action: "list",
5692
+ count: records.length
5693
+ });
5694
+ return records;
5695
+ },
5696
+ namespace,
5697
+ set: async (key, value, metadata) => {
5698
+ const record = await input.memory.store.set({
5699
+ assistantId: input.assistantId,
5700
+ key,
5701
+ metadata,
5702
+ namespace,
5703
+ value
5704
+ });
5705
+ await trace({
5706
+ action: "set",
5707
+ key
5708
+ });
5709
+ return record;
5710
+ }
5711
+ };
5712
+ };
5713
+
5597
5714
  // src/assistant.ts
5598
5715
  var hashString = (value) => {
5599
5716
  let hash = 2166136261;
@@ -5603,6 +5720,47 @@ var hashString = (value) => {
5603
5720
  }
5604
5721
  return hash >>> 0;
5605
5722
  };
5723
+ var increment = (record, key) => {
5724
+ record[key] = (record[key] ?? 0) + 1;
5725
+ };
5726
+ var resolveOutcome = (result) => {
5727
+ if (result.transfer) {
5728
+ return "transferred";
5729
+ }
5730
+ if (result.escalate) {
5731
+ return "escalated";
5732
+ }
5733
+ if (result.voicemail) {
5734
+ return "voicemail";
5735
+ }
5736
+ if (result.noAnswer) {
5737
+ return "no-answer";
5738
+ }
5739
+ if (result.complete) {
5740
+ return "completed";
5741
+ }
5742
+ return "continued";
5743
+ };
5744
+ var resolveArtifactPlanName = (artifactPlan) => {
5745
+ const preset = artifactPlan?.preset;
5746
+ if (!preset) {
5747
+ return artifactPlan?.ops ? "custom" : undefined;
5748
+ }
5749
+ return typeof preset === "string" ? preset : preset.name;
5750
+ };
5751
+ var appendAssistantTrace = async (input) => {
5752
+ await input.trace?.append({
5753
+ at: Date.now(),
5754
+ payload: {
5755
+ assistantId: input.assistantId,
5756
+ ...input.event
5757
+ },
5758
+ scenarioId: input.session.scenarioId,
5759
+ sessionId: input.session.id,
5760
+ turnId: input.turnId,
5761
+ type: input.type
5762
+ });
5763
+ };
5606
5764
  var resolvePresetOps = (artifactPlan) => {
5607
5765
  const preset = artifactPlan?.preset;
5608
5766
  if (!preset) {
@@ -5671,6 +5829,7 @@ var createVoiceExperiment = (options) => {
5671
5829
  };
5672
5830
  var createVoiceAssistant = (options) => {
5673
5831
  const ops = mergeOps(resolvePresetOps(options.artifactPlan), options.ops);
5832
+ const artifactPlanName = resolveArtifactPlanName(options.artifactPlan);
5674
5833
  let agent;
5675
5834
  const baseModelOptions = "model" in options && options.model ? {
5676
5835
  maxToolRounds: options.maxToolRounds,
@@ -5700,14 +5859,63 @@ var createVoiceAssistant = (options) => {
5700
5859
  });
5701
5860
  }
5702
5861
  const onTurn = async (input) => {
5862
+ const memory = options.memory ? await createVoiceAssistantMemoryHandle({
5863
+ assistantId: options.id,
5864
+ context: input.context,
5865
+ memory: options.memory,
5866
+ session: input.session,
5867
+ trace: options.trace
5868
+ }) : undefined;
5703
5869
  const guardrailInput = {
5704
5870
  ...input,
5705
- assistantId: options.id
5871
+ assistantId: options.id,
5872
+ memory
5706
5873
  };
5874
+ if (memory) {
5875
+ await options.memoryLifecycle?.beforeTurn?.({
5876
+ ...input,
5877
+ assistantId: options.id,
5878
+ memory
5879
+ });
5880
+ }
5707
5881
  const blocked = await options.guardrails?.beforeTurn?.(guardrailInput);
5708
5882
  if (blocked) {
5883
+ if (memory) {
5884
+ await options.memoryLifecycle?.afterTurn?.({
5885
+ ...input,
5886
+ assistantId: options.id,
5887
+ memory,
5888
+ result: blocked
5889
+ });
5890
+ }
5891
+ await appendAssistantTrace({
5892
+ assistantId: options.id,
5893
+ event: {
5894
+ action: "blocked",
5895
+ artifactPlan: artifactPlanName,
5896
+ outcome: resolveOutcome(blocked)
5897
+ },
5898
+ session: input.session,
5899
+ trace: options.trace,
5900
+ turnId: input.turn.id,
5901
+ type: "assistant.guardrail"
5902
+ });
5903
+ await appendAssistantTrace({
5904
+ assistantId: options.id,
5905
+ event: {
5906
+ artifactPlan: artifactPlanName,
5907
+ blocked: true,
5908
+ experimentId: options.experiment?.id,
5909
+ outcome: resolveOutcome(blocked)
5910
+ },
5911
+ session: input.session,
5912
+ trace: options.trace,
5913
+ turnId: input.turn.id,
5914
+ type: "assistant.run"
5915
+ });
5709
5916
  return blocked;
5710
5917
  }
5918
+ const startedAt = Date.now();
5711
5919
  const variant = options.experiment?.resolve({
5712
5920
  assistantId: options.id,
5713
5921
  context: input.context,
@@ -5722,12 +5930,56 @@ var createVoiceAssistant = (options) => {
5722
5930
  trace: options.trace,
5723
5931
  tools: variant.tools ?? baseModelOptions.tools
5724
5932
  }) : agent;
5725
- const result = await runner.run(input) ?? {};
5933
+ const runResult = await runner.run(input) ?? {};
5934
+ const result = runResult;
5726
5935
  const guarded = await options.guardrails?.afterTurn?.({
5727
5936
  ...guardrailInput,
5728
5937
  result
5729
5938
  });
5730
- return guarded ?? result;
5939
+ const finalResult = guarded ?? result;
5940
+ if (memory) {
5941
+ await options.memoryLifecycle?.afterTurn?.({
5942
+ ...input,
5943
+ assistantId: options.id,
5944
+ memory,
5945
+ result: finalResult
5946
+ });
5947
+ }
5948
+ if (guarded) {
5949
+ await appendAssistantTrace({
5950
+ assistantId: options.id,
5951
+ event: {
5952
+ action: "rewritten",
5953
+ artifactPlan: artifactPlanName,
5954
+ experimentId: options.experiment?.id,
5955
+ outcome: resolveOutcome(finalResult),
5956
+ variantId: variant?.id
5957
+ },
5958
+ session: input.session,
5959
+ trace: options.trace,
5960
+ turnId: input.turn.id,
5961
+ type: "assistant.guardrail"
5962
+ });
5963
+ }
5964
+ await appendAssistantTrace({
5965
+ assistantId: options.id,
5966
+ event: {
5967
+ artifactPlan: artifactPlanName,
5968
+ blocked: false,
5969
+ elapsedMs: Date.now() - startedAt,
5970
+ escalated: Boolean(finalResult.escalate),
5971
+ experimentId: options.experiment?.id,
5972
+ outcome: resolveOutcome(finalResult),
5973
+ toolNames: result.toolResults?.map((tool) => tool.toolName) ?? [],
5974
+ transferred: Boolean(finalResult.transfer),
5975
+ variantId: variant?.id
5976
+ },
5977
+ session: input.session,
5978
+ trace: options.trace,
5979
+ turnId: input.turn.id,
5980
+ type: "assistant.run"
5981
+ });
5982
+ return finalResult;
5731
5983
  };
5732
5984
  return {
5733
5985
  agent,
@@ -5743,9 +5995,400 @@ var createVoiceAssistant = (options) => {
5743
5995
  })
5744
5996
  };
5745
5997
  };
5746
- // src/fileStore.ts
5747
- import { mkdir, readFile, readdir, rename, rm, writeFile } from "fs/promises";
5748
- import { join } from "path";
5998
+ var summarizeVoiceAssistantRuns = async (input) => {
5999
+ const events = Array.isArray(input) ? input : input.events ?? await input.store?.list() ?? [];
6000
+ const assistantRuns = events.filter((event) => event.type === "assistant.run");
6001
+ const guardrails = events.filter((event) => event.type === "assistant.guardrail");
6002
+ const byAssistant = new Map;
6003
+ const getSummary = (assistantId) => {
6004
+ let summary = byAssistant.get(assistantId);
6005
+ if (!summary) {
6006
+ summary = {
6007
+ assistantId,
6008
+ artifactPlans: {},
6009
+ blockedGuardrailCount: 0,
6010
+ elapsedCount: 0,
6011
+ elapsedTotal: 0,
6012
+ escalationCount: 0,
6013
+ experiments: {},
6014
+ guardrailCount: 0,
6015
+ memory: {
6016
+ deletes: 0,
6017
+ gets: 0,
6018
+ lists: 0,
6019
+ sets: 0
6020
+ },
6021
+ outcomes: {},
6022
+ runCount: 0,
6023
+ sessionIds: new Set,
6024
+ sessions: 0,
6025
+ toolCalls: {},
6026
+ transferCount: 0,
6027
+ variants: {}
6028
+ };
6029
+ byAssistant.set(assistantId, summary);
6030
+ }
6031
+ return summary;
6032
+ };
6033
+ for (const event of assistantRuns) {
6034
+ const assistantId = typeof event.payload.assistantId === "string" ? event.payload.assistantId : "unknown";
6035
+ const summary = getSummary(assistantId);
6036
+ summary.runCount += 1;
6037
+ summary.sessionIds.add(event.sessionId);
6038
+ if (typeof event.payload.artifactPlan === "string") {
6039
+ increment(summary.artifactPlans, event.payload.artifactPlan);
6040
+ }
6041
+ if (typeof event.payload.experimentId === "string") {
6042
+ increment(summary.experiments, event.payload.experimentId);
6043
+ }
6044
+ if (typeof event.payload.variantId === "string") {
6045
+ increment(summary.variants, event.payload.variantId);
6046
+ }
6047
+ if (typeof event.payload.outcome === "string") {
6048
+ increment(summary.outcomes, event.payload.outcome);
6049
+ }
6050
+ if (event.payload.escalated === true) {
6051
+ summary.escalationCount += 1;
6052
+ }
6053
+ if (event.payload.transferred === true) {
6054
+ summary.transferCount += 1;
6055
+ }
6056
+ if (event.payload.blocked === true) {
6057
+ summary.blockedGuardrailCount += 1;
6058
+ }
6059
+ if (typeof event.payload.elapsedMs === "number") {
6060
+ summary.elapsedCount += 1;
6061
+ summary.elapsedTotal += event.payload.elapsedMs;
6062
+ }
6063
+ if (Array.isArray(event.payload.toolNames)) {
6064
+ for (const toolName of event.payload.toolNames) {
6065
+ if (typeof toolName === "string") {
6066
+ increment(summary.toolCalls, toolName);
6067
+ }
6068
+ }
6069
+ }
6070
+ }
6071
+ for (const event of guardrails) {
6072
+ const assistantId = typeof event.payload.assistantId === "string" ? event.payload.assistantId : "unknown";
6073
+ const summary = getSummary(assistantId);
6074
+ summary.guardrailCount += 1;
6075
+ }
6076
+ for (const event of events.filter((event2) => event2.type === "assistant.memory")) {
6077
+ const assistantId = typeof event.payload.assistantId === "string" ? event.payload.assistantId : "unknown";
6078
+ const summary = getSummary(assistantId);
6079
+ switch (event.payload.action) {
6080
+ case "delete":
6081
+ summary.memory.deletes += 1;
6082
+ break;
6083
+ case "get":
6084
+ summary.memory.gets += 1;
6085
+ break;
6086
+ case "list":
6087
+ summary.memory.lists += 1;
6088
+ break;
6089
+ case "set":
6090
+ summary.memory.sets += 1;
6091
+ break;
6092
+ }
6093
+ }
6094
+ const assistants = [...byAssistant.values()].map(({ elapsedCount, elapsedTotal, sessionIds, ...summary }) => ({
6095
+ ...summary,
6096
+ averageElapsedMs: elapsedCount > 0 ? Math.round(elapsedTotal / elapsedCount) : undefined,
6097
+ sessions: sessionIds.size
6098
+ }));
6099
+ return {
6100
+ assistants: assistants.sort((left, right) => left.assistantId.localeCompare(right.assistantId)),
6101
+ totalRuns: assistantRuns.length
6102
+ };
6103
+ };
6104
+ // src/assistantHealth.ts
6105
+ import { Elysia as Elysia3 } from "elysia";
6106
+
6107
+ // src/providerHealth.ts
6108
+ import { Elysia as Elysia2 } from "elysia";
6109
+ var getString = (value) => typeof value === "string" ? value : undefined;
6110
+ var getNumber = (value) => typeof value === "number" && Number.isFinite(value) ? value : undefined;
6111
+ var isProviderStatus = (value) => value === "success" || value === "fallback" || value === "error";
6112
+ var summarizeVoiceProviderHealth = async (input) => {
6113
+ const options = Array.isArray(input) ? { events: input } : input;
6114
+ const events = options.events ?? await options.store?.list() ?? [];
6115
+ const providers = options.providers ?? [];
6116
+ const providerSet = new Set(providers);
6117
+ const now = options.now ?? Date.now();
6118
+ const entries = new Map;
6119
+ const isAllowedProvider = (value) => typeof value === "string" && (providerSet.size === 0 || providerSet.has(value));
6120
+ const getEntry = (provider) => {
6121
+ const existing = entries.get(provider);
6122
+ if (existing) {
6123
+ return existing;
6124
+ }
6125
+ const entry = {
6126
+ elapsedCount: 0,
6127
+ elapsedTotal: 0,
6128
+ errorCount: 0,
6129
+ fallbackCount: 0,
6130
+ provider,
6131
+ rateLimited: false,
6132
+ recommended: false,
6133
+ runCount: 0,
6134
+ status: "idle"
6135
+ };
6136
+ entries.set(provider, entry);
6137
+ return entry;
6138
+ };
6139
+ for (const provider of providers) {
6140
+ getEntry(provider);
6141
+ }
6142
+ const hasProviderRouterEvents = events.some((event) => event.type === "session.error" && isAllowedProvider(event.payload.provider) && isProviderStatus(event.payload.providerStatus));
6143
+ for (const event of events) {
6144
+ if (event.type === "assistant.run") {
6145
+ if (hasProviderRouterEvents) {
6146
+ continue;
6147
+ }
6148
+ const provider2 = event.payload.variantId;
6149
+ if (!isAllowedProvider(provider2)) {
6150
+ continue;
6151
+ }
6152
+ const entry2 = getEntry(provider2);
6153
+ entry2.runCount += 1;
6154
+ const elapsedMs = getNumber(event.payload.elapsedMs);
6155
+ if (elapsedMs !== undefined) {
6156
+ entry2.elapsedCount += 1;
6157
+ entry2.elapsedTotal += elapsedMs;
6158
+ }
6159
+ continue;
6160
+ }
6161
+ if (event.type !== "session.error") {
6162
+ continue;
6163
+ }
6164
+ const provider = event.payload.provider;
6165
+ if (!isAllowedProvider(provider)) {
6166
+ continue;
6167
+ }
6168
+ const providerStatus = isProviderStatus(event.payload.providerStatus) ? event.payload.providerStatus : undefined;
6169
+ const applyProviderHealth = () => {
6170
+ const entry2 = getEntry(provider);
6171
+ const providerHealth = event.payload.providerHealth;
6172
+ if (providerHealth && typeof providerHealth === "object") {
6173
+ const suppressedUntil2 = getNumber(providerHealth.suppressedUntil);
6174
+ if (suppressedUntil2 !== undefined) {
6175
+ entry2.suppressedUntil = suppressedUntil2;
6176
+ }
6177
+ }
6178
+ const suppressedUntil = getNumber(event.payload.suppressedUntil);
6179
+ if (suppressedUntil !== undefined) {
6180
+ entry2.suppressedUntil = suppressedUntil;
6181
+ }
6182
+ const suppressionRemainingMs = getNumber(event.payload.suppressionRemainingMs);
6183
+ if (suppressionRemainingMs !== undefined) {
6184
+ entry2.suppressionRemainingMs = suppressionRemainingMs;
6185
+ }
6186
+ return entry2;
6187
+ };
6188
+ if (providerStatus === "success" || providerStatus === "fallback") {
6189
+ const entry2 = applyProviderHealth();
6190
+ entry2.runCount += 1;
6191
+ entry2.lastSuccessAt = event.at;
6192
+ if (providerStatus === "success") {
6193
+ entry2.lastError = undefined;
6194
+ entry2.rateLimited = false;
6195
+ entry2.suppressedUntil = undefined;
6196
+ entry2.suppressionRemainingMs = undefined;
6197
+ }
6198
+ const elapsedMs = getNumber(event.payload.elapsedMs);
6199
+ if (elapsedMs !== undefined) {
6200
+ entry2.elapsedCount += 1;
6201
+ entry2.elapsedTotal += elapsedMs;
6202
+ }
6203
+ const selectedProvider = event.payload.selectedProvider;
6204
+ if (providerStatus === "fallback" && isAllowedProvider(selectedProvider) && selectedProvider !== provider) {
6205
+ getEntry(selectedProvider).fallbackCount += 1;
6206
+ }
6207
+ continue;
6208
+ }
6209
+ const entry = applyProviderHealth();
6210
+ entry.errorCount += 1;
6211
+ entry.lastError = getString(event.payload.error);
6212
+ entry.lastErrorAt = event.at;
6213
+ entry.rateLimited ||= event.payload.rateLimited === true;
6214
+ }
6215
+ const summaries = [...entries.values()].map((entry) => {
6216
+ const hadSuppression = typeof entry.suppressedUntil === "number" || typeof entry.suppressionRemainingMs === "number";
6217
+ const suppressionRemainingMs = typeof entry.suppressedUntil === "number" ? Math.max(0, entry.suppressedUntil - now) : entry.suppressionRemainingMs;
6218
+ const activeSuppression = typeof suppressionRemainingMs === "number" && suppressionRemainingMs > 0;
6219
+ const recoverable = hadSuppression && !activeSuppression;
6220
+ const averageElapsedMs = entry.elapsedCount > 0 ? Math.round(entry.elapsedTotal / entry.elapsedCount) : undefined;
6221
+ 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";
6222
+ return {
6223
+ averageElapsedMs,
6224
+ errorCount: entry.errorCount,
6225
+ fallbackCount: entry.fallbackCount,
6226
+ lastError: entry.lastError,
6227
+ lastErrorAt: entry.lastErrorAt,
6228
+ lastSuccessAt: entry.lastSuccessAt,
6229
+ provider: entry.provider,
6230
+ rateLimited: entry.rateLimited,
6231
+ recommended: false,
6232
+ runCount: entry.runCount,
6233
+ status,
6234
+ suppressionRemainingMs: activeSuppression ? suppressionRemainingMs : undefined,
6235
+ suppressedUntil: entry.suppressedUntil
6236
+ };
6237
+ });
6238
+ const recommended = summaries.filter((entry) => entry.status === "healthy").sort((left, right) => (left.averageElapsedMs ?? Number.MAX_SAFE_INTEGER) - (right.averageElapsedMs ?? Number.MAX_SAFE_INTEGER))[0];
6239
+ if (recommended) {
6240
+ recommended.recommended = true;
6241
+ }
6242
+ return summaries;
6243
+ };
6244
+ var escapeHtml3 = (value) => value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
6245
+ var renderVoiceProviderHealthHTML = (providers) => providers.length === 0 ? '<p class="voice-provider-empty">No provider status yet.</p>' : [
6246
+ '<div class="voice-provider-health">',
6247
+ ...providers.map((provider) => {
6248
+ const suppressionSeconds = typeof provider.suppressionRemainingMs === "number" ? Math.ceil(provider.suppressionRemainingMs / 1000) : undefined;
6249
+ return [
6250
+ `<article class="voice-provider-card ${escapeHtml3(provider.status)}">`,
6251
+ '<div class="voice-provider-card-header">',
6252
+ `<strong>${escapeHtml3(provider.provider)}</strong>`,
6253
+ `<span>${escapeHtml3(provider.status)}${provider.recommended ? " \xB7 recommended" : ""}</span>`,
6254
+ "</div>",
6255
+ "<dl>",
6256
+ `<div><dt>Runs</dt><dd>${String(provider.runCount)}</dd></div>`,
6257
+ `<div><dt>Avg latency</dt><dd>${String(provider.averageElapsedMs ?? 0)}ms</dd></div>`,
6258
+ `<div><dt>Errors</dt><dd>${String(provider.errorCount)}</dd></div>`,
6259
+ `<div><dt>Fallbacks</dt><dd>${String(provider.fallbackCount)}</dd></div>`,
6260
+ "</dl>",
6261
+ suppressionSeconds ? `<p>Temporarily suppressed for ${String(suppressionSeconds)}s.</p>` : "",
6262
+ provider.lastError ? `<p>${escapeHtml3(provider.lastError)}</p>` : "",
6263
+ "</article>"
6264
+ ].join("");
6265
+ }),
6266
+ "</div>"
6267
+ ].join("");
6268
+ var createVoiceProviderHealthJSONHandler = (options) => async () => summarizeVoiceProviderHealth(options);
6269
+ var createVoiceProviderHealthHTMLHandler = (options) => async () => {
6270
+ const providers = await summarizeVoiceProviderHealth(options);
6271
+ const render = options.render ?? renderVoiceProviderHealthHTML;
6272
+ const body = await render(providers);
6273
+ return new Response(body, {
6274
+ headers: {
6275
+ "Content-Type": "text/html; charset=utf-8",
6276
+ ...options.headers
6277
+ }
6278
+ });
6279
+ };
6280
+ var createVoiceProviderHealthRoutes = (options) => {
6281
+ const path = options.path ?? "/api/provider-status";
6282
+ const htmlPath = options.htmlPath === undefined ? `${path}/htmx` : options.htmlPath;
6283
+ const routes = new Elysia2({
6284
+ name: options.name ?? "absolutejs-voice-provider-health"
6285
+ }).get(path, createVoiceProviderHealthJSONHandler(options));
6286
+ if (htmlPath) {
6287
+ routes.get(htmlPath, createVoiceProviderHealthHTMLHandler(options));
6288
+ }
6289
+ return routes;
6290
+ };
6291
+
6292
+ // src/assistantHealth.ts
6293
+ var escapeHtml4 = (value) => value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
6294
+ var renderCountMap = (values) => {
6295
+ const entries = Object.entries(values).sort((left, right) => right[1] - left[1]);
6296
+ if (entries.length === 0) {
6297
+ return '<p class="voice-assistant-health-empty">No data yet.</p>';
6298
+ }
6299
+ return [
6300
+ '<div class="voice-assistant-health-metrics">',
6301
+ ...entries.map(([label, value]) => `<div><span>${escapeHtml4(label)}</span><strong>${String(value)}</strong></div>`),
6302
+ "</div>"
6303
+ ].join("");
6304
+ };
6305
+ var getString2 = (value) => typeof value === "string" ? value : undefined;
6306
+ var getRecentFailures = (events, maxFailures) => 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) => ({
6307
+ at: event.at,
6308
+ assistantId: getString2(event.payload.assistantId),
6309
+ error: getString2(event.payload.error),
6310
+ provider: getString2(event.payload.provider),
6311
+ rateLimited: event.payload.rateLimited === true ? true : undefined,
6312
+ sessionId: event.sessionId,
6313
+ status: getString2(event.payload.providerStatus),
6314
+ turnId: event.turnId,
6315
+ type: event.type
6316
+ }));
6317
+ var summarizeVoiceAssistantHealth = async (options) => {
6318
+ const events = options.events ?? await options.store?.list() ?? [];
6319
+ return {
6320
+ assistantRuns: await summarizeVoiceAssistantRuns({ events }),
6321
+ providerHealth: await summarizeVoiceProviderHealth({
6322
+ events,
6323
+ providers: options.providers
6324
+ }),
6325
+ recentFailures: getRecentFailures(events, options.maxFailures ?? 8)
6326
+ };
6327
+ };
6328
+ var renderVoiceAssistantHealthHTML = (summary) => {
6329
+ const assistant = summary.assistantRuns.assistants[0];
6330
+ const failures = summary.recentFailures;
6331
+ return [
6332
+ '<div class="voice-assistant-health">',
6333
+ '<section class="voice-assistant-health-grid">',
6334
+ `<article><span>Runs</span><strong>${String(assistant?.runCount ?? 0)}</strong></article>`,
6335
+ `<article><span>Sessions</span><strong>${String(assistant?.sessions ?? 0)}</strong></article>`,
6336
+ `<article><span>Guardrails</span><strong>${String(assistant?.guardrailCount ?? 0)}</strong></article>`,
6337
+ `<article><span>Avg latency</span><strong>${String(assistant?.averageElapsedMs ?? 0)}ms</strong></article>`,
6338
+ "</section>",
6339
+ "<section>",
6340
+ "<h3>Provider Health</h3>",
6341
+ renderVoiceProviderHealthHTML(summary.providerHealth),
6342
+ "</section>",
6343
+ '<section class="voice-assistant-health-columns">',
6344
+ `<article><h3>Outcomes</h3>${renderCountMap(assistant?.outcomes ?? {})}</article>`,
6345
+ `<article><h3>Variants</h3>${renderCountMap(assistant?.variants ?? {})}</article>`,
6346
+ `<article><h3>Tools</h3>${renderCountMap(assistant?.toolCalls ?? {})}</article>`,
6347
+ `<article><h3>Artifact Plans</h3>${renderCountMap(assistant?.artifactPlans ?? {})}</article>`,
6348
+ "</section>",
6349
+ "<section>",
6350
+ "<h3>Recent Failures</h3>",
6351
+ failures.length === 0 ? '<p class="voice-assistant-health-empty">No failures yet.</p>' : [
6352
+ '<div class="voice-assistant-health-failures">',
6353
+ ...failures.map((failure) => [
6354
+ "<article>",
6355
+ `<strong>${escapeHtml4(failure.provider ?? failure.assistantId ?? failure.type)}</strong>`,
6356
+ `<span>${escapeHtml4(failure.status ?? (failure.rateLimited ? "rate-limited" : "error"))}</span>`,
6357
+ failure.error ? `<p>${escapeHtml4(failure.error)}</p>` : "",
6358
+ `<small>${escapeHtml4(failure.sessionId)}${failure.turnId ? ` / ${escapeHtml4(failure.turnId)}` : ""}</small>`,
6359
+ "</article>"
6360
+ ].join("")),
6361
+ "</div>"
6362
+ ].join(""),
6363
+ "</section>",
6364
+ "</div>"
6365
+ ].join("");
6366
+ };
6367
+ var createVoiceAssistantHealthJSONHandler = (options) => async () => summarizeVoiceAssistantHealth(options);
6368
+ var createVoiceAssistantHealthHTMLHandler = (options) => async () => {
6369
+ const summary = await summarizeVoiceAssistantHealth(options);
6370
+ const render = options.render ?? renderVoiceAssistantHealthHTML;
6371
+ const body = await render(summary);
6372
+ return new Response(body, {
6373
+ headers: {
6374
+ "Content-Type": "text/html; charset=utf-8",
6375
+ ...options.headers
6376
+ }
6377
+ });
6378
+ };
6379
+ var createVoiceAssistantHealthRoutes = (options) => {
6380
+ const path = options.path ?? "/api/assistant-health";
6381
+ const htmlPath = options.htmlPath === undefined ? `${path}/htmx` : options.htmlPath;
6382
+ const routes = new Elysia3({
6383
+ name: options.name ?? "absolutejs-voice-assistant-health"
6384
+ }).get(path, createVoiceAssistantHealthJSONHandler(options));
6385
+ if (htmlPath) {
6386
+ routes.get(htmlPath, createVoiceAssistantHealthHTMLHandler(options));
6387
+ }
6388
+ return routes;
6389
+ };
6390
+ // src/sessionReplay.ts
6391
+ import { Elysia as Elysia4 } from "elysia";
5749
6392
 
5750
6393
  // src/trace.ts
5751
6394
  var createVoiceTraceEventId = (event) => [
@@ -6079,7 +6722,7 @@ var exportVoiceTrace = async (input) => {
6079
6722
  };
6080
6723
  };
6081
6724
  var toNumber = (value) => typeof value === "number" && Number.isFinite(value) ? value : 0;
6082
- var escapeHtml3 = (value) => value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
6725
+ var escapeHtml5 = (value) => value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
6083
6726
  var formatTraceValue = (value) => {
6084
6727
  if (value === undefined || value === null) {
6085
6728
  return "";
@@ -6357,10 +7000,10 @@ var renderVoiceTraceHTML = (events, options = {}) => {
6357
7000
  const offset = summary.startedAt === undefined ? event.at : Math.max(0, event.at - summary.startedAt);
6358
7001
  return [
6359
7002
  "<tr>",
6360
- `<td>${escapeHtml3(String(offset))}</td>`,
6361
- `<td>${escapeHtml3(event.type)}</td>`,
6362
- `<td>${escapeHtml3(event.turnId ?? "")}</td>`,
6363
- `<td><code>${escapeHtml3(JSON.stringify(event.payload))}</code></td>`,
7003
+ `<td>${escapeHtml5(String(offset))}</td>`,
7004
+ `<td>${escapeHtml5(event.type)}</td>`,
7005
+ `<td>${escapeHtml5(event.turnId ?? "")}</td>`,
7006
+ `<td><code>${escapeHtml5(JSON.stringify(event.payload))}</code></td>`,
6364
7007
  "</tr>"
6365
7008
  ].join("");
6366
7009
  }).join(`
@@ -6371,7 +7014,7 @@ var renderVoiceTraceHTML = (events, options = {}) => {
6371
7014
  "<head>",
6372
7015
  '<meta charset="utf-8" />',
6373
7016
  '<meta name="viewport" content="width=device-width, initial-scale=1" />',
6374
- `<title>${escapeHtml3(options.title ?? "Voice Trace")}</title>`,
7017
+ `<title>${escapeHtml5(options.title ?? "Voice Trace")}</title>`,
6375
7018
  "<style>",
6376
7019
  "body{font-family:ui-sans-serif,system-ui,sans-serif;margin:2rem;line-height:1.45;background:#f8f7f2;color:#181713}",
6377
7020
  "main{max-width:1100px;margin:auto}",
@@ -6385,7 +7028,7 @@ var renderVoiceTraceHTML = (events, options = {}) => {
6385
7028
  "</style>",
6386
7029
  "</head>",
6387
7030
  "<body><main>",
6388
- `<h1>${escapeHtml3(options.title ?? `Voice Trace ${summary.sessionId ?? ""}`.trim())}</h1>`,
7031
+ `<h1>${escapeHtml5(options.title ?? `Voice Trace ${summary.sessionId ?? ""}`.trim())}</h1>`,
6389
7032
  `<p class="${evaluation.pass ? "pass" : "fail"}">QA: ${evaluation.pass ? "pass" : "fail"}</p>`,
6390
7033
  '<section class="summary">',
6391
7034
  `<div class="card"><strong>Events</strong><br>${summary.eventCount}</div>`,
@@ -6399,7 +7042,7 @@ var renderVoiceTraceHTML = (events, options = {}) => {
6399
7042
  eventRows,
6400
7043
  "</tbody></table>",
6401
7044
  "<h2>Markdown Export</h2>",
6402
- `<pre>${escapeHtml3(markdown)}</pre>`,
7045
+ `<pre>${escapeHtml5(markdown)}</pre>`,
6403
7046
  "</main></body></html>"
6404
7047
  ].join(`
6405
7048
  `);
@@ -6411,7 +7054,119 @@ var buildVoiceTraceReplay = (events, options = {}) => ({
6411
7054
  summary: summarizeVoiceTrace(options.redact ? redactVoiceTraceEvents(events, options.redact) : events)
6412
7055
  });
6413
7056
 
7057
+ // src/sessionReplay.ts
7058
+ var getString3 = (value) => typeof value === "string" ? value : undefined;
7059
+ var buildReplayTurns = (events) => {
7060
+ const turns = new Map;
7061
+ const getTurn = (turnId) => {
7062
+ const existing = turns.get(turnId);
7063
+ if (existing) {
7064
+ return existing;
7065
+ }
7066
+ const turn = {
7067
+ assistantReplies: [],
7068
+ errors: [],
7069
+ id: turnId,
7070
+ modelCalls: [],
7071
+ tools: [],
7072
+ transcripts: []
7073
+ };
7074
+ turns.set(turnId, turn);
7075
+ return turn;
7076
+ };
7077
+ for (const event of events) {
7078
+ const turnId = event.turnId ?? "session";
7079
+ const turn = getTurn(turnId);
7080
+ switch (event.type) {
7081
+ case "turn.transcript":
7082
+ turn.transcripts.push({
7083
+ isFinal: event.payload.isFinal === true,
7084
+ text: getString3(event.payload.text)
7085
+ });
7086
+ break;
7087
+ case "turn.committed":
7088
+ turn.committedText = getString3(event.payload.text);
7089
+ break;
7090
+ case "turn.assistant": {
7091
+ const text = getString3(event.payload.text);
7092
+ if (text) {
7093
+ turn.assistantReplies.push(text);
7094
+ }
7095
+ break;
7096
+ }
7097
+ case "agent.model":
7098
+ case "assistant.run":
7099
+ turn.modelCalls.push(event.payload);
7100
+ break;
7101
+ case "agent.tool":
7102
+ turn.tools.push(event.payload);
7103
+ break;
7104
+ case "session.error":
7105
+ turn.errors.push(event.payload);
7106
+ break;
7107
+ }
7108
+ }
7109
+ return [...turns.values()];
7110
+ };
7111
+ var summarizeVoiceSessionReplay = async (options) => {
7112
+ const sourceEvents = options.events ?? await options.store?.list({ sessionId: options.sessionId }) ?? [];
7113
+ const events = filterVoiceTraceEvents(sourceEvents, {
7114
+ sessionId: options.sessionId
7115
+ });
7116
+ const replay = buildVoiceTraceReplay(events, {
7117
+ evaluation: options.evaluation,
7118
+ redact: options.redact,
7119
+ title: options.title ?? `Voice Session ${options.sessionId}`
7120
+ });
7121
+ const startedAt = replay.summary.startedAt;
7122
+ return {
7123
+ evaluation: replay.evaluation,
7124
+ events,
7125
+ html: replay.html,
7126
+ markdown: replay.markdown,
7127
+ sessionId: options.sessionId,
7128
+ summary: replay.summary,
7129
+ timeline: events.map((event) => ({
7130
+ at: event.at,
7131
+ offsetMs: startedAt === undefined ? undefined : Math.max(0, event.at - startedAt),
7132
+ payload: event.payload,
7133
+ turnId: event.turnId,
7134
+ type: event.type
7135
+ })),
7136
+ turns: buildReplayTurns(events)
7137
+ };
7138
+ };
7139
+ var createVoiceSessionReplayJSONHandler = (options) => async ({ params }) => summarizeVoiceSessionReplay({
7140
+ ...options,
7141
+ sessionId: params.sessionId ?? ""
7142
+ });
7143
+ var createVoiceSessionReplayHTMLHandler = (options) => async ({ params }) => {
7144
+ const replay = await summarizeVoiceSessionReplay({
7145
+ ...options,
7146
+ sessionId: params.sessionId ?? ""
7147
+ });
7148
+ const body = await (options.render?.(replay) ?? replay.html);
7149
+ return new Response(body, {
7150
+ headers: {
7151
+ "Content-Type": "text/html; charset=utf-8",
7152
+ ...options.headers
7153
+ }
7154
+ });
7155
+ };
7156
+ var createVoiceSessionReplayRoutes = (options) => {
7157
+ const path = options.path ?? "/api/voice-sessions/:sessionId/replay";
7158
+ const htmlPath = options.htmlPath === undefined ? `${path}/htmx` : options.htmlPath;
7159
+ const routes = new Elysia4({
7160
+ name: options.name ?? "absolutejs-voice-session-replay"
7161
+ }).get(path, createVoiceSessionReplayJSONHandler(options));
7162
+ if (htmlPath) {
7163
+ routes.get(htmlPath, createVoiceSessionReplayHTMLHandler(options));
7164
+ }
7165
+ return routes;
7166
+ };
6414
7167
  // src/fileStore.ts
7168
+ import { mkdir, readFile, readdir, rename, rm, writeFile } from "fs/promises";
7169
+ import { join } from "path";
6415
7170
  var listJsonFiles = async (directory) => {
6416
7171
  try {
6417
7172
  const entries = await readdir(directory, {
@@ -6427,6 +7182,7 @@ var listJsonFiles = async (directory) => {
6427
7182
  };
6428
7183
  var encodeStoreId = (id) => `${encodeURIComponent(id)}.json`;
6429
7184
  var resolveFilePath = (directory, id) => join(directory, encodeStoreId(id));
7185
+ var createMemoryStoreId = (input) => `${input.assistantId}:${input.namespace}:${input.key}`;
6430
7186
  var readJsonFile = async (path) => JSON.parse(await readFile(path, "utf8"));
6431
7187
  var writeJsonFile = async (path, value, options) => {
6432
7188
  await mkdir(options.directory, {
@@ -6646,6 +7402,40 @@ var createVoiceFileTraceSinkDeliveryStore = (options) => {
6646
7402
  };
6647
7403
  return { get, list, remove, set };
6648
7404
  };
7405
+ var createVoiceFileAssistantMemoryStore = (options) => {
7406
+ const get = async (input) => {
7407
+ const path = resolveFilePath(options.directory, createMemoryStoreId(input));
7408
+ try {
7409
+ return await readJsonFile(path);
7410
+ } catch (error) {
7411
+ if (error.code === "ENOENT") {
7412
+ return;
7413
+ }
7414
+ throw error;
7415
+ }
7416
+ };
7417
+ const list = async (input) => {
7418
+ const files = await listJsonFiles(options.directory);
7419
+ const records = await Promise.all(files.map((file) => readJsonFile(file)));
7420
+ return records.filter((record) => record.assistantId === input.assistantId && (input.namespace === undefined || record.namespace === input.namespace)).sort((left, right) => right.updatedAt - left.updatedAt);
7421
+ };
7422
+ const set = async (input) => {
7423
+ const existing = await get(input);
7424
+ const record = createVoiceAssistantMemoryRecord({
7425
+ ...input,
7426
+ createdAt: input.createdAt ?? existing?.createdAt,
7427
+ updatedAt: input.updatedAt
7428
+ });
7429
+ await writeJsonFile(resolveFilePath(options.directory, createMemoryStoreId(record)), record, options);
7430
+ return record;
7431
+ };
7432
+ const remove = async (input) => {
7433
+ await rm(resolveFilePath(options.directory, createMemoryStoreId(input)), {
7434
+ force: true
7435
+ });
7436
+ };
7437
+ return { delete: remove, get, list, set };
7438
+ };
6649
7439
  var createVoiceFileRuntimeStorage = (options) => ({
6650
7440
  events: createVoiceFileIntegrationEventStore({
6651
7441
  ...options,
@@ -6655,6 +7445,10 @@ var createVoiceFileRuntimeStorage = (options) => ({
6655
7445
  ...options,
6656
7446
  directory: join(options.directory, "external-objects")
6657
7447
  }),
7448
+ memories: createVoiceFileAssistantMemoryStore({
7449
+ ...options,
7450
+ directory: join(options.directory, "memories")
7451
+ }),
6658
7452
  reviews: createVoiceFileReviewStore({
6659
7453
  ...options,
6660
7454
  directory: join(options.directory, "reviews")
@@ -6687,6 +7481,777 @@ var createStoredVoiceExternalObjectMap = (mapping) => createVoiceExternalObjectM
6687
7481
  sourceId: mapping.sourceId,
6688
7482
  sourceType: mapping.sourceType
6689
7483
  });
7484
+ // src/modelAdapters.ts
7485
+ var OUTPUT_SCHEMA = {
7486
+ additionalProperties: false,
7487
+ properties: {
7488
+ assistantText: {
7489
+ type: "string"
7490
+ },
7491
+ complete: {
7492
+ type: "boolean"
7493
+ },
7494
+ escalate: {
7495
+ additionalProperties: false,
7496
+ properties: {
7497
+ metadata: {
7498
+ additionalProperties: true,
7499
+ type: "object"
7500
+ },
7501
+ reason: {
7502
+ type: "string"
7503
+ }
7504
+ },
7505
+ required: ["reason"],
7506
+ type: "object"
7507
+ },
7508
+ noAnswer: {
7509
+ additionalProperties: false,
7510
+ properties: {
7511
+ metadata: {
7512
+ additionalProperties: true,
7513
+ type: "object"
7514
+ }
7515
+ },
7516
+ type: "object"
7517
+ },
7518
+ result: {
7519
+ additionalProperties: true,
7520
+ type: "object"
7521
+ },
7522
+ transfer: {
7523
+ additionalProperties: false,
7524
+ properties: {
7525
+ metadata: {
7526
+ additionalProperties: true,
7527
+ type: "object"
7528
+ },
7529
+ reason: {
7530
+ type: "string"
7531
+ },
7532
+ target: {
7533
+ type: "string"
7534
+ }
7535
+ },
7536
+ required: ["target"],
7537
+ type: "object"
7538
+ },
7539
+ voicemail: {
7540
+ additionalProperties: false,
7541
+ properties: {
7542
+ metadata: {
7543
+ additionalProperties: true,
7544
+ type: "object"
7545
+ }
7546
+ },
7547
+ type: "object"
7548
+ }
7549
+ },
7550
+ type: "object"
7551
+ };
7552
+ 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.";
7553
+ var stripJSONCodeFence = (value) => {
7554
+ const trimmed = value.trim();
7555
+ const match = trimmed.match(/^```(?:json)?\s*([\s\S]*?)\s*```$/i);
7556
+ return match?.[1]?.trim() ?? value;
7557
+ };
7558
+ var parseJSON = (value) => {
7559
+ try {
7560
+ const parsed = JSON.parse(stripJSONCodeFence(value));
7561
+ return parsed && typeof parsed === "object" ? parsed : {};
7562
+ } catch {
7563
+ return {
7564
+ assistantText: value
7565
+ };
7566
+ }
7567
+ };
7568
+ var parseJSONValue = (value) => {
7569
+ try {
7570
+ return JSON.parse(value);
7571
+ } catch {
7572
+ return value;
7573
+ }
7574
+ };
7575
+ var getMessageToolCalls = (message) => {
7576
+ const toolCalls = message.metadata?.toolCalls;
7577
+ return Array.isArray(toolCalls) ? toolCalls.filter((toolCall) => toolCall && typeof toolCall === "object" && typeof toolCall.name === "string") : [];
7578
+ };
7579
+ var createHTTPError = (provider, response) => new Error(`${provider} voice assistant model failed: HTTP ${response.status}`);
7580
+ var sleep4 = (ms) => new Promise((resolve2) => {
7581
+ setTimeout(resolve2, ms);
7582
+ });
7583
+ var errorMessage = (error) => error instanceof Error ? error.message : String(error);
7584
+ var defaultIsRateLimitError = (error) => /(\b429\b|rate limit|quota|too many requests)/i.test(errorMessage(error));
7585
+ var normalizeRouteOutput = (output) => {
7586
+ const result = {};
7587
+ if (typeof output.assistantText === "string") {
7588
+ result.assistantText = output.assistantText;
7589
+ }
7590
+ if (typeof output.complete === "boolean") {
7591
+ result.complete = output.complete;
7592
+ }
7593
+ if (output.result !== undefined) {
7594
+ result.result = output.result;
7595
+ }
7596
+ if (output.transfer && typeof output.transfer === "object") {
7597
+ const transfer = output.transfer;
7598
+ if (typeof transfer.target === "string") {
7599
+ result.transfer = {
7600
+ metadata: transfer.metadata && typeof transfer.metadata === "object" ? transfer.metadata : undefined,
7601
+ reason: typeof transfer.reason === "string" ? transfer.reason : undefined,
7602
+ target: transfer.target
7603
+ };
7604
+ }
7605
+ }
7606
+ if (output.escalate && typeof output.escalate === "object") {
7607
+ const escalate = output.escalate;
7608
+ if (typeof escalate.reason === "string") {
7609
+ result.escalate = {
7610
+ metadata: escalate.metadata && typeof escalate.metadata === "object" ? escalate.metadata : undefined,
7611
+ reason: escalate.reason
7612
+ };
7613
+ }
7614
+ }
7615
+ if (output.voicemail && typeof output.voicemail === "object") {
7616
+ const voicemail = output.voicemail;
7617
+ result.voicemail = {
7618
+ metadata: voicemail.metadata && typeof voicemail.metadata === "object" ? voicemail.metadata : undefined
7619
+ };
7620
+ }
7621
+ if (output.noAnswer && typeof output.noAnswer === "object") {
7622
+ const noAnswer = output.noAnswer;
7623
+ result.noAnswer = {
7624
+ metadata: noAnswer.metadata && typeof noAnswer.metadata === "object" ? noAnswer.metadata : undefined
7625
+ };
7626
+ }
7627
+ return result;
7628
+ };
7629
+ var createJSONVoiceAssistantModel = (options) => ({
7630
+ generate: async (input) => {
7631
+ const output = await options.generate(input);
7632
+ if ("assistantText" in output || "toolCalls" in output || "complete" in output || "transfer" in output || "escalate" in output) {
7633
+ return output;
7634
+ }
7635
+ return options.mapOutput?.(output) ?? normalizeRouteOutput(output);
7636
+ }
7637
+ });
7638
+ var createVoiceProviderRouter = (options) => {
7639
+ const providerIds = Object.keys(options.providers);
7640
+ const firstProvider = providerIds[0];
7641
+ const policy = typeof options.policy === "string" ? {
7642
+ strategy: options.policy
7643
+ } : options.policy;
7644
+ const strategy = policy?.strategy ?? "prefer-selected";
7645
+ const fallbackMode = policy?.fallbackMode ?? options.fallbackMode ?? "provider-error";
7646
+ const healthOptions = typeof options.providerHealth === "object" ? options.providerHealth : options.providerHealth ? {} : undefined;
7647
+ const healthState = new Map;
7648
+ const now = () => healthOptions?.now?.() ?? Date.now();
7649
+ const failureThreshold = Math.max(1, healthOptions?.failureThreshold ?? 1);
7650
+ const cooldownMs = Math.max(0, healthOptions?.cooldownMs ?? 30000);
7651
+ const rateLimitCooldownMs = Math.max(0, healthOptions?.rateLimitCooldownMs ?? 60000);
7652
+ const getHealth = (provider) => {
7653
+ const existing = healthState.get(provider);
7654
+ if (existing) {
7655
+ return existing;
7656
+ }
7657
+ const next = {
7658
+ consecutiveFailures: 0,
7659
+ provider,
7660
+ status: "healthy"
7661
+ };
7662
+ healthState.set(provider, next);
7663
+ return next;
7664
+ };
7665
+ const cloneHealth = (provider) => {
7666
+ if (!healthOptions) {
7667
+ return;
7668
+ }
7669
+ return {
7670
+ ...getHealth(provider)
7671
+ };
7672
+ };
7673
+ const getSuppressionRemainingMs = (provider) => {
7674
+ if (!healthOptions) {
7675
+ return;
7676
+ }
7677
+ const suppressedUntil = getHealth(provider).suppressedUntil;
7678
+ return typeof suppressedUntil === "number" ? Math.max(0, suppressedUntil - now()) : undefined;
7679
+ };
7680
+ const isSuppressed = (provider) => {
7681
+ if (!healthOptions) {
7682
+ return false;
7683
+ }
7684
+ const health = getHealth(provider);
7685
+ return typeof health.suppressedUntil === "number" && health.suppressedUntil > now();
7686
+ };
7687
+ const recordProviderSuccess = (provider) => {
7688
+ if (!healthOptions) {
7689
+ return;
7690
+ }
7691
+ const health = getHealth(provider);
7692
+ health.consecutiveFailures = 0;
7693
+ health.status = "healthy";
7694
+ health.suppressedUntil = undefined;
7695
+ return cloneHealth(provider);
7696
+ };
7697
+ const recordProviderError = (provider, isProviderError, rateLimited) => {
7698
+ if (!healthOptions || !isProviderError) {
7699
+ return cloneHealth(provider);
7700
+ }
7701
+ const currentTime = now();
7702
+ const health = getHealth(provider);
7703
+ health.consecutiveFailures += 1;
7704
+ health.lastFailureAt = currentTime;
7705
+ if (rateLimited) {
7706
+ health.lastRateLimitedAt = currentTime;
7707
+ }
7708
+ if (rateLimited || health.consecutiveFailures >= failureThreshold) {
7709
+ health.status = "suppressed";
7710
+ health.suppressedUntil = currentTime + (rateLimited ? rateLimitCooldownMs : cooldownMs);
7711
+ }
7712
+ return cloneHealth(provider);
7713
+ };
7714
+ const resolveAllowedProviders = async (input) => {
7715
+ const allowProviders = policy?.allowProviders ?? options.allowProviders;
7716
+ const allowed = typeof allowProviders === "function" ? await allowProviders(input) : allowProviders;
7717
+ return new Set(allowed ?? providerIds);
7718
+ };
7719
+ const sortProviders = (providers) => {
7720
+ if (strategy !== "prefer-cheapest" && strategy !== "prefer-fastest") {
7721
+ return providers;
7722
+ }
7723
+ return [...providers].sort((left, right) => {
7724
+ const leftProfile = options.providerProfiles?.[left];
7725
+ const rightProfile = options.providerProfiles?.[right];
7726
+ const leftValue = strategy === "prefer-cheapest" ? leftProfile?.cost ?? Number.MAX_SAFE_INTEGER : leftProfile?.latencyMs ?? Number.MAX_SAFE_INTEGER;
7727
+ const rightValue = strategy === "prefer-cheapest" ? rightProfile?.cost ?? Number.MAX_SAFE_INTEGER : rightProfile?.latencyMs ?? Number.MAX_SAFE_INTEGER;
7728
+ return leftValue - rightValue || (leftProfile?.priority ?? Number.MAX_SAFE_INTEGER) - (rightProfile?.priority ?? Number.MAX_SAFE_INTEGER);
7729
+ });
7730
+ };
7731
+ const resolveOrder = async (input) => {
7732
+ const selectedProvider = await options.selectProvider?.(input);
7733
+ const allowedProviders = await resolveAllowedProviders(input);
7734
+ const fallbackOrder = typeof options.fallback === "function" ? await options.fallback(input) : options.fallback;
7735
+ const rankedProviders = sortProviders([
7736
+ ...fallbackOrder ?? providerIds
7737
+ ]).filter((provider) => allowedProviders.has(provider));
7738
+ const healthyRankedProviders = healthOptions ? rankedProviders.filter((provider) => !isSuppressed(provider)) : rankedProviders;
7739
+ const candidateRankedProviders = healthyRankedProviders.length ? healthyRankedProviders : rankedProviders;
7740
+ const preferred = selectedProvider && allowedProviders.has(selectedProvider) && (!healthOptions || !isSuppressed(selectedProvider)) ? selectedProvider : candidateRankedProviders[0] ?? firstProvider;
7741
+ const seen = new Set;
7742
+ const order = [];
7743
+ const candidates = strategy === "ordered" ? candidateRankedProviders : [
7744
+ preferred,
7745
+ ...candidateRankedProviders,
7746
+ ...providerIds.filter((provider) => !healthOptions || !isSuppressed(provider))
7747
+ ];
7748
+ for (const provider of candidates) {
7749
+ if (!provider || seen.has(provider) || !allowedProviders.has(provider) || !options.providers[provider]) {
7750
+ continue;
7751
+ }
7752
+ seen.add(provider);
7753
+ order.push(provider);
7754
+ }
7755
+ return {
7756
+ order,
7757
+ selectedProvider: preferred
7758
+ };
7759
+ };
7760
+ const emit = async (event, input) => {
7761
+ await options.onProviderEvent?.(event, input);
7762
+ };
7763
+ return {
7764
+ generate: async (input) => {
7765
+ const { order, selectedProvider } = await resolveOrder(input);
7766
+ if (!selectedProvider || order.length === 0) {
7767
+ throw new Error("Voice provider router has no available providers.");
7768
+ }
7769
+ let lastError;
7770
+ for (const [index, provider] of order.entries()) {
7771
+ const model = options.providers[provider];
7772
+ if (!model) {
7773
+ continue;
7774
+ }
7775
+ const startedAt = Date.now();
7776
+ try {
7777
+ const output = await model.generate(input);
7778
+ const providerHealth = recordProviderSuccess(provider);
7779
+ await emit({
7780
+ at: Date.now(),
7781
+ elapsedMs: Date.now() - startedAt,
7782
+ fallbackProvider: provider === selectedProvider ? undefined : provider,
7783
+ provider,
7784
+ providerHealth,
7785
+ recovered: provider !== selectedProvider,
7786
+ selectedProvider,
7787
+ status: provider === selectedProvider ? "success" : "fallback"
7788
+ }, input);
7789
+ return output;
7790
+ } catch (error) {
7791
+ lastError = error;
7792
+ const hasNextProvider = index < order.length - 1;
7793
+ const isProviderError = options.isProviderError?.(error, provider) ?? true;
7794
+ const rateLimited = options.isRateLimitError?.(error, provider) ?? defaultIsRateLimitError(error);
7795
+ const shouldFallback = fallbackMode === "provider-error" ? isProviderError : fallbackMode === "rate-limit" ? isProviderError && rateLimited : false;
7796
+ const providerHealth = recordProviderError(provider, isProviderError, rateLimited);
7797
+ const nextProvider = hasNextProvider ? order[index + 1] : undefined;
7798
+ await emit({
7799
+ at: Date.now(),
7800
+ elapsedMs: Date.now() - startedAt,
7801
+ error: errorMessage(error),
7802
+ fallbackProvider: shouldFallback ? nextProvider : undefined,
7803
+ provider,
7804
+ providerHealth,
7805
+ rateLimited,
7806
+ selectedProvider,
7807
+ suppressionRemainingMs: getSuppressionRemainingMs(provider),
7808
+ suppressedUntil: providerHealth?.suppressedUntil,
7809
+ status: "error"
7810
+ }, input);
7811
+ if (!hasNextProvider || !shouldFallback) {
7812
+ throw error;
7813
+ }
7814
+ }
7815
+ }
7816
+ throw lastError ?? new Error("Voice provider router did not run a provider.");
7817
+ }
7818
+ };
7819
+ };
7820
+ var messageToOpenAIInput = (message) => {
7821
+ if (message.role === "tool") {
7822
+ return [
7823
+ {
7824
+ call_id: message.toolCallId ?? message.name ?? crypto.randomUUID(),
7825
+ output: message.content,
7826
+ type: "function_call_output"
7827
+ }
7828
+ ];
7829
+ }
7830
+ const toolCalls = getMessageToolCalls(message);
7831
+ if (message.role === "assistant" && toolCalls.length) {
7832
+ return toolCalls.map((toolCall) => ({
7833
+ arguments: JSON.stringify(toolCall.args),
7834
+ call_id: toolCall.id ?? crypto.randomUUID(),
7835
+ name: toolCall.name,
7836
+ type: "function_call"
7837
+ }));
7838
+ }
7839
+ return [
7840
+ {
7841
+ content: message.content,
7842
+ role: message.role === "system" ? "developer" : message.role
7843
+ }
7844
+ ];
7845
+ };
7846
+ var messagesToOpenAIInput = (messages) => messages.flatMap(messageToOpenAIInput);
7847
+ var messageToAnthropicMessage = (message) => {
7848
+ if (message.role === "system") {
7849
+ return;
7850
+ }
7851
+ if (message.role === "tool") {
7852
+ if (!message.toolCallId) {
7853
+ return {
7854
+ content: `Tool result from ${message.name ?? "tool"}: ${message.content}`,
7855
+ role: "user"
7856
+ };
7857
+ }
7858
+ return {
7859
+ content: [
7860
+ {
7861
+ content: message.content,
7862
+ tool_use_id: message.toolCallId,
7863
+ type: "tool_result"
7864
+ }
7865
+ ],
7866
+ role: "user"
7867
+ };
7868
+ }
7869
+ const toolCalls = getMessageToolCalls(message);
7870
+ if (message.role === "assistant" && toolCalls.length) {
7871
+ return {
7872
+ content: [
7873
+ ...message.content ? [
7874
+ {
7875
+ text: message.content,
7876
+ type: "text"
7877
+ }
7878
+ ] : [],
7879
+ ...toolCalls.map((toolCall) => ({
7880
+ id: toolCall.id ?? crypto.randomUUID(),
7881
+ input: toolCall.args,
7882
+ name: toolCall.name,
7883
+ type: "tool_use"
7884
+ }))
7885
+ ],
7886
+ role: "assistant"
7887
+ };
7888
+ }
7889
+ return {
7890
+ content: message.content,
7891
+ role: message.role
7892
+ };
7893
+ };
7894
+ var toGeminiSchema = (schema) => {
7895
+ const next = {};
7896
+ for (const [key, value] of Object.entries(schema)) {
7897
+ if (key === "additionalProperties") {
7898
+ continue;
7899
+ }
7900
+ if (key === "type" && typeof value === "string") {
7901
+ next[key] = value.toUpperCase();
7902
+ continue;
7903
+ }
7904
+ if (Array.isArray(value)) {
7905
+ next[key] = value.map((item) => item && typeof item === "object" ? toGeminiSchema(item) : item);
7906
+ continue;
7907
+ }
7908
+ if (value && typeof value === "object") {
7909
+ next[key] = toGeminiSchema(value);
7910
+ continue;
7911
+ }
7912
+ next[key] = value;
7913
+ }
7914
+ return next;
7915
+ };
7916
+ var messageToGeminiContent = (message) => {
7917
+ if (message.role === "system") {
7918
+ return;
7919
+ }
7920
+ if (message.role === "tool") {
7921
+ return {
7922
+ parts: [
7923
+ {
7924
+ functionResponse: {
7925
+ id: message.toolCallId,
7926
+ name: message.name ?? "tool",
7927
+ response: {
7928
+ result: parseJSONValue(message.content)
7929
+ }
7930
+ }
7931
+ }
7932
+ ],
7933
+ role: "user"
7934
+ };
7935
+ }
7936
+ const toolCalls = getMessageToolCalls(message);
7937
+ if (message.role === "assistant" && toolCalls.length) {
7938
+ return {
7939
+ parts: [
7940
+ ...message.content ? [
7941
+ {
7942
+ text: message.content
7943
+ }
7944
+ ] : [],
7945
+ ...toolCalls.map((toolCall) => ({
7946
+ functionCall: {
7947
+ args: toolCall.args,
7948
+ id: toolCall.id,
7949
+ name: toolCall.name
7950
+ }
7951
+ }))
7952
+ ],
7953
+ role: "model"
7954
+ };
7955
+ }
7956
+ return {
7957
+ parts: [
7958
+ {
7959
+ text: message.content
7960
+ }
7961
+ ],
7962
+ role: message.role === "assistant" ? "model" : "user"
7963
+ };
7964
+ };
7965
+ var extractText = (response) => {
7966
+ if (typeof response.output_text === "string") {
7967
+ return response.output_text;
7968
+ }
7969
+ const output = Array.isArray(response.output) ? response.output : [];
7970
+ for (const item of output) {
7971
+ if (!item || typeof item !== "object") {
7972
+ continue;
7973
+ }
7974
+ const record = item;
7975
+ const content = Array.isArray(record.content) ? record.content : [];
7976
+ for (const contentItem of content) {
7977
+ if (!contentItem || typeof contentItem !== "object") {
7978
+ continue;
7979
+ }
7980
+ const contentRecord = contentItem;
7981
+ if (typeof contentRecord.text === "string") {
7982
+ return contentRecord.text;
7983
+ }
7984
+ }
7985
+ }
7986
+ return "";
7987
+ };
7988
+ var extractToolCalls = (response) => {
7989
+ const output = Array.isArray(response.output) ? response.output : [];
7990
+ const toolCalls = [];
7991
+ for (const item of output) {
7992
+ if (!item || typeof item !== "object") {
7993
+ continue;
7994
+ }
7995
+ const record = item;
7996
+ if (record.type !== "function_call" || typeof record.name !== "string") {
7997
+ continue;
7998
+ }
7999
+ const args = typeof record.arguments === "string" ? parseJSON(record.arguments) : {};
8000
+ toolCalls.push({
8001
+ args,
8002
+ id: typeof record.call_id === "string" ? record.call_id : typeof record.id === "string" ? record.id : undefined,
8003
+ name: record.name
8004
+ });
8005
+ }
8006
+ return toolCalls;
8007
+ };
8008
+ var createOpenAIVoiceAssistantModel = (options) => {
8009
+ const fetchImpl = options.fetch ?? globalThis.fetch;
8010
+ const baseUrl = options.baseUrl ?? "https://api.openai.com/v1";
8011
+ const model = options.model ?? "gpt-4.1-mini";
8012
+ return {
8013
+ generate: async (input) => {
8014
+ const response = await fetchImpl(`${baseUrl.replace(/\/$/, "")}/responses`, {
8015
+ body: JSON.stringify({
8016
+ input: messagesToOpenAIInput(input.messages),
8017
+ instructions: [
8018
+ input.system,
8019
+ "Return a JSON object with assistantText, complete, transfer, escalate, voicemail, noAnswer, and result when you are not calling tools."
8020
+ ].filter(Boolean).join(`
8021
+
8022
+ `),
8023
+ max_output_tokens: options.maxOutputTokens,
8024
+ model,
8025
+ temperature: options.temperature,
8026
+ text: {
8027
+ format: {
8028
+ name: "voice_route_result",
8029
+ schema: OUTPUT_SCHEMA,
8030
+ strict: false,
8031
+ type: "json_schema"
8032
+ }
8033
+ },
8034
+ tool_choice: input.tools.length ? "auto" : "none",
8035
+ tools: input.tools.map((tool) => ({
8036
+ description: tool.description,
8037
+ name: tool.name,
8038
+ parameters: tool.parameters ?? {
8039
+ additionalProperties: true,
8040
+ type: "object"
8041
+ },
8042
+ strict: false,
8043
+ type: "function"
8044
+ }))
8045
+ }),
8046
+ headers: {
8047
+ authorization: `Bearer ${options.apiKey}`,
8048
+ "content-type": "application/json"
8049
+ },
8050
+ method: "POST"
8051
+ });
8052
+ if (!response.ok) {
8053
+ throw createHTTPError("OpenAI", response);
8054
+ }
8055
+ const body = await response.json();
8056
+ if (body.usage && typeof body.usage === "object") {
8057
+ await options.onUsage?.(body.usage);
8058
+ }
8059
+ const toolCalls = extractToolCalls(body);
8060
+ if (toolCalls.length) {
8061
+ return {
8062
+ toolCalls
8063
+ };
8064
+ }
8065
+ return normalizeRouteOutput(parseJSON(extractText(body)));
8066
+ }
8067
+ };
8068
+ };
8069
+ var extractAnthropicText = (response) => {
8070
+ const content = Array.isArray(response.content) ? response.content : [];
8071
+ return content.map((item) => item && typeof item === "object" && item.type === "text" && typeof item.text === "string" ? item.text : "").filter(Boolean).join(`
8072
+ `);
8073
+ };
8074
+ var extractAnthropicToolCalls = (response) => {
8075
+ const content = Array.isArray(response.content) ? response.content : [];
8076
+ const toolCalls = [];
8077
+ for (const item of content) {
8078
+ if (!item || typeof item !== "object") {
8079
+ continue;
8080
+ }
8081
+ const record = item;
8082
+ if (record.type !== "tool_use" || typeof record.name !== "string") {
8083
+ continue;
8084
+ }
8085
+ toolCalls.push({
8086
+ args: record.input && typeof record.input === "object" ? record.input : {},
8087
+ id: typeof record.id === "string" ? record.id : undefined,
8088
+ name: record.name
8089
+ });
8090
+ }
8091
+ return toolCalls;
8092
+ };
8093
+ var createAnthropicVoiceAssistantModel = (options) => {
8094
+ const fetchImpl = options.fetch ?? globalThis.fetch;
8095
+ const baseUrl = options.baseUrl ?? "https://api.anthropic.com/v1";
8096
+ const model = options.model ?? "claude-sonnet-4-5";
8097
+ return {
8098
+ generate: async (input) => {
8099
+ const response = await fetchImpl(`${baseUrl.replace(/\/$/, "")}/messages`, {
8100
+ body: JSON.stringify({
8101
+ max_tokens: options.maxOutputTokens ?? 1024,
8102
+ messages: input.messages.map(messageToAnthropicMessage).filter(Boolean),
8103
+ model,
8104
+ system: [input.system, ROUTE_RESULT_INSTRUCTION].filter(Boolean).join(`
8105
+
8106
+ `),
8107
+ temperature: options.temperature,
8108
+ tool_choice: input.tools.length ? { type: "auto" } : { type: "none" },
8109
+ tools: input.tools.map((tool) => ({
8110
+ description: tool.description,
8111
+ input_schema: tool.parameters ?? {
8112
+ additionalProperties: true,
8113
+ type: "object"
8114
+ },
8115
+ name: tool.name
8116
+ }))
8117
+ }),
8118
+ headers: {
8119
+ "anthropic-version": options.version ?? "2023-06-01",
8120
+ "content-type": "application/json",
8121
+ "x-api-key": options.apiKey
8122
+ },
8123
+ method: "POST"
8124
+ });
8125
+ if (!response.ok) {
8126
+ throw createHTTPError("Anthropic", response);
8127
+ }
8128
+ const body = await response.json();
8129
+ if (body.usage && typeof body.usage === "object") {
8130
+ await options.onUsage?.(body.usage);
8131
+ }
8132
+ const toolCalls = extractAnthropicToolCalls(body);
8133
+ if (toolCalls.length) {
8134
+ return {
8135
+ assistantText: extractAnthropicText(body) || undefined,
8136
+ toolCalls
8137
+ };
8138
+ }
8139
+ return normalizeRouteOutput(parseJSON(extractAnthropicText(body)));
8140
+ }
8141
+ };
8142
+ };
8143
+ var extractGeminiCandidateParts = (response) => {
8144
+ const candidates = Array.isArray(response.candidates) ? response.candidates : [];
8145
+ const first = candidates[0];
8146
+ if (!first || typeof first !== "object") {
8147
+ return [];
8148
+ }
8149
+ const content = first.content;
8150
+ if (!content || typeof content !== "object") {
8151
+ return [];
8152
+ }
8153
+ const parts = content.parts;
8154
+ return Array.isArray(parts) ? parts : [];
8155
+ };
8156
+ var extractGeminiText = (response) => extractGeminiCandidateParts(response).map((part) => part && typeof part === "object" && typeof part.text === "string" ? part.text : "").filter(Boolean).join(`
8157
+ `);
8158
+ var extractGeminiToolCalls = (response) => {
8159
+ const toolCalls = [];
8160
+ for (const part of extractGeminiCandidateParts(response)) {
8161
+ if (!part || typeof part !== "object") {
8162
+ continue;
8163
+ }
8164
+ const functionCall = part.functionCall;
8165
+ if (!functionCall || typeof functionCall !== "object") {
8166
+ continue;
8167
+ }
8168
+ const record = functionCall;
8169
+ if (typeof record.name !== "string") {
8170
+ continue;
8171
+ }
8172
+ toolCalls.push({
8173
+ args: record.args && typeof record.args === "object" ? record.args : {},
8174
+ id: typeof record.id === "string" ? record.id : undefined,
8175
+ name: record.name
8176
+ });
8177
+ }
8178
+ return toolCalls;
8179
+ };
8180
+ var createGeminiVoiceAssistantModel = (options) => {
8181
+ const fetchImpl = options.fetch ?? globalThis.fetch;
8182
+ const baseUrl = options.baseUrl ?? "https://generativelanguage.googleapis.com/v1beta";
8183
+ const model = options.model ?? "gemini-2.5-flash";
8184
+ const maxRetries = Math.max(0, options.maxRetries ?? 2);
8185
+ return {
8186
+ generate: async (input) => {
8187
+ const endpoint = `${baseUrl.replace(/\/$/, "")}/models/${encodeURIComponent(model)}:generateContent?key=${encodeURIComponent(options.apiKey)}`;
8188
+ let response;
8189
+ for (let attempt = 0;attempt <= maxRetries; attempt += 1) {
8190
+ response = await fetchImpl(endpoint, {
8191
+ body: JSON.stringify({
8192
+ contents: input.messages.map(messageToGeminiContent).filter(Boolean),
8193
+ generationConfig: {
8194
+ maxOutputTokens: options.maxOutputTokens,
8195
+ ...input.tools.length ? {} : {
8196
+ responseMimeType: "application/json",
8197
+ responseSchema: toGeminiSchema(OUTPUT_SCHEMA)
8198
+ },
8199
+ temperature: options.temperature
8200
+ },
8201
+ systemInstruction: {
8202
+ parts: [
8203
+ {
8204
+ text: [input.system, ROUTE_RESULT_INSTRUCTION].filter(Boolean).join(`
8205
+
8206
+ `)
8207
+ }
8208
+ ]
8209
+ },
8210
+ tools: input.tools.length ? [
8211
+ {
8212
+ functionDeclarations: input.tools.map((tool) => ({
8213
+ description: tool.description,
8214
+ name: tool.name,
8215
+ parameters: toGeminiSchema(tool.parameters ?? {
8216
+ additionalProperties: true,
8217
+ type: "object"
8218
+ })
8219
+ }))
8220
+ }
8221
+ ] : undefined
8222
+ }),
8223
+ headers: {
8224
+ "content-type": "application/json"
8225
+ },
8226
+ method: "POST"
8227
+ });
8228
+ if (response.ok || response.status !== 429 && response.status < 500 || attempt === maxRetries) {
8229
+ break;
8230
+ }
8231
+ const retryAfter = Number(response.headers.get("retry-after"));
8232
+ await sleep4(Number.isFinite(retryAfter) && retryAfter > 0 ? retryAfter * 1000 : 500 * 2 ** attempt);
8233
+ }
8234
+ if (!response) {
8235
+ throw new Error("Gemini voice assistant model failed: no response");
8236
+ }
8237
+ if (!response.ok) {
8238
+ throw createHTTPError("Gemini", response);
8239
+ }
8240
+ const body = await response.json();
8241
+ if (body.usageMetadata && typeof body.usageMetadata === "object") {
8242
+ await options.onUsage?.(body.usageMetadata);
8243
+ }
8244
+ const toolCalls = extractGeminiToolCalls(body);
8245
+ if (toolCalls.length) {
8246
+ return {
8247
+ assistantText: extractGeminiText(body) || undefined,
8248
+ toolCalls
8249
+ };
8250
+ }
8251
+ return normalizeRouteOutput(parseJSON(extractGeminiText(body)));
8252
+ }
8253
+ };
8254
+ };
6690
8255
  // src/sqliteStore.ts
6691
8256
  import { Database } from "bun:sqlite";
6692
8257
  var normalizeTableNameSegment = (value) => value.trim().replace(/[^a-zA-Z0-9_]+/g, "_").replace(/^_+|_+$/g, "") || "voice";
@@ -7884,10 +9449,10 @@ var createVoiceOpsTaskProcessorWorker = (options) => ({
7884
9449
  result.completed += 1;
7885
9450
  } catch (error) {
7886
9451
  await options.onError?.(error, task);
7887
- const errorMessage = error instanceof Error ? error.message : String(error);
9452
+ const errorMessage2 = error instanceof Error ? error.message : String(error);
7888
9453
  const failedTask = failVoiceOpsTask(task, {
7889
9454
  actor: task.claimedBy ?? "ops-worker",
7890
- error: errorMessage
9455
+ error: errorMessage2
7891
9456
  });
7892
9457
  if (shouldDeadLetterTask(failedTask, options.maxFailures)) {
7893
9458
  const deadLetterTask = deadLetterVoiceOpsTask(failedTask, {
@@ -9137,10 +10702,14 @@ export {
9137
10702
  transcodePCMToTwilioOutboundPayload,
9138
10703
  summarizeVoiceTraceSinkDeliveries,
9139
10704
  summarizeVoiceTrace,
10705
+ summarizeVoiceSessionReplay,
10706
+ summarizeVoiceProviderHealth,
9140
10707
  summarizeVoiceOpsTasks,
9141
10708
  summarizeVoiceOpsTaskQueue,
9142
10709
  summarizeVoiceOpsTaskAnalytics,
9143
10710
  summarizeVoiceIntegrationEvents,
10711
+ summarizeVoiceAssistantRuns,
10712
+ summarizeVoiceAssistantHealth,
9144
10713
  startVoiceOpsTask,
9145
10714
  shapeTelephonyAssistantText,
9146
10715
  selectVoiceTraceEventsForPrune,
@@ -9152,14 +10721,17 @@ export {
9152
10721
  resolveVoiceOpsTaskAssignment,
9153
10722
  resolveVoiceOpsTaskAgeBucket,
9154
10723
  resolveVoiceOpsPreset,
10724
+ resolveVoiceAssistantMemoryNamespace,
9155
10725
  resolveTurnDetectionConfig,
9156
10726
  resolveAudioConditioningConfig,
9157
10727
  requeueVoiceOpsTask,
9158
10728
  reopenVoiceOpsTask,
9159
10729
  renderVoiceTraceMarkdown,
9160
10730
  renderVoiceTraceHTML,
10731
+ renderVoiceProviderHealthHTML,
9161
10732
  renderVoiceCallReviewMarkdown,
9162
10733
  renderVoiceCallReviewHTML,
10734
+ renderVoiceAssistantHealthHTML,
9163
10735
  redactVoiceTraceText,
9164
10736
  redactVoiceTraceEvents,
9165
10737
  redactVoiceTraceEvent,
@@ -9197,6 +10769,9 @@ export {
9197
10769
  createVoiceTaskUpdatedEvent,
9198
10770
  createVoiceTaskSLABreachedEvent,
9199
10771
  createVoiceTaskCreatedEvent,
10772
+ createVoiceSessionReplayRoutes,
10773
+ createVoiceSessionReplayJSONHandler,
10774
+ createVoiceSessionReplayHTMLHandler,
9200
10775
  createVoiceSessionRecord,
9201
10776
  createVoiceSession,
9202
10777
  createVoiceSTTRoutingCorrectionHandler,
@@ -9212,6 +10787,10 @@ export {
9212
10787
  createVoiceReviewSavedEvent,
9213
10788
  createVoiceRedisTaskLeaseCoordinator,
9214
10789
  createVoiceRedisIdempotencyStore,
10790
+ createVoiceProviderRouter,
10791
+ createVoiceProviderHealthRoutes,
10792
+ createVoiceProviderHealthJSONHandler,
10793
+ createVoiceProviderHealthHTMLHandler,
9215
10794
  createVoicePostgresTraceSinkDeliveryStore,
9216
10795
  createVoicePostgresTraceEventStore,
9217
10796
  createVoicePostgresTaskStore,
@@ -9227,6 +10806,7 @@ export {
9227
10806
  createVoiceMemoryTraceSinkDeliveryStore,
9228
10807
  createVoiceMemoryTraceEventStore,
9229
10808
  createVoiceMemoryStore,
10809
+ createVoiceMemoryAssistantMemoryStore,
9230
10810
  createVoiceLinearIssueUpdateSink,
9231
10811
  createVoiceLinearIssueSyncSinks,
9232
10812
  createVoiceLinearIssueSink,
@@ -9246,6 +10826,7 @@ export {
9246
10826
  createVoiceFileReviewStore,
9247
10827
  createVoiceFileIntegrationEventStore,
9248
10828
  createVoiceFileExternalObjectMapStore,
10829
+ createVoiceFileAssistantMemoryStore,
9249
10830
  createVoiceExternalObjectMapId,
9250
10831
  createVoiceExternalObjectMap,
9251
10832
  createVoiceExperiment,
@@ -9254,6 +10835,11 @@ export {
9254
10835
  createVoiceCallReviewFromLiveTelephonyReport,
9255
10836
  createVoiceCallCompletedEvent,
9256
10837
  createVoiceCRMActivitySink,
10838
+ createVoiceAssistantMemoryRecord,
10839
+ createVoiceAssistantMemoryHandle,
10840
+ createVoiceAssistantHealthRoutes,
10841
+ createVoiceAssistantHealthJSONHandler,
10842
+ createVoiceAssistantHealthHTMLHandler,
9257
10843
  createVoiceAssistant,
9258
10844
  createVoiceAgentTool,
9259
10845
  createVoiceAgentSquad,
@@ -9266,9 +10852,13 @@ export {
9266
10852
  createStoredVoiceCallReviewArtifact,
9267
10853
  createRiskyTurnCorrectionHandler,
9268
10854
  createPhraseHintCorrectionHandler,
10855
+ createOpenAIVoiceAssistantModel,
10856
+ createJSONVoiceAssistantModel,
9269
10857
  createId,
10858
+ createGeminiVoiceAssistantModel,
9270
10859
  createDomainPhraseHints,
9271
10860
  createDomainLexicon,
10861
+ createAnthropicVoiceAssistantModel,
9272
10862
  conditionAudioChunk,
9273
10863
  completeVoiceOpsTask,
9274
10864
  claimVoiceOpsTask,