@absolutejs/voice 0.0.22-beta.101 → 0.0.22-beta.103

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -11,6 +11,7 @@ export { createVoiceAgent, createVoiceAgentSquad, createVoiceAgentTool } from '.
11
11
  export { createVoiceToolIdempotencyKey, createVoiceToolRuntime } from './toolRuntime';
12
12
  export { createVoiceToolContract, createVoiceToolContractHTMLHandler, createVoiceToolContractJSONHandler, createVoiceToolContractRoutes, createVoiceToolRuntimeContractDefaults, renderVoiceToolContractHTML, runVoiceToolContractSuite, runVoiceToolContract } from './toolContract';
13
13
  export { createVoiceTurnLatencyHTMLHandler, createVoiceTurnLatencyJSONHandler, createVoiceTurnLatencyRoutes, renderVoiceTurnLatencyHTML, summarizeVoiceTurnLatency } from './turnLatency';
14
+ export { createVoiceLiveLatencyRoutes, renderVoiceLiveLatencyHTML, summarizeVoiceLiveLatency } from './liveLatency';
14
15
  export { createVoiceTurnQualityHTMLHandler, createVoiceTurnQualityJSONHandler, createVoiceTurnQualityRoutes, renderVoiceTurnQualityHTML, summarizeVoiceTurnQuality } from './turnQuality';
15
16
  export { createVoiceOutcomeContractHTMLHandler, createVoiceOutcomeContractJSONHandler, createVoiceOutcomeContractRoutes, renderVoiceOutcomeContractHTML, runVoiceOutcomeContractSuite } from './outcomeContract';
16
17
  export { applyVoiceTelephonyOutcome, createMemoryVoiceTelephonyWebhookIdempotencyStore, createVoiceTelephonyOutcomePolicy, createVoiceTelephonyWebhookHandler, createVoiceTelephonyWebhookRoutes, parseVoiceTelephonyWebhookEvent, resolveVoiceTelephonyOutcome, signVoiceTwilioWebhook, verifyVoiceTwilioWebhookSignature, voiceTelephonyOutcomeToRouteResult } from './telephonyOutcome';
@@ -63,6 +64,7 @@ export type { OpenAIVoiceTTSOptions, OpenAIVoiceTTSVoice } from './openaiTTS';
63
64
  export type { VoiceProviderHealthStatus, VoiceProviderHealthSummary, VoiceProviderHealthSummaryOptions } from './providerHealth';
64
65
  export type { VoiceProviderCapabilityDefinition, VoiceProviderCapabilityHandlerOptions, VoiceProviderCapabilityHTMLHandlerOptions, VoiceProviderCapabilityKind, VoiceProviderCapabilityOptions, VoiceProviderCapabilityReport, VoiceProviderCapabilityRoutesOptions, VoiceProviderCapabilitySummary } from './providerCapabilities';
65
66
  export type { VoiceTurnLatencyHTMLHandlerOptions, VoiceTurnLatencyItem, VoiceTurnLatencyOptions, VoiceTurnLatencyReport, VoiceTurnLatencyRoutesOptions, VoiceTurnLatencyStage, VoiceTurnLatencyStatus } from './turnLatency';
67
+ export type { VoiceLiveLatencyOptions, VoiceLiveLatencyReport, VoiceLiveLatencyRoutesOptions, VoiceLiveLatencySample, VoiceLiveLatencyStatus } from './liveLatency';
66
68
  export type { VoiceTurnQualityHTMLHandlerOptions, VoiceTurnQualityItem, VoiceTurnQualityOptions, VoiceTurnQualityReport, VoiceTurnQualityRoutesOptions, VoiceTurnQualityStatus } from './turnQuality';
67
69
  export type { VoiceOutcomeContractDefinition, VoiceOutcomeContractHTMLHandlerOptions, VoiceOutcomeContractIssue, VoiceOutcomeContractOptions, VoiceOutcomeContractReport, VoiceOutcomeContractRoutesOptions, VoiceOutcomeContractStatus, VoiceOutcomeContractSuiteReport } from './outcomeContract';
68
70
  export type { VoiceTelephonyOutcomeAction, VoiceTelephonyOutcomeDecision, VoiceTelephonyOutcomePolicy, VoiceTelephonyOutcomeProviderEvent, VoiceTelephonyOutcomeRouteResult, VoiceTelephonyOutcomeStatusDecision, VoiceTelephonyWebhookDecision, VoiceTelephonyWebhookHandlerOptions, VoiceTelephonyWebhookIdempotencyStore, VoiceTelephonyWebhookParseInput, VoiceTelephonyWebhookProvider, VoiceTelephonyWebhookRoutesOptions, VoiceTelephonyWebhookVerificationResult, StoredVoiceTelephonyWebhookDecision } from './telephonyOutcome';
package/dist/index.js CHANGED
@@ -9744,12 +9744,28 @@ var resolveCarriers = async (options, input) => {
9744
9744
  providers: [...providers]
9745
9745
  });
9746
9746
  };
