@absolutejs/voice 0.0.22-beta.200 → 0.0.22-beta.202

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/README.md CHANGED
@@ -1378,7 +1378,7 @@ app.use(
1378
1378
  );
1379
1379
  ```
1380
1380
 
1381
- `createVoiceOperationsRecordRoutes(...)` links the call/session timeline, replay, provider events, tools, handoffs, audit, reviews, ops tasks, integration events, and sink delivery attempts into one debuggable object. Use `/voice-operations/:sessionId` as the first place to investigate failed calls, provider failures, handoff failures, slow turns, and campaign attempts.
1381
+ `createVoiceOperationsRecordRoutes(...)` links the call/session timeline, transcript, replay, provider decisions, tools, handoffs, audit, reviews, ops tasks, integration events, and sink delivery attempts into one debuggable object. Use `/voice-operations/:sessionId` as the first place to investigate failed calls, provider failures, handoff failures, slow turns, and campaign attempts. The same mount also exposes incident handoff Markdown at `/voice-operations/:sessionId/incident.md` and `/api/voice-operations/:sessionId/incident.md` for support tooling.
1382
1382
 
1383
1383
  Mount `createVoiceOpsRecoveryRoutes(...)` beside it when operators need one deploy-checkable recovery signal:
1384
1384
 
@@ -5,6 +5,7 @@ export type VoiceEvalStatus = 'pass' | 'fail';
5
5
  export type VoiceEvalSessionReport = {
6
6
  endedAt?: number;
7
7
  eventCount: number;
8
+ operationsRecordHref?: string;
8
9
  quality: VoiceQualityReport;
9
10
  scenarioId?: string;
10
11
  sessionId: string;
@@ -81,6 +82,7 @@ export type VoiceScenarioEvalDefinition = {
81
82
  export type VoiceScenarioEvalSessionResult = {
82
83
  eventCount: number;
83
84
  issues: string[];
85
+ operationsRecordHref?: string;
84
86
  sessionId: string;
85
87
  status: VoiceEvalStatus;
86
88
  };
@@ -142,6 +144,7 @@ export type VoiceEvalRoutesOptions = {
142
144
  links?: VoiceEvalLink[];
143
145
  limit?: number;
144
146
  name?: string;
147
+ operationsRecordHref?: false | string | ((sessionId: string) => string);
145
148
  path?: string;
146
149
  scenarios?: VoiceScenarioEvalDefinition[];
147
150
  store?: VoiceTraceEventStore;
@@ -151,17 +154,20 @@ export type VoiceEvalRoutesOptions = {
151
154
  export declare const runVoiceSessionEvals: (options?: {
152
155
  events?: StoredVoiceTraceEvent[];
153
156
  limit?: number;
157
+ operationsRecordHref?: false | string | ((sessionId: string) => string);
154
158
  store?: VoiceTraceEventStore;
155
159
  thresholds?: VoiceQualityThresholds;
156
160
  }) => Promise<VoiceEvalReport>;
157
161
  export declare const runVoiceScenarioEvals: (options?: {
158
162
  events?: StoredVoiceTraceEvent[];
163
+ operationsRecordHref?: false | string | ((sessionId: string) => string);
159
164
  scenarios?: VoiceScenarioEvalDefinition[];
160
165
  store?: VoiceTraceEventStore;
161
166
  }) => Promise<VoiceScenarioEvalReport>;
162
167
  export declare const runVoiceScenarioFixtureEvals: (options?: {
163
168
  fixtures?: VoiceScenarioFixture[];
164
169
  fixtureStore?: VoiceScenarioFixtureStore;
170
+ operationsRecordHref?: false | string | ((sessionId: string) => string);
165
171
  scenarios?: VoiceScenarioEvalDefinition[];
166
172
  }) => Promise<VoiceScenarioFixtureEvalReport>;
167
173
  export declare const compareVoiceEvalBaseline: (currentReport: VoiceEvalReport, baselineReport: VoiceEvalReport, options?: VoiceEvalBaselineComparisonOptions) => VoiceEvalBaselineComparison;
package/dist/index.js CHANGED
@@ -13573,6 +13573,17 @@ var escapeHtml20 = (value) => value.replaceAll("&", "&amp;").replaceAll("<", "&l
13573
13573
  var rate2 = (count, total) => count / Math.max(1, total);
13574
13574
  var normalizeSearchText = (value) => value.trim().toLowerCase();
13575
13575
  var getString9 = (value) => typeof value === "string" ? value : undefined;
13576
+ var resolveSessionHref = (value, sessionId) => {
13577
+ if (value === false) {
13578
+ return;
13579
+ }
13580
+ const href = value ?? "/voice-operations/:sessionId";
13581
+ if (typeof href === "function") {
13582
+ return href(sessionId);
13583
+ }
13584
+ const encoded = encodeURIComponent(sessionId);
13585
+ return href.includes(":sessionId") ? href.replace(":sessionId", encoded) : `${href.replace(/\/$/, "")}/${encoded}`;
13586
+ };
13576
13587
  var getObject = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : undefined;
13577
13588
  var getPathValue = (value, path) => {
13578
13589
  let current = value;
@@ -13638,6 +13649,7 @@ var runVoiceSessionEvals = async (options = {}) => {
13638
13649
  return {
13639
13650
  endedAt,
13640
13651
  eventCount: sorted.length,
13652
+ operationsRecordHref: resolveSessionHref(options.operationsRecordHref, sessionId),
13641
13653
  quality,
13642
13654
  scenarioId,
13643
13655
  sessionId,
@@ -13662,7 +13674,7 @@ var runVoiceSessionEvals = async (options = {}) => {
13662
13674
  var getSessionText = (events, type) => events.filter((event) => event.type === type).map((event) => getString9(event.payload.text)).filter((text) => Boolean(text?.trim())).join(`
