@absolutejs/voice 0.0.22-beta.37 → 0.0.22-beta.39

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 { createVoiceQualityRoutes, evaluateVoiceQuality, renderVoiceQualityHTML } from './qualityRoutes';
10
12
  export { createVoiceResilienceRoutes, listVoiceRoutingEvents, renderVoiceResilienceHTML } from './resilienceRoutes';
11
13
  export { createVoiceSTTProviderRouter, createVoiceTTSProviderRouter } from './providerAdapters';
12
14
  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';
@@ -35,9 +37,11 @@ export { createVoiceCallReviewFromLiveTelephonyReport, createVoiceCallReviewReco
35
37
  export type { VoiceAssistant, VoiceAssistantArtifactPlan, VoiceAssistantExperiment, VoiceAssistantExperimentOptions, VoiceAssistantGuardrailInput, VoiceAssistantGuardrails, VoiceAssistantMemoryLifecycle, VoiceAssistantMemoryLifecycleInput, VoiceAssistantOptions, VoiceAssistantOutputGuardrailInput, VoiceAssistantPreset, VoiceAssistantRunsSummary, VoiceAssistantRunSummary, VoiceAssistantVariant } from './assistant';
36
38
  export type { VoiceAssistantHealthFailure, VoiceAssistantHealthHTMLHandlerOptions, VoiceAssistantHealthRoutesOptions, VoiceAssistantHealthSummary, VoiceAssistantHealthSummaryOptions } from './assistantHealth';
37
39
  export type { VoiceAssistantMemoryBinding, VoiceAssistantMemoryHandle, VoiceAssistantMemoryOptions, VoiceAssistantMemoryRecord, VoiceAssistantMemoryStore } from './assistantMemory';
40
+ export type { VoiceDiagnosticsRoutesOptions } from './diagnosticsRoutes';
38
41
  export type { VoiceSessionListHTMLHandlerOptions, VoiceSessionListItem, VoiceSessionListOptions, VoiceSessionListRoutesOptions, VoiceSessionListStatus, VoiceSessionReplay, VoiceSessionReplayHTMLHandlerOptions, VoiceSessionReplayOptions, VoiceSessionReplayRoutesOptions, VoiceSessionReplayTurn } from './sessionReplay';
39
42
  export type { AnthropicVoiceAssistantModelOptions, GeminiVoiceAssistantModelOptions, OpenAIVoiceAssistantModelOptions, VoiceProviderRouterEvent, VoiceProviderRouterFallbackMode, VoiceProviderRouterHealthOptions, VoiceProviderRouterOptions, VoiceProviderRouterPolicy, VoiceProviderRouterProviderHealth, VoiceProviderRouterProviderProfile, VoiceJSONAssistantModelHandler, VoiceJSONAssistantModelOptions } from './modelAdapters';
40
43
  export type { VoiceProviderHealthStatus, VoiceProviderHealthSummary, VoiceProviderHealthSummaryOptions } from './providerHealth';
44
+ export type { VoiceQualityMetric, VoiceQualityReport, VoiceQualityRoutesOptions, VoiceQualityStatus, VoiceQualityThresholds } from './qualityRoutes';
41
45
  export type { VoiceResilienceIOSimulator, VoiceResilienceLink, VoiceResiliencePageData, VoiceResilienceRoutesOptions, VoiceResilienceSimulationProvider, VoiceRoutingEvent, VoiceRoutingEventKind } from './resilienceRoutes';
42
46
  export type { VoiceIOProviderRouterEvent, VoiceSTTProviderRouterOptions, VoiceTTSProviderRouterOptions } from './providerAdapters';
43
47
  export type { VoiceAgent, VoiceAgentMessage, VoiceAgentMessageRole, VoiceAgentModel, VoiceAgentModelInput, VoiceAgentModelOutput, VoiceAgentOptions, VoiceAgentRunResult, VoiceAgentSquadOptions, VoiceAgentTool, VoiceAgentToolCall, VoiceAgentToolResult } from './agent';
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,12 +8993,356 @@ var createGeminiVoiceAssistantModel = (options) => {
8858
8993
  }
8859
8994
  };
8860
8995
  };
