@absolutejs/voice 0.0.22-beta.36 → 0.0.22-beta.38

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.
@@ -0,0 +1,44 @@
1
+ import { Elysia } from 'elysia';
2
+ import { evaluateVoiceTrace, type StoredVoiceTraceEvent, type VoiceTraceEventFilter, type VoiceTraceEventStore, type VoiceTraceRedactionConfig } from './trace';
3
+ export type VoiceDiagnosticsRoutesOptions = {
4
+ evaluation?: Parameters<typeof evaluateVoiceTrace>[1];
5
+ headers?: HeadersInit;
6
+ name?: string;
7
+ path?: string;
8
+ redact?: VoiceTraceRedactionConfig;
9
+ store: VoiceTraceEventStore;
10
+ title?: string;
11
+ };
12
+ export declare const resolveVoiceDiagnosticsTraceFilter: (query: Record<string, unknown>) => VoiceTraceEventFilter;
13
+ export declare const buildVoiceDiagnosticsMarkdown: (events: StoredVoiceTraceEvent[], options?: {
14
+ evaluation?: Parameters<typeof evaluateVoiceTrace>[1];
15
+ title?: string;
16
+ }) => string;
17
+ export declare const createVoiceDiagnosticsRoutes: (options: VoiceDiagnosticsRoutesOptions) => Elysia<"", {
18
+ decorator: {};
19
+ store: {};
20
+ derive: {};
21
+ resolve: {};
22
+ }, {
23
+ typebox: {};
24
+ error: {};
25
+ }, {
26
+ schema: {};
27
+ standaloneSchema: {};
28
+ macro: {};
29
+ macroFn: {};
30
+ parser: {};
31
+ response: {};
32
+ }, {}, {
33
+ derive: {};
34
+ resolve: {};
35
+ schema: {};
36
+ standaloneSchema: {};
37
+ response: {};
38
+ }, {
39
+ derive: {};
40
+ resolve: {};
41
+ schema: {};
42
+ standaloneSchema: {};
43
+ response: {};
44
+ }>;
package/dist/index.d.ts CHANGED
@@ -1,12 +1,14 @@
1
1
  export { voice } from './plugin';
2
2
  export { createVoiceAssistant, createVoiceExperiment, summarizeVoiceAssistantRuns } from './assistant';
3
3
  export { createVoiceAssistantHealthHTMLHandler, createVoiceAssistantHealthJSONHandler, createVoiceAssistantHealthRoutes, renderVoiceAssistantHealthHTML, summarizeVoiceAssistantHealth } from './assistantHealth';
4
+ export { buildVoiceDiagnosticsMarkdown, createVoiceDiagnosticsRoutes, resolveVoiceDiagnosticsTraceFilter } from './diagnosticsRoutes';
4
5
  export { createVoiceSessionListRoutes, createVoiceSessionReplayHTMLHandler, createVoiceSessionReplayJSONHandler, createVoiceSessionReplayRoutes, createVoiceSessionsHTMLHandler, createVoiceSessionsJSONHandler, renderVoiceSessionsHTML, summarizeVoiceSessions, summarizeVoiceSessionReplay } from './sessionReplay';
5
6
  export { createVoiceAgent, createVoiceAgentSquad, createVoiceAgentTool } from './agent';
6
7
  export { createStoredVoiceCallReviewArtifact, createStoredVoiceExternalObjectMap, createStoredVoiceIntegrationEvent, createStoredVoiceOpsTask, createVoiceFileExternalObjectMapStore, createVoiceFileAssistantMemoryStore, createVoiceFileIntegrationEventStore, createVoiceFileReviewStore, createVoiceFileRuntimeStorage, createVoiceFileSessionStore, createVoiceFileTaskStore, createVoiceFileTraceSinkDeliveryStore, createVoiceFileTraceEventStore } from './fileStore';
7
8
  export { createVoiceAssistantMemoryHandle, createVoiceAssistantMemoryRecord, createVoiceMemoryAssistantMemoryStore, resolveVoiceAssistantMemoryNamespace } from './assistantMemory';
8
9
  export { createAnthropicVoiceAssistantModel, createGeminiVoiceAssistantModel, createJSONVoiceAssistantModel, createOpenAIVoiceAssistantModel, createVoiceProviderRouter } from './modelAdapters';
9
10
  export { createVoiceProviderHealthHTMLHandler, createVoiceProviderHealthJSONHandler, createVoiceProviderHealthRoutes, renderVoiceProviderHealthHTML, summarizeVoiceProviderHealth } from './providerHealth';
11
+ export { createVoiceResilienceRoutes, listVoiceRoutingEvents, renderVoiceResilienceHTML } from './resilienceRoutes';
10
12
  export { createVoiceSTTProviderRouter, createVoiceTTSProviderRouter } from './providerAdapters';
11
13
  export { buildVoiceTraceReplay, createVoiceMemoryTraceSinkDeliveryStore, createVoiceTraceHTTPSink, createVoiceMemoryTraceEventStore, createVoiceTraceSinkDeliveryId, createVoiceTraceSinkDeliveryRecord, createVoiceTraceSinkStore, createVoiceTraceEvent, createVoiceTraceEventId, deliverVoiceTraceEventsToSinks, evaluateVoiceTrace, exportVoiceTrace, filterVoiceTraceEvents, pruneVoiceTraceEvents, redactVoiceTraceEvent, redactVoiceTraceEvents, redactVoiceTraceText, renderVoiceTraceHTML, renderVoiceTraceMarkdown, resolveVoiceTraceRedactionOptions, selectVoiceTraceEventsForPrune, summarizeVoiceTrace } from './trace';
12
14
  export { createVoiceSQLiteExternalObjectMapStore, createVoiceSQLiteIntegrationEventStore, createVoiceSQLiteReviewStore, createVoiceSQLiteRuntimeStorage, createVoiceSQLiteSessionStore, createVoiceSQLiteTaskStore, createVoiceSQLiteTraceSinkDeliveryStore, createVoiceSQLiteTraceEventStore } from './sqliteStore';