9747
+ var summarizeLiveLatency = (events, options) => {
9748
+ const warnAfterMs = options.liveLatencyWarnAfterMs ?? 1800;
9749
+ const failAfterMs = options.liveLatencyFailAfterMs ?? 3200;
9750
+ const latencies = events.filter((event) => event.type === "client.live_latency").map((event) => typeof event.payload.latencyMs === "number" ? event.payload.latencyMs : typeof event.payload.elapsedMs === "number" ? event.payload.elapsedMs : undefined).filter((value) => typeof value === "number");
9751
+ const failed = latencies.filter((value) => value > failAfterMs).length;
9752
+ const warnings = latencies.filter((value) => value > warnAfterMs && value <= failAfterMs).length;
9753
+ const averageLatencyMs = latencies.length > 0 ? Math.round(latencies.reduce((total, value) => total + value, 0) / latencies.length) : undefined;
9754
+ return {
9755
+ averageLatencyMs,
9756
+ failed,
9757
+ status: latencies.length === 0 ? "warn" : failed > 0 ? "fail" : warnings > 0 ? "warn" : "pass",
9758
+ total: latencies.length,
9759
+ warnings
9760
+ };
9761
+ };
9747
9762
  var buildVoiceProductionReadinessReport = async (options, input = {}) => {
9748
9763
  const request = input.request ?? new Request("http://localhost/");
9749
9764
  const query = input.query ?? {};
9750
9765
  const events = await options.store.list();
9751
9766
  const routingEvents = listVoiceRoutingEvents(events);
9752
9767
  const routingSessions = summarizeVoiceRoutingSessions(routingEvents);
9768
+ const liveLatency = summarizeLiveLatency(events, options);
9753
9769
  const [quality, providers, sessions, handoffs, carriers] = await Promise.all([
9754
9770
  evaluateVoiceQuality({ events }),
9755
9771
  Promise.all([
@@ -9856,6 +9872,20 @@ var buildVoiceProductionReadinessReport = async (options, input = {}) => {
9856
9872
  ]
9857
9873
  }
9858
9874
  ];
9875
+ checks.push({
9876
+ detail: liveLatency.total === 0 ? "No browser live-latency measurements are recorded yet." : liveLatency.status === "pass" ? `Live browser turn latency averages ${liveLatency.averageLatencyMs}ms.` : `${liveLatency.failed} failed and ${liveLatency.warnings} warned live-latency measurement(s).`,
9877
+ href: options.links?.liveLatency ?? "/traces",
9878
+ label: "Live latency proof",
9879
+ status: liveLatency.status,
9880
+ value: liveLatency.averageLatencyMs === undefined ? `${liveLatency.total} samples` : `${liveLatency.averageLatencyMs}ms avg`,
9881
+ actions: liveLatency.status === "pass" ? [] : [
9882
+ {
9883
+ description: "Run a live browser voice turn and inspect the persisted latency trace.",
9884
+ href: options.links?.liveLatency ?? "/traces",
9885
+ label: "Open live latency traces"
9886
+ }
9887
+ ]
9888
+ });
9859
9889
  const carrierSummary = carriers ? {
9860
9890
  failing: carriers.summary.failing,
9861
9891
  providers: carriers.summary.providers,
@@ -9886,6 +9916,7 @@ var buildVoiceProductionReadinessReport = async (options, input = {}) => {
9886
9916
  carriers: "/carriers",
9887
9917
  handoffs: "/handoffs",
9888
9918
  handoffRetry: "/api/voice-handoffs/retry",
9919
+ liveLatency: "/traces",
9889
9920
  quality: "/quality",
9890
9921
  resilience: "/resilience",
9891
9922
  sessions: "/sessions",
@@ -9898,6 +9929,7 @@ var buildVoiceProductionReadinessReport = async (options, input = {}) => {
9898
9929
  failed: handoffs.failed,
9899
9930
  total: handoffs.total
9900
9931
  },
9932
+ liveLatency,
9901
9933
  providers: {
9902
9934
  degraded: degradedProviders,
9903
9935
  total: providers.length
@@ -11375,10 +11407,82 @@ var createVoiceTurnLatencyRoutes = (options) => {
11375
11407
  }
11376
11408
  return routes;
11377
11409
  };
11378
- // src/turnQuality.ts
11410
+ // src/liveLatency.ts
11379
11411
  import { Elysia as Elysia19 } from "elysia";
11380
- var DEFAULT_CONFIDENCE_WARN_THRESHOLD = 0.72;
11381
11412
  var escapeHtml20 = (value) => value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
11413
+ var percentile = (values, percentileValue) => {
11414
+ if (values.length === 0) {
11415
+ return;
11416
+ }
11417
+ const sorted = [...values].sort((left, right) => left - right);
11418
+ const index = Math.min(sorted.length - 1, Math.max(0, Math.ceil(percentileValue / 100 * sorted.length) - 1));
11419
+ return Math.round(sorted[index] ?? 0);
11420
+ };
11421
+ var getLatency = (payload) => typeof payload.latencyMs === "number" ? payload.latencyMs : typeof payload.elapsedMs === "number" ? payload.elapsedMs : undefined;
11422
+ var summarizeVoiceLiveLatency = async (options) => {
11423
+ const warnAfterMs = options.warnAfterMs ?? 1800;
11424
+ const failAfterMs = options.failAfterMs ?? 3200;
11425
+ const events = await options.store.list({
11426
+ limit: options.limit ?? 100,
11427
+ type: "client.live_latency"
11428
+ });
11429
+ const recent = events.map((event) => {
11430
+ const latencyMs = getLatency(event.payload);
11431
+ if (latencyMs === undefined) {
11432
+ return;
11433
+ }
11434
+ return {
11435
+ at: event.at,
11436
+ latencyMs,
11437
+ sessionId: event.sessionId,
11438
+ status: typeof event.payload.status === "string" ? event.payload.status : undefined,
11439
+ traceId: event.traceId
11440
+ };
11441
+ }).filter((sample) => Boolean(sample)).sort((left, right) => right.at - left.at);
11442
+ const latencies = recent.map((sample) => sample.latencyMs);
11443
+ const failed = latencies.filter((value) => value > failAfterMs).length;
11444
+ const warnings = latencies.filter((value) => value > warnAfterMs && value <= failAfterMs).length;
11445
+ return {
11446
+ averageLatencyMs: latencies.length > 0 ? Math.round(latencies.reduce((total, value) => total + value, 0) / latencies.length) : undefined,
11447
+ checkedAt: Date.now(),
11448
+ failed,
11449
+ p50LatencyMs: percentile(latencies, 50),
11450
+ p95LatencyMs: percentile(latencies, 95),
11451
+ recent,
11452
+ status: latencies.length === 0 ? "empty" : failed > 0 ? "fail" : warnings > 0 ? "warn" : "pass",
11453
+ total: latencies.length,
11454
+ warnings
11455
+ };
11456
+ };
11457
+ var formatMs3 = (value) => typeof value === "number" ? `${Math.round(value)}ms` : "n/a";
11458
+ var renderVoiceLiveLatencyHTML = (report, options = {}) => {
11459
+ const title = options.title ?? "Voice Live Latency";
11460
+ const rows = report.recent.map((sample) => `<tr><td>${escapeHtml20(sample.sessionId)}</td><td>${escapeHtml20(formatMs3(sample.latencyMs))}</td><td>${escapeHtml20(sample.status ?? "unknown")}</td><td>${escapeHtml20(new Date(sample.at).toLocaleString())}</td></tr>`).join("");
11461
+ return `<!doctype html><html lang="en"><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /><title>${escapeHtml20(title)}</title><style>body{background:#0c0f14;color:#f6f2e8;font-family:ui-sans-serif,system-ui,sans-serif;margin:0}main{margin:auto;max-width:1060px;padding:32px}.hero{background:linear-gradient(135deg,rgba(94,234,212,.16),rgba(245,158,11,.1));border:1px solid #26313d;border-radius:28px;margin-bottom:18px;padding:28px}.eyebrow{color:#5eead4;font-weight:900;letter-spacing:.12em;text-transform:uppercase}h1{font-size:clamp(2.4rem,6vw,5rem);line-height:.9;margin:.2rem 0 1rem}.status{border:1px solid #3f3f46;border-radius:999px;display:inline-flex;padding:8px 12px}.pass{color:#86efac}.warn,.empty{color:#fbbf24}.fail{color:#fca5a5}.metrics{display:grid;gap:14px;grid-template-columns:repeat(auto-fit,minmax(160px,1fr));margin:18px 0}.metrics article,table{background:#141922;border:1px solid #26313d;border-radius:18px}.metrics article{padding:16px}.metrics span{color:#a8b0b8}.metrics strong{display:block;font-size:2rem;margin-top:.25rem}table{border-collapse:collapse;overflow:hidden;width:100%}td,th{border-bottom:1px solid #26313d;padding:12px;text-align:left}@media(max-width:760px){main{padding:20px}}</style></head><body><main><section class="hero"><p class="eyebrow">Browser proof</p><h1>${escapeHtml20(title)}</h1><p>Recent real browser speech-to-assistant response measurements from persisted <code>client.live_latency</code> traces.</p><p class="status ${escapeHtml20(report.status)}">Status: ${escapeHtml20(report.status)}</p><section class="metrics"><article><span>p50</span><strong>${escapeHtml20(formatMs3(report.p50LatencyMs))}</strong></article><article><span>p95</span><strong>${escapeHtml20(formatMs3(report.p95LatencyMs))}</strong></article><article><span>Average</span><strong>${escapeHtml20(formatMs3(report.averageLatencyMs))}</strong></article><article><span>Samples</span><strong>${String(report.total)}</strong></article></section></section><table><thead><tr><th>Session</th><th>Latency</th><th>Status</th><th>Measured</th></tr></thead><tbody>${rows || '<tr><td colspan="4">No live latency samples yet.</td></tr>'}</tbody></table></main></body></html>`;
11462
+ };
11463
+ var createVoiceLiveLatencyRoutes = (options) => {
11464
+ const path = options.path ?? "/api/live-latency";
11465
+ const htmlPath = options.htmlPath === undefined ? "/live-latency" : options.htmlPath;
11466
+ const routes = new Elysia19({
11467
+ name: options.name ?? "absolutejs-voice-live-latency"
11468
+ }).get(path, () => summarizeVoiceLiveLatency(options));
11469
+ if (htmlPath) {
11470
+ routes.get(htmlPath, async () => {
11471
+ const report = await summarizeVoiceLiveLatency(options);
11472
+ return new Response(renderVoiceLiveLatencyHTML(report, options), {
11473
+ headers: {
11474
+ "content-type": "text/html; charset=utf-8",
11475
+ ...options.headers
11476
+ }
11477
+ });
11478
+ });
11479
+ }
11480
+ return routes;
11481
+ };
11482
+ // src/turnQuality.ts
11483
+ import { Elysia as Elysia20 } from "elysia";
11484
+ var DEFAULT_CONFIDENCE_WARN_THRESHOLD = 0.72;
11485
+ var escapeHtml21 = (value) => value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
11382
11486
  var getTurnLatencyMs = (turn) => {
11383
11487
  const firstTranscriptAt = turn.transcripts.map((transcript) => transcript.endedAtMs ?? transcript.startedAtMs).filter((value) => typeof value === "number").sort((left, right) => left - right)[0];
11384
11488
  if (firstTranscriptAt === undefined) {
@@ -11449,24 +11553,24 @@ var summarizeVoiceTurnQuality = async (options) => {
11449
11553
  };
11450
11554
  var renderVoiceTurnQualityHTML = (report, options = {}) => {
11451
11555
  const title = options.title ?? "Voice Turn Quality";
11452
- const turns = report.turns.map((turn) => `<article class="turn ${escapeHtml20(turn.status)}">
11556
+ const turns = report.turns.map((turn) => `<article class="turn ${escapeHtml21(turn.status)}">
11453
11557
  <div class="turn-header">
11454
11558
  <div>
11455
- <p class="eyebrow">${escapeHtml20(turn.sessionId)} \xB7 ${escapeHtml20(turn.turnId)}</p>
11456
- <h2>${escapeHtml20(turn.text || "Empty turn")}</h2>
11559
+ <p class="eyebrow">${escapeHtml21(turn.sessionId)} \xB7 ${escapeHtml21(turn.turnId)}</p>
11560
+ <h2>${escapeHtml21(turn.text || "Empty turn")}</h2>
11457
11561
  </div>
11458
- <strong>${escapeHtml20(turn.status)}</strong>
11562
+ <strong>${escapeHtml21(turn.status)}</strong>
11459
11563
  </div>
11460
11564
  <dl>
11461
- <div><dt>Source</dt><dd>${escapeHtml20(turn.source ?? "unknown")}</dd></div>
11565
+ <div><dt>Source</dt><dd>${escapeHtml21(turn.source ?? "unknown")}</dd></div>
11462
11566
  <div><dt>Confidence</dt><dd>${turn.averageConfidence === undefined ? "n/a" : `${Math.round(turn.averageConfidence * 100)}%`}</dd></div>
11463
- <div><dt>Fallback</dt><dd>${turn.fallbackUsed ? `yes (${escapeHtml20(turn.fallbackSelectionReason ?? "selected")})` : "no"}</dd></div>
11464
- <div><dt>Correction</dt><dd>${turn.correctionChanged ? `changed${turn.correctionProvider ? ` by ${escapeHtml20(turn.correctionProvider)}` : ""}` : "none"}</dd></div>
11567
+ <div><dt>Fallback</dt><dd>${turn.fallbackUsed ? `yes (${escapeHtml21(turn.fallbackSelectionReason ?? "selected")})` : "no"}</dd></div>
11568
+ <div><dt>Correction</dt><dd>${turn.correctionChanged ? `changed${turn.correctionProvider ? ` by ${escapeHtml21(turn.correctionProvider)}` : ""}` : "none"}</dd></div>
11465
11569
  <div><dt>Transcripts</dt><dd>${String(turn.selectedTranscriptCount)} selected \xB7 ${String(turn.finalTranscriptCount)} final \xB7 ${String(turn.partialTranscriptCount)} partial</dd></div>
11466
11570
  <div><dt>Cost</dt><dd>${turn.costUnits === undefined ? "n/a" : String(turn.costUnits)}</dd></div>
11467
11571
  </dl>
11468
11572
  </article>`).join("");
11469
- return `<!doctype html><html lang="en"><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /><title>${escapeHtml20(title)}</title><style>body{background:#101316;color:#f6f2e8;font-family:ui-sans-serif,system-ui,sans-serif;margin:0}main{margin:auto;max-width:1180px;padding:32px}.hero,.turn{background:#181d22;border:1px solid #2a323a;border-radius:20px;margin-bottom:16px;padding:20px}.hero{background:linear-gradient(135deg,rgba(251,191,36,.16),rgba(34,197,94,.1))}.eyebrow{color:#fbbf24;font-size:.78rem;font-weight:900;letter-spacing:.08em;text-transform:uppercase}h1{font-size:clamp(2.3rem,6vw,5rem);letter-spacing:-.06em;line-height:.9;margin:.2rem 0 1rem}h2{margin:.2rem 0 1rem}.summary{display:flex;flex-wrap:wrap;gap:10px}.pill{background:#0f1217;border:1px solid #3f3f46;border-radius:999px;padding:7px 10px}.turn-header{align-items:flex-start;display:flex;gap:16px;justify-content:space-between}.pass{color:#86efac}.warn,.unknown{color:#fde68a}.fail{color:#fca5a5}.turn.fail{border-color:rgba(248,113,113,.45)}dl{display:grid;gap:8px;grid-template-columns:repeat(auto-fit,minmax(160px,1fr))}dt{color:#a8b0b8;font-size:.8rem}dd{margin:0}@media(max-width:800px){main{padding:18px}.turn-header{display:block}}</style></head><body><main><section class="hero"><p class="eyebrow">Realtime STT Debugging</p><h1>${escapeHtml20(title)}</h1><div class="summary"><span class="pill ${escapeHtml20(report.status)}">${escapeHtml20(report.status)}</span><span class="pill">${String(report.total)} turns</span><span class="pill">${String(report.warnings)} warnings</span><span class="pill">${String(report.failed)} failed</span><span class="pill">${String(report.sessions)} sessions</span></div></section>${turns || '<section class="turn"><p>No committed turns found.</p></section>'}</main></body></html>`;
11573
+ return `<!doctype html><html lang="en"><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /><title>${escapeHtml21(title)}</title><style>body{background:#101316;color:#f6f2e8;font-family:ui-sans-serif,system-ui,sans-serif;margin:0}main{margin:auto;max-width:1180px;padding:32px}.hero,.turn{background:#181d22;border:1px solid #2a323a;border-radius:20px;margin-bottom:16px;padding:20px}.hero{background:linear-gradient(135deg,rgba(251,191,36,.16),rgba(34,197,94,.1))}.eyebrow{color:#fbbf24;font-size:.78rem;font-weight:900;letter-spacing:.08em;text-transform:uppercase}h1{font-size:clamp(2.3rem,6vw,5rem);letter-spacing:-.06em;line-height:.9;margin:.2rem 0 1rem}h2{margin:.2rem 0 1rem}.summary{display:flex;flex-wrap:wrap;gap:10px}.pill{background:#0f1217;border:1px solid #3f3f46;border-radius:999px;padding:7px 10px}.turn-header{align-items:flex-start;display:flex;gap:16px;justify-content:space-between}.pass{color:#86efac}.warn,.unknown{color:#fde68a}.fail{color:#fca5a5}.turn.fail{border-color:rgba(248,113,113,.45)}dl{display:grid;gap:8px;grid-template-columns:repeat(auto-fit,minmax(160px,1fr))}dt{color:#a8b0b8;font-size:.8rem}dd{margin:0}@media(max-width:800px){main{padding:18px}.turn-header{display:block}}</style></head><body><main><section class="hero"><p class="eyebrow">Realtime STT Debugging</p><h1>${escapeHtml21(title)}</h1><div class="summary"><span class="pill ${escapeHtml21(report.status)}">${escapeHtml21(report.status)}</span><span class="pill">${String(report.total)} turns</span><span class="pill">${String(report.warnings)} warnings</span><span class="pill">${String(report.failed)} failed</span><span class="pill">${String(report.sessions)} sessions</span></div></section>${turns || '<section class="turn"><p>No committed turns found.</p></section>'}</main></body></html>`;
11470
11574
  };
11471
11575
  var createVoiceTurnQualityJSONHandler = (options) => async () => summarizeVoiceTurnQuality(options);
11472
11576
  var createVoiceTurnQualityHTMLHandler = (options) => async () => {
@@ -11483,7 +11587,7 @@ var createVoiceTurnQualityHTMLHandler = (options) => async () => {
11483
11587
  var createVoiceTurnQualityRoutes = (options) => {
11484
11588
  const path = options.path ?? "/api/turn-quality";
11485
11589
  const htmlPath = options.htmlPath === undefined ? `${path}/htmx` : options.htmlPath;
11486
- const routes = new Elysia19({
11590
+ const routes = new Elysia20({
11487
11591
  name: options.name ?? "absolutejs-voice-turn-quality"
11488
11592
  }).get(path, createVoiceTurnQualityJSONHandler(options));
11489
11593
  if (htmlPath) {
@@ -11492,8 +11596,8 @@ var createVoiceTurnQualityRoutes = (options) => {
11492
11596
  return routes;
11493
11597
  };
11494
11598
  // src/outcomeContract.ts
11495
- import { Elysia as Elysia20 } from "elysia";
11496
- var escapeHtml21 = (value) => value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
11599
+ import { Elysia as Elysia21 } from "elysia";
11600
+ var escapeHtml22 = (value) => value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
11497
11601
  var getPayloadString = (event, key) => typeof event.payload[key] === "string" ? event.payload[key] : undefined;
11498
11602
  var toList = async (input) => Array.isArray(input) ? input : await input?.list() ?? [];
11499
11603
  var hydrateSessions = async (input) => {
@@ -11601,9 +11705,9 @@ var renderVoiceOutcomeContractHTML = (report, options = {}) => {
11601
11705
  const contracts = report.contracts.map((contract) => `<section class="contract ${contract.pass ? "pass" : "fail"}">
11602
11706
  <div class="contract-header">
11603
11707
  <div>
11604
- <p class="eyebrow">${escapeHtml21(contract.contractId)}</p>
11605
- <h2>${escapeHtml21(contract.label ?? contract.contractId)}</h2>
11606
- ${contract.description ? `<p>${escapeHtml21(contract.description)}</p>` : ""}
11708
+ <p class="eyebrow">${escapeHtml22(contract.contractId)}</p>
11709
+ <h2>${escapeHtml22(contract.label ?? contract.contractId)}</h2>
11710
+ ${contract.description ? `<p>${escapeHtml22(contract.description)}</p>` : ""}
11607
11711
  </div>
11608
11712
  <strong>${contract.pass ? "pass" : "fail"}</strong>
11609
11713
  </div>
@@ -11614,9 +11718,9 @@ var renderVoiceOutcomeContractHTML = (report, options = {}) => {
11614
11718
  <span>handoffs ${String(contract.matched.handoffs)}</span>
11615
11719
  <span>events ${String(contract.matched.integrationEvents)}</span>
11616
11720
  </div>
11617
- ${contract.issues.length ? `<ul>${contract.issues.map((issue) => `<li>${escapeHtml21(issue.message)}</li>`).join("")}</ul>` : ""}
11721
+ ${contract.issues.length ? `<ul>${contract.issues.map((issue) => `<li>${escapeHtml22(issue.message)}</li>`).join("")}</ul>` : ""}
11618
11722
  </section>`).join("");
11619
- return `<!doctype html><html lang="en"><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /><title>${escapeHtml21(title)}</title><style>body{background:#101316;color:#f6f2e8;font-family:ui-sans-serif,system-ui,sans-serif;margin:0}main{margin:auto;max-width:1180px;padding:32px}.hero,.contract{background:#181d22;border:1px solid #2a323a;border-radius:20px;margin-bottom:16px;padding:20px}.hero{background:linear-gradient(135deg,rgba(34,197,94,.14),rgba(14,165,233,.12))}.eyebrow{color:#7dd3fc;font-size:.78rem;font-weight:900;letter-spacing:.08em;text-transform:uppercase}h1{font-size:clamp(2.3rem,6vw,5rem);letter-spacing:-.06em;line-height:.9;margin:.2rem 0 1rem}h2{margin:.2rem 0}.summary,.grid{display:flex;flex-wrap:wrap;gap:10px}.pill,.grid span{background:#0f1217;border:1px solid #3f3f46;border-radius:999px;padding:7px 10px}.contract-header{display:flex;gap:16px;justify-content:space-between}.pass{color:#86efac}.fail{color:#fca5a5}.contract.fail{border-color:rgba(248,113,113,.45)}li{margin:8px 0}@media(max-width:800px){main{padding:18px}.contract-header{display:block}}</style></head><body><main><section class="hero"><p class="eyebrow">Business Outcome Verification</p><h1>${escapeHtml21(title)}</h1><div class="summary"><span class="pill ${report.status}">${report.status}</span><span class="pill">${String(report.passed)} passing</span><span class="pill">${String(report.failed)} failing</span><span class="pill">${String(report.total)} contracts</span></div></section>${contracts || '<section class="contract"><p>No outcome contracts configured.</p></section>'}</main></body></html>`;
11723
+ return `<!doctype html><html lang="en"><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /><title>${escapeHtml22(title)}</title><style>body{background:#101316;color:#f6f2e8;font-family:ui-sans-serif,system-ui,sans-serif;margin:0}main{margin:auto;max-width:1180px;padding:32px}.hero,.contract{background:#181d22;border:1px solid #2a323a;border-radius:20px;margin-bottom:16px;padding:20px}.hero{background:linear-gradient(135deg,rgba(34,197,94,.14),rgba(14,165,233,.12))}.eyebrow{color:#7dd3fc;font-size:.78rem;font-weight:900;letter-spacing:.08em;text-transform:uppercase}h1{font-size:clamp(2.3rem,6vw,5rem);letter-spacing:-.06em;line-height:.9;margin:.2rem 0 1rem}h2{margin:.2rem 0}.summary,.grid{display:flex;flex-wrap:wrap;gap:10px}.pill,.grid span{background:#0f1217;border:1px solid #3f3f46;border-radius:999px;padding:7px 10px}.contract-header{display:flex;gap:16px;justify-content:space-between}.pass{color:#86efac}.fail{color:#fca5a5}.contract.fail{border-color:rgba(248,113,113,.45)}li{margin:8px 0}@media(max-width:800px){main{padding:18px}.contract-header{display:block}}</style></head><body><main><section class="hero"><p class="eyebrow">Business Outcome Verification</p><h1>${escapeHtml22(title)}</h1><div class="summary"><span class="pill ${report.status}">${report.status}</span><span class="pill">${String(report.passed)} passing</span><span class="pill">${String(report.failed)} failing</span><span class="pill">${String(report.total)} contracts</span></div></section>${contracts || '<section class="contract"><p>No outcome contracts configured.</p></section>'}</main></body></html>`;
11620
11724
  };
11621
11725
  var createVoiceOutcomeContractJSONHandler = (options) => async () => runVoiceOutcomeContractSuite(options);
11622
11726
  var createVoiceOutcomeContractHTMLHandler = (options) => async () => {
@@ -11632,7 +11736,7 @@ var createVoiceOutcomeContractHTMLHandler = (options) => async () => {
11632
11736
  var createVoiceOutcomeContractRoutes = (options) => {
11633
11737
  const path = options.path ?? "/api/outcome-contracts";
11634
11738
  const htmlPath = options.htmlPath === undefined ? `${path}/htmx` : options.htmlPath;
11635
- const routes = new Elysia20({
11739
+ const routes = new Elysia21({
11636
11740
  name: options.name ?? "absolutejs-voice-outcome-contracts"
11637
11741
  }).get(path, createVoiceOutcomeContractJSONHandler(options));
11638
11742
  if (htmlPath) {
@@ -11641,7 +11745,7 @@ var createVoiceOutcomeContractRoutes = (options) => {
11641
11745
  return routes;
11642
11746
  };
11643
11747
  // src/telephonyOutcome.ts
11644
- import { Elysia as Elysia21 } from "elysia";
11748
+ import { Elysia as Elysia22 } from "elysia";
11645
11749
  var DEFAULT_COMPLETED_STATUSES = [
11646
11750
  "answered",
11647
11751
  "completed",
@@ -12288,7 +12392,7 @@ var createVoiceTelephonyWebhookHandler = (options = {}) => async (input) => {
12288
12392
  var createVoiceTelephonyWebhookRoutes = (options = {}) => {
12289
12393
  const path = options.path ?? "/api/voice/telephony/webhook";
12290
12394
  const handler = createVoiceTelephonyWebhookHandler(options);
12291
- return new Elysia21({
12395
+ return new Elysia22({
12292
12396
  name: options.name ?? "absolutejs-voice-telephony-webhooks"
12293
12397
  }).post(path, async ({ query, request }) => {
12294
12398
  try {
@@ -14556,7 +14660,7 @@ var createVoiceMemoryStore = () => {
14556
14660
  return { get, getOrCreate, list, remove, set };
14557
14661
  };
14558
14662
  // src/opsWebhook.ts
14559
- import { Elysia as Elysia22 } from "elysia";
14663
+ import { Elysia as Elysia23 } from "elysia";
14560
14664
  var toHex5 = (bytes) => Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
14561
14665
  var signVoiceOpsWebhookBody = async (input) => {
14562
14666
  const encoder = new TextEncoder;
@@ -14686,7 +14790,7 @@ var verifyVoiceOpsWebhookSignature = async (input) => {
14686
14790
  };
14687
14791
  var createVoiceOpsWebhookReceiverRoutes = (options = {}) => {
14688
14792
  const path = options.path ?? "/api/voice-ops/webhook";
14689
- return new Elysia22().post(path, async ({ body, request, set }) => {
14793
+ return new Elysia23().post(path, async ({ body, request, set }) => {
14690
14794
  const bodyText = typeof body === "string" ? body : JSON.stringify(body);
14691
14795
  if (options.signingSecret) {
14692
14796
  const verification = await verifyVoiceOpsWebhookSignature({
@@ -16415,7 +16519,7 @@ var createVoiceSTTRoutingCorrectionHandler = (mode = "generic") => {
16415
16519
  };
16416
16520
  // src/telephony/twilio.ts
16417
16521
  import { Buffer as Buffer3 } from "buffer";
16418
- import { Elysia as Elysia23 } from "elysia";
16522
+ import { Elysia as Elysia24 } from "elysia";
16419
16523
  var TWILIO_MULAW_SAMPLE_RATE = 8000;
16420
16524
  var VOICE_PCM_SAMPLE_RATE = 16000;
16421
16525
  var escapeXml2 = (value) => value.replaceAll("&", "&amp;").replaceAll('"', "&quot;").replaceAll("'", "&apos;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
@@ -16445,7 +16549,7 @@ var resolveTwilioStreamParameters = async (parameters, input) => {
16445
16549
  return parameters;
16446
16550
  };
16447
16551
  var joinUrlPath = (origin, path) => `${origin.replace(/\/$/, "")}${path.startsWith("/") ? path : `/${path}`}`;
16448
- var escapeHtml22 = (value) => value.replaceAll("&", "&amp;").replaceAll('"', "&quot;").replaceAll("'", "&#39;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
16552
+ var escapeHtml23 = (value) => value.replaceAll("&", "&amp;").replaceAll('"', "&quot;").replaceAll("'", "&#39;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
16449
16553
  var getWebhookVerificationUrl = (webhook, input) => {
16450
16554
  if (!webhook?.verificationUrl) {
16451
16555
  return;
@@ -16488,23 +16592,23 @@ var buildTwilioVoiceSetupStatus = async (options, input) => {
16488
16592
  };
16489
16593
  var renderTwilioVoiceSetupHTML = (status, title) => `<main style="font-family: ui-sans-serif, system-ui; max-width: 860px; margin: 40px auto; padding: 0 20px;">
16490
16594
  <p style="letter-spacing: .12em; text-transform: uppercase; color: #52606d;">Twilio setup</p>
16491
- <h1>${escapeHtml22(title)}</h1>
16595
+ <h1>${escapeHtml23(title)}</h1>
16492
16596
  <p><strong>Status:</strong> ${status.ready ? "Ready" : "Needs attention"}</p>
16493
16597
  <section>
16494
16598
  <h2>URLs</h2>
16495
16599
  <ul>
16496
- <li><strong>TwiML:</strong> <code>${escapeHtml22(status.urls.twiml)}</code></li>
16497
- <li><strong>Media stream:</strong> <code>${escapeHtml22(status.urls.stream)}</code></li>
16498
- <li><strong>Status webhook:</strong> <code>${escapeHtml22(status.urls.webhook)}</code></li>
16600
+ <li><strong>TwiML:</strong> <code>${escapeHtml23(status.urls.twiml)}</code></li>
16601
+ <li><strong>Media stream:</strong> <code>${escapeHtml23(status.urls.stream)}</code></li>
16602
+ <li><strong>Status webhook:</strong> <code>${escapeHtml23(status.urls.webhook)}</code></li>
16499
16603
  </ul>
16500
16604
  </section>
16501
16605
  <section>
16502
16606
  <h2>Signing</h2>
16503
16607
  <p>Mode: <code>${status.signing.mode}</code></p>
16504
- ${status.signing.verificationUrl ? `<p>Verification URL: <code>${escapeHtml22(status.signing.verificationUrl)}</code></p>` : ""}
16608
+ ${status.signing.verificationUrl ? `<p>Verification URL: <code>${escapeHtml23(status.signing.verificationUrl)}</code></p>` : ""}
16505
16609
  </section>
16506
- ${status.missing.length ? `<section><h2>Missing env</h2><ul>${status.missing.map((name) => `<li><code>${escapeHtml22(name)}</code></li>`).join("")}</ul></section>` : ""}
16507
- ${status.warnings.length ? `<section><h2>Warnings</h2><ul>${status.warnings.map((warning) => `<li>${escapeHtml22(warning)}</li>`).join("")}</ul></section>` : ""}
16610
+ ${status.missing.length ? `<section><h2>Missing env</h2><ul>${status.missing.map((name) => `<li><code>${escapeHtml23(name)}</code></li>`).join("")}</ul></section>` : ""}
16611
+ ${status.warnings.length ? `<section><h2>Warnings</h2><ul>${status.warnings.map((warning) => `<li>${escapeHtml23(warning)}</li>`).join("")}</ul></section>` : ""}
16508
16612
  </main>`;
16509
16613
  var extractTwilioStreamUrl = (twiml) => twiml.match(/<Stream\b[^>]*\surl="([^"]+)"/i)?.[1]?.replaceAll("&amp;", "&");
16510
16614
  var createSmokeCheck = (name, status, message, details) => ({
@@ -16515,20 +16619,20 @@ var createSmokeCheck = (name, status, message, details) => ({
16515
16619
  });
16516
16620
  var renderTwilioVoiceSmokeHTML = (report, title) => `<main style="font-family: ui-sans-serif, system-ui; max-width: 860px; margin: 40px auto; padding: 0 20px;">
16517
16621
  <p style="letter-spacing: .12em; text-transform: uppercase; color: #52606d;">Twilio smoke test</p>
16518
- <h1>${escapeHtml22(title)}</h1>
16622
+ <h1>${escapeHtml23(title)}</h1>
16519
16623
  <p><strong>Status:</strong> ${report.pass ? "Pass" : "Fail"}</p>
16520
16624
  <section>
16521
16625
  <h2>Checks</h2>
16522
16626
  <ul>
16523
- ${report.checks.map((check) => `<li><strong>${escapeHtml22(check.name)}</strong>: ${escapeHtml22(check.status)}${check.message ? ` - ${escapeHtml22(check.message)}` : ""}</li>`).join("")}
16627
+ ${report.checks.map((check) => `<li><strong>${escapeHtml23(check.name)}</strong>: ${escapeHtml23(check.status)}${check.message ? ` - ${escapeHtml23(check.message)}` : ""}</li>`).join("")}
16524
16628
  </ul>
16525
16629
  </section>
16526
16630
  <section>
16527
16631
  <h2>Observed URLs</h2>
16528
16632
  <ul>
16529
- <li><strong>TwiML:</strong> <code>${escapeHtml22(report.setup.urls.twiml)}</code></li>
16530
- <li><strong>Stream:</strong> <code>${escapeHtml22(report.twiml?.streamUrl ?? report.setup.urls.stream)}</code></li>
16531
- <li><strong>Webhook:</strong> <code>${escapeHtml22(report.setup.urls.webhook)}</code></li>
16633
+ <li><strong>TwiML:</strong> <code>${escapeHtml23(report.setup.urls.twiml)}</code></li>
16634
+ <li><strong>Stream:</strong> <code>${escapeHtml23(report.twiml?.streamUrl ?? report.setup.urls.stream)}</code></li>
16635
+ <li><strong>Webhook:</strong> <code>${escapeHtml23(report.setup.urls.webhook)}</code></li>
16532
16636
  </ul>
16533
16637
  </section>
16534
16638
  </main>`;
@@ -16988,7 +17092,7 @@ var createTwilioVoiceRoutes = (options) => {
16988
17092
  const smokePath = options.smoke?.path === false ? false : options.smoke?.path ?? "/api/voice/twilio/smoke";
16989
17093
  const bridges = new WeakMap;
16990
17094
  const webhookPolicy = options.webhook?.policy ?? options.outcomePolicy ?? createVoiceTelephonyOutcomePolicy();
16991
- const app = new Elysia23({
17095
+ const app = new Elysia24({
16992
17096
  name: options.name ?? "absolutejs-voice-twilio"
16993
17097
  }).get(twimlPath, async ({ query, request }) => {
16994
17098
  const streamUrl = await resolveTwilioStreamUrl(options, {
@@ -17124,9 +17228,9 @@ var createTwilioVoiceRoutes = (options) => {
17124
17228
  };
17125
17229
  // src/telephony/telnyx.ts
17126
17230
  import { Buffer as Buffer4 } from "buffer";
17127
- import { Elysia as Elysia24 } from "elysia";
17231
+ import { Elysia as Elysia25 } from "elysia";
17128
17232
  var escapeXml3 = (value) => value.replaceAll("&", "&amp;").replaceAll('"', "&quot;").replaceAll("'", "&apos;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
17129
- var escapeHtml23 = (value) => value.replaceAll("&", "&amp;").replaceAll('"', "&quot;").replaceAll("'", "&#39;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
17233
+ var escapeHtml24 = (value) => value.replaceAll("&", "&amp;").replaceAll('"', "&quot;").replaceAll("'", "&#39;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
17130
17234
  var joinUrlPath2 = (origin, path) => `${origin.replace(/\/$/, "")}${path.startsWith("/") ? path : `/${path}`}`;
17131
17235
  var resolveRequestOrigin2 = (request) => {
17132
17236
  const url = new URL(request.url);
@@ -17327,21 +17431,21 @@ var buildTelnyxVoiceSetupStatus = async (options, input) => {
17327
17431
  };
17328
17432
  var renderTelnyxSetupHTML = (status, title) => `<main style="font-family: ui-sans-serif, system-ui; max-width: 860px; margin: 40px auto; padding: 0 20px;">
17329
17433
  <p style="letter-spacing: .12em; text-transform: uppercase; color: #52606d;">Telnyx setup</p>
17330
- <h1>${escapeHtml23(title)}</h1>
17434
+ <h1>${escapeHtml24(title)}</h1>
17331
17435
  <p><strong>Status:</strong> ${status.ready ? "Ready" : "Needs attention"}</p>
17332
17436
  <ul>
17333
- <li><strong>TeXML:</strong> <code>${escapeHtml23(status.urls.texml)}</code></li>
17334
- <li><strong>Media stream:</strong> <code>${escapeHtml23(status.urls.stream)}</code></li>
17335
- <li><strong>Status webhook:</strong> <code>${escapeHtml23(status.urls.webhook)}</code></li>
17437
+ <li><strong>TeXML:</strong> <code>${escapeHtml24(status.urls.texml)}</code></li>
17438
+ <li><strong>Media stream:</strong> <code>${escapeHtml24(status.urls.stream)}</code></li>
17439
+ <li><strong>Status webhook:</strong> <code>${escapeHtml24(status.urls.webhook)}</code></li>
17336
17440
  </ul>
17337
- ${status.missing.length ? `<h2>Missing env</h2><ul>${status.missing.map((name) => `<li><code>${escapeHtml23(name)}</code></li>`).join("")}</ul>` : ""}
17338
- ${status.warnings.length ? `<h2>Warnings</h2><ul>${status.warnings.map((warning) => `<li>${escapeHtml23(warning)}</li>`).join("")}</ul>` : ""}
17441
+ ${status.missing.length ? `<h2>Missing env</h2><ul>${status.missing.map((name) => `<li><code>${escapeHtml24(name)}</code></li>`).join("")}</ul>` : ""}
17442
+ ${status.warnings.length ? `<h2>Warnings</h2><ul>${status.warnings.map((warning) => `<li>${escapeHtml24(warning)}</li>`).join("")}</ul>` : ""}
17339
17443
  </main>`;
17340
17444
  var renderTelnyxSmokeHTML = (report, title) => `<main style="font-family: ui-sans-serif, system-ui; max-width: 860px; margin: 40px auto; padding: 0 20px;">
17341
17445
  <p style="letter-spacing: .12em; text-transform: uppercase; color: #52606d;">Telnyx smoke test</p>
17342
- <h1>${escapeHtml23(title)}</h1>
17446
+ <h1>${escapeHtml24(title)}</h1>
17343
17447
  <p><strong>Status:</strong> ${report.pass ? "Pass" : "Fail"}</p>
17344
- <ul>${report.checks.map((check) => `<li><strong>${escapeHtml23(check.name)}</strong>: ${escapeHtml23(check.status)}${check.message ? ` - ${escapeHtml23(check.message)}` : ""}</li>`).join("")}</ul>
17448
+ <ul>${report.checks.map((check) => `<li><strong>${escapeHtml24(check.name)}</strong>: ${escapeHtml24(check.status)}${check.message ? ` - ${escapeHtml24(check.message)}` : ""}</li>`).join("")}</ul>
17345
17449
  </main>`;
17346
17450
  var runTelnyxSmokeTest = async (input) => {
17347
17451
  const setup = await buildTelnyxVoiceSetupStatus(input.options, input);
@@ -17435,7 +17539,7 @@ var createTelnyxVoiceRoutes = (options = {}) => {
17435
17539
  publicKey: options.webhook?.publicKey,
17436
17540
  toleranceSeconds: options.webhook?.toleranceSeconds
17437
17541
  }) : undefined);
17438
- const app = new Elysia24({
17542
+ const app = new Elysia25({
17439
17543
  name: options.name ?? "absolutejs-voice-telnyx"
17440
17544
  }).get(texmlPath, async ({ query, request }) => {
17441
17545
  const streamUrl = await resolveTelnyxStreamUrl(options, {
@@ -17545,9 +17649,9 @@ var createTelnyxVoiceRoutes = (options = {}) => {
17545
17649
  };
17546
17650
  // src/telephony/plivo.ts
17547
17651
  import { Buffer as Buffer5 } from "buffer";
17548
- import { Elysia as Elysia25 } from "elysia";
17652
+ import { Elysia as Elysia26 } from "elysia";
17549
17653
  var escapeXml4 = (value) => value.replaceAll("&", "&amp;").replaceAll('"', "&quot;").replaceAll("'", "&apos;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
17550
- var escapeHtml24 = (value) => value.replaceAll("&", "&amp;").replaceAll('"', "&quot;").replaceAll("'", "&#39;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
17654
+ var escapeHtml25 = (value) => value.replaceAll("&", "&amp;").replaceAll('"', "&quot;").replaceAll("'", "&#39;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
17551
17655
  var joinUrlPath3 = (origin, path) => `${origin.replace(/\/$/, "")}${path.startsWith("/") ? path : `/${path}`}`;
17552
17656
  var resolveRequestOrigin3 = (request) => {
17553
17657
  const url = new URL(request.url);
@@ -17798,21 +17902,21 @@ var buildPlivoVoiceSetupStatus = async (options, input) => {
17798
17902
  };
17799
17903
  var renderPlivoSetupHTML = (status, title) => `<main style="font-family: ui-sans-serif, system-ui; max-width: 860px; margin: 40px auto; padding: 0 20px;">
17800
17904
  <p style="letter-spacing: .12em; text-transform: uppercase; color: #52606d;">Plivo setup</p>
17801
- <h1>${escapeHtml24(title)}</h1>
17905
+ <h1>${escapeHtml25(title)}</h1>
17802
17906
  <p><strong>Status:</strong> ${status.ready ? "Ready" : "Needs attention"}</p>
17803
17907
  <ul>
17804
- <li><strong>Answer XML:</strong> <code>${escapeHtml24(status.urls.answer)}</code></li>
17805
- <li><strong>Audio stream:</strong> <code>${escapeHtml24(status.urls.stream)}</code></li>
17806
- <li><strong>Status webhook:</strong> <code>${escapeHtml24(status.urls.webhook)}</code></li>
17908
+ <li><strong>Answer XML:</strong> <code>${escapeHtml25(status.urls.answer)}</code></li>
17909
+ <li><strong>Audio stream:</strong> <code>${escapeHtml25(status.urls.stream)}</code></li>
17910
+ <li><strong>Status webhook:</strong> <code>${escapeHtml25(status.urls.webhook)}</code></li>
17807
17911
  </ul>
17808
- ${status.missing.length ? `<h2>Missing env</h2><ul>${status.missing.map((name) => `<li><code>${escapeHtml24(name)}</code></li>`).join("")}</ul>` : ""}
17809
- ${status.warnings.length ? `<h2>Warnings</h2><ul>${status.warnings.map((warning) => `<li>${escapeHtml24(warning)}</li>`).join("")}</ul>` : ""}
17912
+ ${status.missing.length ? `<h2>Missing env</h2><ul>${status.missing.map((name) => `<li><code>${escapeHtml25(name)}</code></li>`).join("")}</ul>` : ""}
17913
+ ${status.warnings.length ? `<h2>Warnings</h2><ul>${status.warnings.map((warning) => `<li>${escapeHtml25(warning)}</li>`).join("")}</ul>` : ""}
17810
17914
  </main>`;
17811
17915
  var renderPlivoSmokeHTML = (report, title) => `<main style="font-family: ui-sans-serif, system-ui; max-width: 860px; margin: 40px auto; padding: 0 20px;">
17812
17916
  <p style="letter-spacing: .12em; text-transform: uppercase; color: #52606d;">Plivo smoke test</p>
17813
- <h1>${escapeHtml24(title)}</h1>
17917
+ <h1>${escapeHtml25(title)}</h1>
17814
17918
  <p><strong>Status:</strong> ${report.pass ? "Pass" : "Fail"}</p>
17815
- <ul>${report.checks.map((check) => `<li><strong>${escapeHtml24(check.name)}</strong>: ${escapeHtml24(check.status)}${check.message ? ` - ${escapeHtml24(check.message)}` : ""}</li>`).join("")}</ul>
17919
+ <ul>${report.checks.map((check) => `<li><strong>${escapeHtml25(check.name)}</strong>: ${escapeHtml25(check.status)}${check.message ? ` - ${escapeHtml25(check.message)}` : ""}</li>`).join("")}</ul>
17816
17920
  </main>`;
17817
17921
  var runPlivoSmokeTest = async (input) => {
17818
17922
  const setup = await buildPlivoVoiceSetupStatus(input.options, input);
@@ -17907,7 +18011,7 @@ var createPlivoVoiceRoutes = (options = {}) => {
17907
18011
  request: input.request
17908
18012
  }) : verificationUrl ?? input.request.url
17909
18013
  }) : undefined);
17910
- const app = new Elysia25({
18014
+ const app = new Elysia26({
17911
18015
  name: options.name ?? "absolutejs-voice-plivo"
17912
18016
  }).get(answerPath, async ({ query, request }) => {
17913
18017
  const streamUrl = await resolvePlivoStreamUrl(options, {
@@ -18093,6 +18197,7 @@ export {
18093
18197
  summarizeVoiceOpsTasks,
18094
18198
  summarizeVoiceOpsTaskQueue,
18095
18199
  summarizeVoiceOpsTaskAnalytics,
18200
+ summarizeVoiceLiveLatency,
18096
18201
  summarizeVoiceIntegrationEvents,
18097
18202
  summarizeVoiceHandoffHealth,
18098
18203
  summarizeVoiceHandoffDeliveries,
@@ -18145,6 +18250,7 @@ export {
18145
18250
  renderVoiceProductionReadinessHTML,
18146
18251
  renderVoiceOutcomeContractHTML,
18147
18252
  renderVoiceOpsConsoleHTML,
18253
+ renderVoiceLiveLatencyHTML,
18148
18254
  renderVoiceHandoffHealthHTML,
18149
18255
  renderVoiceEvalHTML,
18150
18256
  renderVoiceEvalBaselineHTML,
@@ -18282,6 +18388,7 @@ export {
18282
18388
  createVoiceMemoryStore,
18283
18389
  createVoiceMemoryHandoffDeliveryStore,
18284
18390
  createVoiceMemoryAssistantMemoryStore,
18391
+ createVoiceLiveLatencyRoutes,
18285
18392
  createVoiceLinearIssueUpdateSink,
18286
18393
  createVoiceLinearIssueSyncSinks,
18287
18394
  createVoiceLinearIssueSink,
@@ -0,0 +1,78 @@
1
+ import { Elysia } from 'elysia';
2
+ import type { VoiceTraceEventStore } from './trace';
3
+ export type VoiceLiveLatencyStatus = 'empty' | 'fail' | 'pass' | 'warn';
4
+ export type VoiceLiveLatencySample = {
5
+ at: number;
6
+ latencyMs: number;
7
+ sessionId: string;
8
+ status?: string;
9
+ traceId?: string;
10
+ };
11
+ export type VoiceLiveLatencyReport = {
12
+ averageLatencyMs?: number;
13
+ checkedAt: number;
14
+ failed: number;
15
+ p50LatencyMs?: number;
16
+ p95LatencyMs?: number;
17
+ recent: VoiceLiveLatencySample[];
18
+ status: VoiceLiveLatencyStatus;
19
+ total: number;
20
+ warnings: number;
21
+ };
22
+ export type VoiceLiveLatencyOptions = {
23
+ failAfterMs?: number;
24
+ limit?: number;
25
+ store: VoiceTraceEventStore;
26
+ warnAfterMs?: number;
27
+ };
28
+ export type VoiceLiveLatencyRoutesOptions = VoiceLiveLatencyOptions & {
29
+ headers?: HeadersInit;
30
+ htmlPath?: false | string;
31
+ name?: string;
32
+ path?: string;
33
+ title?: string;
34
+ };
35
+ export declare const summarizeVoiceLiveLatency: (options: VoiceLiveLatencyOptions) => Promise<VoiceLiveLatencyReport>;
36
+ export declare const renderVoiceLiveLatencyHTML: (report: VoiceLiveLatencyReport, options?: {
37
+ title?: string;
38
+ }) => string;
39
+ export declare const createVoiceLiveLatencyRoutes: (options: VoiceLiveLatencyRoutesOptions) => Elysia<"", {
40
+ decorator: {};
41
+ store: {};
42
+ derive: {};
43
+ resolve: {};
44
+ }, {
45
+ typebox: {};
46
+ error: {};
47
+ }, {
48
+ schema: {};
49
+ standaloneSchema: {};
50
+ macro: {};
51
+ macroFn: {};
52
+ parser: {};
53
+ response: {};
54
+ }, {
55
+ [x: string]: {
56
+ get: {
57
+ body: unknown;
58
+ params: {};
59
+ query: unknown;
60
+ headers: unknown;
61
+ response: {
62
+ 200: VoiceLiveLatencyReport;
63
+ };
64
+ };
65
+ };
66
+ }, {
67
+ derive: {};
68
+ resolve: {};
69
+ schema: {};
70
+ standaloneSchema: {};
71
+ response: {};
72
+ }, {
73
+ derive: {};
74
+ resolve: {};
75
+ schema: {};
76
+ standaloneSchema: {};
77
+ response: {};
78
+ }>;
@@ -23,6 +23,7 @@ export type VoiceProductionReadinessReport = {
23
23
  carriers?: string;
24
24
  handoffs?: string;
25
25
  handoffRetry?: string;
26
+ liveLatency?: string;
26
27
  quality?: string;
27
28
  resilience?: string;
28
29
  sessions?: string;
@@ -40,6 +41,13 @@ export type VoiceProductionReadinessReport = {
40
41
  failed: number;
41
42
  total: number;
42
43
  };
44
+ liveLatency: {
45
+ averageLatencyMs?: number;
46
+ failed: number;
47
+ status: VoiceProductionReadinessStatus;
48
+ total: number;
49
+ warnings: number;
50
+ };
43
51
  providers: {
44
52
  degraded: number;
45
53
  total: number;
@@ -73,6 +81,8 @@ export type VoiceProductionReadinessRoutesOptions = {
73
81
  sttProviders?: readonly string[];
74
82
  title?: string;
75
83
  ttsProviders?: readonly string[];
84
+ liveLatencyWarnAfterMs?: number;
85
+ liveLatencyFailAfterMs?: number;
76
86
  };
77
87
  export declare const buildVoiceProductionReadinessReport: (options: VoiceProductionReadinessRoutesOptions, input?: {
78
88
  query?: Record<string, unknown>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@absolutejs/voice",
3
- "version": "0.0.22-beta.101",
3
+ "version": "0.0.22-beta.103",
4
4
  "description": "Voice primitives and Elysia plugin for AbsoluteJS",
5
5
  "repository": {
6
6
  "type": "git",