@absolutejs/voice 0.0.22-beta.89 → 0.0.22-beta.90

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/appKit.d.ts CHANGED
@@ -11,7 +11,8 @@ import { type VoiceQualityRoutesOptions } from './qualityRoutes';
11
11
  import { type VoiceResilienceRoutesOptions } from './resilienceRoutes';
12
12
  import { type VoiceSessionListRoutesOptions, type VoiceSessionReplayRoutesOptions } from './sessionReplay';
13
13
  import { type VoiceTraceEventStore } from './trace';
14
- export type VoiceAppKitSurface = 'assistantHealth' | 'diagnostics' | 'evals' | 'handoffs' | 'opsConsole' | 'providerCapabilities' | 'providerHealth' | 'productionReadiness' | 'quality' | 'resilience' | 'sessionReplay' | 'sessions';
14
+ import { type VoiceTraceTimelineRoutesOptions } from './traceTimeline';
15
+ export type VoiceAppKitSurface = 'assistantHealth' | 'diagnostics' | 'evals' | 'handoffs' | 'opsConsole' | 'providerCapabilities' | 'providerHealth' | 'productionReadiness' | 'quality' | 'resilience' | 'sessionReplay' | 'sessions' | 'traceTimeline';
15
16
  export type VoiceAppKitLink = VoiceEvalLink & {
16
17
  description?: string;
17
18
  statusHref?: string;
@@ -37,6 +38,7 @@ export type VoiceAppKitRoutesOptions<TProvider extends string = string> = {
37
38
  store: VoiceTraceEventStore;
38
39
  sttProviders?: readonly string[];
39
40
  title?: string;
41
+ traceTimeline?: false | Partial<VoiceTraceTimelineRoutesOptions>;
40
42
  ttsProviders?: readonly string[];
41
43
  };
42
44
  export type VoiceAppKitStatus = 'pass' | 'fail';
package/dist/index.d.ts CHANGED
@@ -24,6 +24,7 @@ export { createVoiceQualityRoutes, evaluateVoiceQuality, renderVoiceQualityHTML
24
24
  export { createVoiceResilienceRoutes, createVoiceRoutingDecisionSummary, listVoiceRoutingEvents, renderVoiceResilienceHTML, summarizeVoiceRoutingDecision, summarizeVoiceRoutingSessions } from './resilienceRoutes';
25
25
  export { createVoiceSTTProviderRouter, createVoiceTTSProviderRouter } from './providerAdapters';
26
26
  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';
27
+ export { createVoiceTraceTimelineRoutes, renderVoiceTraceTimelineHTML, renderVoiceTraceTimelineSessionHTML, summarizeVoiceTraceTimeline } from './traceTimeline';
27
28
  export { createVoiceSQLiteExternalObjectMapStore, createVoiceSQLiteIntegrationEventStore, createVoiceSQLiteReviewStore, createVoiceSQLiteRuntimeStorage, createVoiceSQLiteSessionStore, createVoiceSQLiteTaskStore, createVoiceSQLiteTelephonyWebhookIdempotencyStore, createVoiceSQLiteTraceSinkDeliveryStore, createVoiceSQLiteTraceEventStore } from './sqliteStore';
28
29
  export { createVoicePostgresExternalObjectMapStore, createVoicePostgresIntegrationEventStore, createVoicePostgresReviewStore, createVoicePostgresRuntimeStorage, createVoicePostgresSessionStore, createVoicePostgresTaskStore, createVoicePostgresTelephonyWebhookIdempotencyStore, createVoicePostgresTraceSinkDeliveryStore, createVoicePostgresTraceEventStore } from './postgresStore';
29
30
  export { createVoiceS3ReviewStore } from './s3Store';
@@ -79,6 +80,7 @@ export type { VoiceHandoffHealthDelivery, VoiceHandoffHealthEvent, VoiceHandoffH
79
80
  export type { StoredVoiceCallReviewArtifact, VoiceCallReviewArtifact, VoiceCallReviewConfig, VoiceCallReviewPostCallSummary, VoiceCallReviewRecorder, VoiceCallReviewRecorderOptions, VoiceCallReviewStore, VoiceCallReviewSummary, VoiceCallReviewTimelineEvent } from './testing/review';
80
81
  export type { VoiceFileRuntimeStorage, VoiceFileStoreOptions } from './fileStore';
81
82
  export type { StoredVoiceTraceEvent, VoiceTraceEvaluation, VoiceTraceEvaluationOptions, VoiceTraceEvent, VoiceTraceEventFilter, VoiceTraceEventStore, VoiceTraceEventType, VoiceTraceIssue, VoiceTraceIssueSeverity, VoiceTraceHTTPSinkOptions, VoiceTracePruneFilter, VoiceTracePruneOptions, VoiceTracePruneResult, VoiceTraceRedactionConfig, VoiceTraceRedactionOptions, VoiceTraceRedactionReplacement, VoiceResolvedTraceRedactionOptions, VoiceTraceSink, VoiceTraceSinkDeliveryQueueStatus, VoiceTraceSinkDeliveryRecord, VoiceTraceSinkDeliveryResult, VoiceTraceSinkDeliveryStatus, VoiceTraceSinkDeliveryStore, VoiceTraceSinkFanoutResult, VoiceTraceSinkStoreOptions, VoiceTraceSummary } from './trace';
83
+ export type { VoiceTraceTimelineEvent, VoiceTraceTimelineProviderSummary, VoiceTraceTimelineReport, VoiceTraceTimelineRoutesOptions, VoiceTraceTimelineSession } from './traceTimeline';
82
84
  export type { VoicePostgresClient, VoicePostgresRuntimeStorage, VoicePostgresStoreOptions } from './postgresStore';
83
85
  export type { VoiceOpsTaskLease, VoiceOpsTaskWorker, VoiceOpsTaskWorkerOptions, VoiceHandoffDeliveryQueueSummary, VoiceHandoffDeliveryWorkerLoop, VoiceHandoffDeliveryWorkerLoopOptions, VoiceHandoffDeliveryWorkerOptions, VoiceHandoffDeliveryWorkerResult, VoiceIdempotencyStore, VoiceIntegrationEventQueueSummary, VoiceIntegrationSinkWorkerLoop, VoiceIntegrationSinkWorkerLoopOptions, VoiceIntegrationSinkWorkerOptions, VoiceIntegrationSinkWorkerResult, VoiceRedisIdempotencyClient, VoiceRedisIdempotencyStoreOptions, VoiceRedisTelephonyWebhookIdempotencyClient, VoiceRedisTelephonyWebhookIdempotencyStoreOptions, VoiceRedisTaskLeaseClient, VoiceRedisTaskLeaseCoordinator, VoiceRedisTaskLeaseCoordinatorOptions, VoiceTraceSinkDeliveryQueueSummary, VoiceTraceSinkDeliveryWorkerLoop, VoiceTraceSinkDeliveryWorkerLoopOptions, VoiceTraceSinkDeliveryWorkerOptions, VoiceTraceSinkDeliveryWorkerResult, VoiceOpsTaskClaimFilters, VoiceWebhookDeliveryWorkerLoop, VoiceWebhookDeliveryWorkerLoopOptions, VoiceWebhookDeliveryWorkerOptions, VoiceWebhookDeliveryWorkerResult, VoiceOpsTaskProcessorWorkerLoop, VoiceOpsTaskProcessorWorkerLoopOptions, VoiceOpsTaskProcessorWorkerOptions, VoiceOpsTaskProcessorWorkerResult, VoiceOpsTaskQueueSummary } from './queue';
84
86
  export type { VoiceS3ReviewStoreClient, VoiceS3ReviewStoreFile, VoiceS3ReviewStoreOptions } from './s3Store';
package/dist/index.js CHANGED
@@ -5474,7 +5474,7 @@ var voice = (config) => {
5474
5474
  }).use(htmxRoutes());
5475
5475
  };
5476
5476
  // src/appKit.ts
5477
- import { Elysia as Elysia14 } from "elysia";
5477
+ import { Elysia as Elysia15 } from "elysia";
5478
5478
 
5479
5479
  // src/assistantHealth.ts
5480
5480
  import { Elysia as Elysia3 } from "elysia";
@@ -9821,6 +9821,232 @@ var createVoiceProductionReadinessRoutes = (options) => {
9821
9821
  return routes;
9822
9822
  };
9823
9823
 
9824
+ // src/traceTimeline.ts
9825
+ import { Elysia as Elysia14 } from "elysia";
9826
+ var escapeHtml16 = (value) => value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
9827
+ var getString9 = (value) => typeof value === "string" && value.trim() ? value : undefined;
9828
+ var getNumber5 = (value) => typeof value === "number" && Number.isFinite(value) ? value : undefined;
9829
+ var firstString = (payload, keys) => {
9830
+ for (const key of keys) {
9831
+ const value = getString9(payload[key]);
9832
+ if (value) {
9833
+ return value;
9834
+ }
9835
+ }
9836
+ return;
9837
+ };
9838
+ var firstNumber = (payload, keys) => {
9839
+ for (const key of keys) {
9840
+ const value = getNumber5(payload[key]);
9841
+ if (value !== undefined) {
9842
+ return value;
9843
+ }
9844
+ }
9845
+ return;
9846
+ };
9847
+ var eventProvider = (event) => firstString(event.payload, [
9848
+ "provider",
9849
+ "selectedProvider",
9850
+ "fallbackProvider",
9851
+ "variantId"
9852
+ ]);
9853
+ var eventStatus = (event) => firstString(event.payload, [
9854
+ "providerStatus",
9855
+ "status",
9856
+ "disposition",
9857
+ "type",
9858
+ "reason"
9859
+ ]);
9860
+ var eventElapsedMs = (event) => firstNumber(event.payload, ["elapsedMs", "latencyMs", "durationMs"]);
9861
+ var timelineLabel = (event) => {
9862
+ switch (event.type) {
9863
+ case "call.lifecycle":
9864
+ return `Call ${eventStatus(event) ?? "lifecycle"}`;
9865
+ case "turn.transcript":
9866
+ return event.payload.isFinal === true ? "Final transcript" : "Partial transcript";
9867
+ case "turn.committed":
9868
+ return `Committed turn${getString9(event.payload.reason) ? ` (${getString9(event.payload.reason)})` : ""}`;
9869
+ case "turn.assistant":
9870
+ return "Assistant reply";
9871
+ case "agent.model":
9872
+ return `Model call${eventProvider(event) ? ` via ${eventProvider(event)}` : ""}`;
9873
+ case "agent.tool":
9874
+ return `Tool ${getString9(event.payload.toolName) ?? "call"}`;
9875
+ case "agent.handoff":
9876
+ return `Agent handoff${getString9(event.payload.targetAgentId) ? ` to ${getString9(event.payload.targetAgentId)}` : ""}`;
9877
+ case "assistant.run":
9878
+ return `Assistant run${eventProvider(event) ? ` via ${eventProvider(event)}` : ""}`;
9879
+ case "assistant.guardrail":
9880
+ return `Guardrail ${eventStatus(event) ?? "check"}`;
9881
+ case "call.handoff":
9882
+ return `Call handoff ${eventStatus(event) ?? ""}`.trim();
9883
+ case "session.error":
9884
+ return `Error${getString9(event.payload.error) ? `: ${getString9(event.payload.error)}` : ""}`;
9885
+ case "turn.cost":
9886
+ return "Cost telemetry";
9887
+ case "workflow.contract":
9888
+ return `Workflow contract ${eventStatus(event) ?? ""}`.trim();
9889
+ default:
9890
+ return event.type;
9891
+ }
9892
+ };
9893
+ var summarizeProviders = (events) => {
9894
+ const entries = new Map;
9895
+ const getEntry = (provider) => {
9896
+ const existing = entries.get(provider);
9897
+ if (existing) {
9898
+ return existing;
9899
+ }
9900
+ const entry = {
9901
+ elapsed: [],
9902
+ errorCount: 0,
9903
+ eventCount: 0,
9904
+ fallbackCount: 0,
9905
+ successCount: 0,
9906
+ timeoutCount: 0
9907
+ };
9908
+ entries.set(provider, entry);
9909
+ return entry;
9910
+ };
9911
+ for (const event of events) {
9912
+ const provider = eventProvider(event);
9913
+ if (!provider) {
9914
+ continue;
9915
+ }
9916
+ const entry = getEntry(provider);
9917
+ const status = eventStatus(event);
9918
+ const elapsedMs = eventElapsedMs(event);
9919
+ entry.eventCount += 1;
9920
+ if (elapsedMs !== undefined) {
9921
+ entry.elapsed.push(elapsedMs);
9922
+ }
9923
+ if (status === "success") {
9924
+ entry.successCount += 1;
9925
+ }
9926
+ if (status === "fallback") {
9927
+ entry.fallbackCount += 1;
9928
+ }
9929
+ if (status === "error") {
9930
+ entry.errorCount += 1;
9931
+ }
9932
+ if (status === "timeout") {
9933
+ entry.timeoutCount += 1;
9934
+ }
9935
+ }
9936
+ return [...entries.entries()].map(([provider, entry]) => ({
9937
+ averageElapsedMs: entry.elapsed.length > 0 ? Math.round(entry.elapsed.reduce((total, value) => total + value, 0) / entry.elapsed.length) : undefined,
9938
+ errorCount: entry.errorCount,
9939
+ eventCount: entry.eventCount,
9940
+ fallbackCount: entry.fallbackCount,
9941
+ maxElapsedMs: entry.elapsed.length > 0 ? Math.max(...entry.elapsed) : undefined,
9942
+ provider,
9943
+ successCount: entry.successCount,
9944
+ timeoutCount: entry.timeoutCount
9945
+ })).sort((left, right) => right.eventCount - left.eventCount);
9946
+ };
9947
+ var summarizeVoiceTraceTimeline = (events, options = {}) => {
9948
+ const source = options.redact ? redactVoiceTraceEvents(events, options.redact) : events;
9949
+ const grouped = new Map;
9950
+ for (const event of filterVoiceTraceEvents(source)) {
9951
+ grouped.set(event.sessionId, [...grouped.get(event.sessionId) ?? [], event]);
9952
+ }
9953
+ const sessions = [...grouped.entries()].map(([sessionId, sessionEvents]) => {
9954
+ const sorted = filterVoiceTraceEvents(sessionEvents);
9955
+ const summary = summarizeVoiceTrace(sorted);
9956
+ const evaluation = evaluateVoiceTrace(sorted, options.evaluation);
9957
+ const startedAt = summary.startedAt ?? sorted[0]?.at ?? 0;
9958
+ const status = summary.failed ? "failed" : evaluation.issues.length > 0 ? "warning" : "healthy";
9959
+ return {
9960
+ endedAt: summary.endedAt,
9961
+ evaluation,
9962
+ events: sorted.map((event) => ({
9963
+ at: event.at,
9964
+ elapsedMs: eventElapsedMs(event),
9965
+ id: event.id,
9966
+ label: timelineLabel(event),
9967
+ offsetMs: Math.max(0, event.at - startedAt),
9968
+ provider: eventProvider(event),
9969
+ status: eventStatus(event),
9970
+ turnId: event.turnId,
9971
+ type: event.type
9972
+ })),
9973
+ lastEventAt: sorted.at(-1)?.at,
9974
+ providers: summarizeProviders(sorted),
9975
+ sessionId,
9976
+ startedAt: summary.startedAt,
9977
+ status,
9978
+ summary
9979
+ };
9980
+ }).sort((left, right) => (right.lastEventAt ?? 0) - (left.lastEventAt ?? 0)).slice(0, options.limit ?? 50);
9981
+ return {
9982
+ checkedAt: Date.now(),
9983
+ failed: sessions.filter((session) => session.status === "failed").length,
9984
+ sessions,
9985
+ total: sessions.length,
9986
+ warnings: sessions.filter((session) => session.status === "warning").length
9987
+ };
9988
+ };
9989
+ var formatMs = (value) => value === undefined ? "n/a" : `${String(value)}ms`;
9990
+ var renderProviderCards2 = (session) => session.providers.length === 0 ? '<p class="muted">No provider events recorded for this session.</p>' : `<div class="providers">${session.providers.map((provider) => `<article><strong>${escapeHtml16(provider.provider)}</strong><dl><div><dt>Events</dt><dd>${String(provider.eventCount)}</dd></div><div><dt>Avg</dt><dd>${formatMs(provider.averageElapsedMs)}</dd></div><div><dt>Max</dt><dd>${formatMs(provider.maxElapsedMs)}</dd></div><div><dt>Errors</dt><dd>${String(provider.errorCount)}</dd></div><div><dt>Fallbacks</dt><dd>${String(provider.fallbackCount)}</dd></div><div><dt>Timeouts</dt><dd>${String(provider.timeoutCount)}</dd></div></dl></article>`).join("")}</div>`;
9991
+ var renderVoiceTraceTimelineSessionHTML = (session, options = {}) => {
9992
+ const events = session.events.map((event) => `<tr class="${escapeHtml16(event.status ?? "")}"><td>+${String(event.offsetMs)}ms</td><td>${escapeHtml16(event.type)}</td><td>${escapeHtml16(event.label)}</td><td>${escapeHtml16(event.provider ?? "")}</td><td>${escapeHtml16(event.status ?? "")}</td><td>${formatMs(event.elapsedMs)}</td></tr>`).join("");
9993
+ const issues = session.evaluation.issues.length ? session.evaluation.issues.map((issue) => `<li class="${escapeHtml16(issue.severity)}">${escapeHtml16(issue.code)}: ${escapeHtml16(issue.message)}</li>`).join("") : "<li>none</li>";
9994
+ return `<!doctype html><html lang="en"><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /><title>${escapeHtml16(options.title ?? "Voice Trace Timeline")}</title><style>${timelineCSS}</style></head><body><main><a href="/traces">Back to traces</a><header><p class="eyebrow">Call timeline</p><h1>${escapeHtml16(session.sessionId)}</h1><p class="status ${escapeHtml16(session.status)}">${escapeHtml16(session.status)}</p></header><section class="metrics"><article><span>Events</span><strong>${String(session.summary.eventCount)}</strong></article><article><span>Turns</span><strong>${String(session.summary.turnCount)}</strong></article><article><span>Errors</span><strong>${String(session.summary.errorCount)}</strong></article><article><span>Duration</span><strong>${formatMs(session.summary.callDurationMs)}</strong></article></section><section><h2>Providers</h2>${renderProviderCards2(session)}</section><section><h2>Issues</h2><ul>${issues}</ul></section><section><h2>Timeline</h2><table><thead><tr><th>Offset</th><th>Type</th><th>Event</th><th>Provider</th><th>Status</th><th>Latency</th></tr></thead><tbody>${events}</tbody></table></section></main></body></html>`;
9995
+ };
9996
+ var renderSessionRows = (report) => report.sessions.length === 0 ? '<tr><td colspan="7">No trace events recorded yet.</td></tr>' : report.sessions.map((session) => `<tr class="${escapeHtml16(session.status)}"><td><a href="/traces/${encodeURIComponent(session.sessionId)}">${escapeHtml16(session.sessionId)}</a></td><td>${escapeHtml16(session.status)}</td><td>${String(session.summary.eventCount)}</td><td>${String(session.summary.turnCount)}</td><td>${String(session.summary.errorCount)}</td><td>${formatMs(session.summary.callDurationMs)}</td><td>${session.providers.map((provider) => escapeHtml16(provider.provider)).join(", ")}</td></tr>`).join("");
9997
+ var timelineCSS = "body{background:#0f1318;color:#f6f2e8;font-family:ui-sans-serif,system-ui,sans-serif;margin:0}main{margin:auto;max-width:1180px;padding:32px}a{color:#fbbf24}.eyebrow{color:#fbbf24;font-weight:900;letter-spacing:.12em;text-transform:uppercase}h1{font-size:clamp(2.4rem,6vw,4.5rem);line-height:.92;margin:.2rem 0 1rem}.status{border:1px solid #475569;border-radius:999px;display:inline-flex;padding:8px 12px}.healthy{color:#86efac}.warning{color:#fbbf24}.failed,.error{color:#fca5a5}.metrics,.providers{display:grid;gap:14px;grid-template-columns:repeat(auto-fit,minmax(170px,1fr));margin:20px 0}.metrics article,.providers article{background:#181f27;border:1px solid #2b3642;border-radius:20px;padding:16px}.metrics span,dt,.muted{color:#a8b0b8}.metrics strong{display:block;font-size:2rem}dl{display:grid;gap:8px;grid-template-columns:repeat(2,minmax(0,1fr));margin:12px 0 0}dd{font-weight:800;margin:4px 0 0}table{background:#181f27;border-collapse:collapse;border-radius:18px;overflow:hidden;width:100%}td,th{border-bottom:1px solid #2b3642;padding:12px;text-align:left}section{margin-top:28px}@media(max-width:760px){main{padding:20px}table{font-size:.9rem}}";
9998
+ var renderVoiceTraceTimelineHTML = (report, options = {}) => `<!doctype html><html lang="en"><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /><title>${escapeHtml16(options.title ?? "Voice Trace Timelines")}</title><style>${timelineCSS}</style></head><body><main><header><p class="eyebrow">Self-hosted voice debugging</p><h1>${escapeHtml16(options.title ?? "Voice Trace Timelines")}</h1><p class="muted">Per-call event timelines with provider latency, fallback, timeout, handoff, and error context.</p></header><section class="metrics"><article><span>Sessions</span><strong>${String(report.total)}</strong></article><article><span>Failed</span><strong>${String(report.failed)}</strong></article><article><span>Warnings</span><strong>${String(report.warnings)}</strong></article></section><table><thead><tr><th>Session</th><th>Status</th><th>Events</th><th>Turns</th><th>Errors</th><th>Duration</th><th>Providers</th></tr></thead><tbody>${renderSessionRows(report)}</tbody></table></main></body></html>`;
9999
+ var createVoiceTraceTimelineRoutes = (options) => {
10000
+ const path = options.path ?? "/api/voice-traces";
10001
+ const htmlPath = options.htmlPath ?? "/traces";
10002
+ const title = options.title ?? "AbsoluteJS Voice Trace Timelines";
10003
+ const routes = new Elysia14({
10004
+ name: options.name ?? "absolutejs-voice-trace-timelines"
10005
+ });
10006
+ const buildReport = async () => summarizeVoiceTraceTimeline(await options.store.list(), {
10007
+ evaluation: options.evaluation,
10008
+ limit: options.limit,
10009
+ redact: options.redact
10010
+ });
10011
+ const findSession = async (sessionId) => {
10012
+ const report = summarizeVoiceTraceTimeline(await options.store.list({ sessionId }), {
10013
+ evaluation: options.evaluation,
10014
+ limit: 1,
10015
+ redact: options.redact
10016
+ });
10017
+ return report.sessions[0];
10018
+ };
10019
+ routes.get(path, async () => Response.json(await buildReport()));
10020
+ routes.get(`${path}/:sessionId`, async ({ params }) => {
10021
+ const session = await findSession(params.sessionId);
10022
+ return session ? Response.json(session) : Response.json({ error: "Voice trace session not found." }, { status: 404 });
10023
+ });
10024
+ routes.get(htmlPath, async () => {
10025
+ const report = await buildReport();
10026
+ const body = await (options.render ?? ((input) => renderVoiceTraceTimelineHTML(input, { title })))(report);
10027
+ return new Response(body, {
10028
+ headers: {
10029
+ "Content-Type": "text/html; charset=utf-8",
10030
+ ...options.headers
10031
+ }
10032
+ });
10033
+ });
10034
+ routes.get(`${htmlPath}/:sessionId`, async ({ params }) => {
10035
+ const session = await findSession(params.sessionId);
10036
+ if (!session) {
10037
+ return Response.json({ error: "Voice trace session not found." }, { status: 404 });
10038
+ }
10039
+ const body = await (options.renderSession ?? ((input) => renderVoiceTraceTimelineSessionHTML(input, { title })))(session);
10040
+ return new Response(body, {
10041
+ headers: {
10042
+ "Content-Type": "text/html; charset=utf-8",
10043
+ ...options.headers
10044
+ }
10045
+ });
10046
+ });
10047
+ return routes;
10048
+ };
10049
+
9824
10050
  // src/appKit.ts
9825
10051
  var DEFAULT_LINKS2 = [
9826
10052
  {
@@ -9983,7 +10209,7 @@ var summarizeVoiceAppKitStatus = async (options) => {
9983
10209
  };
9984
10210
  };
9985
10211
  var createVoiceAppKitRoutes = (options) => {
9986
- const routes = new Elysia14({
10212
+ const routes = new Elysia15({
9987
10213
  name: options.name ?? "absolutejs-voice-app-kit"
9988
10214
  });
9989
10215
  const links = resolveLinks(options.links);
@@ -10078,6 +10304,16 @@ var createVoiceAppKitRoutes = (options) => {
10078
10304
  ...options.diagnostics
10079
10305
  }));
10080
10306
  }
10307
+ if (options.traceTimeline !== false) {
10308
+ surfaces.push("traceTimeline");
10309
+ routes.use(createVoiceTraceTimelineRoutes({
10310
+ ...common,
10311
+ htmlPath: "/traces",
10312
+ path: "/api/voice-traces",
10313
+ title: options.title ? `${options.title} Trace Timelines` : undefined,
10314
+ ...options.traceTimeline
10315
+ }));
10316
+ }
10081
10317
  if (options.resilience !== false) {
10082
10318
  surfaces.push("resilience");
10083
10319
  routes.use(createVoiceResilienceRoutes({
@@ -10618,7 +10854,7 @@ var createVoiceToolIdempotencyKey = (input) => {
10618
10854
  ].join(":");
10619
10855
  };
10620
10856
  // src/toolContract.ts
10621
- import { Elysia as Elysia15 } from "elysia";
10857
+ import { Elysia as Elysia16 } from "elysia";
10622
10858
  var createDefaultSession = (contractId, caseId) => createVoiceSessionRecord(`tool-contract-${contractId}-${caseId}`);
10623
10859
  var createDefaultTurn = (caseId) => ({
10624
10860
  committedAt: Date.now(),
@@ -10628,7 +10864,7 @@ var createDefaultTurn = (caseId) => ({
10628
10864
  });
10629
10865
  var defaultApi = {};
10630
10866
  var sameJSON = (left, right) => JSON.stringify(left) === JSON.stringify(right);
10631
- var escapeHtml16 = (value) => value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
10867
+ var escapeHtml17 = (value) => value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
10632
10868
  var evaluateExpectation = (input) => {
10633
10869
  const issues = [];
10634
10870
  const expect = input.expect;
@@ -10794,19 +11030,19 @@ var renderVoiceToolContractHTML = (report, options = {}) => {
10794
11030
  const title = options.title ?? "Voice Tool Contracts";
10795
11031
  const contracts = report.contracts.map((contract) => {
10796
11032
  const cases = contract.cases.map((testCase) => `<tr>
10797
- <td>${escapeHtml16(testCase.label ?? testCase.caseId)}</td>
11033
+ <td>${escapeHtml17(testCase.label ?? testCase.caseId)}</td>
10798
11034
  <td class="${testCase.pass ? "pass" : "fail"}">${testCase.pass ? "pass" : "fail"}</td>
10799
- <td>${escapeHtml16(testCase.status)}</td>
11035
+ <td>${escapeHtml17(testCase.status)}</td>
10800
11036
  <td>${String(testCase.attempts)}</td>
10801
11037
  <td>${String(testCase.elapsedMs)}ms</td>
10802
11038
  <td>${testCase.timedOut ? "yes" : "no"}</td>
10803
- <td>${escapeHtml16(testCase.issues.map((issue) => issue.message).join(" ") || testCase.error || "")}</td>
11039
+ <td>${escapeHtml17(testCase.issues.map((issue) => issue.message).join(" ") || testCase.error || "")}</td>
10804
11040
  </tr>`).join("");
10805
11041
  return `<section class="contract ${contract.pass ? "pass" : "fail"}">
10806
11042
  <div class="contract-header">
10807
11043
  <div>
10808
- <p class="eyebrow">${escapeHtml16(contract.toolName)}</p>
10809
- <h2>${escapeHtml16(contract.label ?? contract.contractId)}</h2>
11044
+ <p class="eyebrow">${escapeHtml17(contract.toolName)}</p>
11045
+ <h2>${escapeHtml17(contract.label ?? contract.contractId)}</h2>
10810
11046
  </div>
10811
11047
  <strong class="${contract.pass ? "pass" : "fail"}">${contract.pass ? "Passing" : "Failing"}</strong>
10812
11048
  </div>
@@ -10816,7 +11052,7 @@ var renderVoiceToolContractHTML = (report, options = {}) => {
10816
11052
  </table>
10817
11053
  </section>`;
10818
11054
  }).join("");
10819
- return `<!doctype html><html lang="en"><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /><title>${escapeHtml16(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(245,158,11,.12))}.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}.summary{display:flex;flex-wrap:wrap;gap:10px}.pill{background:#0f1217;border:1px solid #3f3f46;border-radius:999px;padding:7px 10px}.contract-header{align-items:flex-start;display:flex;gap:16px;justify-content:space-between}h2{margin:.2rem 0 1rem}.pass{color:#86efac}.fail{color:#fca5a5}.contract.fail{border-color:rgba(248,113,113,.45)}table{border-collapse:collapse;width:100%}td,th{border-bottom:1px solid #2a323a;padding:12px;text-align:left;vertical-align:top}th{color:#a8b0b8;font-size:.82rem}@media(max-width:800px){main{padding:18px}table{display:block;overflow:auto}.contract-header{display:block}}</style></head><body><main><section class="hero"><p class="eyebrow">Tool Reliability</p><h1>${escapeHtml16(title)}</h1><div class="summary"><span class="pill ${report.status === "pass" ? "pass" : "fail"}">${escapeHtml16(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 tool contracts configured.</p></section>'}</main></body></html>`;
11055
+ return `<!doctype html><html lang="en"><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /><title>${escapeHtml17(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(245,158,11,.12))}.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}.summary{display:flex;flex-wrap:wrap;gap:10px}.pill{background:#0f1217;border:1px solid #3f3f46;border-radius:999px;padding:7px 10px}.contract-header{align-items:flex-start;display:flex;gap:16px;justify-content:space-between}h2{margin:.2rem 0 1rem}.pass{color:#86efac}.fail{color:#fca5a5}.contract.fail{border-color:rgba(248,113,113,.45)}table{border-collapse:collapse;width:100%}td,th{border-bottom:1px solid #2a323a;padding:12px;text-align:left;vertical-align:top}th{color:#a8b0b8;font-size:.82rem}@media(max-width:800px){main{padding:18px}table{display:block;overflow:auto}.contract-header{display:block}}</style></head><body><main><section class="hero"><p class="eyebrow">Tool Reliability</p><h1>${escapeHtml17(title)}</h1><div class="summary"><span class="pill ${report.status === "pass" ? "pass" : "fail"}">${escapeHtml17(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 tool contracts configured.</p></section>'}</main></body></html>`;
10820
11056
  };
10821
11057
  var createVoiceToolContractJSONHandler = (options) => () => runVoiceToolContractSuite(options);
10822
11058
  var createVoiceToolContractHTMLHandler = (options) => async () => {
@@ -10833,7 +11069,7 @@ var createVoiceToolContractHTMLHandler = (options) => async () => {
10833
11069
  var createVoiceToolContractRoutes = (options) => {
10834
11070
  const path = options.path ?? "/api/tool-contracts";
10835
11071
  const htmlPath = options.htmlPath === undefined ? `${path}/htmx` : options.htmlPath;
10836
- const routes = new Elysia15({
11072
+ const routes = new Elysia16({
10837
11073
  name: options.name ?? "absolutejs-voice-tool-contracts"
10838
11074
  }).get(path, createVoiceToolContractJSONHandler(options));
10839
11075
  if (htmlPath) {
@@ -10842,9 +11078,9 @@ var createVoiceToolContractRoutes = (options) => {
10842
11078
  return routes;
10843
11079
  };
10844
11080
  // src/turnQuality.ts
10845
- import { Elysia as Elysia16 } from "elysia";
11081
+ import { Elysia as Elysia17 } from "elysia";
10846
11082
  var DEFAULT_CONFIDENCE_WARN_THRESHOLD = 0.72;
10847
- var escapeHtml17 = (value) => value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
11083
+ var escapeHtml18 = (value) => value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
10848
11084
  var getTurnLatencyMs = (turn) => {
10849
11085
  const firstTranscriptAt = turn.transcripts.map((transcript) => transcript.endedAtMs ?? transcript.startedAtMs).filter((value) => typeof value === "number").sort((left, right) => left - right)[0];
10850
11086
  if (firstTranscriptAt === undefined) {
@@ -10915,24 +11151,24 @@ var summarizeVoiceTurnQuality = async (options) => {
10915
11151
  };
10916
11152
  var renderVoiceTurnQualityHTML = (report, options = {}) => {
10917
11153
  const title = options.title ?? "Voice Turn Quality";
10918
- const turns = report.turns.map((turn) => `<article class="turn ${escapeHtml17(turn.status)}">
11154
+ const turns = report.turns.map((turn) => `<article class="turn ${escapeHtml18(turn.status)}">
10919
11155
  <div class="turn-header">
10920
11156
  <div>
10921
- <p class="eyebrow">${escapeHtml17(turn.sessionId)} \xB7 ${escapeHtml17(turn.turnId)}</p>
10922
- <h2>${escapeHtml17(turn.text || "Empty turn")}</h2>
11157
+ <p class="eyebrow">${escapeHtml18(turn.sessionId)} \xB7 ${escapeHtml18(turn.turnId)}</p>
11158
+ <h2>${escapeHtml18(turn.text || "Empty turn")}</h2>
10923
11159
  </div>
10924
- <strong>${escapeHtml17(turn.status)}</strong>
11160
+ <strong>${escapeHtml18(turn.status)}</strong>
10925
11161
  </div>
10926
11162
  <dl>
10927
- <div><dt>Source</dt><dd>${escapeHtml17(turn.source ?? "unknown")}</dd></div>
11163
+ <div><dt>Source</dt><dd>${escapeHtml18(turn.source ?? "unknown")}</dd></div>
10928
11164
  <div><dt>Confidence</dt><dd>${turn.averageConfidence === undefined ? "n/a" : `${Math.round(turn.averageConfidence * 100)}%`}</dd></div>
10929
- <div><dt>Fallback</dt><dd>${turn.fallbackUsed ? `yes (${escapeHtml17(turn.fallbackSelectionReason ?? "selected")})` : "no"}</dd></div>
10930
- <div><dt>Correction</dt><dd>${turn.correctionChanged ? `changed${turn.correctionProvider ? ` by ${escapeHtml17(turn.correctionProvider)}` : ""}` : "none"}</dd></div>
11165
+ <div><dt>Fallback</dt><dd>${turn.fallbackUsed ? `yes (${escapeHtml18(turn.fallbackSelectionReason ?? "selected")})` : "no"}</dd></div>
11166
+ <div><dt>Correction</dt><dd>${turn.correctionChanged ? `changed${turn.correctionProvider ? ` by ${escapeHtml18(turn.correctionProvider)}` : ""}` : "none"}</dd></div>
10931
11167
  <div><dt>Transcripts</dt><dd>${String(turn.selectedTranscriptCount)} selected \xB7 ${String(turn.finalTranscriptCount)} final \xB7 ${String(turn.partialTranscriptCount)} partial</dd></div>
10932
11168
  <div><dt>Cost</dt><dd>${turn.costUnits === undefined ? "n/a" : String(turn.costUnits)}</dd></div>
10933
11169
  </dl>
10934
11170
  </article>`).join("");
10935
- return `<!doctype html><html lang="en"><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /><title>${escapeHtml17(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>${escapeHtml17(title)}</h1><div class="summary"><span class="pill ${escapeHtml17(report.status)}">${escapeHtml17(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>`;
11171
+ return `<!doctype html><html lang="en"><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /><title>${escapeHtml18(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>${escapeHtml18(title)}</h1><div class="summary"><span class="pill ${escapeHtml18(report.status)}">${escapeHtml18(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>`;
10936
11172
  };
10937
11173
  var createVoiceTurnQualityJSONHandler = (options) => async () => summarizeVoiceTurnQuality(options);
10938
11174
  var createVoiceTurnQualityHTMLHandler = (options) => async () => {
@@ -10949,7 +11185,7 @@ var createVoiceTurnQualityHTMLHandler = (options) => async () => {
10949
11185
  var createVoiceTurnQualityRoutes = (options) => {
10950
11186
  const path = options.path ?? "/api/turn-quality";
10951
11187
  const htmlPath = options.htmlPath === undefined ? `${path}/htmx` : options.htmlPath;
10952
- const routes = new Elysia16({
11188
+ const routes = new Elysia17({
10953
11189
  name: options.name ?? "absolutejs-voice-turn-quality"
10954
11190
  }).get(path, createVoiceTurnQualityJSONHandler(options));
10955
11191
  if (htmlPath) {
@@ -10958,8 +11194,8 @@ var createVoiceTurnQualityRoutes = (options) => {
10958
11194
  return routes;
10959
11195
  };
10960
11196
  // src/outcomeContract.ts
10961
- import { Elysia as Elysia17 } from "elysia";
10962
- var escapeHtml18 = (value) => value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
11197
+ import { Elysia as Elysia18 } from "elysia";
11198
+ var escapeHtml19 = (value) => value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
10963
11199
  var getPayloadString = (event, key) => typeof event.payload[key] === "string" ? event.payload[key] : undefined;
10964
11200
  var toList = async (input) => Array.isArray(input) ? input : await input?.list() ?? [];
10965
11201
  var hydrateSessions = async (input) => {
@@ -11067,9 +11303,9 @@ var renderVoiceOutcomeContractHTML = (report, options = {}) => {
11067
11303
  const contracts = report.contracts.map((contract) => `<section class="contract ${contract.pass ? "pass" : "fail"}">
11068
11304
  <div class="contract-header">
11069
11305
  <div>
11070
- <p class="eyebrow">${escapeHtml18(contract.contractId)}</p>
11071
- <h2>${escapeHtml18(contract.label ?? contract.contractId)}</h2>
11072
- ${contract.description ? `<p>${escapeHtml18(contract.description)}</p>` : ""}
11306
+ <p class="eyebrow">${escapeHtml19(contract.contractId)}</p>
11307
+ <h2>${escapeHtml19(contract.label ?? contract.contractId)}</h2>
11308
+ ${contract.description ? `<p>${escapeHtml19(contract.description)}</p>` : ""}
11073
11309
  </div>
11074
11310
  <strong>${contract.pass ? "pass" : "fail"}</strong>
11075
11311
  </div>
@@ -11080,9 +11316,9 @@ var renderVoiceOutcomeContractHTML = (report, options = {}) => {
11080
11316
  <span>handoffs ${String(contract.matched.handoffs)}</span>
11081
11317
  <span>events ${String(contract.matched.integrationEvents)}</span>
11082
11318
  </div>
11083
- ${contract.issues.length ? `<ul>${contract.issues.map((issue) => `<li>${escapeHtml18(issue.message)}</li>`).join("")}</ul>` : ""}
11319
+ ${contract.issues.length ? `<ul>${contract.issues.map((issue) => `<li>${escapeHtml19(issue.message)}</li>`).join("")}</ul>` : ""}
11084
11320
  </section>`).join("");
11085
- return `<!doctype html><html lang="en"><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /><title>${escapeHtml18(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>${escapeHtml18(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>`;
11321
+ return `<!doctype html><html lang="en"><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /><title>${escapeHtml19(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>${escapeHtml19(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>`;
11086
11322
  };
11087
11323
  var createVoiceOutcomeContractJSONHandler = (options) => async () => runVoiceOutcomeContractSuite(options);
11088
11324
  var createVoiceOutcomeContractHTMLHandler = (options) => async () => {
@@ -11098,7 +11334,7 @@ var createVoiceOutcomeContractHTMLHandler = (options) => async () => {
11098
11334
  var createVoiceOutcomeContractRoutes = (options) => {
11099
11335
  const path = options.path ?? "/api/outcome-contracts";
11100
11336
  const htmlPath = options.htmlPath === undefined ? `${path}/htmx` : options.htmlPath;
11101
- const routes = new Elysia17({
11337
+ const routes = new Elysia18({
11102
11338
  name: options.name ?? "absolutejs-voice-outcome-contracts"
11103
11339
  }).get(path, createVoiceOutcomeContractJSONHandler(options));
11104
11340
  if (htmlPath) {
@@ -11107,7 +11343,7 @@ var createVoiceOutcomeContractRoutes = (options) => {
11107
11343
  return routes;
11108
11344
  };
11109
11345
  // src/telephonyOutcome.ts
11110
- import { Elysia as Elysia18 } from "elysia";
11346
+ import { Elysia as Elysia19 } from "elysia";
11111
11347
  var DEFAULT_COMPLETED_STATUSES = [
11112
11348
  "answered",
11113
11349
  "completed",
@@ -11167,7 +11403,7 @@ var createMemoryVoiceTelephonyWebhookIdempotencyStore = () => {
11167
11403
  };
11168
11404
  };
11169
11405
  var normalizeToken = (value) => typeof value === "string" ? value.trim().toLowerCase().replace(/\s+/g, "-").replace(/_+/g, "-") : undefined;
11170
- var firstString = (source, keys) => {
11406
+ var firstString2 = (source, keys) => {
11171
11407
  for (const key of keys) {
11172
11408
  const value = source[key];
11173
11409
  if (typeof value === "string" && value.trim()) {
@@ -11178,7 +11414,7 @@ var firstString = (source, keys) => {
11178
11414
  }
11179
11415
  }
11180
11416
  };
11181
- var firstNumber = (source, keys) => {
11417
+ var firstNumber2 = (source, keys) => {
11182
11418
  for (const key of keys) {
11183
11419
  const value = source[key];
11184
11420
  if (typeof value === "number" && Number.isFinite(value)) {
@@ -11539,8 +11775,8 @@ var verifyVoiceTelephonyWebhook = async (input) => {
11539
11775
  var durationMsFromSeconds = (value) => typeof value === "number" ? value * 1000 : undefined;
11540
11776
  var parseVoiceTelephonyWebhookEvent = (input) => {
11541
11777
  const payload = flattenPayload(input.body);
11542
- const provider = firstString(payload, ["provider", "Provider"]) ?? input.provider;
11543
- const status = firstString(payload, [
11778
+ const provider = firstString2(payload, ["provider", "Provider"]) ?? input.provider;
11779
+ const status = firstString2(payload, [
11544
11780
  "CallStatus",
11545
11781
  "call_status",
11546
11782
  "callStatus",
@@ -11550,7 +11786,7 @@ var parseVoiceTelephonyWebhookEvent = (input) => {
11550
11786
  "event_type",
11551
11787
  "type"
11552
11788
  ]);
11553
- const durationMs = firstNumber(payload, ["durationMs", "duration_ms"]) ?? durationMsFromSeconds(firstNumber(payload, [
11789
+ const durationMs = firstNumber2(payload, ["durationMs", "duration_ms"]) ?? durationMsFromSeconds(firstNumber2(payload, [
11554
11790
  "CallDuration",
11555
11791
  "call_duration",
11556
11792
  "callDuration",
@@ -11558,16 +11794,16 @@ var parseVoiceTelephonyWebhookEvent = (input) => {
11558
11794
  "dial_call_duration",
11559
11795
  "duration"
11560
11796
  ]));
11561
- const sipCode = firstNumber(payload, [
11797
+ const sipCode = firstNumber2(payload, [
11562
11798
  "SipResponseCode",
11563
11799
  "sip_response_code",
11564
11800
  "sipCode",
11565
11801
  "sip_code",
11566
11802
  "hangupCauseCode"
11567
11803
  ]);
11568
- const from = firstString(payload, ["From", "from", "caller_id", "callerId"]);
11569
- const to = firstString(payload, ["To", "to", "called_number", "calledNumber"]);
11570
- const target = firstString(payload, [
11804
+ const from = firstString2(payload, ["From", "from", "caller_id", "callerId"]);
11805
+ const to = firstString2(payload, ["To", "to", "called_number", "calledNumber"]);
11806
+ const target = firstString2(payload, [
11571
11807
  "transferTarget",
11572
11808
  "TransferTarget",
11573
11809
  "target",
@@ -11575,7 +11811,7 @@ var parseVoiceTelephonyWebhookEvent = (input) => {
11575
11811
  "department"
11576
11812
  ]);
11577
11813
  return {
11578
- answeredBy: firstString(payload, [
11814
+ answeredBy: firstString2(payload, [
11579
11815
  "AnsweredBy",
11580
11816
  "answered_by",
11581
11817
  "answeredBy",
@@ -11586,7 +11822,7 @@ var parseVoiceTelephonyWebhookEvent = (input) => {
11586
11822
  from,
11587
11823
  metadata: payload,
11588
11824
  provider,
11589
- reason: firstString(payload, [
11825
+ reason: firstString2(payload, [
11590
11826
  "Reason",
11591
11827
  "reason",
11592
11828
  "HangupCause",
@@ -11602,7 +11838,7 @@ var parseVoiceTelephonyWebhookEvent = (input) => {
11602
11838
  var defaultSessionId = (input) => {
11603
11839
  const payload = flattenPayload(input.body);
11604
11840
  const metadataSessionId = input.event.metadata?.sessionId;
11605
- return firstString(input.query, ["sessionId", "session_id"]) ?? firstString(payload, [
11841
+ return firstString2(input.query, ["sessionId", "session_id"]) ?? firstString2(payload, [
11606
11842
  "sessionId",
11607
11843
  "session_id",
11608
11844
  "SessionId",
@@ -11617,7 +11853,7 @@ var defaultSessionId = (input) => {
11617
11853
  };
11618
11854
  var defaultIdempotencyKey = (input) => {
11619
11855
  const payload = flattenPayload(input.body);
11620
- const eventId = firstString(payload, [
11856
+ const eventId = firstString2(payload, [
11621
11857
  "id",
11622
11858
  "event_id",
11623
11859
  "eventId",
@@ -11754,7 +11990,7 @@ var createVoiceTelephonyWebhookHandler = (options = {}) => async (input) => {
11754
11990
  var createVoiceTelephonyWebhookRoutes = (options = {}) => {
11755
11991
  const path = options.path ?? "/api/voice/telephony/webhook";
11756
11992
  const handler = createVoiceTelephonyWebhookHandler(options);
11757
- return new Elysia18({
11993
+ return new Elysia19({
11758
11994
  name: options.name ?? "absolutejs-voice-telephony-webhooks"
11759
11995
  }).post(path, async ({ query, request }) => {
11760
11996
  try {
@@ -14022,7 +14258,7 @@ var createVoiceMemoryStore = () => {
14022
14258
  return { get, getOrCreate, list, remove, set };
14023
14259
  };
14024
14260
  // src/opsWebhook.ts
14025
- import { Elysia as Elysia19 } from "elysia";
14261
+ import { Elysia as Elysia20 } from "elysia";
14026
14262
  var toHex5 = (bytes) => Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
14027
14263
  var signVoiceOpsWebhookBody = async (input) => {
14028
14264
  const encoder = new TextEncoder;
@@ -14152,7 +14388,7 @@ var verifyVoiceOpsWebhookSignature = async (input) => {
14152
14388
  };
14153
14389
  var createVoiceOpsWebhookReceiverRoutes = (options = {}) => {
14154
14390
  const path = options.path ?? "/api/voice-ops/webhook";
14155
- return new Elysia19().post(path, async ({ body, request, set }) => {
14391
+ return new Elysia20().post(path, async ({ body, request, set }) => {
14156
14392
  const bodyText = typeof body === "string" ? body : JSON.stringify(body);
14157
14393
  if (options.signingSecret) {
14158
14394
  const verification = await verifyVoiceOpsWebhookSignature({
@@ -15881,7 +16117,7 @@ var createVoiceSTTRoutingCorrectionHandler = (mode = "generic") => {
15881
16117
  };
15882
16118
  // src/telephony/twilio.ts
15883
16119
  import { Buffer as Buffer3 } from "buffer";
15884
- import { Elysia as Elysia20 } from "elysia";
16120
+ import { Elysia as Elysia21 } from "elysia";
15885
16121
  var TWILIO_MULAW_SAMPLE_RATE = 8000;
15886
16122
  var VOICE_PCM_SAMPLE_RATE = 16000;
15887
16123
  var escapeXml2 = (value) => value.replaceAll("&", "&amp;").replaceAll('"', "&quot;").replaceAll("'", "&apos;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
@@ -15911,7 +16147,7 @@ var resolveTwilioStreamParameters = async (parameters, input) => {
15911
16147
  return parameters;
15912
16148
  };
15913
16149
  var joinUrlPath = (origin, path) => `${origin.replace(/\/$/, "")}${path.startsWith("/") ? path : `/${path}`}`;
15914
- var escapeHtml19 = (value) => value.replaceAll("&", "&amp;").replaceAll('"', "&quot;").replaceAll("'", "&#39;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
16150
+ var escapeHtml20 = (value) => value.replaceAll("&", "&amp;").replaceAll('"', "&quot;").replaceAll("'", "&#39;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
15915
16151
  var getWebhookVerificationUrl = (webhook, input) => {
15916
16152
  if (!webhook?.verificationUrl) {
15917
16153
  return;
@@ -15954,23 +16190,23 @@ var buildTwilioVoiceSetupStatus = async (options, input) => {
15954
16190
  };
15955
16191
  var renderTwilioVoiceSetupHTML = (status, title) => `<main style="font-family: ui-sans-serif, system-ui; max-width: 860px; margin: 40px auto; padding: 0 20px;">
15956
16192
  <p style="letter-spacing: .12em; text-transform: uppercase; color: #52606d;">Twilio setup</p>
15957
- <h1>${escapeHtml19(title)}</h1>
16193
+ <h1>${escapeHtml20(title)}</h1>
15958
16194
  <p><strong>Status:</strong> ${status.ready ? "Ready" : "Needs attention"}</p>
15959
16195
  <section>
15960
16196
  <h2>URLs</h2>
15961
16197
  <ul>
15962
- <li><strong>TwiML:</strong> <code>${escapeHtml19(status.urls.twiml)}</code></li>
15963
- <li><strong>Media stream:</strong> <code>${escapeHtml19(status.urls.stream)}</code></li>
15964
- <li><strong>Status webhook:</strong> <code>${escapeHtml19(status.urls.webhook)}</code></li>
16198
+ <li><strong>TwiML:</strong> <code>${escapeHtml20(status.urls.twiml)}</code></li>
16199
+ <li><strong>Media stream:</strong> <code>${escapeHtml20(status.urls.stream)}</code></li>
16200
+ <li><strong>Status webhook:</strong> <code>${escapeHtml20(status.urls.webhook)}</code></li>
15965
16201
  </ul>
15966
16202
  </section>
15967
16203
  <section>
15968
16204
  <h2>Signing</h2>
15969
16205
  <p>Mode: <code>${status.signing.mode}</code></p>
15970
- ${status.signing.verificationUrl ? `<p>Verification URL: <code>${escapeHtml19(status.signing.verificationUrl)}</code></p>` : ""}
16206
+ ${status.signing.verificationUrl ? `<p>Verification URL: <code>${escapeHtml20(status.signing.verificationUrl)}</code></p>` : ""}
15971
16207
  </section>
15972
- ${status.missing.length ? `<section><h2>Missing env</h2><ul>${status.missing.map((name) => `<li><code>${escapeHtml19(name)}</code></li>`).join("")}</ul></section>` : ""}
15973
- ${status.warnings.length ? `<section><h2>Warnings</h2><ul>${status.warnings.map((warning) => `<li>${escapeHtml19(warning)}</li>`).join("")}</ul></section>` : ""}
16208
+ ${status.missing.length ? `<section><h2>Missing env</h2><ul>${status.missing.map((name) => `<li><code>${escapeHtml20(name)}</code></li>`).join("")}</ul></section>` : ""}
16209
+ ${status.warnings.length ? `<section><h2>Warnings</h2><ul>${status.warnings.map((warning) => `<li>${escapeHtml20(warning)}</li>`).join("")}</ul></section>` : ""}
15974
16210
  </main>`;
15975
16211
  var extractTwilioStreamUrl = (twiml) => twiml.match(/<Stream\b[^>]*\surl="([^"]+)"/i)?.[1]?.replaceAll("&amp;", "&");
15976
16212
  var createSmokeCheck = (name, status, message, details) => ({
@@ -15981,20 +16217,20 @@ var createSmokeCheck = (name, status, message, details) => ({
15981
16217
  });
15982
16218
  var renderTwilioVoiceSmokeHTML = (report, title) => `<main style="font-family: ui-sans-serif, system-ui; max-width: 860px; margin: 40px auto; padding: 0 20px;">
15983
16219
  <p style="letter-spacing: .12em; text-transform: uppercase; color: #52606d;">Twilio smoke test</p>
15984
- <h1>${escapeHtml19(title)}</h1>
16220
+ <h1>${escapeHtml20(title)}</h1>
15985
16221
  <p><strong>Status:</strong> ${report.pass ? "Pass" : "Fail"}</p>
15986
16222
  <section>
15987
16223
  <h2>Checks</h2>
15988
16224
  <ul>
15989
- ${report.checks.map((check) => `<li><strong>${escapeHtml19(check.name)}</strong>: ${escapeHtml19(check.status)}${check.message ? ` - ${escapeHtml19(check.message)}` : ""}</li>`).join("")}
16225
+ ${report.checks.map((check) => `<li><strong>${escapeHtml20(check.name)}</strong>: ${escapeHtml20(check.status)}${check.message ? ` - ${escapeHtml20(check.message)}` : ""}</li>`).join("")}
15990
16226
  </ul>
15991
16227
  </section>
15992
16228
  <section>
15993
16229
  <h2>Observed URLs</h2>
15994
16230
  <ul>
15995
- <li><strong>TwiML:</strong> <code>${escapeHtml19(report.setup.urls.twiml)}</code></li>
15996
- <li><strong>Stream:</strong> <code>${escapeHtml19(report.twiml?.streamUrl ?? report.setup.urls.stream)}</code></li>
15997
- <li><strong>Webhook:</strong> <code>${escapeHtml19(report.setup.urls.webhook)}</code></li>
16231
+ <li><strong>TwiML:</strong> <code>${escapeHtml20(report.setup.urls.twiml)}</code></li>
16232
+ <li><strong>Stream:</strong> <code>${escapeHtml20(report.twiml?.streamUrl ?? report.setup.urls.stream)}</code></li>
16233
+ <li><strong>Webhook:</strong> <code>${escapeHtml20(report.setup.urls.webhook)}</code></li>
15998
16234
  </ul>
15999
16235
  </section>
16000
16236
  </main>`;
@@ -16454,7 +16690,7 @@ var createTwilioVoiceRoutes = (options) => {
16454
16690
  const smokePath = options.smoke?.path === false ? false : options.smoke?.path ?? "/api/voice/twilio/smoke";
16455
16691
  const bridges = new WeakMap;
16456
16692
  const webhookPolicy = options.webhook?.policy ?? options.outcomePolicy ?? createVoiceTelephonyOutcomePolicy();
16457
- const app = new Elysia20({
16693
+ const app = new Elysia21({
16458
16694
  name: options.name ?? "absolutejs-voice-twilio"
16459
16695
  }).get(twimlPath, async ({ query, request }) => {
16460
16696
  const streamUrl = await resolveTwilioStreamUrl(options, {
@@ -16590,9 +16826,9 @@ var createTwilioVoiceRoutes = (options) => {
16590
16826
  };
16591
16827
  // src/telephony/telnyx.ts
16592
16828
  import { Buffer as Buffer4 } from "buffer";
16593
- import { Elysia as Elysia21 } from "elysia";
16829
+ import { Elysia as Elysia22 } from "elysia";
16594
16830
  var escapeXml3 = (value) => value.replaceAll("&", "&amp;").replaceAll('"', "&quot;").replaceAll("'", "&apos;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
16595
- var escapeHtml20 = (value) => value.replaceAll("&", "&amp;").replaceAll('"', "&quot;").replaceAll("'", "&#39;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
16831
+ var escapeHtml21 = (value) => value.replaceAll("&", "&amp;").replaceAll('"', "&quot;").replaceAll("'", "&#39;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
16596
16832
  var joinUrlPath2 = (origin, path) => `${origin.replace(/\/$/, "")}${path.startsWith("/") ? path : `/${path}`}`;
16597
16833
  var resolveRequestOrigin2 = (request) => {
16598
16834
  const url = new URL(request.url);
@@ -16793,21 +17029,21 @@ var buildTelnyxVoiceSetupStatus = async (options, input) => {
16793
17029
  };
16794
17030
  var renderTelnyxSetupHTML = (status, title) => `<main style="font-family: ui-sans-serif, system-ui; max-width: 860px; margin: 40px auto; padding: 0 20px;">
16795
17031
  <p style="letter-spacing: .12em; text-transform: uppercase; color: #52606d;">Telnyx setup</p>
16796
- <h1>${escapeHtml20(title)}</h1>
17032
+ <h1>${escapeHtml21(title)}</h1>
16797
17033
  <p><strong>Status:</strong> ${status.ready ? "Ready" : "Needs attention"}</p>
16798
17034
  <ul>
16799
- <li><strong>TeXML:</strong> <code>${escapeHtml20(status.urls.texml)}</code></li>
16800
- <li><strong>Media stream:</strong> <code>${escapeHtml20(status.urls.stream)}</code></li>
16801
- <li><strong>Status webhook:</strong> <code>${escapeHtml20(status.urls.webhook)}</code></li>
17035
+ <li><strong>TeXML:</strong> <code>${escapeHtml21(status.urls.texml)}</code></li>
17036
+ <li><strong>Media stream:</strong> <code>${escapeHtml21(status.urls.stream)}</code></li>
17037
+ <li><strong>Status webhook:</strong> <code>${escapeHtml21(status.urls.webhook)}</code></li>
16802
17038
  </ul>
16803
- ${status.missing.length ? `<h2>Missing env</h2><ul>${status.missing.map((name) => `<li><code>${escapeHtml20(name)}</code></li>`).join("")}</ul>` : ""}
16804
- ${status.warnings.length ? `<h2>Warnings</h2><ul>${status.warnings.map((warning) => `<li>${escapeHtml20(warning)}</li>`).join("")}</ul>` : ""}
17039
+ ${status.missing.length ? `<h2>Missing env</h2><ul>${status.missing.map((name) => `<li><code>${escapeHtml21(name)}</code></li>`).join("")}</ul>` : ""}
17040
+ ${status.warnings.length ? `<h2>Warnings</h2><ul>${status.warnings.map((warning) => `<li>${escapeHtml21(warning)}</li>`).join("")}</ul>` : ""}
16805
17041
  </main>`;
16806
17042
  var renderTelnyxSmokeHTML = (report, title) => `<main style="font-family: ui-sans-serif, system-ui; max-width: 860px; margin: 40px auto; padding: 0 20px;">
16807
17043
  <p style="letter-spacing: .12em; text-transform: uppercase; color: #52606d;">Telnyx smoke test</p>
16808
- <h1>${escapeHtml20(title)}</h1>
17044
+ <h1>${escapeHtml21(title)}</h1>
16809
17045
  <p><strong>Status:</strong> ${report.pass ? "Pass" : "Fail"}</p>
16810
- <ul>${report.checks.map((check) => `<li><strong>${escapeHtml20(check.name)}</strong>: ${escapeHtml20(check.status)}${check.message ? ` - ${escapeHtml20(check.message)}` : ""}</li>`).join("")}</ul>
17046
+ <ul>${report.checks.map((check) => `<li><strong>${escapeHtml21(check.name)}</strong>: ${escapeHtml21(check.status)}${check.message ? ` - ${escapeHtml21(check.message)}` : ""}</li>`).join("")}</ul>
16811
17047
  </main>`;
16812
17048
  var runTelnyxSmokeTest = async (input) => {
16813
17049
  const setup = await buildTelnyxVoiceSetupStatus(input.options, input);
@@ -16901,7 +17137,7 @@ var createTelnyxVoiceRoutes = (options = {}) => {
16901
17137
  publicKey: options.webhook?.publicKey,
16902
17138
  toleranceSeconds: options.webhook?.toleranceSeconds
16903
17139
  }) : undefined);
16904
- const app = new Elysia21({
17140
+ const app = new Elysia22({
16905
17141
  name: options.name ?? "absolutejs-voice-telnyx"
16906
17142
  }).get(texmlPath, async ({ query, request }) => {
16907
17143
  const streamUrl = await resolveTelnyxStreamUrl(options, {
@@ -17011,9 +17247,9 @@ var createTelnyxVoiceRoutes = (options = {}) => {
17011
17247
  };
17012
17248
  // src/telephony/plivo.ts
17013
17249
  import { Buffer as Buffer5 } from "buffer";
17014
- import { Elysia as Elysia22 } from "elysia";
17250
+ import { Elysia as Elysia23 } from "elysia";
17015
17251
  var escapeXml4 = (value) => value.replaceAll("&", "&amp;").replaceAll('"', "&quot;").replaceAll("'", "&apos;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
17016
- var escapeHtml21 = (value) => value.replaceAll("&", "&amp;").replaceAll('"', "&quot;").replaceAll("'", "&#39;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
17252
+ var escapeHtml22 = (value) => value.replaceAll("&", "&amp;").replaceAll('"', "&quot;").replaceAll("'", "&#39;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
17017
17253
  var joinUrlPath3 = (origin, path) => `${origin.replace(/\/$/, "")}${path.startsWith("/") ? path : `/${path}`}`;
17018
17254
  var resolveRequestOrigin3 = (request) => {
17019
17255
  const url = new URL(request.url);
@@ -17264,21 +17500,21 @@ var buildPlivoVoiceSetupStatus = async (options, input) => {
17264
17500
  };
17265
17501
  var renderPlivoSetupHTML = (status, title) => `<main style="font-family: ui-sans-serif, system-ui; max-width: 860px; margin: 40px auto; padding: 0 20px;">
17266
17502
  <p style="letter-spacing: .12em; text-transform: uppercase; color: #52606d;">Plivo setup</p>
17267
- <h1>${escapeHtml21(title)}</h1>
17503
+ <h1>${escapeHtml22(title)}</h1>
17268
17504
  <p><strong>Status:</strong> ${status.ready ? "Ready" : "Needs attention"}</p>
17269
17505
  <ul>
17270
- <li><strong>Answer XML:</strong> <code>${escapeHtml21(status.urls.answer)}</code></li>
17271
- <li><strong>Audio stream:</strong> <code>${escapeHtml21(status.urls.stream)}</code></li>
17272
- <li><strong>Status webhook:</strong> <code>${escapeHtml21(status.urls.webhook)}</code></li>
17506
+ <li><strong>Answer XML:</strong> <code>${escapeHtml22(status.urls.answer)}</code></li>
17507
+ <li><strong>Audio stream:</strong> <code>${escapeHtml22(status.urls.stream)}</code></li>
17508
+ <li><strong>Status webhook:</strong> <code>${escapeHtml22(status.urls.webhook)}</code></li>
17273
17509
  </ul>
17274
- ${status.missing.length ? `<h2>Missing env</h2><ul>${status.missing.map((name) => `<li><code>${escapeHtml21(name)}</code></li>`).join("")}</ul>` : ""}
17275
- ${status.warnings.length ? `<h2>Warnings</h2><ul>${status.warnings.map((warning) => `<li>${escapeHtml21(warning)}</li>`).join("")}</ul>` : ""}
17510
+ ${status.missing.length ? `<h2>Missing env</h2><ul>${status.missing.map((name) => `<li><code>${escapeHtml22(name)}</code></li>`).join("")}</ul>` : ""}
17511
+ ${status.warnings.length ? `<h2>Warnings</h2><ul>${status.warnings.map((warning) => `<li>${escapeHtml22(warning)}</li>`).join("")}</ul>` : ""}
17276
17512
  </main>`;
17277
17513
  var renderPlivoSmokeHTML = (report, title) => `<main style="font-family: ui-sans-serif, system-ui; max-width: 860px; margin: 40px auto; padding: 0 20px;">
17278
17514
  <p style="letter-spacing: .12em; text-transform: uppercase; color: #52606d;">Plivo smoke test</p>
17279
- <h1>${escapeHtml21(title)}</h1>
17515
+ <h1>${escapeHtml22(title)}</h1>
17280
17516
  <p><strong>Status:</strong> ${report.pass ? "Pass" : "Fail"}</p>
17281
- <ul>${report.checks.map((check) => `<li><strong>${escapeHtml21(check.name)}</strong>: ${escapeHtml21(check.status)}${check.message ? ` - ${escapeHtml21(check.message)}` : ""}</li>`).join("")}</ul>
17517
+ <ul>${report.checks.map((check) => `<li><strong>${escapeHtml22(check.name)}</strong>: ${escapeHtml22(check.status)}${check.message ? ` - ${escapeHtml22(check.message)}` : ""}</li>`).join("")}</ul>
17282
17518
  </main>`;
17283
17519
  var runPlivoSmokeTest = async (input) => {
17284
17520
  const setup = await buildPlivoVoiceSetupStatus(input.options, input);
@@ -17373,7 +17609,7 @@ var createPlivoVoiceRoutes = (options = {}) => {
17373
17609
  request: input.request
17374
17610
  }) : verificationUrl ?? input.request.url
17375
17611
  }) : undefined);
17376
- const app = new Elysia22({
17612
+ const app = new Elysia23({
17377
17613
  name: options.name ?? "absolutejs-voice-plivo"
17378
17614
  }).get(answerPath, async ({ query, request }) => {
17379
17615
  const streamUrl = await resolvePlivoStreamUrl(options, {
@@ -17546,6 +17782,7 @@ export {
17546
17782
  transcodeTwilioInboundPayloadToPCM16,
17547
17783
  transcodePCMToTwilioOutboundPayload,
17548
17784
  summarizeVoiceTurnQuality,
17785
+ summarizeVoiceTraceTimeline,
17549
17786
  summarizeVoiceTraceSinkDeliveries,
17550
17787
  summarizeVoiceTrace,
17551
17788
  summarizeVoiceSessions,
@@ -17591,6 +17828,8 @@ export {
17591
17828
  requeueVoiceOpsTask,
17592
17829
  reopenVoiceOpsTask,
17593
17830
  renderVoiceTurnQualityHTML,
17831
+ renderVoiceTraceTimelineSessionHTML,
17832
+ renderVoiceTraceTimelineHTML,
17594
17833
  renderVoiceTraceMarkdown,
17595
17834
  renderVoiceTraceHTML,
17596
17835
  renderVoiceToolContractHTML,
@@ -17653,6 +17892,7 @@ export {
17653
17892
  createVoiceTurnQualityRoutes,
17654
17893
  createVoiceTurnQualityJSONHandler,
17655
17894
  createVoiceTurnQualityHTMLHandler,
17895
+ createVoiceTraceTimelineRoutes,
17656
17896
  createVoiceTraceSinkStore,
17657
17897
  createVoiceTraceSinkDeliveryWorkerLoop,
17658
17898
  createVoiceTraceSinkDeliveryWorker,
@@ -0,0 +1,93 @@
1
+ import { Elysia } from 'elysia';
2
+ import { evaluateVoiceTrace, type StoredVoiceTraceEvent, type VoiceTraceEvaluationOptions, type VoiceTraceEventStore, type VoiceTraceRedactionConfig, type VoiceTraceSummary } from './trace';
3
+ export type VoiceTraceTimelineEvent = {
4
+ at: number;
5
+ elapsedMs?: number;
6
+ id: string;
7
+ label: string;
8
+ offsetMs: number;
9
+ provider?: string;
10
+ status?: string;
11
+ turnId?: string;
12
+ type: StoredVoiceTraceEvent['type'];
13
+ };
14
+ export type VoiceTraceTimelineProviderSummary = {
15
+ averageElapsedMs?: number;
16
+ errorCount: number;
17
+ eventCount: number;
18
+ fallbackCount: number;
19
+ maxElapsedMs?: number;
20
+ provider: string;
21
+ successCount: number;
22
+ timeoutCount: number;
23
+ };
24
+ export type VoiceTraceTimelineSession = {
25
+ endedAt?: number;
26
+ evaluation: ReturnType<typeof evaluateVoiceTrace>;
27
+ events: VoiceTraceTimelineEvent[];
28
+ lastEventAt?: number;
29
+ providers: VoiceTraceTimelineProviderSummary[];
30
+ sessionId: string;
31
+ startedAt?: number;
32
+ status: 'failed' | 'healthy' | 'warning';
33
+ summary: VoiceTraceSummary;
34
+ };
35
+ export type VoiceTraceTimelineReport = {
36
+ checkedAt: number;
37
+ failed: number;
38
+ sessions: VoiceTraceTimelineSession[];
39
+ total: number;
40
+ warnings: number;
41
+ };
42
+ export type VoiceTraceTimelineRoutesOptions = {
43
+ evaluation?: VoiceTraceEvaluationOptions;
44
+ headers?: HeadersInit;
45
+ htmlPath?: string;
46
+ limit?: number;
47
+ name?: string;
48
+ path?: string;
49
+ redact?: VoiceTraceRedactionConfig;
50
+ render?: (report: VoiceTraceTimelineReport) => string | Promise<string>;
51
+ renderSession?: (session: VoiceTraceTimelineSession) => string | Promise<string>;
52
+ store: VoiceTraceEventStore;
53
+ title?: string;
54
+ };
55
+ export declare const summarizeVoiceTraceTimeline: (events: StoredVoiceTraceEvent[], options?: {
56
+ evaluation?: VoiceTraceEvaluationOptions;
57
+ limit?: number;
58
+ redact?: VoiceTraceRedactionConfig;
59
+ }) => VoiceTraceTimelineReport;
60
+ export declare const renderVoiceTraceTimelineSessionHTML: (session: VoiceTraceTimelineSession, options?: {
61
+ title?: string;
62
+ }) => string;
63
+ export declare const renderVoiceTraceTimelineHTML: (report: VoiceTraceTimelineReport, options?: {
64
+ title?: string;
65
+ }) => string;
66
+ export declare const createVoiceTraceTimelineRoutes: (options: VoiceTraceTimelineRoutesOptions) => Elysia<"", {
67
+ decorator: {};
68
+ store: {};
69
+ derive: {};
70
+ resolve: {};
71
+ }, {
72
+ typebox: {};
73
+ error: {};
74
+ }, {
75
+ schema: {};
76
+ standaloneSchema: {};
77
+ macro: {};
78
+ macroFn: {};
79
+ parser: {};
80
+ response: {};
81
+ }, {}, {
82
+ derive: {};
83
+ resolve: {};
84
+ schema: {};
85
+ standaloneSchema: {};
86
+ response: {};
87
+ }, {
88
+ derive: {};
89
+ resolve: {};
90
+ schema: {};
91
+ standaloneSchema: {};
92
+ response: {};
93
+ }>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@absolutejs/voice",
3
- "version": "0.0.22-beta.89",
3
+ "version": "0.0.22-beta.90",
4
4
  "description": "Voice primitives and Elysia plugin for AbsoluteJS",
5
5
  "repository": {
6
6
  "type": "git",