@@ -34,9 +36,11 @@ export { createVoiceCallReviewFromLiveTelephonyReport, createVoiceCallReviewReco
34
36
  export type { VoiceAssistant, VoiceAssistantArtifactPlan, VoiceAssistantExperiment, VoiceAssistantExperimentOptions, VoiceAssistantGuardrailInput, VoiceAssistantGuardrails, VoiceAssistantMemoryLifecycle, VoiceAssistantMemoryLifecycleInput, VoiceAssistantOptions, VoiceAssistantOutputGuardrailInput, VoiceAssistantPreset, VoiceAssistantRunsSummary, VoiceAssistantRunSummary, VoiceAssistantVariant } from './assistant';
35
37
  export type { VoiceAssistantHealthFailure, VoiceAssistantHealthHTMLHandlerOptions, VoiceAssistantHealthRoutesOptions, VoiceAssistantHealthSummary, VoiceAssistantHealthSummaryOptions } from './assistantHealth';
36
38
  export type { VoiceAssistantMemoryBinding, VoiceAssistantMemoryHandle, VoiceAssistantMemoryOptions, VoiceAssistantMemoryRecord, VoiceAssistantMemoryStore } from './assistantMemory';
39
+ export type { VoiceDiagnosticsRoutesOptions } from './diagnosticsRoutes';
37
40
  export type { VoiceSessionListHTMLHandlerOptions, VoiceSessionListItem, VoiceSessionListOptions, VoiceSessionListRoutesOptions, VoiceSessionListStatus, VoiceSessionReplay, VoiceSessionReplayHTMLHandlerOptions, VoiceSessionReplayOptions, VoiceSessionReplayRoutesOptions, VoiceSessionReplayTurn } from './sessionReplay';
38
41
  export type { AnthropicVoiceAssistantModelOptions, GeminiVoiceAssistantModelOptions, OpenAIVoiceAssistantModelOptions, VoiceProviderRouterEvent, VoiceProviderRouterFallbackMode, VoiceProviderRouterHealthOptions, VoiceProviderRouterOptions, VoiceProviderRouterPolicy, VoiceProviderRouterProviderHealth, VoiceProviderRouterProviderProfile, VoiceJSONAssistantModelHandler, VoiceJSONAssistantModelOptions } from './modelAdapters';
39
42
  export type { VoiceProviderHealthStatus, VoiceProviderHealthSummary, VoiceProviderHealthSummaryOptions } from './providerHealth';
43
+ export type { VoiceResilienceIOSimulator, VoiceResilienceLink, VoiceResiliencePageData, VoiceResilienceRoutesOptions, VoiceResilienceSimulationProvider, VoiceRoutingEvent, VoiceRoutingEventKind } from './resilienceRoutes';
40
44
  export type { VoiceIOProviderRouterEvent, VoiceSTTProviderRouterOptions, VoiceTTSProviderRouterOptions } from './providerAdapters';
41
45
  export type { VoiceAgent, VoiceAgentMessage, VoiceAgentMessageRole, VoiceAgentModel, VoiceAgentModelInput, VoiceAgentModelOutput, VoiceAgentOptions, VoiceAgentRunResult, VoiceAgentSquadOptions, VoiceAgentTool, VoiceAgentToolCall, VoiceAgentToolResult } from './agent';
42
46
  export type { VoiceOpsRuntime, VoiceOpsRuntimeConfig, VoiceOpsRuntimeSummary, VoiceOpsRuntimeSinkWorkerConfig, VoiceOpsRuntimeTaskWorkerConfig, VoiceOpsRuntimeTickResult, VoiceOpsRuntimeWebhookWorkerConfig } from './opsRuntime';
package/dist/index.js CHANGED
@@ -6822,7 +6822,7 @@ var createVoiceAssistantHealthRoutes = (options) => {
6822
6822
  }
6823
6823
  return routes;
6824
6824
  };
6825
- // src/sessionReplay.ts
6825
+ // src/diagnosticsRoutes.ts
6826
6826
  import { Elysia as Elysia4 } from "elysia";
6827
6827
 
6828
6828
  // src/trace.ts
@@ -7489,9 +7489,144 @@ var buildVoiceTraceReplay = (events, options = {}) => ({
7489
7489
  summary: summarizeVoiceTrace(options.redact ? redactVoiceTraceEvents(events, options.redact) : events)
7490
7490
  });
7491
7491
 
7492
- // src/sessionReplay.ts
7493
- var getString3 = (value) => typeof value === "string" ? value : undefined;
7492
+ // src/diagnosticsRoutes.ts
7494
7493
  var escapeHtml6 = (value) => value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
7494
+ var getString3 = (value) => typeof value === "string" && value.trim() ? value : undefined;
7495
+ var getNumber2 = (value) => {
7496
+ const parsed = typeof value === "number" ? value : typeof value === "string" ? Number(value) : undefined;
7497
+ return typeof parsed === "number" && Number.isFinite(parsed) ? parsed : undefined;
7498
+ };
7499
+ var getBoolean = (value) => value === true || value === "true" || value === "1";
7500
+ var parseTraceTypeFilter = (value) => {
7501
+ if (typeof value !== "string" || !value.trim()) {
7502
+ return;
7503
+ }
7504
+ const types = value.split(",").map((entry) => entry.trim()).filter(Boolean);
7505
+ return types.length <= 1 ? types[0] : types;
7506
+ };
7507
+ var resolveVoiceDiagnosticsTraceFilter = (query) => ({
7508
+ limit: getNumber2(query.limit),
7509
+ scenarioId: getString3(query.scenarioId),
7510
+ sessionId: getString3(query.sessionId),
7511
+ traceId: getString3(query.traceId),
7512
+ turnId: getString3(query.turnId),
7513
+ type: parseTraceTypeFilter(query.type)
7514
+ });
7515
+ var filterByDiagnosticsQuery = (events, query) => {
7516
+ const provider = getString3(query.provider);
7517
+ const status = getString3(query.status);
7518
+ const since = getNumber2(query.since);
7519
+ const until = getNumber2(query.until);
7520
+ return filterVoiceTraceEvents(events, resolveVoiceDiagnosticsTraceFilter(query)).filter((event) => (!provider || event.payload.provider === provider) && (!status || event.payload.providerStatus === status || event.payload.status === status) && (since === undefined || event.at >= since) && (until === undefined || event.at <= until));
7521
+ };
7522
+ var buildVoiceDiagnosticsMarkdown = (events, options = {}) => {
7523
+ const summary = summarizeVoiceTrace(events);
7524
+ const evaluation = evaluateVoiceTrace(events, options.evaluation);
7525
+ const trace = renderVoiceTraceMarkdown(events, {
7526
+ evaluation: options.evaluation,
7527
+ title: options.title ?? `Voice Diagnostics ${summary.sessionId ?? ""}`.trim()
7528
+ });
7529
+ return [
7530
+ `# ${options.title ?? "Voice Diagnostics Bug Report"}`,
7531
+ "",
7532
+ `Session: ${summary.sessionId ?? "unknown"}`,
7533
+ `Pass: ${evaluation.pass ? "yes" : "no"}`,
7534
+ `Events: ${summary.eventCount}`,
7535
+ `Turns: ${summary.turnCount}`,
7536
+ `Errors: ${summary.errorCount}`,
7537
+ `Tool errors: ${summary.toolErrorCount}`,
7538
+ `Estimated cost units: ${summary.cost.estimatedRelativeCostUnits}`,
7539
+ "",
7540
+ "## Issues",
7541
+ "",
7542
+ evaluation.issues.length ? evaluation.issues.map((issue) => `- [${issue.severity}] ${issue.code}: ${issue.message}`).join(`
7543
+ `) : "- none",
7544
+ "",
7545
+ "## Trace",
7546
+ "",
7547
+ trace
7548
+ ].join(`
7549
+ `);
7550
+ };
7551
+ var renderDiagnosticsIndex = (input) => {
7552
+ const sessions = new Map;
7553
+ for (const event of input.events) {
7554
+ sessions.set(event.sessionId, [...sessions.get(event.sessionId) ?? [], event]);
7555
+ }
7556
+ const rows = [...sessions.entries()].sort(([, left], [, right]) => (right.at(-1)?.at ?? 0) - (left.at(-1)?.at ?? 0)).slice(0, 50).map(([sessionId, events]) => {
7557
+ const summary = summarizeVoiceTrace(events);
7558
+ const encoded = encodeURIComponent(sessionId);
7559
+ return `<tr><td>${escapeHtml6(sessionId)}</td><td>${summary.eventCount}</td><td>${summary.turnCount}</td><td>${summary.errorCount}</td><td><a href="${input.basePath}/html?sessionId=${encoded}&redact=true">HTML</a> \xB7 <a href="${input.basePath}/markdown?sessionId=${encoded}&redact=true">Markdown</a> \xB7 <a href="${input.basePath}/json?sessionId=${encoded}&redact=true">JSON</a></td></tr>`;
7560
+ }).join("");
7561
+ return `<!doctype html><html lang="en"><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /><title>${escapeHtml6(input.title)}</title><style>body{font-family:ui-sans-serif,system-ui,sans-serif;margin:2rem;background:#f8f7f2;color:#181713}main{max-width:1100px;margin:auto}table{width:100%;border-collapse:collapse;background:white}td,th{border-bottom:1px solid #eee;padding:.7rem;text-align:left}a{color:#9a3412}</style></head><body><main><h1>${escapeHtml6(input.title)}</h1><p>Recent voice trace diagnostics. Exports support filters: sessionId, traceId, turnId, scenarioId, type, provider, status, since, until, limit, redact.</p><table><thead><tr><th>Session</th><th>Events</th><th>Turns</th><th>Errors</th><th>Exports</th></tr></thead><tbody>${rows}</tbody></table></main></body></html>`;
7562
+ };
7563
+ var withRedaction = (events, query, defaultRedact) => {
7564
+ const shouldRedact = query.redact === undefined ? defaultRedact : getBoolean(query.redact);
7565
+ return shouldRedact ? redactVoiceTraceEvents(events, shouldRedact) : events;
7566
+ };
7567
+ var createVoiceDiagnosticsRoutes = (options) => {
7568
+ const path = options.path ?? "/diagnostics";
7569
+ const title = options.title ?? "AbsoluteJS Voice Diagnostics";
7570
+ const routes = new Elysia4({
7571
+ name: options.name ?? "absolutejs-voice-diagnostics"
7572
+ });
7573
+ routes.get(path, async () => {
7574
+ const events = await options.store.list();
7575
+ return new Response(renderDiagnosticsIndex({ basePath: path, events, title }), {
7576
+ headers: {
7577
+ "Content-Type": "text/html; charset=utf-8",
7578
+ ...options.headers
7579
+ }
7580
+ });
7581
+ });
7582
+ routes.get(`${path}/json`, async ({ query }) => {
7583
+ const events = filterByDiagnosticsQuery(await options.store.list(), query);
7584
+ const redacted = withRedaction(events, query, options.redact);
7585
+ return Response.json({
7586
+ ...await exportVoiceTrace({
7587
+ filter: resolveVoiceDiagnosticsTraceFilter(query),
7588
+ redact: false,
7589
+ store: {
7590
+ ...options.store,
7591
+ list: async () => redacted
7592
+ }
7593
+ }),
7594
+ filteredCount: events.length,
7595
+ redacted: redacted !== events
7596
+ });
7597
+ });
7598
+ routes.get(`${path}/markdown`, async ({ query }) => {
7599
+ const events = withRedaction(filterByDiagnosticsQuery(await options.store.list(), query), query, options.redact ?? true);
7600
+ const body = buildVoiceDiagnosticsMarkdown(events, {
7601
+ evaluation: options.evaluation,
7602
+ title
7603
+ });
7604
+ return new Response(body, {
7605
+ headers: {
7606
+ "Content-Type": "text/markdown; charset=utf-8",
7607
+ ...options.headers
7608
+ }
7609
+ });
7610
+ });
7611
+ routes.get(`${path}/html`, async ({ query }) => {
7612
+ const events = withRedaction(filterByDiagnosticsQuery(await options.store.list(), query), query, options.redact ?? true);
7613
+ const body = renderVoiceTraceHTML(events, {
7614
+ evaluation: options.evaluation,
7615
+ title
7616
+ });
7617
+ return new Response(body, {
7618
+ headers: {
7619
+ "Content-Type": "text/html; charset=utf-8",
7620
+ ...options.headers
7621
+ }
7622
+ });
7623
+ });
7624
+ return routes;
7625
+ };
7626
+ // src/sessionReplay.ts
7627
+ import { Elysia as Elysia5 } from "elysia";
7628
+ var getString4 = (value) => typeof value === "string" ? value : undefined;
7629
+ var escapeHtml7 = (value) => value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
7495
7630
  var increment2 = (record, key) => {
7496
7631
  record[key] = (record[key] ?? 0) + 1;
7497
7632
  };