8996
+ // src/qualityRoutes.ts
8997
+ import { Elysia as Elysia7 } from "elysia";
8998
+
8999
+ // src/handoffHealth.ts
9000
+ import { Elysia as Elysia6 } from "elysia";
9001
+ var escapeHtml8 = (value) => value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
9002
+ var getString5 = (value) => typeof value === "string" && value.length > 0 ? value : undefined;
9003
+ var isStatus = (value) => value === "delivered" || value === "failed" || value === "skipped";
9004
+ var increment3 = (record, key) => {
9005
+ record[key] = (record[key] ?? 0) + 1;
9006
+ };
9007
+ var normalizeDelivery = (adapterId, value) => {
9008
+ const record = value && typeof value === "object" ? value : {};
9009
+ return {
9010
+ adapterId: getString5(record.adapterId) ?? adapterId,
9011
+ adapterKind: getString5(record.adapterKind),
9012
+ deliveredAt: typeof record.deliveredAt === "number" ? record.deliveredAt : undefined,
9013
+ deliveredTo: getString5(record.deliveredTo),
9014
+ error: getString5(record.error),
9015
+ status: isStatus(record.status) ? record.status : "failed"
9016
+ };
9017
+ };
9018
+ var normalizeDeliveries = (payload) => {
9019
+ const deliveries = payload.deliveries;
9020
+ if (!deliveries || typeof deliveries !== "object") {
9021
+ return [];
9022
+ }
9023
+ return Object.entries(deliveries).map(([adapterId, value]) => normalizeDelivery(adapterId, value));
9024
+ };
9025
+ var resolveReplayHref = (event, replayHref) => {
9026
+ if (replayHref === false) {
9027
+ return;
9028
+ }
9029
+ if (typeof replayHref === "function") {
9030
+ return replayHref(event);
9031
+ }
9032
+ return `${replayHref ?? "/api/voice-sessions"}/${encodeURIComponent(event.sessionId)}/replay/htmx`;
9033
+ };
9034
+ var summarizeVoiceHandoffHealth = async (options = {}) => {
9035
+ const sourceEvents = options.events ?? await options.store?.list() ?? [];
9036
+ const search = options.q?.trim().toLowerCase();
9037
+ const byAction = {};
9038
+ const byAdapter = {};
9039
+ const byStatus = {
9040
+ delivered: 0,
9041
+ failed: 0,
9042
+ skipped: 0
9043
+ };
9044
+ const events = sourceEvents.filter((event) => event.type === "call.handoff").map((event) => {
9045
+ const status = isStatus(event.payload.status) ? event.payload.status : "failed";
9046
+ const deliveries = normalizeDeliveries(event.payload);
9047
+ const item = {
9048
+ action: getString5(event.payload.action),
9049
+ at: event.at,
9050
+ deliveries,
9051
+ reason: getString5(event.payload.reason),
9052
+ sessionId: event.sessionId,
9053
+ status,
9054
+ target: getString5(event.payload.target)
9055
+ };
9056
+ return {
9057
+ ...item,
9058
+ replayHref: resolveReplayHref(item, options.replayHref)
9059
+ };
9060
+ }).filter((event) => {
9061
+ if (options.status && options.status !== "all" && event.status !== options.status) {
9062
+ return false;
9063
+ }
9064
+ if (!search) {
9065
+ return true;
9066
+ }
9067
+ return [
9068
+ event.action,
9069
+ event.reason,
9070
+ event.sessionId,
9071
+ event.status,
9072
+ event.target,
9073
+ ...event.deliveries.flatMap((delivery) => [
9074
+ delivery.adapterId,
9075
+ delivery.adapterKind,
9076
+ delivery.deliveredTo,
9077
+ delivery.error,
9078
+ delivery.status
9079
+ ])
9080
+ ].some((value) => value?.toLowerCase().includes(search));
9081
+ }).sort((left, right) => right.at - left.at).slice(0, options.limit ?? 50);
9082
+ for (const event of events) {
9083
+ byStatus[event.status] += 1;
9084
+ if (event.action) {
9085
+ increment3(byAction, event.action);
9086
+ }
9087
+ for (const delivery of event.deliveries) {
9088
+ byAdapter[delivery.adapterId] ??= {
9089
+ delivered: 0,
9090
+ failed: 0,
9091
+ skipped: 0
9092
+ };
9093
+ byAdapter[delivery.adapterId][delivery.status] += 1;
9094
+ }
9095
+ }
9096
+ return {
9097
+ byAction,
9098
+ byAdapter,
9099
+ byStatus,
9100
+ events,
9101
+ failed: byStatus.failed,
9102
+ total: events.length
9103
+ };
9104
+ };
9105
+ var renderMetricGrid = (summary) => [
9106
+ '<section class="voice-handoff-health-grid">',
9107
+ `<article><span>Total</span><strong>${String(summary.total)}</strong></article>`,
9108
+ `<article><span>Delivered</span><strong>${String(summary.byStatus.delivered)}</strong></article>`,
9109
+ `<article><span>Failed</span><strong>${String(summary.byStatus.failed)}</strong></article>`,
9110
+ `<article><span>Skipped</span><strong>${String(summary.byStatus.skipped)}</strong></article>`,
9111
+ "</section>"
9112
+ ].join("");
9113
+ var renderActionSummary = (summary) => {
9114
+ const actions = Object.entries(summary.byAction).sort((left, right) => right[1] - left[1]);
9115
+ const adapters = Object.entries(summary.byAdapter).sort(([left], [right]) => left.localeCompare(right));
9116
+ return [
9117
+ '<section class="voice-handoff-health-columns">',
9118
+ "<article><h3>Actions</h3>",
9119
+ actions.length === 0 ? "<p>No handoff actions yet.</p>" : `<ul>${actions.map(([action, count]) => `<li>${escapeHtml8(action)}: ${String(count)}</li>`).join("")}</ul>`,
9120
+ "</article>",
9121
+ "<article><h3>Adapters</h3>",
9122
+ adapters.length === 0 ? "<p>No adapter deliveries yet.</p>" : `<ul>${adapters.map(([adapterId, counts]) => `<li>${escapeHtml8(adapterId)}: ${String(counts.delivered)} delivered / ${String(counts.failed)} failed / ${String(counts.skipped)} skipped</li>`).join("")}</ul>`,
9123
+ "</article>",
9124
+ "</section>"
9125
+ ].join("");
9126
+ };
9127
+ var renderVoiceHandoffHealthHTML = (summary) => [
9128
+ '<div class="voice-handoff-health">',
9129
+ renderMetricGrid(summary),
9130
+ renderActionSummary(summary),
9131
+ "<section>",
9132
+ "<h3>Recent Handoffs</h3>",
9133
+ summary.events.length === 0 ? '<p class="voice-handoff-health-empty">No handoffs found.</p>' : [
9134
+ '<div class="voice-handoff-health-events">',
9135
+ ...summary.events.map((event) => [
9136
+ `<article class="${escapeHtml8(event.status)}">`,
9137
+ '<div class="voice-handoff-health-event-header">',
9138
+ `<strong>${escapeHtml8(event.action ?? "handoff")}</strong>`,
9139
+ `<span>${escapeHtml8(event.status)}</span>`,
9140
+ "</div>",
9141
+ `<p><small>${escapeHtml8(event.sessionId)}</small></p>`,
9142
+ event.target ? `<p>Target: ${escapeHtml8(event.target)}</p>` : "",
9143
+ event.reason ? `<p>Reason: ${escapeHtml8(event.reason)}</p>` : "",
9144
+ event.deliveries.length ? `<ul>${event.deliveries.map((delivery) => [
9145
+ "<li>",
9146
+ `${escapeHtml8(delivery.adapterId)}: ${escapeHtml8(delivery.status)}`,
9147
+ delivery.deliveredTo ? ` to ${escapeHtml8(delivery.deliveredTo)}` : "",
9148
+ delivery.error ? ` (${escapeHtml8(delivery.error)})` : "",
9149
+ "</li>"
9150
+ ].join("")).join("")}</ul>` : "",
9151
+ event.replayHref ? `<p><a href="${escapeHtml8(event.replayHref)}">Open replay</a></p>` : "",
9152
+ "</article>"
9153
+ ].join("")),
9154
+ "</div>"
9155
+ ].join(""),
9156
+ "</section>",
9157
+ "</div>"
9158
+ ].join("");
9159
+ var createVoiceHandoffHealthJSONHandler = (options = {}) => async ({ query }) => summarizeVoiceHandoffHealth({
9160
+ ...options,
9161
+ limit: typeof query?.limit === "string" ? Number(query.limit) : options.limit,
9162
+ q: query?.q ?? options.q,
9163
+ status: query?.status === "delivered" || query?.status === "failed" || query?.status === "skipped" || query?.status === "all" ? query.status : options.status
9164
+ });
9165
+ var createVoiceHandoffHealthHTMLHandler = (options = {}) => async ({ query }) => {
9166
+ const summary = await summarizeVoiceHandoffHealth({
9167
+ ...options,
9168
+ limit: typeof query?.limit === "string" ? Number(query.limit) : options.limit,
9169
+ q: query?.q ?? options.q,
9170
+ status: query?.status === "delivered" || query?.status === "failed" || query?.status === "skipped" || query?.status === "all" ? query.status : options.status
9171
+ });
9172
+ const body = await (options.render?.(summary) ?? renderVoiceHandoffHealthHTML(summary));
9173
+ return new Response(body, {
9174
+ headers: {
9175
+ "Content-Type": "text/html; charset=utf-8",
9176
+ ...options.headers
9177
+ }
9178
+ });
9179
+ };
9180
+ var createVoiceHandoffHealthRoutes = (options = {}) => {
9181
+ const path = options.path ?? "/api/voice-handoffs";
9182
+ const htmlPath = options.htmlPath === undefined ? `${path}/htmx` : options.htmlPath;
9183
+ const routes = new Elysia6({
9184
+ name: options.name ?? "absolutejs-voice-handoff-health"
9185
+ }).get(path, createVoiceHandoffHealthJSONHandler(options));
9186
+ if (htmlPath) {
9187
+ routes.get(htmlPath, createVoiceHandoffHealthHTMLHandler(options));
9188
+ }
9189
+ return routes;
9190
+ };
9191
+
9192
+ // src/qualityRoutes.ts
9193
+ var DEFAULT_THRESHOLDS = {
9194
+ maxDuplicateTurnRate: 0,
9195
+ maxEmptyTurnRate: 0.02,
9196
+ maxHandoffFailureRate: 0,
9197
+ maxMissingAssistantReplyRate: 0.05,
9198
+ maxProviderAverageLatencyMs: 3000,
9199
+ maxProviderErrorRate: 0.05,
9200
+ maxProviderFallbackRate: 0.25,
9201
+ maxProviderTimeoutRate: 0.03
9202
+ };
9203
+ var getString6 = (value) => typeof value === "string" ? value : undefined;
9204
+ var getNumber3 = (value) => typeof value === "number" && Number.isFinite(value) ? value : undefined;
9205
+ var rate = (count, total) => count / Math.max(1, total);
9206
+ var roundMetric2 = (value) => Math.round(value * 1e4) / 1e4;
9207
+ var createMetric = (input) => ({
9208
+ ...input,
9209
+ actual: roundMetric2(input.actual),
9210
+ pass: input.actual <= input.threshold
9211
+ });
9212
+ var evaluateVoiceQuality = async (input) => {
9213
+ const events = filterVoiceTraceEvents(input.events ?? await input.store?.list() ?? []);
9214
+ const thresholds = {
9215
+ ...DEFAULT_THRESHOLDS,
9216
+ ...input.thresholds
9217
+ };
9218
+ const committedTurns = events.filter((event) => event.type === "turn.committed");
9219
+ const assistantReplies = events.filter((event) => event.type === "turn.assistant");
9220
+ const sessionIdsWithAssistantReply = new Set(assistantReplies.map((event) => event.sessionId));
9221
+ const sessionsWithTurns = new Set(committedTurns.map((event) => event.sessionId));
9222
+ const emptyTurns = committedTurns.filter((event) => !getString6(event.payload.text)?.trim());
9223
+ const turnTextsBySession = new Map;
9224
+ let duplicateTurns = 0;
9225
+ for (const turn of committedTurns) {
9226
+ const normalized = getString6(turn.payload.text)?.trim().toLowerCase();
9227
+ if (!normalized) {
9228
+ continue;
9229
+ }
9230
+ const seen = turnTextsBySession.get(turn.sessionId) ?? new Set;
9231
+ if (seen.has(normalized)) {
9232
+ duplicateTurns += 1;
9233
+ }
9234
+ seen.add(normalized);
9235
+ turnTextsBySession.set(turn.sessionId, seen);
9236
+ }
9237
+ const missingAssistantReplySessions = [...sessionsWithTurns].filter((sessionId) => !sessionIdsWithAssistantReply.has(sessionId)).length;
9238
+ const providerEvents = events.filter((event) => event.type === "session.error" && typeof event.payload.provider === "string" && typeof event.payload.providerStatus === "string");
9239
+ const providerErrors = providerEvents.filter((event) => event.payload.providerStatus === "error");
9240
+ const providerFallbacks = providerEvents.filter((event) => event.payload.providerStatus === "fallback");
9241
+ const providerTimeouts = providerEvents.filter((event) => event.payload.timedOut === true);
9242
+ const providerLatencies = providerEvents.map((event) => getNumber3(event.payload.elapsedMs)).filter((value) => value !== undefined);
9243
+ const averageProviderLatencyMs = providerLatencies.length > 0 ? providerLatencies.reduce((sum, value) => sum + value, 0) / providerLatencies.length : 0;
9244
+ const handoffHealth = await summarizeVoiceHandoffHealth({ events });
9245
+ const metrics = {
9246
+ duplicateTurnRate: createMetric({
9247
+ actual: rate(duplicateTurns, committedTurns.length),
9248
+ label: "Duplicate turn rate",
9249
+ threshold: thresholds.maxDuplicateTurnRate,
9250
+ unit: "rate"
9251
+ }),
9252
+ emptyTurnRate: createMetric({
9253
+ actual: rate(emptyTurns.length, committedTurns.length),
9254
+ label: "Empty turn rate",
9255
+ threshold: thresholds.maxEmptyTurnRate,
9256
+ unit: "rate"
9257
+ }),
9258
+ handoffFailureRate: createMetric({
9259
+ actual: rate(handoffHealth.failed, handoffHealth.total),
9260
+ label: "Handoff failure rate",
9261
+ threshold: thresholds.maxHandoffFailureRate,
9262
+ unit: "rate"
9263
+ }),
9264
+ missingAssistantReplyRate: createMetric({
9265
+ actual: rate(missingAssistantReplySessions, sessionsWithTurns.size),
9266
+ label: "Missing assistant reply rate",
9267
+ threshold: thresholds.maxMissingAssistantReplyRate,
9268
+ unit: "rate"
9269
+ }),
9270
+ providerAverageLatencyMs: createMetric({
9271
+ actual: averageProviderLatencyMs,
9272
+ label: "Average provider latency",
9273
+ threshold: thresholds.maxProviderAverageLatencyMs,
9274
+ unit: "ms"
9275
+ }),
9276
+ providerErrorRate: createMetric({
9277
+ actual: rate(providerErrors.length, providerEvents.length),
9278
+ label: "Provider error rate",
9279
+ threshold: thresholds.maxProviderErrorRate,
9280
+ unit: "rate"
9281
+ }),
9282
+ providerFallbackRate: createMetric({
9283
+ actual: rate(providerFallbacks.length, providerEvents.length),
9284
+ label: "Provider fallback rate",
9285
+ threshold: thresholds.maxProviderFallbackRate,
9286
+ unit: "rate"
9287
+ }),
9288
+ providerTimeoutRate: createMetric({
9289
+ actual: rate(providerTimeouts.length, providerEvents.length),
9290
+ label: "Provider timeout rate",
9291
+ threshold: thresholds.maxProviderTimeoutRate,
9292
+ unit: "rate"
9293
+ })
9294
+ };
9295
+ const status = Object.values(metrics).every((metric) => metric.pass) ? "pass" : "fail";
9296
+ return {
9297
+ checkedAt: Date.now(),
9298
+ eventCount: events.length,
9299
+ metrics,
9300
+ status,
9301
+ thresholds
9302
+ };
9303
+ };
9304
+ var escapeHtml9 = (value) => value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
9305
+ var formatMetricValue = (metric) => metric.unit === "rate" ? `${(metric.actual * 100).toFixed(2)}%` : metric.unit === "ms" ? `${Math.round(metric.actual)}ms` : String(metric.actual);
9306
+ var formatThreshold = (metric) => metric.unit === "rate" ? `${(metric.threshold * 100).toFixed(2)}%` : metric.unit === "ms" ? `${Math.round(metric.threshold)}ms` : String(metric.threshold);
9307
+ var renderVoiceQualityHTML = (report) => {
9308
+ const rows = Object.entries(report.metrics).map(([key, metric]) => `<tr class="${metric.pass ? "pass" : "fail"}"><td>${escapeHtml9(metric.label)}</td><td>${escapeHtml9(formatMetricValue(metric))}</td><td>${escapeHtml9(formatThreshold(metric))}</td><td>${metric.pass ? "pass" : "fail"}</td><td><code>${escapeHtml9(key)}</code></td></tr>`).join("");
9309
+ return `<!doctype html><html lang="en"><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /><title>AbsoluteJS Voice Quality</title><style>body{font-family:ui-sans-serif,system-ui,sans-serif;margin:2rem;background:#f8f7f2;color:#181713}main{max-width:1100px;margin:auto}.status{border-radius:999px;display:inline-flex;padding:.35rem .75rem;font-weight:800}.status.pass{background:#dcfce7;color:#166534}.status.fail{background:#fee2e2;color:#991b1b}table{border-collapse:collapse;width:100%;background:white;margin-top:1rem}td,th{border-bottom:1px solid #eee;padding:.75rem;text-align:left}.pass td{border-left:4px solid #16a34a}.fail td{border-left:4px solid #dc2626}code{background:#f3f4f6;padding:.15rem .3rem;border-radius:.3rem}</style></head><body><main><h1>Voice quality gates</h1><p class="status ${report.status}">${report.status}</p><p>${report.eventCount} event(s) checked.</p><table><thead><tr><th>Metric</th><th>Actual</th><th>Threshold</th><th>Status</th><th>Key</th></tr></thead><tbody>${rows}</tbody></table></main></body></html>`;
9310
+ };
9311
+ var createVoiceQualityRoutes = (options) => {
9312
+ const path = options.path ?? "/quality";
9313
+ const routes = new Elysia7({
9314
+ name: options.name ?? "absolutejs-voice-quality"
9315
+ });
9316
+ const getReport = () => evaluateVoiceQuality({
9317
+ events: options.events,
9318
+ store: options.store,
9319
+ thresholds: options.thresholds
9320
+ });
9321
+ routes.get(path, async () => {
9322
+ const report = await getReport();
9323
+ return new Response(renderVoiceQualityHTML(report), {
9324
+ headers: {
9325
+ "Content-Type": "text/html; charset=utf-8",
9326
+ ...options.headers
9327
+ }
9328
+ });
9329
+ });
9330
+ routes.get(`${path}/json`, async () => getReport());
9331
+ routes.get(`${path}/status`, async ({ set }) => {
9332
+ const report = await getReport();
9333
+ if (report.status === "fail") {
9334
+ set.status = 503;
9335
+ }
9336
+ return report;
9337
+ });
9338
+ return routes;
9339
+ };
8861
9340
  // src/resilienceRoutes.ts