13663
13675
  `);
13664
13676
  var countProviderErrors = (events) => events.filter((event) => event.type === "session.error" && (event.payload.providerStatus === "error" || typeof event.payload.provider === "string")).length;
13665
- var evaluateScenarioSession = (scenario, sessionId, events) => {
13677
+ var evaluateScenarioSession = (scenario, sessionId, events, operationsRecordHref) => {
13666
13678
  const issues = [];
13667
13679
  const committedText = getSessionText(events, "turn.committed");
13668
13680
  const assistantText = getSessionText(events, "turn.assistant");
@@ -13729,6 +13741,7 @@ var evaluateScenarioSession = (scenario, sessionId, events) => {
13729
13741
  return {
13730
13742
  eventCount: events.length,
13731
13743
  issues,
13744
+ operationsRecordHref,
13732
13745
  sessionId,
13733
13746
  status: issues.length > 0 ? "fail" : "pass"
13734
13747
  };
@@ -13741,7 +13754,7 @@ var runVoiceScenarioEvals = async (options = {}) => {
13741
13754
  grouped.set(event.sessionId, [...grouped.get(event.sessionId) ?? [], event]);
13742
13755
  }
13743
13756
  const results = scenarios.map((scenario) => {
13744
- const sessions = [...grouped.entries()].filter(([, sessionEvents]) => scenario.scenarioId ? sessionEvents.some((event) => event.scenarioId === scenario.scenarioId) : true).map(([sessionId, sessionEvents]) => evaluateScenarioSession(scenario, sessionId, filterVoiceTraceEvents(sessionEvents))).sort((left, right) => left.sessionId.localeCompare(right.sessionId));
13757
+ const sessions = [...grouped.entries()].filter(([, sessionEvents]) => scenario.scenarioId ? sessionEvents.some((event) => event.scenarioId === scenario.scenarioId) : true).map(([sessionId, sessionEvents]) => evaluateScenarioSession(scenario, sessionId, filterVoiceTraceEvents(sessionEvents), resolveSessionHref(options.operationsRecordHref, sessionId))).sort((left, right) => left.sessionId.localeCompare(right.sessionId));
13745
13758
  const issues = [];
13746
13759
  const minSessions = scenario.minSessions ?? 1;
13747
13760
  if (sessions.length < minSessions) {
@@ -13778,6 +13791,7 @@ var runVoiceScenarioFixtureEvals = async (options = {}) => {
13778
13791
  const results = await Promise.all(fixtures.map(async (fixture) => {
13779
13792
  const report = await runVoiceScenarioEvals({
13780
13793
  events: fixture.events,
13794
+ operationsRecordHref: options.operationsRecordHref,
13781
13795
  scenarios: options.scenarios
13782
13796
  });
13783
13797
  return {
@@ -13902,7 +13916,8 @@ var renderVoiceEvalHTML = (report, options = {}) => {
13902
13916
  const trend = report.trend.length ? report.trend.map((bucket) => `<tr><td>${escapeHtml20(bucket.key)}</td><td>${bucket.total}</td><td>${bucket.passed}</td><td>${bucket.failed}</td></tr>`).join("") : '<tr><td colspan="4">No eval buckets yet.</td></tr>';
13903
13917
  const sessions = report.sessions.length ? report.sessions.map((session) => {
13904
13918
  const failedMetrics = Object.entries(session.quality.metrics).filter(([, metric]) => !metric.pass).map(([, metric]) => metric.label).join(", ");
13905
- return `<tr class="${session.status}"><td>${escapeHtml20(session.sessionId)}</td><td>${escapeHtml20(session.status)}</td><td>${session.eventCount}</td><td>${session.summary.turnCount}</td><td>${session.summary.errorCount}</td><td>${escapeHtml20(formatTime(session.endedAt))}</td><td>${escapeHtml20(failedMetrics || "none")}</td></tr>`;
13919
+ const sessionLabel = session.operationsRecordHref ? `<a href="${escapeHtml20(session.operationsRecordHref)}">${escapeHtml20(session.sessionId)}</a>` : escapeHtml20(session.sessionId);
13920
+ return `<tr class="${session.status}"><td>${sessionLabel}</td><td>${escapeHtml20(session.status)}</td><td>${session.eventCount}</td><td>${session.summary.turnCount}</td><td>${session.summary.errorCount}</td><td>${escapeHtml20(formatTime(session.endedAt))}</td><td>${escapeHtml20(failedMetrics || "none")}</td></tr>`;
13906
13921
  }).join("") : '<tr><td colspan="7">No sessions found.</td></tr>';
13907
13922
  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{font-family:ui-sans-serif,system-ui,sans-serif;margin:2rem;background:#f8f7f2;color:#181713}main{max-width:1180px;margin:auto}nav{display:flex;gap:.5rem;flex-wrap:wrap;margin-bottom:1rem}nav a{background:#181713;border-radius:999px;color:white;padding:.35rem .7rem;text-decoration:none}.eyebrow{font-size:.78rem;font-weight:900;letter-spacing:.08em;text-transform:uppercase}.status{border-radius:999px;display:inline-flex;font-weight:800;padding:.35rem .75rem}.pass{color:#166534}.fail{color:#991b1b}.status.pass{background:#dcfce7}.status.fail{background:#fee2e2}.grid{display:grid;gap:1rem;grid-template-columns:repeat(auto-fit,minmax(160px,1fr));margin:1rem 0}.card,.primitive{background:white;border:1px solid #e7e5e4;border-radius:1rem;padding:1rem}.primitive{background:#fffdf7;border-color:#d6c7a3;margin:1rem 0}.primitive p{line-height:1.55}.primitive pre{background:#181713;border-radius:.85rem;color:#fef3c7;overflow:auto;padding:1rem}.primitive code{color:#fef3c7}.card strong{display:block;font-size:2rem}table{border-collapse:collapse;background:white;width:100%;margin:1rem 0 2rem}td,th{border-bottom:1px solid #eee;padding:.75rem;text-align:left}tr.fail td{border-left:4px solid #dc2626}tr.pass td{border-left:4px solid #16a34a}</style></head><body><main>${links}<h1>${escapeHtml20(title)}</h1><p class="status ${report.status}">${report.status}</p><div class="grid"><article class="card"><span>Total</span><strong>${report.total}</strong></article><article class="card"><span>Passed</span><strong>${report.passed}</strong></article><article class="card"><span>Failed</span><strong>${report.failed}</strong></article></div>${renderVoiceEvalPrimitiveCopy()}<h2>Trend</h2><table><thead><tr><th>Day</th><th>Total</th><th>Passed</th><th>Failed</th></tr></thead><tbody>${trend}</tbody></table><h2>Session Eval Results</h2><table><thead><tr><th>Session</th><th>Status</th><th>Events</th><th>Turns</th><th>Errors</th><th>Last event</th><th>Failed metrics</th></tr></thead><tbody>${sessions}</tbody></table></main></body></html>`;