@@ -7520,14 +7655,14 @@ var buildReplayTurns = (events) => {
7520
7655
  case "turn.transcript":
7521
7656
  turn.transcripts.push({
7522
7657
  isFinal: event.payload.isFinal === true,
7523
- text: getString3(event.payload.text)
7658
+ text: getString4(event.payload.text)
7524
7659
  });
7525
7660
  break;
7526
7661
  case "turn.committed":
7527
- turn.committedText = getString3(event.payload.text);
7662
+ turn.committedText = getString4(event.payload.text);
7528
7663
  break;
7529
7664
  case "turn.assistant": {
7530
- const text = getString3(event.payload.text);
7665
+ const text = getString4(event.payload.text);
7531
7666
  if (text) {
7532
7667
  turn.assistantReplies.push(text);
7533
7668
  }
@@ -7596,7 +7731,7 @@ var summarizeVoiceSessions = async (options = {}) => {
7596
7731
  let latestOutcome;
7597
7732
  let errorCount = 0;
7598
7733
  for (const event of sorted) {
7599
- const provider = getString3(event.payload.provider);
7734
+ const provider = getString4(event.payload.provider);
7600
7735
  if (provider) {
7601
7736
  providers.add(provider);
7602
7737
  }
@@ -7604,7 +7739,7 @@ var summarizeVoiceSessions = async (options = {}) => {
7604
7739
  errorCount += 1;
7605
7740
  increment2(providerErrors, provider ?? "unknown");
7606
7741
  }
7607
- const outcome = getString3(event.payload.outcome);
7742
+ const outcome = getString4(event.payload.outcome);
7608
7743
  if (outcome) {
7609
7744
  latestOutcome = outcome;
7610
7745
  }
@@ -7650,10 +7785,10 @@ var summarizeVoiceSessions = async (options = {}) => {
7650
7785
  var renderVoiceSessionsHTML = (sessions) => sessions.length === 0 ? '<p class="voice-sessions-empty">No voice sessions found.</p>' : [
7651
7786
  '<div class="voice-sessions-list">',
7652
7787
  ...sessions.map((session) => [
7653
- `<article class="voice-session-card ${escapeHtml6(session.status)}">`,
7788
+ `<article class="voice-session-card ${escapeHtml7(session.status)}">`,
7654
7789
  '<div class="voice-session-card-header">',
7655
- `<strong>${escapeHtml6(session.sessionId)}</strong>`,
7656
- `<span>${escapeHtml6(session.status)}</span>`,
7790
+ `<strong>${escapeHtml7(session.sessionId)}</strong>`,
7791
+ `<span>${escapeHtml7(session.status)}</span>`,
7657
7792
  "</div>",
7658
7793
  "<dl>",
7659
7794
  `<div><dt>Events</dt><dd>${String(session.eventCount)}</dd></div>`,
@@ -7661,9 +7796,9 @@ var renderVoiceSessionsHTML = (sessions) => sessions.length === 0 ? '<p class="v
7661
7796
  `<div><dt>Transcripts</dt><dd>${String(session.transcriptCount)}</dd></div>`,
7662
7797
  `<div><dt>Errors</dt><dd>${String(session.errorCount)}</dd></div>`,
7663
7798
  "</dl>",
7664
- session.latestOutcome ? `<p>Outcome: ${escapeHtml6(session.latestOutcome)}</p>` : "",
7665
- session.providers.length ? `<p>Providers: ${session.providers.map(escapeHtml6).join(", ")}</p>` : "",
7666
- session.replayHref ? `<p><a href="${escapeHtml6(session.replayHref)}">Open replay</a></p>` : "",
7799
+ session.latestOutcome ? `<p>Outcome: ${escapeHtml7(session.latestOutcome)}</p>` : "",
7800
+ session.providers.length ? `<p>Providers: ${session.providers.map(escapeHtml7).join(", ")}</p>` : "",
7801
+ session.replayHref ? `<p><a href="${escapeHtml7(session.replayHref)}">Open replay</a></p>` : "",
7667
7802
  "</article>"
7668
7803
  ].join("")),
7669
7804
  "</div>"
@@ -7694,7 +7829,7 @@ var createVoiceSessionsHTMLHandler = (options = {}) => async ({ query }) => {
7694
7829
  var createVoiceSessionListRoutes = (options = {}) => {
7695
7830
  const path = options.path ?? "/api/voice-sessions";
7696
7831
  const htmlPath = options.htmlPath === undefined ? `${path}/htmx` : options.htmlPath;
7697
- const routes = new Elysia4({
7832
+ const routes = new Elysia5({
7698
7833
  name: options.name ?? "absolutejs-voice-session-list"
7699
7834
  }).get(path, createVoiceSessionsJSONHandler(options));
7700
7835
  if (htmlPath) {
@@ -7722,7 +7857,7 @@ var createVoiceSessionReplayHTMLHandler = (options) => async ({ params }) => {
7722
7857
  var createVoiceSessionReplayRoutes = (options) => {
7723
7858
  const path = options.path ?? "/api/voice-sessions/:sessionId/replay";
7724
7859
  const htmlPath = options.htmlPath === undefined ? `${path}/htmx` : options.htmlPath;
7725
- const routes = new Elysia4({
7860
+ const routes = new Elysia5({
7726
7861
  name: options.name ?? "absolutejs-voice-session-replay"
7727
7862
  }).get(path, createVoiceSessionReplayJSONHandler(options));
7728
7863
  if (htmlPath) {
@@ -8858,6 +8993,319 @@ var createGeminiVoiceAssistantModel = (options) => {
8858
8993
  }
8859
8994
  };
8860
8995
  };
8996
+ // src/resilienceRoutes.ts
8997
+ import { Elysia as Elysia6 } from "elysia";
8998
+ var escapeHtml8 = (value) => value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
8999
+ var getString5 = (value) => typeof value === "string" ? value : undefined;
9000
+ var getNumber3 = (value) => typeof value === "number" && Number.isFinite(value) ? value : undefined;
9001
+ var getBoolean2 = (value) => value === true;
9002
+ var isProviderStatus2 = (value) => value === "error" || value === "fallback" || value === "success";
9003
+ var listVoiceRoutingEvents = (events) => {
9004
+ const routingEvents = [];
9005
+ for (const event of events) {
9006
+ if (event.type !== "session.error") {
9007
+ continue;
9008
+ }
9009
+ const provider = getString5(event.payload.provider);
9010
+ const providerStatus = isProviderStatus2(event.payload.providerStatus) ? event.payload.providerStatus : undefined;
9011
+ if (!provider || !providerStatus) {
9012
+ continue;
9013
+ }
9014
+ const kind = getString5(event.payload.kind);
9015
+ routingEvents.push({
9016
+ at: event.at,
9017
+ attempt: getNumber3(event.payload.attempt),
9018
+ elapsedMs: getNumber3(event.payload.elapsedMs),
9019
+ error: getString5(event.payload.error),
9020
+ fallbackProvider: getString5(event.payload.fallbackProvider),
9021
+ kind: kind === "stt" || kind === "tts" ? kind : "llm",
9022
+ latencyBudgetMs: getNumber3(event.payload.latencyBudgetMs),
9023
+ operation: getString5(event.payload.operation),
9024
+ provider,
9025
+ selectedProvider: getString5(event.payload.selectedProvider),
9026
+ sessionId: event.sessionId,
9027
+ status: providerStatus,
9028
+ timedOut: getBoolean2(event.payload.timedOut),
9029
+ turnId: event.turnId
9030
+ });
9031
+ }
9032
+ return routingEvents.sort((left, right) => right.at - left.at);
9033
+ };
9034
+ var summarizeRoutingEvents = (events) => {
9035
+ const byKind = new Map;
9036
+ let errors = 0;
9037
+ let fallbacks = 0;
9038
+ let timeouts = 0;
9039
+ for (const event of events) {
9040
+ byKind.set(event.kind, (byKind.get(event.kind) ?? 0) + 1);
9041
+ if (event.status === "error") {
9042
+ errors += 1;
9043
+ }
9044
+ if (event.status === "fallback") {
9045
+ fallbacks += 1;
9046
+ }
9047
+ if (event.timedOut) {
9048
+ timeouts += 1;
9049
+ }
9050
+ }
9051
+ return {
9052
+ byKind,
9053
+ errors,
9054
+ fallbacks,
9055
+ timeouts,
9056
+ total: events.length
9057
+ };
9058
+ };
9059
+ var renderProviderCards = (title, providers) => {
9060
+ if (providers.length === 0) {
9061
+ return `<p class="muted">No ${escapeHtml8(title)} provider health yet.</p>`;
9062
+ }
9063
+ return `<div class="provider-grid">${providers.map((provider) => `
9064
+ <article class="card provider ${escapeHtml8(provider.status)}">
9065
+ <div class="card-header">
9066
+ <strong>${escapeHtml8(provider.provider)}</strong>
9067
+ <span>${escapeHtml8(provider.status)}${provider.recommended ? " \xB7 recommended" : ""}</span>
9068
+ </div>
9069
+ <dl>
9070
+ <div><dt>Runs</dt><dd>${provider.runCount}</dd></div>
9071
+ <div><dt>Avg latency</dt><dd>${provider.averageElapsedMs ?? 0}ms</dd></div>
9072
+ <div><dt>Errors</dt><dd>${provider.errorCount}</dd></div>
9073
+ <div><dt>Timeouts</dt><dd>${provider.timeoutCount}</dd></div>
9074
+ <div><dt>Fallbacks</dt><dd>${provider.fallbackCount}</dd></div>
9075
+ </dl>
9076
+ ${provider.lastError ? `<p class="muted">${escapeHtml8(provider.lastError)}</p>` : ""}
9077
+ </article>
9078
+ `).join("")}</div>`;
9079
+ };
9080
+ var renderTimeline2 = (events) => {
9081
+ if (events.length === 0) {
9082
+ return '<p class="muted">No provider routing events yet. Run the app or simulate provider failover.</p>';
9083
+ }
9084
+ return `<div class="timeline">${events.slice(0, 40).map((event) => `
9085
+ <article class="card event ${escapeHtml8(event.status ?? "unknown")}">
9086
+ <div class="card-header">
9087
+ <strong>${escapeHtml8(event.kind.toUpperCase())} ${escapeHtml8(event.operation ?? "generate")}</strong>
9088
+ <span>${new Date(event.at).toLocaleString()}</span>
9089
+ </div>
9090
+ <p>
9091
+ <span class="pill">${escapeHtml8(event.status ?? "unknown")}</span>
9092
+ <span class="pill">provider: ${escapeHtml8(event.provider ?? "unknown")}</span>
9093
+ ${event.fallbackProvider ? `<span class="pill">fallback: ${escapeHtml8(event.fallbackProvider)}</span>` : ""}
9094
+ ${event.timedOut ? '<span class="pill danger">timed out</span>' : ""}
9095
+ </p>
9096
+ <dl>
9097
+ <div><dt>Attempt</dt><dd>${event.attempt ?? 0}</dd></div>
9098
+ <div><dt>Elapsed</dt><dd>${event.elapsedMs ?? 0}ms</dd></div>
9099
+ <div><dt>Budget</dt><dd>${event.latencyBudgetMs ?? 0}ms</dd></div>
9100
+ <div><dt>Session</dt><dd>${escapeHtml8(event.sessionId)}</dd></div>
9101
+ </dl>
9102
+ ${event.error ? `<p class="muted">${escapeHtml8(event.error)}</p>` : ""}
9103
+ </article>
9104
+ `).join("")}</div>`;
9105
+ };
9106
+ var renderSimulationControls = (kind, simulation) => {
9107
+ if (!simulation) {
9108
+ return "";
9109
+ }
9110
+ const configuredProviders = simulation.providers.filter((provider) => provider.configured !== false);
9111
+ if (configuredProviders.length === 0) {
9112
+ return `<p class="muted">No ${kind.toUpperCase()} providers are configured for simulation.</p>`;
9113
+ }
9114
+ const pathPrefix = simulation.pathPrefix ?? `/api/${kind}-simulate`;
9115
+ const failureProviders = simulation.failureProviders ?? configuredProviders.map(({ provider }) => provider);
9116
+ const canFail = (provider) => configuredProviders.some((entry) => entry.provider === provider) && (!simulation.fallbackRequiredProvider || configuredProviders.some((entry) => entry.provider === simulation.fallbackRequiredProvider));
9117
+ return `<div class="simulate-panel" data-sim-kind="${kind}" data-sim-prefix="${escapeHtml8(pathPrefix)}">
9118
+ <p class="muted">${escapeHtml8(simulation.failureMessage ?? `Simulate ${kind.toUpperCase()} provider failure without changing provider credentials.`)}</p>
9119
+ <div class="simulate-actions">
9120
+ ${failureProviders.map((provider) => `<button type="button" data-provider-fail="${escapeHtml8(provider)}"${canFail(provider) ? "" : " disabled"}>Simulate ${escapeHtml8(provider)} ${kind.toUpperCase()} failure</button>`).join("")}
9121
+ ${configuredProviders.map((provider) => `<button type="button" data-provider-recover="${escapeHtml8(provider.provider)}">Mark ${escapeHtml8(provider.provider)} recovered</button>`).join("")}
9122
+ </div>
9123
+ ${simulation.fallbackRequiredProvider && !configuredProviders.some((entry) => entry.provider === simulation.fallbackRequiredProvider) ? `<p class="muted">${escapeHtml8(simulation.fallbackRequiredMessage ?? `Configure ${simulation.fallbackRequiredProvider} to enable fallback simulation.`)}</p>` : ""}
9124
+ <pre class="simulate-output" hidden></pre>
9125
+ </div>`;
9126
+ };
9127
+ var renderVoiceResilienceHTML = (input) => {
9128
+ const summary = summarizeRoutingEvents(input.routingEvents);
9129
+ const kindCounts = [...summary.byKind.entries()].map(([kind, count]) => `<span class="pill">${escapeHtml8(kind)}: ${String(count)}</span>`).join("");
9130
+ const links = input.links?.length ? input.links.map((link) => `<a href="${escapeHtml8(link.href)}">${escapeHtml8(link.label)}</a>`).join(" \xB7 ") : "";
9131
+ return `<!doctype html>
9132
+ <html lang="en">
9133
+ <head>
9134
+ <meta charset="utf-8" />
9135
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
9136
+ <title>${escapeHtml8(input.title ?? "AbsoluteJS Voice Resilience")}</title>
9137
+ <style>
9138
+ :root { color-scheme: dark; }
9139
+ body { background: radial-gradient(circle at top left, #172554, #09090b 36%, #050505); color: #f4f4f5; font-family: ui-sans-serif, system-ui, sans-serif; margin: 0; padding: 24px; }
9140
+ main { display: grid; gap: 16px; margin: 0 auto; max-width: 1180px; }
9141
+ section, .card { background: rgba(19, 22, 27, 0.92); border: 1px solid #27272a; border-radius: 20px; padding: 20px; }
9142
+ .hero { background: linear-gradient(135deg, rgba(14, 165, 233, 0.18), rgba(245, 158, 11, 0.12)); }
9143
+ .grid, .provider-grid { display: grid; gap: 14px; grid-template-columns: repeat(4, minmax(0, 1fr)); }
9144
+ .timeline { display: grid; gap: 12px; }
9145
+ .card-header { align-items: center; display: flex; gap: 12px; justify-content: space-between; }
9146
+ .card-header strong { font-size: 1.05rem; }
9147
+ .metric strong { display: block; font-size: 2rem; margin-top: 6px; }
9148
+ .muted, dt, span { color: #a1a1aa; }
9149
+ dl { display: grid; gap: 8px; grid-template-columns: repeat(4, minmax(0, 1fr)); }
9150
+ dl div { background: #0f1217; border: 1px solid #27272a; border-radius: 12px; padding: 10px; }
9151
+ dd { font-weight: 800; margin: 4px 0 0; }
9152
+ .pill { background: #0f1217; border: 1px solid #3f3f46; border-radius: 999px; color: #d4d4d8; display: inline-flex; margin: 3px 4px 3px 0; padding: 5px 9px; }
9153
+ .danger { border-color: rgba(239, 68, 68, 0.75); color: #fecaca; }
9154
+ .event.error { border-color: rgba(239, 68, 68, 0.7); }
9155
+ .event.fallback { border-color: rgba(245, 158, 11, 0.7); }
9156
+ .event.success, .provider.healthy { border-color: rgba(34, 197, 94, 0.5); }
9157
+ .provider.suppressed, .provider.degraded, .provider.rate-limited { border-color: rgba(239, 68, 68, 0.7); }
9158
+ .provider.recoverable { border-color: rgba(59, 130, 246, 0.7); }
9159
+ button { background: #f59e0b; border: 0; border-radius: 999px; color: #111827; cursor: pointer; font-weight: 800; padding: 10px 14px; }
9160
+ button:disabled { cursor: not-allowed; opacity: 0.45; }
9161
+ .simulate-actions { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 12px; }
9162
+ .simulate-output { background: #050505; border: 1px solid #27272a; border-radius: 14px; color: #d4d4d8; overflow: auto; padding: 12px; white-space: pre-wrap; }
9163
+ a { color: #f59e0b; }
9164
+ @media (max-width: 850px) { .grid, .provider-grid, dl { grid-template-columns: 1fr; } }
9165
+ </style>
9166
+ </head>
9167
+ <body>
9168
+ <main>
9169
+ <section class="hero">
9170
+ <h1>Provider routing and resilience</h1>
9171
+ <p>One view for the production reliability story: LLM failover, STT/TTS routing, latency budgets, timeouts, and fallback decisions.</p>
9172
+ ${links ? `<p>${links}</p>` : ""}
9173
+ <p>${kindCounts || '<span class="pill">No routing events yet</span>'}</p>
9174
+ </section>
9175
+ <section class="grid">
9176
+ <article class="card metric"><span>Total routing events</span><strong>${summary.total}</strong></article>
9177
+ <article class="card metric"><span>Fallbacks</span><strong>${summary.fallbacks}</strong></article>
9178
+ <article class="card metric"><span>Errors</span><strong>${summary.errors}</strong></article>
9179
+ <article class="card metric"><span>Timeouts</span><strong>${summary.timeouts}</strong></article>
9180
+ </section>
9181
+ <section>
9182
+ <h2>LLM provider health</h2>
9183
+ ${renderProviderCards("LLM", input.llmProviderHealth)}
9184
+ </section>
9185
+ <section>
9186
+ <h2>STT provider health</h2>
9187
+ ${renderSimulationControls("stt", input.sttSimulation)}
9188
+ ${renderProviderCards("STT", input.sttProviderHealth)}
9189
+ </section>
9190
+ <section>
9191
+ <h2>TTS provider health</h2>
9192
+ ${renderSimulationControls("tts", input.ttsSimulation)}
9193
+ ${renderProviderCards("TTS", input.ttsProviderHealth)}
9194
+ </section>
9195
+ <section>
9196
+ <h2>Routing timeline</h2>
9197
+ ${renderTimeline2(input.routingEvents)}
9198
+ </section>
9199
+ </main>
9200
+ <script>
9201
+ const showResult = (panel, result) => {
9202
+ const output = panel.querySelector(".simulate-output");
9203
+ if (!output) return;
9204
+ output.hidden = false;
9205
+ output.textContent = JSON.stringify(result, null, 2);
9206
+ };
9207
+ document.querySelectorAll("[data-sim-prefix]").forEach((panel) => {
9208
+ const prefix = panel.getAttribute("data-sim-prefix");
9209
+ panel.querySelectorAll("[data-provider-fail]").forEach((button) => {
9210
+ button.addEventListener("click", async () => {
9211
+ const provider = button.getAttribute("data-provider-fail");
9212
+ const response = await fetch(prefix + "/failure?provider=" + encodeURIComponent(provider || ""), { method: "POST" });
9213
+ showResult(panel, await response.json());
9214
+ if (response.ok) window.setTimeout(() => window.location.reload(), 450);
9215
+ });
9216
+ });
9217
+ panel.querySelectorAll("[data-provider-recover]").forEach((button) => {
9218
+ button.addEventListener("click", async () => {
9219
+ const provider = button.getAttribute("data-provider-recover");
9220
+ const response = await fetch(prefix + "/recovery?provider=" + encodeURIComponent(provider || ""), { method: "POST" });
9221
+ showResult(panel, await response.json());
9222
+ if (response.ok) window.setTimeout(() => window.location.reload(), 450);
9223
+ });
9224
+ });
9225
+ });
9226
+ </script>
9227
+ </body>
9228
+ </html>`;
9229
+ };
9230
+ var providerFromQuery = (value, providers) => typeof value === "string" && providers.some((provider) => provider.provider === value && provider.configured !== false) ? value : undefined;
9231
+ var registerSimulationRoutes = (routes, simulation, defaultPathPrefix) => {
9232
+ if (!simulation) {
9233
+ return routes;
9234
+ }
9235
+ const pathPrefix = simulation.pathPrefix ?? defaultPathPrefix;
9236
+ routes.post(`${pathPrefix}/failure`, async ({ query, set }) => {
9237
+ const provider = providerFromQuery(query.provider, simulation.providers);
9238
+ if (!provider) {
9239
+ set.status = 400;
9240
+ return {
9241
+ error: "Provider is not configured for simulation."
9242
+ };
9243
+ }
9244
+ if (simulation.failureProviders && !simulation.failureProviders.includes(provider)) {
9245
+ set.status = 400;
9246
+ return {
9247
+ error: `${provider} is not configured for failure simulation.`
9248
+ };
9249
+ }
9250
+ if (simulation.fallbackRequiredProvider && !simulation.providers.some((entry) => entry.provider === simulation.fallbackRequiredProvider && entry.configured !== false)) {
9251
+ set.status = 400;
9252
+ return {
9253
+ error: simulation.fallbackRequiredMessage ?? `Configure ${simulation.fallbackRequiredProvider} before simulating fallback.`
9254
+ };
9255
+ }
9256
+ return simulation.run(provider, "failure");
9257
+ });
9258
+ routes.post(`${pathPrefix}/recovery`, async ({ query, set }) => {
9259
+ const provider = providerFromQuery(query.provider, simulation.providers);
9260
+ if (!provider) {
9261
+ set.status = 400;
9262
+ return {
9263
+ error: "Provider is not configured for simulation."
9264
+ };
9265
+ }
9266
+ return simulation.run(provider, "recovery");
9267
+ });
9268
+ return routes;
9269
+ };
9270
+ var createVoiceResilienceRoutes = (options) => {
9271
+ const path = options.path ?? "/resilience";
9272
+ const routes = new Elysia6({
9273
+ name: options.name ?? "absolutejs-voice-resilience"
9274
+ }).get(path, async () => {
9275
+ const events = await options.store.list();
9276
+ const sttEvents = events.filter((event) => event.payload.kind === "stt");
9277
+ const ttsEvents = events.filter((event) => event.payload.kind === "tts");
9278
+ const data = {
9279
+ links: options.links,
9280
+ llmProviderHealth: await summarizeVoiceProviderHealth({
9281
+ events,
9282
+ providers: options.llmProviders ?? []
9283
+ }),
9284
+ routingEvents: listVoiceRoutingEvents(events),
9285
+ sttProviderHealth: await summarizeVoiceProviderHealth({
9286
+ events: sttEvents,
9287
+ providers: options.sttProviders ?? []
9288
+ }),
9289
+ sttSimulation: options.sttSimulation,
9290
+ title: options.title,
9291
+ ttsProviderHealth: await summarizeVoiceProviderHealth({
9292
+ events: ttsEvents,
9293
+ providers: options.ttsProviders ?? []
9294
+ }),
9295
+ ttsSimulation: options.ttsSimulation
9296
+ };
9297
+ const body = await (options.render ?? renderVoiceResilienceHTML)(data);
9298
+ return new Response(body, {
9299
+ headers: {
9300
+ "Content-Type": "text/html; charset=utf-8",
9301
+ ...options.headers
9302
+ }
9303
+ });
9304
+ });
9305
+ registerSimulationRoutes(routes, options.sttSimulation, "/api/stt-simulate");
9306
+ registerSimulationRoutes(routes, options.ttsSimulation, "/api/tts-simulate");
9307
+ return routes;
9308
+ };
8861
9309
  // src/providerAdapters.ts
8862
9310
  class VoiceIOProviderTimeoutError extends Error {
8863
9311
  provider;
@@ -9702,7 +10150,7 @@ var createVoiceMemoryStore = () => {
9702
10150
  return { get, getOrCreate, list, remove, set };
9703
10151
  };
9704
10152
  // src/opsWebhook.ts
9705
- import { Elysia as Elysia5 } from "elysia";
10153
+ import { Elysia as Elysia7 } from "elysia";
9706
10154
  var toHex5 = (bytes) => Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
9707
10155
  var signVoiceOpsWebhookBody = async (input) => {
9708
10156
  const encoder = new TextEncoder;
@@ -9832,7 +10280,7 @@ var verifyVoiceOpsWebhookSignature = async (input) => {
9832
10280
  };
9833
10281
  var createVoiceOpsWebhookReceiverRoutes = (options = {}) => {
9834
10282
  const path = options.path ?? "/api/voice-ops/webhook";
9835
- return new Elysia5().post(path, async ({ body, request, set }) => {
10283
+ return new Elysia7().post(path, async ({ body, request, set }) => {
9836
10284
  const bodyText = typeof body === "string" ? body : JSON.stringify(body);
9837
10285
  if (options.signingSecret) {
9838
10286
  const verification = await verifyVoiceOpsWebhookSignature({
@@ -9865,9 +10313,9 @@ var createVoiceOpsWebhookReceiverRoutes = (options = {}) => {
9865
10313
  });
9866
10314
  };
9867
10315
  // src/handoffHealth.ts
9868
- import { Elysia as Elysia6 } from "elysia";
9869
- var escapeHtml7 = (value) => value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
9870
- var getString4 = (value) => typeof value === "string" && value.length > 0 ? value : undefined;
10316
+ import { Elysia as Elysia8 } from "elysia";
10317
+ var escapeHtml9 = (value) => value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
10318
+ var getString6 = (value) => typeof value === "string" && value.length > 0 ? value : undefined;
9871
10319
  var isStatus = (value) => value === "delivered" || value === "failed" || value === "skipped";
9872
10320
  var increment3 = (record, key) => {
9873
10321
  record[key] = (record[key] ?? 0) + 1;
@@ -9875,11 +10323,11 @@ var increment3 = (record, key) => {
9875
10323
  var normalizeDelivery = (adapterId, value) => {
9876
10324
  const record = value && typeof value === "object" ? value : {};
9877
10325
  return {
9878
- adapterId: getString4(record.adapterId) ?? adapterId,
9879
- adapterKind: getString4(record.adapterKind),
10326
+ adapterId: getString6(record.adapterId) ?? adapterId,
10327
+ adapterKind: getString6(record.adapterKind),
9880
10328
  deliveredAt: typeof record.deliveredAt === "number" ? record.deliveredAt : undefined,
9881
- deliveredTo: getString4(record.deliveredTo),
9882
- error: getString4(record.error),
10329
+ deliveredTo: getString6(record.deliveredTo),
10330
+ error: getString6(record.error),
9883
10331
  status: isStatus(record.status) ? record.status : "failed"
9884
10332
  };
9885
10333
  };
@@ -9913,13 +10361,13 @@ var summarizeVoiceHandoffHealth = async (options = {}) => {
9913
10361
  const status = isStatus(event.payload.status) ? event.payload.status : "failed";
9914
10362
  const deliveries = normalizeDeliveries(event.payload);
9915
10363
  const item = {
9916
- action: getString4(event.payload.action),
10364
+ action: getString6(event.payload.action),
9917
10365
  at: event.at,
9918
10366
  deliveries,
9919
- reason: getString4(event.payload.reason),
10367
+ reason: getString6(event.payload.reason),
9920
10368
  sessionId: event.sessionId,
9921
10369
  status,
9922
- target: getString4(event.payload.target)
10370
+ target: getString6(event.payload.target)
9923
10371
  };
9924
10372
  return {
9925
10373
  ...item,
@@ -9984,10 +10432,10 @@ var renderActionSummary = (summary) => {
9984
10432
  return [
9985
10433
  '<section class="voice-handoff-health-columns">',
9986
10434
  "<article><h3>Actions</h3>",
9987
- actions.length === 0 ? "<p>No handoff actions yet.</p>" : `<ul>${actions.map(([action, count]) => `<li>${escapeHtml7(action)}: ${String(count)}</li>`).join("")}</ul>`,
10435
+ actions.length === 0 ? "<p>No handoff actions yet.</p>" : `<ul>${actions.map(([action, count]) => `<li>${escapeHtml9(action)}: ${String(count)}</li>`).join("")}</ul>`,
9988
10436
  "</article>",
9989
10437
  "<article><h3>Adapters</h3>",
9990
- 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>`,
10438
+ adapters.length === 0 ? "<p>No adapter deliveries yet.</p>" : `<ul>${adapters.map(([adapterId, counts]) => `<li>${escapeHtml9(adapterId)}: ${String(counts.delivered)} delivered / ${String(counts.failed)} failed / ${String(counts.skipped)} skipped</li>`).join("")}</ul>`,
9991
10439
  "</article>",
9992
10440
  "</section>"
9993
10441
  ].join("");
@@ -10001,22 +10449,22 @@ var renderVoiceHandoffHealthHTML = (summary) => [
10001
10449
  summary.events.length === 0 ? '<p class="voice-handoff-health-empty">No handoffs found.</p>' : [
10002
10450
  '<div class="voice-handoff-health-events">',
10003
10451
  ...summary.events.map((event) => [
10004
- `<article class="${escapeHtml7(event.status)}">`,
10452
+ `<article class="${escapeHtml9(event.status)}">`,
10005
10453
  '<div class="voice-handoff-health-event-header">',
10006
- `<strong>${escapeHtml7(event.action ?? "handoff")}</strong>`,
10007
- `<span>${escapeHtml7(event.status)}</span>`,
10454
+ `<strong>${escapeHtml9(event.action ?? "handoff")}</strong>`,
10455
+ `<span>${escapeHtml9(event.status)}</span>`,
10008
10456
  "</div>",
10009
- `<p><small>${escapeHtml7(event.sessionId)}</small></p>`,
10010
- event.target ? `<p>Target: ${escapeHtml7(event.target)}</p>` : "",
10011
- event.reason ? `<p>Reason: ${escapeHtml7(event.reason)}</p>` : "",
10457
+ `<p><small>${escapeHtml9(event.sessionId)}</small></p>`,
10458
+ event.target ? `<p>Target: ${escapeHtml9(event.target)}</p>` : "",
10459
+ event.reason ? `<p>Reason: ${escapeHtml9(event.reason)}</p>` : "",
10012
10460
  event.deliveries.length ? `<ul>${event.deliveries.map((delivery) => [
10013
10461
  "<li>",
10014
- `${escapeHtml7(delivery.adapterId)}: ${escapeHtml7(delivery.status)}`,
10015
- delivery.deliveredTo ? ` to ${escapeHtml7(delivery.deliveredTo)}` : "",
10016
- delivery.error ? ` (${escapeHtml7(delivery.error)})` : "",
10462
+ `${escapeHtml9(delivery.adapterId)}: ${escapeHtml9(delivery.status)}`,
10463
+ delivery.deliveredTo ? ` to ${escapeHtml9(delivery.deliveredTo)}` : "",
10464
+ delivery.error ? ` (${escapeHtml9(delivery.error)})` : "",
10017
10465
  "</li>"
10018
10466
  ].join("")).join("")}</ul>` : "",
10019
- event.replayHref ? `<p><a href="${escapeHtml7(event.replayHref)}">Open replay</a></p>` : "",
10467
+ event.replayHref ? `<p><a href="${escapeHtml9(event.replayHref)}">Open replay</a></p>` : "",
10020
10468
  "</article>"
10021
10469
  ].join("")),
10022
10470
  "</div>"
@@ -10048,7 +10496,7 @@ var createVoiceHandoffHealthHTMLHandler = (options = {}) => async ({ query }) =>
10048
10496
  var createVoiceHandoffHealthRoutes = (options = {}) => {
10049
10497
  const path = options.path ?? "/api/voice-handoffs";
10050
10498
  const htmlPath = options.htmlPath === undefined ? `${path}/htmx` : options.htmlPath;
10051
- const routes = new Elysia6({
10499
+ const routes = new Elysia8({
10052
10500
  name: options.name ?? "absolutejs-voice-handoff-health"
10053
10501
  }).get(path, createVoiceHandoffHealthJSONHandler(options));
10054
10502
  if (htmlPath) {
@@ -12194,6 +12642,7 @@ export {
12194
12642
  resolveVoiceOpsTaskAssignment,
12195
12643
  resolveVoiceOpsTaskAgeBucket,
12196
12644
  resolveVoiceOpsPreset,
12645
+ resolveVoiceDiagnosticsTraceFilter,
12197
12646
  resolveVoiceAssistantMemoryNamespace,
12198
12647
  resolveTurnDetectionConfig,
12199
12648
  resolveAudioConditioningConfig,
@@ -12202,6 +12651,7 @@ export {
12202
12651
  renderVoiceTraceMarkdown,
12203
12652
  renderVoiceTraceHTML,
12204
12653
  renderVoiceSessionsHTML,
12654
+ renderVoiceResilienceHTML,
12205
12655
  renderVoiceProviderHealthHTML,
12206
12656
  renderVoiceHandoffHealthHTML,
12207
12657
  renderVoiceCallReviewMarkdown,
@@ -12214,6 +12664,7 @@ export {
12214
12664
  pruneVoiceTraceEvents,
12215
12665
  matchesVoiceOpsTaskAssignmentRule,
12216
12666
  markVoiceOpsTaskSLABreached,
12667
+ listVoiceRoutingEvents,
12217
12668
  listVoiceOpsTasks,
12218
12669
  isVoiceOpsTaskOverdue,
12219
12670
  heartbeatVoiceOpsTask,
@@ -12269,6 +12720,7 @@ export {
12269
12720
  createVoiceSQLiteExternalObjectMapStore,
12270
12721
  createVoiceS3ReviewStore,
12271
12722
  createVoiceReviewSavedEvent,
12723
+ createVoiceResilienceRoutes,
12272
12724
  createVoiceRedisTaskLeaseCoordinator,
12273
12725
  createVoiceRedisIdempotencyStore,
12274
12726
  createVoiceProviderRouter,
@@ -12324,6 +12776,7 @@ export {
12324
12776
  createVoiceExternalObjectMapId,
12325
12777
  createVoiceExternalObjectMap,
12326
12778
  createVoiceExperiment,
12779
+ createVoiceDiagnosticsRoutes,
12327
12780
  createVoiceCallReviewRecorder,
12328
12781
  createVoiceCallReviewFromSession,
12329
12782
  createVoiceCallReviewFromLiveTelephonyReport,
@@ -12359,6 +12812,7 @@ export {
12359
12812
  buildVoiceTraceReplay,
12360
12813
  buildVoiceOpsTaskFromSLABreach,
12361
12814
  buildVoiceOpsTaskFromReview,
12815
+ buildVoiceDiagnosticsMarkdown,
12362
12816
  assignVoiceOpsTask,
12363
12817
  applyVoiceOpsTaskPolicy,
12364
12818
  applyVoiceOpsTaskAssignmentRule,
@@ -0,0 +1,106 @@
1
+ import { Elysia } from 'elysia';
2
+ import { type VoiceProviderHealthSummary } from './providerHealth';
3
+ import type { StoredVoiceTraceEvent, VoiceTraceEventStore } from './trace';
4
+ import type { VoiceIOProviderFailureSimulationMode, VoiceIOProviderFailureSimulationResult } from './testing/ioProviderSimulator';
5
+ export type VoiceRoutingEventKind = 'llm' | 'stt' | 'tts';
6
+ export type VoiceRoutingEvent = {
7
+ at: number;
8
+ attempt?: number;
9
+ elapsedMs?: number;
10
+ error?: string;
11
+ fallbackProvider?: string;
12
+ kind: VoiceRoutingEventKind;
13
+ latencyBudgetMs?: number;
14
+ operation?: string;
15
+ provider?: string;
16
+ selectedProvider?: string;
17
+ sessionId: string;
18
+ status?: string;
19
+ timedOut: boolean;
20
+ turnId?: string;
21
+ };
22
+ export type VoiceResilienceLink = {
23
+ href: string;
24
+ label: string;
25
+ };
26
+ export type VoiceResilienceSimulationProvider<TProvider extends string = string> = {
27
+ configured?: boolean;
28
+ provider: TProvider;
29
+ };
30
+ export type VoiceResilienceIOSimulator<TProvider extends string = string> = {
31
+ failureProviders?: readonly TProvider[];
32
+ fallbackRequiredProvider?: TProvider;
33
+ fallbackRequiredMessage?: string;
34
+ failureMessage?: string;
35
+ label?: string;
36
+ pathPrefix?: string;
37
+ providers: readonly VoiceResilienceSimulationProvider<TProvider>[];
38
+ recoveryMessage?: string;
39
+ run: (provider: TProvider, mode: VoiceIOProviderFailureSimulationMode) => Promise<VoiceIOProviderFailureSimulationResult<TProvider>>;
40
+ };
41
+ export type VoiceResiliencePageData = {
42
+ links?: readonly VoiceResilienceLink[];
43
+ llmProviderHealth: VoiceProviderHealthSummary<string>[];
44
+ routingEvents: VoiceRoutingEvent[];
45
+ sttProviderHealth: VoiceProviderHealthSummary<string>[];
46
+ sttSimulation?: VoiceResilienceIOSimulator<string>;
47
+ title?: string;
48
+ ttsProviderHealth: VoiceProviderHealthSummary<string>[];
49
+ ttsSimulation?: VoiceResilienceIOSimulator<string>;
50
+ };
51
+ export type VoiceResilienceRoutesOptions = {
52
+ headers?: HeadersInit;
53
+ links?: readonly VoiceResilienceLink[];
54
+ llmProviders?: readonly string[];
55
+ name?: string;
56
+ path?: string;
57
+ render?: (input: VoiceResiliencePageData) => string | Promise<string>;
58
+ sttProviders?: readonly string[];
59
+ sttSimulation?: VoiceResilienceIOSimulator<string>;
60
+ store: VoiceTraceEventStore;
61
+ title?: string;
62
+ ttsProviders?: readonly string[];
63
+ ttsSimulation?: VoiceResilienceIOSimulator<string>;
64
+ };
65
+ export declare const listVoiceRoutingEvents: (events: StoredVoiceTraceEvent[]) => VoiceRoutingEvent[];
66
+ export declare const renderVoiceResilienceHTML: (input: VoiceResiliencePageData) => string;
67
+ export declare const createVoiceResilienceRoutes: (options: VoiceResilienceRoutesOptions) => Elysia<"", {
68
+ decorator: {};
69
+ store: {};
70
+ derive: {};
71
+ resolve: {};
72
+ }, {
73
+ typebox: {};
74
+ error: {};
75
+ }, {
76
+ schema: {};
77
+ standaloneSchema: {};
78
+ macro: {};
79
+ macroFn: {};
80
+ parser: {};
81
+ response: {};
82
+ }, {
83
+ [x: string]: {
84
+ get: {
85
+ body: unknown;
86
+ params: {};
87
+ query: unknown;
88
+ headers: unknown;
89
+ response: {
90
+ 200: Response;
91
+ };
92
+ };
93
+ };
94
+ }, {
95
+ derive: {};
96
+ resolve: {};
97
+ schema: {};
98
+ standaloneSchema: {};
99
+ response: {};
100
+ }, {
101
+ derive: {};
102
+ resolve: {};
103
+ schema: {};
104
+ standaloneSchema: {};
105
+ response: {};
106
+ }>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@absolutejs/voice",
3
- "version": "0.0.22-beta.36",
3
+ "version": "0.0.22-beta.38",
4
4
  "description": "Voice primitives and Elysia plugin for AbsoluteJS",
5
5
  "repository": {
6
6
  "type": "git",