8862
- import { Elysia as Elysia5 } from "elysia";
8863
- var escapeHtml7 = (value) => value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
8864
- var getString4 = (value) => typeof value === "string" ? value : undefined;
8865
- var getNumber2 = (value) => typeof value === "number" && Number.isFinite(value) ? value : undefined;
8866
- var getBoolean = (value) => value === true;
9341
+ import { Elysia as Elysia8 } from "elysia";
9342
+ var escapeHtml10 = (value) => value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
9343
+ var getString7 = (value) => typeof value === "string" ? value : undefined;
9344
+ var getNumber4 = (value) => typeof value === "number" && Number.isFinite(value) ? value : undefined;
9345
+ var getBoolean2 = (value) => value === true;
8867
9346
  var isProviderStatus2 = (value) => value === "error" || value === "fallback" || value === "success";
8868
9347
  var listVoiceRoutingEvents = (events) => {
8869
9348
  const routingEvents = [];
@@ -8871,26 +9350,26 @@ var listVoiceRoutingEvents = (events) => {
8871
9350
  if (event.type !== "session.error") {
8872
9351
  continue;
8873
9352
  }
8874
- const provider = getString4(event.payload.provider);
9353
+ const provider = getString7(event.payload.provider);
8875
9354
  const providerStatus = isProviderStatus2(event.payload.providerStatus) ? event.payload.providerStatus : undefined;
8876
9355
  if (!provider || !providerStatus) {
8877
9356
  continue;
8878
9357
  }
8879
- const kind = getString4(event.payload.kind);
9358
+ const kind = getString7(event.payload.kind);
8880
9359
  routingEvents.push({
8881
9360
  at: event.at,
8882
- attempt: getNumber2(event.payload.attempt),
8883
- elapsedMs: getNumber2(event.payload.elapsedMs),
8884
- error: getString4(event.payload.error),
8885
- fallbackProvider: getString4(event.payload.fallbackProvider),
9361
+ attempt: getNumber4(event.payload.attempt),
9362
+ elapsedMs: getNumber4(event.payload.elapsedMs),
9363
+ error: getString7(event.payload.error),
9364
+ fallbackProvider: getString7(event.payload.fallbackProvider),
8886
9365
  kind: kind === "stt" || kind === "tts" ? kind : "llm",
8887
- latencyBudgetMs: getNumber2(event.payload.latencyBudgetMs),
8888
- operation: getString4(event.payload.operation),
9366
+ latencyBudgetMs: getNumber4(event.payload.latencyBudgetMs),
9367
+ operation: getString7(event.payload.operation),
8889
9368
  provider,
8890
- selectedProvider: getString4(event.payload.selectedProvider),
9369
+ selectedProvider: getString7(event.payload.selectedProvider),
8891
9370
  sessionId: event.sessionId,
8892
9371
  status: providerStatus,
8893
- timedOut: getBoolean(event.payload.timedOut),
9372
+ timedOut: getBoolean2(event.payload.timedOut),
8894
9373
  turnId: event.turnId
8895
9374
  });
8896
9375
  }
@@ -8923,13 +9402,13 @@ var summarizeRoutingEvents = (events) => {
8923
9402
  };
8924
9403
  var renderProviderCards = (title, providers) => {
8925
9404
  if (providers.length === 0) {
8926
- return `<p class="muted">No ${escapeHtml7(title)} provider health yet.</p>`;
9405
+ return `<p class="muted">No ${escapeHtml10(title)} provider health yet.</p>`;
8927
9406
  }
8928
9407
  return `<div class="provider-grid">${providers.map((provider) => `
8929
- <article class="card provider ${escapeHtml7(provider.status)}">
9408
+ <article class="card provider ${escapeHtml10(provider.status)}">
8930
9409
  <div class="card-header">
8931
- <strong>${escapeHtml7(provider.provider)}</strong>
8932
- <span>${escapeHtml7(provider.status)}${provider.recommended ? " \xB7 recommended" : ""}</span>
9410
+ <strong>${escapeHtml10(provider.provider)}</strong>
9411
+ <span>${escapeHtml10(provider.status)}${provider.recommended ? " \xB7 recommended" : ""}</span>
8933
9412
  </div>
8934
9413
  <dl>
8935
9414
  <div><dt>Runs</dt><dd>${provider.runCount}</dd></div>
@@ -8938,7 +9417,7 @@ var renderProviderCards = (title, providers) => {
8938
9417
  <div><dt>Timeouts</dt><dd>${provider.timeoutCount}</dd></div>
8939
9418
  <div><dt>Fallbacks</dt><dd>${provider.fallbackCount}</dd></div>
8940
9419
  </dl>
8941
- ${provider.lastError ? `<p class="muted">${escapeHtml7(provider.lastError)}</p>` : ""}
9420
+ ${provider.lastError ? `<p class="muted">${escapeHtml10(provider.lastError)}</p>` : ""}
8942
9421
  </article>
8943
9422
  `).join("")}</div>`;
8944
9423
  };
@@ -8947,24 +9426,24 @@ var renderTimeline2 = (events) => {
8947
9426
  return '<p class="muted">No provider routing events yet. Run the app or simulate provider failover.</p>';
8948
9427
  }
8949
9428
  return `<div class="timeline">${events.slice(0, 40).map((event) => `
8950
- <article class="card event ${escapeHtml7(event.status ?? "unknown")}">
9429
+ <article class="card event ${escapeHtml10(event.status ?? "unknown")}">
8951
9430
  <div class="card-header">
8952
- <strong>${escapeHtml7(event.kind.toUpperCase())} ${escapeHtml7(event.operation ?? "generate")}</strong>
9431
+ <strong>${escapeHtml10(event.kind.toUpperCase())} ${escapeHtml10(event.operation ?? "generate")}</strong>
8953
9432
  <span>${new Date(event.at).toLocaleString()}</span>
8954
9433
  </div>
8955
9434
  <p>
8956
- <span class="pill">${escapeHtml7(event.status ?? "unknown")}</span>
8957
- <span class="pill">provider: ${escapeHtml7(event.provider ?? "unknown")}</span>
8958
- ${event.fallbackProvider ? `<span class="pill">fallback: ${escapeHtml7(event.fallbackProvider)}</span>` : ""}
9435
+ <span class="pill">${escapeHtml10(event.status ?? "unknown")}</span>
9436
+ <span class="pill">provider: ${escapeHtml10(event.provider ?? "unknown")}</span>
9437
+ ${event.fallbackProvider ? `<span class="pill">fallback: ${escapeHtml10(event.fallbackProvider)}</span>` : ""}
8959
9438
  ${event.timedOut ? '<span class="pill danger">timed out</span>' : ""}
8960
9439
  </p>
8961
9440
  <dl>
8962
9441
  <div><dt>Attempt</dt><dd>${event.attempt ?? 0}</dd></div>
8963
9442
  <div><dt>Elapsed</dt><dd>${event.elapsedMs ?? 0}ms</dd></div>
8964
9443
  <div><dt>Budget</dt><dd>${event.latencyBudgetMs ?? 0}ms</dd></div>
8965
- <div><dt>Session</dt><dd>${escapeHtml7(event.sessionId)}</dd></div>
9444
+ <div><dt>Session</dt><dd>${escapeHtml10(event.sessionId)}</dd></div>
8966
9445
  </dl>
8967
- ${event.error ? `<p class="muted">${escapeHtml7(event.error)}</p>` : ""}
9446
+ ${event.error ? `<p class="muted">${escapeHtml10(event.error)}</p>` : ""}
8968
9447
  </article>
8969
9448
  `).join("")}</div>`;
8970
9449
  };
@@ -8979,26 +9458,26 @@ var renderSimulationControls = (kind, simulation) => {
8979
9458
  const pathPrefix = simulation.pathPrefix ?? `/api/${kind}-simulate`;
8980
9459
  const failureProviders = simulation.failureProviders ?? configuredProviders.map(({ provider }) => provider);
8981
9460
  const canFail = (provider) => configuredProviders.some((entry) => entry.provider === provider) && (!simulation.fallbackRequiredProvider || configuredProviders.some((entry) => entry.provider === simulation.fallbackRequiredProvider));
8982
- return `<div class="simulate-panel" data-sim-kind="${kind}" data-sim-prefix="${escapeHtml7(pathPrefix)}">
8983
- <p class="muted">${escapeHtml7(simulation.failureMessage ?? `Simulate ${kind.toUpperCase()} provider failure without changing provider credentials.`)}</p>
9461
+ return `<div class="simulate-panel" data-sim-kind="${kind}" data-sim-prefix="${escapeHtml10(pathPrefix)}">
9462
+ <p class="muted">${escapeHtml10(simulation.failureMessage ?? `Simulate ${kind.toUpperCase()} provider failure without changing provider credentials.`)}</p>
8984
9463
  <div class="simulate-actions">
8985
- ${failureProviders.map((provider) => `<button type="button" data-provider-fail="${escapeHtml7(provider)}"${canFail(provider) ? "" : " disabled"}>Simulate ${escapeHtml7(provider)} ${kind.toUpperCase()} failure</button>`).join("")}
8986
- ${configuredProviders.map((provider) => `<button type="button" data-provider-recover="${escapeHtml7(provider.provider)}">Mark ${escapeHtml7(provider.provider)} recovered</button>`).join("")}
9464
+ ${failureProviders.map((provider) => `<button type="button" data-provider-fail="${escapeHtml10(provider)}"${canFail(provider) ? "" : " disabled"}>Simulate ${escapeHtml10(provider)} ${kind.toUpperCase()} failure</button>`).join("")}
9465
+ ${configuredProviders.map((provider) => `<button type="button" data-provider-recover="${escapeHtml10(provider.provider)}">Mark ${escapeHtml10(provider.provider)} recovered</button>`).join("")}
8987
9466
  </div>
8988
- ${simulation.fallbackRequiredProvider && !configuredProviders.some((entry) => entry.provider === simulation.fallbackRequiredProvider) ? `<p class="muted">${escapeHtml7(simulation.fallbackRequiredMessage ?? `Configure ${simulation.fallbackRequiredProvider} to enable fallback simulation.`)}</p>` : ""}
9467
+ ${simulation.fallbackRequiredProvider && !configuredProviders.some((entry) => entry.provider === simulation.fallbackRequiredProvider) ? `<p class="muted">${escapeHtml10(simulation.fallbackRequiredMessage ?? `Configure ${simulation.fallbackRequiredProvider} to enable fallback simulation.`)}</p>` : ""}
8989
9468
  <pre class="simulate-output" hidden></pre>
8990
9469
  </div>`;
8991
9470
  };
8992
9471
  var renderVoiceResilienceHTML = (input) => {
8993
9472
  const summary = summarizeRoutingEvents(input.routingEvents);
8994
- const kindCounts = [...summary.byKind.entries()].map(([kind, count]) => `<span class="pill">${escapeHtml7(kind)}: ${String(count)}</span>`).join("");
8995
- const links = input.links?.length ? input.links.map((link) => `<a href="${escapeHtml7(link.href)}">${escapeHtml7(link.label)}</a>`).join(" \xB7 ") : "";
9473
+ const kindCounts = [...summary.byKind.entries()].map(([kind, count]) => `<span class="pill">${escapeHtml10(kind)}: ${String(count)}</span>`).join("");
9474
+ const links = input.links?.length ? input.links.map((link) => `<a href="${escapeHtml10(link.href)}">${escapeHtml10(link.label)}</a>`).join(" \xB7 ") : "";
8996
9475
  return `<!doctype html>
8997
9476
  <html lang="en">
8998
9477
  <head>
8999
9478
  <meta charset="utf-8" />
9000
9479
  <meta name="viewport" content="width=device-width, initial-scale=1" />
9001
- <title>${escapeHtml7(input.title ?? "AbsoluteJS Voice Resilience")}</title>
9480
+ <title>${escapeHtml10(input.title ?? "AbsoluteJS Voice Resilience")}</title>
9002
9481
  <style>
9003
9482
  :root { color-scheme: dark; }
9004
9483
  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; }
@@ -9134,7 +9613,7 @@ var registerSimulationRoutes = (routes, simulation, defaultPathPrefix) => {
9134
9613
  };
9135
9614
  var createVoiceResilienceRoutes = (options) => {
9136
9615
  const path = options.path ?? "/resilience";
9137
- const routes = new Elysia5({
9616
+ const routes = new Elysia8({
9138
9617
  name: options.name ?? "absolutejs-voice-resilience"
9139
9618
  }).get(path, async () => {
9140
9619
  const events = await options.store.list();
@@ -10015,7 +10494,7 @@ var createVoiceMemoryStore = () => {
10015
10494
  return { get, getOrCreate, list, remove, set };
10016
10495
  };
10017
10496
  // src/opsWebhook.ts
10018
- import { Elysia as Elysia6 } from "elysia";
10497
+ import { Elysia as Elysia9 } from "elysia";
10019
10498
  var toHex5 = (bytes) => Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
10020
10499
  var signVoiceOpsWebhookBody = async (input) => {
10021
10500
  const encoder = new TextEncoder;
@@ -10145,7 +10624,7 @@ var verifyVoiceOpsWebhookSignature = async (input) => {
10145
10624
  };
10146
10625
  var createVoiceOpsWebhookReceiverRoutes = (options = {}) => {
10147
10626
  const path = options.path ?? "/api/voice-ops/webhook";
10148
- return new Elysia6().post(path, async ({ body, request, set }) => {
10627
+ return new Elysia9().post(path, async ({ body, request, set }) => {
10149
10628
  const bodyText = typeof body === "string" ? body : JSON.stringify(body);
10150
10629
  if (options.signingSecret) {
10151
10630
  const verification = await verifyVoiceOpsWebhookSignature({
@@ -10177,198 +10656,6 @@ var createVoiceOpsWebhookReceiverRoutes = (options = {}) => {
10177
10656
  parse: "text"
10178
10657
  });
10179
10658
  };
10180
- // src/handoffHealth.ts
10181
- import { Elysia as Elysia7 } from "elysia";
10182
- var escapeHtml8 = (value) => value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
10183
- var getString5 = (value) => typeof value === "string" && value.length > 0 ? value : undefined;
10184
- var isStatus = (value) => value === "delivered" || value === "failed" || value === "skipped";
10185
- var increment3 = (record, key) => {
10186
- record[key] = (record[key] ?? 0) + 1;
10187
- };
10188
- var normalizeDelivery = (adapterId, value) => {
10189
- const record = value && typeof value === "object" ? value : {};
10190
- return {
10191
- adapterId: getString5(record.adapterId) ?? adapterId,
10192
- adapterKind: getString5(record.adapterKind),
10193
- deliveredAt: typeof record.deliveredAt === "number" ? record.deliveredAt : undefined,
10194
- deliveredTo: getString5(record.deliveredTo),
10195
- error: getString5(record.error),
10196
- status: isStatus(record.status) ? record.status : "failed"
10197
- };
10198
- };
10199
- var normalizeDeliveries = (payload) => {
10200
- const deliveries = payload.deliveries;
10201
- if (!deliveries || typeof deliveries !== "object") {
10202
- return [];
10203
- }
10204
- return Object.entries(deliveries).map(([adapterId, value]) => normalizeDelivery(adapterId, value));
10205
- };
10206
- var resolveReplayHref = (event, replayHref) => {
10207
- if (replayHref === false) {
10208
- return;
10209
- }
10210
- if (typeof replayHref === "function") {
10211
- return replayHref(event);
10212
- }
10213
- return `${replayHref ?? "/api/voice-sessions"}/${encodeURIComponent(event.sessionId)}/replay/htmx`;
10214
- };
10215
- var summarizeVoiceHandoffHealth = async (options = {}) => {
10216
- const sourceEvents = options.events ?? await options.store?.list() ?? [];
10217
- const search = options.q?.trim().toLowerCase();
10218
- const byAction = {};
10219
- const byAdapter = {};
10220
- const byStatus = {
10221
- delivered: 0,
10222
- failed: 0,
10223
- skipped: 0
10224
- };
10225
- const events = sourceEvents.filter((event) => event.type === "call.handoff").map((event) => {
10226
- const status = isStatus(event.payload.status) ? event.payload.status : "failed";
10227
- const deliveries = normalizeDeliveries(event.payload);
10228
- const item = {
10229
- action: getString5(event.payload.action),
10230
- at: event.at,
10231
- deliveries,
10232
- reason: getString5(event.payload.reason),
10233
- sessionId: event.sessionId,
10234
- status,
10235
- target: getString5(event.payload.target)
10236
- };
10237
- return {
10238
- ...item,
10239
- replayHref: resolveReplayHref(item, options.replayHref)
10240
- };
10241
- }).filter((event) => {
10242
- if (options.status && options.status !== "all" && event.status !== options.status) {
10243
- return false;
10244
- }
10245
- if (!search) {
10246
- return true;
10247
- }
10248
- return [
10249
- event.action,
10250
- event.reason,
10251
- event.sessionId,
10252
- event.status,
10253
- event.target,
10254
- ...event.deliveries.flatMap((delivery) => [
10255
- delivery.adapterId,
10256
- delivery.adapterKind,
10257
- delivery.deliveredTo,
10258
- delivery.error,
10259
- delivery.status
10260
- ])
10261
- ].some((value) => value?.toLowerCase().includes(search));
10262
- }).sort((left, right) => right.at - left.at).slice(0, options.limit ?? 50);
10263
- for (const event of events) {
10264
- byStatus[event.status] += 1;
10265
- if (event.action) {
10266
- increment3(byAction, event.action);
10267
- }
10268
- for (const delivery of event.deliveries) {
10269
- byAdapter[delivery.adapterId] ??= {
10270
- delivered: 0,
10271
- failed: 0,
10272
- skipped: 0
10273
- };
10274
- byAdapter[delivery.adapterId][delivery.status] += 1;
10275
- }
10276
- }
10277
- return {
10278
- byAction,
10279
- byAdapter,
10280
- byStatus,
10281
- events,
10282
- failed: byStatus.failed,
10283
- total: events.length
10284
- };
10285
- };
10286
- var renderMetricGrid = (summary) => [
10287
- '<section class="voice-handoff-health-grid">',
10288
- `<article><span>Total</span><strong>${String(summary.total)}</strong></article>`,
10289
- `<article><span>Delivered</span><strong>${String(summary.byStatus.delivered)}</strong></article>`,
10290
- `<article><span>Failed</span><strong>${String(summary.byStatus.failed)}</strong></article>`,
10291
- `<article><span>Skipped</span><strong>${String(summary.byStatus.skipped)}</strong></article>`,
10292
- "</section>"
10293
- ].join("");
10294
- var renderActionSummary = (summary) => {
10295
- const actions = Object.entries(summary.byAction).sort((left, right) => right[1] - left[1]);
10296
- const adapters = Object.entries(summary.byAdapter).sort(([left], [right]) => left.localeCompare(right));
10297
- return [
10298
- '<section class="voice-handoff-health-columns">',
10299
- "<article><h3>Actions</h3>",
10300
- actions.length === 0 ? "<p>No handoff actions yet.</p>" : `<ul>${actions.map(([action, count]) => `<li>${escapeHtml8(action)}: ${String(count)}</li>`).join("")}</ul>`,
10301
- "</article>",
10302
- "<article><h3>Adapters</h3>",
10303
- adapters.length === 0 ? "<p>No adapter deliveries yet.</p>" : `<ul>${adapters.map(([adapterId, counts]) => `<li>${escapeHtml8(adapterId)}: ${String(counts.delivered)} delivered / ${String(counts.failed)} failed / ${String(counts.skipped)} skipped</li>`).join("")}</ul>`,
10304
- "</article>",
10305
- "</section>"
10306
- ].join("");
10307
- };
10308
- var renderVoiceHandoffHealthHTML = (summary) => [
10309
- '<div class="voice-handoff-health">',
10310
- renderMetricGrid(summary),
10311
- renderActionSummary(summary),
10312
- "<section>",
10313
- "<h3>Recent Handoffs</h3>",
10314
- summary.events.length === 0 ? '<p class="voice-handoff-health-empty">No handoffs found.</p>' : [
10315
- '<div class="voice-handoff-health-events">',
10316
- ...summary.events.map((event) => [
10317
- `<article class="${escapeHtml8(event.status)}">`,
10318
- '<div class="voice-handoff-health-event-header">',
10319
- `<strong>${escapeHtml8(event.action ?? "handoff")}</strong>`,
10320
- `<span>${escapeHtml8(event.status)}</span>`,
10321
- "</div>",
10322
- `<p><small>${escapeHtml8(event.sessionId)}</small></p>`,
10323
- event.target ? `<p>Target: ${escapeHtml8(event.target)}</p>` : "",
10324
- event.reason ? `<p>Reason: ${escapeHtml8(event.reason)}</p>` : "",
10325
- event.deliveries.length ? `<ul>${event.deliveries.map((delivery) => [
10326
- "<li>",
10327
- `${escapeHtml8(delivery.adapterId)}: ${escapeHtml8(delivery.status)}`,
10328
- delivery.deliveredTo ? ` to ${escapeHtml8(delivery.deliveredTo)}` : "",
10329
- delivery.error ? ` (${escapeHtml8(delivery.error)})` : "",
10330
- "</li>"
10331
- ].join("")).join("")}</ul>` : "",
10332
- event.replayHref ? `<p><a href="${escapeHtml8(event.replayHref)}">Open replay</a></p>` : "",
10333
- "</article>"
10334
- ].join("")),
10335
- "</div>"
10336
- ].join(""),
10337
- "</section>",
10338
- "</div>"
10339
- ].join("");
10340
- var createVoiceHandoffHealthJSONHandler = (options = {}) => async ({ query }) => summarizeVoiceHandoffHealth({
10341
- ...options,
10342
- limit: typeof query?.limit === "string" ? Number(query.limit) : options.limit,
10343
- q: query?.q ?? options.q,
10344
- status: query?.status === "delivered" || query?.status === "failed" || query?.status === "skipped" || query?.status === "all" ? query.status : options.status
10345
- });
10346
- var createVoiceHandoffHealthHTMLHandler = (options = {}) => async ({ query }) => {
10347
- const summary = await summarizeVoiceHandoffHealth({
10348
- ...options,
10349
- limit: typeof query?.limit === "string" ? Number(query.limit) : options.limit,
10350
- q: query?.q ?? options.q,
10351
- status: query?.status === "delivered" || query?.status === "failed" || query?.status === "skipped" || query?.status === "all" ? query.status : options.status
10352
- });
10353
- const body = await (options.render?.(summary) ?? renderVoiceHandoffHealthHTML(summary));
10354
- return new Response(body, {
10355
- headers: {
10356
- "Content-Type": "text/html; charset=utf-8",
10357
- ...options.headers
10358
- }
10359
- });
10360
- };
10361
- var createVoiceHandoffHealthRoutes = (options = {}) => {
10362
- const path = options.path ?? "/api/voice-handoffs";
10363
- const htmlPath = options.htmlPath === undefined ? `${path}/htmx` : options.htmlPath;
10364
- const routes = new Elysia7({
10365
- name: options.name ?? "absolutejs-voice-handoff-health"
10366
- }).get(path, createVoiceHandoffHealthJSONHandler(options));
10367
- if (htmlPath) {
10368
- routes.get(htmlPath, createVoiceHandoffHealthHTMLHandler(options));
10369
- }
10370
- return routes;
10371
- };
10372
10659
  // src/queue.ts
10373
10660
  var releaseLeaseScript = `
10374
10661
  if redis.call("GET", KEYS[1]) == ARGV[1] then
@@ -12507,6 +12794,7 @@ export {
12507
12794
  resolveVoiceOpsTaskAssignment,
12508
12795
  resolveVoiceOpsTaskAgeBucket,
12509
12796
  resolveVoiceOpsPreset,
12797
+ resolveVoiceDiagnosticsTraceFilter,
12510
12798
  resolveVoiceAssistantMemoryNamespace,
12511
12799
  resolveTurnDetectionConfig,
12512
12800
  resolveAudioConditioningConfig,
@@ -12516,6 +12804,7 @@ export {
12516
12804
  renderVoiceTraceHTML,
12517
12805
  renderVoiceSessionsHTML,
12518
12806
  renderVoiceResilienceHTML,
12807
+ renderVoiceQualityHTML,
12519
12808
  renderVoiceProviderHealthHTML,
12520
12809
  renderVoiceHandoffHealthHTML,
12521
12810
  renderVoiceCallReviewMarkdown,
@@ -12537,6 +12826,7 @@ export {
12537
12826
  failVoiceOpsTask,
12538
12827
  exportVoiceTrace,
12539
12828
  evaluateVoiceTrace,
12829
+ evaluateVoiceQuality,
12540
12830
  encodeTwilioMulawBase64,
12541
12831
  deliverVoiceTraceEventsToSinks,
12542
12832
  deliverVoiceIntegrationEventToSinks,
@@ -12587,6 +12877,7 @@ export {
12587
12877
  createVoiceResilienceRoutes,
12588
12878
  createVoiceRedisTaskLeaseCoordinator,
12589
12879
  createVoiceRedisIdempotencyStore,
12880
+ createVoiceQualityRoutes,
12590
12881
  createVoiceProviderRouter,
12591
12882
  createVoiceProviderHealthRoutes,
12592
12883
  createVoiceProviderHealthJSONHandler,
@@ -12640,6 +12931,7 @@ export {
12640
12931
  createVoiceExternalObjectMapId,
12641
12932
  createVoiceExternalObjectMap,
12642
12933
  createVoiceExperiment,
12934
+ createVoiceDiagnosticsRoutes,
12643
12935
  createVoiceCallReviewRecorder,
12644
12936
  createVoiceCallReviewFromSession,
12645
12937
  createVoiceCallReviewFromLiveTelephonyReport,
@@ -12675,6 +12967,7 @@ export {
12675
12967
  buildVoiceTraceReplay,
12676
12968
  buildVoiceOpsTaskFromSLABreach,
12677
12969
  buildVoiceOpsTaskFromReview,
12970
+ buildVoiceDiagnosticsMarkdown,
12678
12971
  assignVoiceOpsTask,
12679
12972
  applyVoiceOpsTaskPolicy,
12680
12973
  applyVoiceOpsTaskAssignmentRule,
@@ -0,0 +1,69 @@
1
+ import { Elysia } from 'elysia';
2
+ import { type StoredVoiceTraceEvent, type VoiceTraceEventStore } from './trace';
3
+ export type VoiceQualityStatus = 'pass' | 'fail';
4
+ export type VoiceQualityThresholds = {
5
+ maxDuplicateTurnRate?: number;
6
+ maxEmptyTurnRate?: number;
7
+ maxHandoffFailureRate?: number;
8
+ maxMissingAssistantReplyRate?: number;
9
+ maxProviderAverageLatencyMs?: number;
10
+ maxProviderErrorRate?: number;
11
+ maxProviderFallbackRate?: number;
12
+ maxProviderTimeoutRate?: number;
13
+ };
14
+ export type VoiceQualityMetric = {
15
+ actual: number;
16
+ label: string;
17
+ pass: boolean;
18
+ threshold: number;
19
+ unit: 'count' | 'ms' | 'rate';
20
+ };
21
+ export type VoiceQualityReport = {
22
+ checkedAt: number;
23
+ eventCount: number;
24
+ metrics: Record<string, VoiceQualityMetric>;
25
+ status: VoiceQualityStatus;
26
+ thresholds: Required<VoiceQualityThresholds>;
27
+ };
28
+ export type VoiceQualityRoutesOptions = {
29
+ events?: StoredVoiceTraceEvent[];
30
+ headers?: HeadersInit;
31
+ name?: string;
32
+ path?: string;
33
+ store?: VoiceTraceEventStore;
34
+ thresholds?: VoiceQualityThresholds;
35
+ };
36
+ export declare const evaluateVoiceQuality: (input: {
37
+ events?: StoredVoiceTraceEvent[];
38
+ store?: VoiceTraceEventStore;
39
+ thresholds?: VoiceQualityThresholds;
40
+ }) => Promise<VoiceQualityReport>;
41
+ export declare const renderVoiceQualityHTML: (report: VoiceQualityReport) => string;
42
+ export declare const createVoiceQualityRoutes: (options: VoiceQualityRoutesOptions) => Elysia<"", {
43
+ decorator: {};
44
+ store: {};
45
+ derive: {};
46
+ resolve: {};
47
+ }, {
48
+ typebox: {};
49
+ error: {};
50
+ }, {
51
+ schema: {};
52
+ standaloneSchema: {};
53
+ macro: {};
54
+ macroFn: {};
55
+ parser: {};
56
+ response: {};
57
+ }, {}, {
58
+ derive: {};
59
+ resolve: {};
60
+ schema: {};
61
+ standaloneSchema: {};
62
+ response: {};
63
+ }, {
64
+ derive: {};
65
+ resolve: {};
66
+ schema: {};
67
+ standaloneSchema: {};
68
+ response: {};
69
+ }>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@absolutejs/voice",
3
- "version": "0.0.22-beta.37",
3
+ "version": "0.0.22-beta.39",
4
4
  "description": "Voice primitives and Elysia plugin for AbsoluteJS",
5
5
  "repository": {
6
6
  "type": "git",