13908
13923
  };
@@ -13919,7 +13934,10 @@ var renderVoiceScenarioEvalHTML = (report, options = {}) => {
13919
13934
  const links = options.links?.length ? `<nav>${options.links.map((link) => `<a href="${escapeHtml20(link.href)}">${escapeHtml20(link.label)}</a>`).join("")}</nav>` : "";
13920
13935
  const scenarios = report.scenarios.length ? report.scenarios.map((scenario) => {
13921
13936
  const scenarioIssues = scenario.issues.length ? `<ul>${scenario.issues.map((issue) => `<li>${escapeHtml20(issue)}</li>`).join("")}</ul>` : "";
13922
- const sessions = scenario.sessions.length ? scenario.sessions.map((session) => `<tr class="${session.status}"><td>${escapeHtml20(session.sessionId)}</td><td>${escapeHtml20(session.status)}</td><td>${session.eventCount}</td><td>${escapeHtml20(session.issues.join(", ") || "none")}</td></tr>`).join("") : '<tr><td colspan="4">No matching sessions.</td></tr>';
13937
+ const sessions = scenario.sessions.length ? scenario.sessions.map((session) => {
13938
+ const sessionLabel = session.operationsRecordHref ? `<a href="${escapeHtml20(session.operationsRecordHref)}">${escapeHtml20(session.sessionId)}</a>` : escapeHtml20(session.sessionId);
13939
+ return `<tr class="${session.status}"><td>${sessionLabel}</td><td>${escapeHtml20(session.status)}</td><td>${session.eventCount}</td><td>${escapeHtml20(session.issues.join(", ") || "none")}</td></tr>`;
13940
+ }).join("") : '<tr><td colspan="4">No matching sessions.</td></tr>';
13923
13941
  return `<section class="scenario ${scenario.status}"><h2>${escapeHtml20(scenario.label)}</h2>${scenario.description ? `<p>${escapeHtml20(scenario.description)}</p>` : ""}<p class="status ${scenario.status}">${scenario.status}</p><p>${scenario.passed} passed, ${scenario.failed} failed, ${scenario.matchedSessions} matched.</p>${scenarioIssues}<table><thead><tr><th>Session</th><th>Status</th><th>Events</th><th>Issues</th></tr></thead><tbody>${sessions}</tbody></table></section>`;
13924
13942
  }).join("") : "<section><p>No scenarios configured.</p></section>";
13925
13943
  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{font-family:ui-sans-serif,system-ui,sans-serif;margin:2rem;background:#f8f7f2;color:#181713}main{max-width:1180px;margin:auto}nav{display:flex;gap:.5rem;flex-wrap:wrap;margin-bottom:1rem}nav a{background:#181713;border-radius:999px;color:white;padding:.35rem .7rem;text-decoration:none}.eyebrow{font-size:.78rem;font-weight:900;letter-spacing:.08em;text-transform:uppercase}.status{border-radius:999px;display:inline-flex;font-weight:800;padding:.35rem .75rem}.status.pass{background:#dcfce7;color:#166534}.status.fail{background:#fee2e2;color:#991b1b}.grid{display:grid;gap:1rem;grid-template-columns:repeat(auto-fit,minmax(160px,1fr));margin:1rem 0}.card,section{background:white;border:1px solid #e7e5e4;border-radius:1rem;padding:1rem}.primitive{background:#fffdf7;border-color:#d6c7a3}.primitive p{line-height:1.55}.primitive pre{background:#181713;border-radius:.85rem;color:#fef3c7;overflow:auto;padding:1rem}.primitive code{color:#fef3c7}.card strong{display:block;font-size:2rem}section{margin:1rem 0}table{border-collapse:collapse;width:100%;margin-top:1rem}td,th{border-bottom:1px solid #eee;padding:.75rem;text-align:left}tr.fail td{border-left:4px solid #dc2626}tr.pass td{border-left:4px solid #16a34a}</style></head><body><main>${links}<h1>${escapeHtml20(title)}</h1><p class="status ${report.status}">${report.status}</p><div class="grid"><article class="card"><span>Total</span><strong>${report.total}</strong></article><article class="card"><span>Passed</span><strong>${report.passed}</strong></article><article class="card"><span>Failed</span><strong>${report.failed}</strong></article></div>${renderVoiceEvalPrimitiveCopy()}${scenarios}</main></body></html>`;
@@ -13941,6 +13959,7 @@ var createVoiceEvalRoutes = (options) => {
13941
13959
  const getReport = () => runVoiceSessionEvals({
13942
13960
  events: options.events,
13943
13961
  limit: options.limit,
13962
+ operationsRecordHref: options.operationsRecordHref,
13944
13963
  store: options.store,
13945
13964
  thresholds: options.thresholds
13946
13965
  });
@@ -13951,12 +13970,14 @@ var createVoiceEvalRoutes = (options) => {
13951
13970
  };
13952
13971
  const getScenarioReport = () => runVoiceScenarioEvals({
13953
13972
  events: options.events,
13973
+ operationsRecordHref: options.operationsRecordHref,
13954
13974
  scenarios: options.scenarios,
13955
13975
  store: options.store
13956
13976
  });
13957
13977
  const getFixtureReport = () => runVoiceScenarioFixtureEvals({
13958
13978
  fixtures: options.fixtures,
13959
13979
  fixtureStore: options.fixtureStore,
13980
+ operationsRecordHref: options.operationsRecordHref,
13960
13981
  scenarios: options.scenarios
13961
13982
  });
13962
13983
  routes.get(path, async () => {
@@ -15170,7 +15191,7 @@ var escapeHtml24 = (value) => value.replaceAll("&", "&amp;").replaceAll("<", "&l
15170
15191
  var increment4 = (record, key) => {
15171
15192
  record[key] = (record[key] ?? 0) + 1;
15172
15193
  };
15173
- var resolveSessionHref = (value, session) => {
15194
+ var resolveSessionHref2 = (value, session) => {
15174
15195
  if (value === false) {
15175
15196
  return;
15176
15197
  }
@@ -15334,7 +15355,7 @@ var summarizeVoiceSessions = async (options = {}) => {
15334
15355
  const replayHref = options.replayHref === false ? "" : typeof options.replayHref === "function" ? options.replayHref(item) : `${options.replayHref ?? "/api/voice-sessions"}/${encodeURIComponent(sessionId)}/replay/htmx`;
15335
15356
  return {
15336
15357
  ...item,
15337
- operationsRecordHref: resolveSessionHref(options.operationsRecordHref, item),
15358
+ operationsRecordHref: resolveSessionHref2(options.operationsRecordHref, item),
15338
15359
  replayHref
15339
15360
  };
15340
15361
  });
@@ -23959,7 +23980,7 @@ var eventStatus = (event) => firstString3(event.payload, [
23959
23980
  "reason"
23960
23981
  ]);
23961
23982
  var eventElapsedMs2 = (event) => firstNumber3(event.payload, ["elapsedMs", "latencyMs", "durationMs"]);
23962
- var resolveSessionHref2 = (value, sessionId) => {
23983
+ var resolveSessionHref3 = (value, sessionId) => {
23963
23984
  if (value === false) {
23964
23985
  return;
23965
23986
  }
@@ -24089,7 +24110,7 @@ var summarizeVoiceTraceTimeline = (events, options = {}) => {
24089
24110
  type: event.type
24090
24111
  })),
24091
24112
  lastEventAt: sorted.at(-1)?.at,
24092
- operationsRecordHref: resolveSessionHref2(options.operationsRecordHref, sessionId),
24113
+ operationsRecordHref: resolveSessionHref3(options.operationsRecordHref, sessionId),
24093
24114
  providers: summarizeProviders(sorted),
24094
24115
  sessionId,
24095
24116
  startedAt: summary.startedAt,
@@ -24207,6 +24228,7 @@ var hasPayloadValue = (payload, key, values) => {
24207
24228
  return typeof value === "string" && values.has(value);
24208
24229
  };
24209
24230
  var countIntegrationDeliveryStatus = (events, status) => events.filter((event) => event.deliveryStatus === status).length;
24231
+ var resolveRoutePath = (path, sessionId) => path.replace(":sessionId", encodeURIComponent(sessionId));
24210
24232
  var toHandoff = (event) => ({
24211
24233
  at: event.at,
24212
24234
  fromAgentId: getString17(event.payload.fromAgentId),
@@ -24390,7 +24412,8 @@ var renderVoiceOperationsRecordHTML = (record, options = {}) => {
24390
24412
  })
24391
24413
  );`);
24392
24414
  const incidentMarkdown = escapeHtml41(renderVoiceOperationsRecordIncidentMarkdown(record));
24393
- return `<!doctype html><html lang="en"><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /><title>${escapeHtml41(options.title ?? "Voice Operations Record")}</title><style>body{background:#101417;color:#f9f4e8;font-family:ui-sans-serif,system-ui,sans-serif;margin:0}main{margin:auto;max-width:1120px;padding:32px}.eyebrow{color:#fbbf24;font-size:.8rem;font-weight:900;letter-spacing:.14em;text-transform:uppercase}h1{font-size:clamp(2.4rem,6vw,4.8rem);line-height:.9;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}.grid{display:grid;gap:14px;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));margin:20px 0}.card,.primitive{background:#182025;border:1px solid #2d3a43;border-radius:20px;padding:16px}.card span,.muted,.label{color:#a9b4bd}.label{display:block;font-size:.72rem;font-weight:900;letter-spacing:.12em;text-transform:uppercase}.card strong{display:block;font-size:2rem}section{margin-top:28px}article{display:grid;gap:8px}ul{display:grid;gap:10px;list-style:none;padding:0}li{background:#182025;border:1px solid #2d3a43;border-radius:16px;padding:14px}pre{background:#080d10;border:1px solid #2d3a43;border-radius:16px;color:#dbeafe;overflow:auto;padding:14px}.hero-actions{display:flex;flex-wrap:wrap;gap:10px;margin-top:16px}.hero-actions a{background:#fbbf24;border-radius:999px;color:#111827;font-weight:900;padding:10px 14px;text-decoration:none}.two-column{display:grid;gap:18px;grid-template-columns:minmax(0,1.15fr) minmax(280px,.85fr)}@media(max-width:860px){main{padding:20px}.two-column{grid-template-columns:1fr}}</style></head><body><main><p class="eyebrow">Call log replacement</p><h1>${escapeHtml41(options.title ?? "Voice Operations Record")}</h1><p class="status ${escapeHtml41(record.status)}">${escapeHtml41(record.status)}</p><div class="hero-actions"><a href="#transcript">Transcript</a><a href="#provider-decisions">Provider decisions</a><a href="#incident-handoff">Incident handoff</a></div><section class="grid"><div class="card"><span>Events</span><strong>${String(record.summary.eventCount)}</strong></div><div class="card"><span>Turns</span><strong>${String(record.summary.turnCount)}</strong></div><div class="card"><span>Errors</span><strong>${String(record.summary.errorCount)}</strong></div><div class="card"><span>Duration</span><strong>${formatMs4(record.summary.callDurationMs)}</strong></div><div class="card"><span>Audit</span><strong>${String(record.audit?.total ?? 0)}</strong></div><div class="card"><span>Reviews</span><strong>${String(record.reviews?.total ?? 0)}</strong></div><div class="card"><span>Tasks</span><strong>${String(record.tasks?.total ?? 0)}</strong></div><div class="card"><span>Integrations</span><strong>${String(record.integrationEvents?.total ?? 0)}</strong></div></section><section class="two-column"><div><h2 id="transcript">Transcript</h2><ul>${transcript}</ul></div><div><h2 id="provider-decisions">Provider Decisions</h2><ul>${providerDecisions}</ul></div></section><section id="incident-handoff"><h2>Copyable Incident Handoff</h2><p class="muted">Paste this into Slack, Linear, Zendesk, or an incident review.</p><pre><code>${incidentMarkdown}</code></pre></section><section class="primitive"><p class="eyebrow">Copy into your app</p><h2><code>createVoiceOperationsRecordRoutes(...)</code> gives every call one debuggable object</h2><p class="muted">Use this as the support/debug payload across traces, provider routing, tools, handoffs, audit, latency, replay, reviews, tasks, and webhook delivery.</p><pre><code>${snippet}</code></pre></section><section><h2>Provider Summary</h2><div class="grid">${providers}</div></section><section><h2>Handoffs</h2><ul>${handoffs}</ul></section><section><h2>Tools</h2><ul>${tools}</ul></section><section><h2>Reviews</h2><ul>${reviews}</ul></section><section><h2>Tasks</h2><ul>${tasks}</ul></section><section><h2>Integration Events</h2><ul>${integrationEvents}</ul></section></main></body></html>`;
24415
+ const incidentLink = options.incidentHref ? `<a href="${escapeHtml41(options.incidentHref)}">Download incident.md</a>` : "";
24416
+ return `<!doctype html><html lang="en"><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /><title>${escapeHtml41(options.title ?? "Voice Operations Record")}</title><style>body{background:#101417;color:#f9f4e8;font-family:ui-sans-serif,system-ui,sans-serif;margin:0}main{margin:auto;max-width:1120px;padding:32px}.eyebrow{color:#fbbf24;font-size:.8rem;font-weight:900;letter-spacing:.14em;text-transform:uppercase}h1{font-size:clamp(2.4rem,6vw,4.8rem);line-height:.9;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}.grid{display:grid;gap:14px;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));margin:20px 0}.card,.primitive{background:#182025;border:1px solid #2d3a43;border-radius:20px;padding:16px}.card span,.muted,.label{color:#a9b4bd}.label{display:block;font-size:.72rem;font-weight:900;letter-spacing:.12em;text-transform:uppercase}.card strong{display:block;font-size:2rem}section{margin-top:28px}article{display:grid;gap:8px}ul{display:grid;gap:10px;list-style:none;padding:0}li{background:#182025;border:1px solid #2d3a43;border-radius:16px;padding:14px}pre{background:#080d10;border:1px solid #2d3a43;border-radius:16px;color:#dbeafe;overflow:auto;padding:14px}.hero-actions{display:flex;flex-wrap:wrap;gap:10px;margin-top:16px}.hero-actions a{background:#fbbf24;border-radius:999px;color:#111827;font-weight:900;padding:10px 14px;text-decoration:none}.two-column{display:grid;gap:18px;grid-template-columns:minmax(0,1.15fr) minmax(280px,.85fr)}@media(max-width:860px){main{padding:20px}.two-column{grid-template-columns:1fr}}</style></head><body><main><p class="eyebrow">Call log replacement</p><h1>${escapeHtml41(options.title ?? "Voice Operations Record")}</h1><p class="status ${escapeHtml41(record.status)}">${escapeHtml41(record.status)}</p><div class="hero-actions"><a href="#transcript">Transcript</a><a href="#provider-decisions">Provider decisions</a><a href="#incident-handoff">Incident handoff</a>${incidentLink}</div><section class="grid"><div class="card"><span>Events</span><strong>${String(record.summary.eventCount)}</strong></div><div class="card"><span>Turns</span><strong>${String(record.summary.turnCount)}</strong></div><div class="card"><span>Errors</span><strong>${String(record.summary.errorCount)}</strong></div><div class="card"><span>Duration</span><strong>${formatMs4(record.summary.callDurationMs)}</strong></div><div class="card"><span>Audit</span><strong>${String(record.audit?.total ?? 0)}</strong></div><div class="card"><span>Reviews</span><strong>${String(record.reviews?.total ?? 0)}</strong></div><div class="card"><span>Tasks</span><strong>${String(record.tasks?.total ?? 0)}</strong></div><div class="card"><span>Integrations</span><strong>${String(record.integrationEvents?.total ?? 0)}</strong></div></section><section class="two-column"><div><h2 id="transcript">Transcript</h2><ul>${transcript}</ul></div><div><h2 id="provider-decisions">Provider Decisions</h2><ul>${providerDecisions}</ul></div></section><section id="incident-handoff"><h2>Copyable Incident Handoff</h2><p class="muted">Paste this into Slack, Linear, Zendesk, or an incident review. ${incidentLink}</p><pre><code>${incidentMarkdown}</code></pre></section><section class="primitive"><p class="eyebrow">Copy into your app</p><h2><code>createVoiceOperationsRecordRoutes(...)</code> gives every call one debuggable object</h2><p class="muted">Use this as the support/debug payload across traces, provider routing, tools, handoffs, audit, latency, replay, reviews, tasks, and webhook delivery.</p><pre><code>${snippet}</code></pre></section><section><h2>Provider Summary</h2><div class="grid">${providers}</div></section><section><h2>Handoffs</h2><ul>${handoffs}</ul></section><section><h2>Tools</h2><ul>${tools}</ul></section><section><h2>Reviews</h2><ul>${reviews}</ul></section><section><h2>Tasks</h2><ul>${tasks}</ul></section><section><h2>Integration Events</h2><ul>${integrationEvents}</ul></section></main></body></html>`;
24394
24417
  };
24395
24418
  var createVoiceOperationsRecordRoutes = (options) => {
24396
24419
  const path = options.path ?? "/api/voice-operations/:sessionId";
@@ -24432,6 +24455,7 @@ var createVoiceOperationsRecordRoutes = (options) => {
24432
24455
  routes.get(htmlPath, async ({ params }) => {
24433
24456
  const record = await buildRecord(getSessionId(params));
24434
24457
  const body = await (options.render ?? ((input) => renderVoiceOperationsRecordHTML(input, {
24458
+ incidentHref: incidentHtmlPath ? resolveRoutePath(incidentHtmlPath, input.sessionId) : undefined,
24435
24459
  title: options.title
24436
24460
  })))(record);
24437
24461
  return new Response(body, {
@@ -125,6 +125,7 @@ export type VoiceOperationsRecordRoutesOptions = Omit<VoiceOperationsRecordOptio
125
125
  export declare const buildVoiceOperationsRecord: (options: VoiceOperationsRecordOptions) => Promise<VoiceOperationsRecord>;
126
126
  export declare const renderVoiceOperationsRecordIncidentMarkdown: (record: VoiceOperationsRecord) => string;
127
127
  export declare const renderVoiceOperationsRecordHTML: (record: VoiceOperationsRecord, options?: {
128
+ incidentHref?: string;
128
129
  title?: string;
129
130
  }) => string;
130
131
  export declare const createVoiceOperationsRecordRoutes: (options: VoiceOperationsRecordRoutesOptions) => Elysia<"", {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@absolutejs/voice",
3
- "version": "0.0.22-beta.200",
3
+ "version": "0.0.22-beta.202",
4
4
  "description": "Voice primitives and Elysia plugin for AbsoluteJS",
5
5
  "repository": {
6
6
  "type": "git",