@absolutejs/voice 0.0.22-beta.297 → 0.0.22-beta.299

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
@@ -2807,7 +2807,7 @@ app.use(
2807
2807
  );
2808
2808
  ```
2809
2809
 
2810
- `createVoiceOperationsRecordRoutes(...)` links the call/session timeline, transcript, replay, provider decisions, tools, handoffs, guardrail decisions, audit, reviews, ops tasks, integration events, and sink delivery attempts into one debuggable object. Provider decisions include both older provider-routing events and explicit `provider.decision` traces, so the call log can show the surface, selected provider, fallback provider, and human-readable reason for each runtime choice. Use `/voice-operations/:sessionId` as the first place to investigate failed calls, blocked assistant output, blocked tool payloads, 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, including provider-decision summaries and an `assistant.guardrail` blocked-stage summary when those trace events exist.
2810
+ `createVoiceOperationsRecordRoutes(...)` links the call/session timeline, transcript, replay, provider decisions, tools, handoffs, guardrail decisions, audit, reviews, ops tasks, integration events, and sink delivery attempts into one debuggable object. Provider decisions include both older provider-routing events and explicit `provider.decision` traces, so the call log can show the surface, selected provider, fallback provider, recovery status, fallback/degradation counts, and human-readable reason for each runtime choice. Use `/voice-operations/:sessionId` as the first place to investigate failed calls, blocked assistant output, blocked tool payloads, 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, including provider-decision recovery summaries and an `assistant.guardrail` blocked-stage summary when those trace events exist.
2811
2811
 
2812
2812
  Use `evaluateVoiceOperationsRecordGuardrails(...)` when a proof pack or deploy gate needs JSON evidence that guardrails actually ran, blocked the expected stages, and produced named proofs/rule IDs. Use `assertVoiceOperationsRecordGuardrails(...)` in tests or smoke scripts when missing guardrail evidence should fail fast:
2813
2813
 
@@ -4116,7 +4116,7 @@ app.use(
4116
4116
  );
4117
4117
  ```
4118
4118
 
4119
- The routes expose JSON at `/api/voice/provider-decisions`, HTML at `/voice/provider-decisions`, and Markdown at `/voice/provider-decisions.md`. Use this next to provider SLOs when a customer asks not just "is fallback working?" but "why did the system choose this provider for this call?".
4119
+ The routes expose JSON at `/api/voice/provider-decisions`, HTML at `/voice/provider-decisions`, and Markdown at `/voice/provider-decisions.md`. Use this next to provider SLOs when a customer asks not just "is fallback working?" but "why did the system choose this provider for this call?". For proof packs, gate fallback and degradation directly with `minFallbacks`, `minDegraded`, `requiredStatuses`, `requiredFallbackProviders`, and `requiredReasonIncludes` so deploy evidence fails when fallback behavior is missing or unexplained.
4120
4120
 
4121
4121
  Use `createVoiceProviderContractMatrixPreset(...)` when you want readiness proof for the whole provider stack without hand-writing every LLM, STT, and TTS contract row. The preset stays primitive: you still own provider lists, selected providers, latency budgets, env, capabilities, and route mounting.
4122
4122
 
package/dist/index.js CHANGED
@@ -13146,7 +13146,7 @@ var uniqueSorted = (values) => [
13146
13146
  ].sort();
13147
13147
  var createVoiceProviderDecisionTraceEvent = (input) => {
13148
13148
  const surface = input.surface ?? surfaceForKind(input.kind);
13149
- const reason = input.reason ?? (input.status === "fallback" ? `Fallback from ${input.provider} to ${input.fallbackProvider ?? input.selectedProvider ?? "next provider"}.` : input.status === "error" ? `Provider ${input.provider} errored before recovery.` : input.status === "skipped" ? `Provider ${input.provider} was skipped by policy.` : `Provider ${input.selectedProvider ?? input.provider} selected by policy.`);
13149
+ const reason = input.reason ?? (input.status === "degraded" ? `Provider ${input.provider} degraded to ${input.fallbackProvider ?? input.selectedProvider ?? "lower-fidelity fallback"}.` : input.status === "fallback" ? `Fallback from ${input.provider} to ${input.fallbackProvider ?? input.selectedProvider ?? "next provider"}.` : input.status === "error" ? `Provider ${input.provider} errored before recovery.` : input.status === "skipped" ? `Provider ${input.provider} was skipped by policy.` : `Provider ${input.selectedProvider ?? input.provider} selected by policy.`);
13150
13150
  return {
13151
13151
  at: input.at ?? Date.now(),
13152
13152
  payload: {
@@ -13170,7 +13170,7 @@ var listVoiceProviderDecisionTraces = (events) => {
13170
13170
  const provider = getString8(event.payload.provider);
13171
13171
  const status = getString8(event.payload.status);
13172
13172
  const surface = getString8(event.payload.surface);
13173
- if (!provider || !surface || status !== "error" && status !== "fallback" && status !== "selected" && status !== "skipped" && status !== "success") {
13173
+ if (!provider || !surface || status !== "error" && status !== "fallback" && status !== "degraded" && status !== "selected" && status !== "skipped" && status !== "success") {
13174
13174
  return;
13175
13175
  }
13176
13176
  return {
@@ -13237,6 +13237,18 @@ var buildVoiceProviderDecisionTraceReport = async (options) => {
13237
13237
  });
13238
13238
  }
13239
13239
  }
13240
+ const fallbackCount = decisions.filter((decision) => decision.status === "fallback").length;
13241
+ const degradedCount = decisions.filter((decision) => decision.status === "degraded").length;
13242
+ const statuses = new Set(decisions.map((decision) => decision.status));
13243
+ const providers = uniqueSorted(decisions.flatMap((decision) => [
13244
+ decision.provider,
13245
+ decision.selectedProvider,
13246
+ decision.fallbackProvider
13247
+ ]));
13248
+ const fallbackProviders = uniqueSorted(decisions.flatMap((decision) => [
13249
+ decision.fallbackProvider,
13250
+ decision.status === "fallback" || decision.status === "degraded" ? decision.selectedProvider : undefined
13251
+ ]));
13240
13252
  if (options.minDecisions !== undefined && decisions.length < options.minDecisions) {
13241
13253
  issues.push({
13242
13254
  code: "voice.provider_decision_trace.min_decisions",
@@ -13244,9 +13256,60 @@ var buildVoiceProviderDecisionTraceReport = async (options) => {
13244
13256
  status: "fail"
13245
13257
  });
13246
13258
  }
13259
+ if (options.minFallbacks !== undefined && fallbackCount < options.minFallbacks) {
13260
+ issues.push({
13261
+ code: "voice.provider_decision_trace.min_fallbacks",
13262
+ message: `Found ${String(fallbackCount)} provider fallback trace(s); expected at least ${String(options.minFallbacks)}.`,
13263
+ status: "fail"
13264
+ });
13265
+ }
13266
+ if (options.minDegraded !== undefined && degradedCount < options.minDegraded) {
13267
+ issues.push({
13268
+ code: "voice.provider_decision_trace.min_degraded",
13269
+ message: `Found ${String(degradedCount)} provider degradation trace(s); expected at least ${String(options.minDegraded)}.`,
13270
+ status: "fail"
13271
+ });
13272
+ }
13273
+ for (const status of options.requiredStatuses ?? []) {
13274
+ if (!statuses.has(status)) {
13275
+ issues.push({
13276
+ code: "voice.provider_decision_trace.status_missing",
13277
+ message: `Missing provider decision status: ${status}.`,
13278
+ status: "fail"
13279
+ });
13280
+ }
13281
+ }
13282
+ for (const provider of options.requiredProviders ?? []) {
13283
+ if (!providers.includes(provider)) {
13284
+ issues.push({
13285
+ code: "voice.provider_decision_trace.provider_missing",
13286
+ message: `Missing provider decision provider: ${provider}.`,
13287
+ status: "fail"
13288
+ });
13289
+ }
13290
+ }
13291
+ for (const provider of options.requiredFallbackProviders ?? []) {
13292
+ if (!fallbackProviders.includes(provider)) {
13293
+ issues.push({
13294
+ code: "voice.provider_decision_trace.fallback_provider_missing",
13295
+ message: `Missing provider decision fallback provider: ${provider}.`,
13296
+ status: "fail"
13297
+ });
13298
+ }
13299
+ }
13300
+ for (const phrase of options.requiredReasonIncludes ?? []) {
13301
+ if (!decisions.some((decision) => decision.reason.includes(phrase))) {
13302
+ issues.push({
13303
+ code: "voice.provider_decision_trace.reason_missing",
13304
+ message: `Missing provider decision reason containing: ${phrase}.`,
13305
+ status: "fail"
13306
+ });
13307
+ }
13308
+ }
13247
13309
  const surfaceReports = [...surfaces.entries()].sort(([left], [right]) => left.localeCompare(right)).map(([surface, surfaceDecisions]) => {
13248
13310
  const surfaceIssues = issues.filter((issue) => issue.surface === surface);
13249
13311
  return {
13312
+ degraded: surfaceDecisions.filter((decision) => decision.status === "degraded").length,
13250
13313
  decisions: surfaceDecisions.length,
13251
13314
  errors: surfaceDecisions.filter((decision) => decision.status === "error").length,
13252
13315
  fallbacks: surfaceDecisions.filter((decision) => decision.status === "fallback").length,
@@ -13263,20 +13326,16 @@ var buildVoiceProviderDecisionTraceReport = async (options) => {
13263
13326
  surface
13264
13327
  };
13265
13328
  });
13266
- const providers = uniqueSorted(decisions.flatMap((decision) => [
13267
- decision.provider,
13268
- decision.selectedProvider,
13269
- decision.fallbackProvider
13270
- ]));
13271
13329
  return {
13272
13330
  checkedAt: now,
13273
13331
  decisions,
13274
13332
  issues,
13275
13333
  status: reportStatus(issues),
13276
13334
  summary: {
13335
+ degraded: degradedCount,
13277
13336
  decisions: decisions.length,
13278
13337
  errors: decisions.filter((decision) => decision.status === "error").length,
13279
- fallbacks: decisions.filter((decision) => decision.status === "fallback").length,
13338
+ fallbacks: fallbackCount,
13280
13339
  providers: providers.length,
13281
13340
  selected: decisions.filter((decision) => decision.status === "selected" || decision.status === "success").length,
13282
13341
  surfaces: surfaces.size
@@ -13291,11 +13350,12 @@ var renderVoiceProviderDecisionTraceMarkdown = (report) => [
13291
13350
  `Decisions: ${String(report.summary.decisions)}`,
13292
13351
  `Providers: ${String(report.summary.providers)}`,
13293
13352
  `Fallbacks: ${String(report.summary.fallbacks)}`,
13353
+ `Degraded: ${String(report.summary.degraded)}`,
13294
13354
  `Errors: ${String(report.summary.errors)}`,
13295
13355
  "",
13296
- "| Surface | Status | Decisions | Selected | Fallbacks | Errors | Providers |",
13297
- "| --- | --- | ---: | ---: | ---: | ---: | --- |",
13298
- ...report.surfaces.map((surface) => `| ${surface.surface} | ${surface.status} | ${String(surface.decisions)} | ${String(surface.selected)} | ${String(surface.fallbacks)} | ${String(surface.errors)} | ${surface.providers.join(", ")} |`),
13356
+ "| Surface | Status | Decisions | Selected | Fallbacks | Degraded | Errors | Providers |",
13357
+ "| --- | --- | ---: | ---: | ---: | ---: | ---: | --- |",
13358
+ ...report.surfaces.map((surface) => `| ${surface.surface} | ${surface.status} | ${String(surface.decisions)} | ${String(surface.selected)} | ${String(surface.fallbacks)} | ${String(surface.degraded)} | ${String(surface.errors)} | ${surface.providers.join(", ")} |`),
13299
13359
  "",
13300
13360
  ...report.issues.map((issue) => `- ${issue.status}: ${issue.message}`)
13301
13361
  ].join(`
@@ -13326,12 +13386,13 @@ code{background:#e2e8f0;border-radius:8px;padding:2px 6px}
13326
13386
  <article class="card"><strong>${String(report.summary.decisions)}</strong><p>decisions</p></article>
13327
13387
  <article class="card"><strong>${String(report.summary.providers)}</strong><p>providers</p></article>
13328
13388
  <article class="card"><strong>${String(report.summary.fallbacks)}</strong><p>fallbacks</p></article>
13389
+ <article class="card"><strong>${String(report.summary.degraded)}</strong><p>degraded</p></article>
13329
13390
  <article class="card"><strong>${String(report.summary.errors)}</strong><p>errors</p></article>
13330
13391
  </section>
13331
13392
  <section class="surfaces">
13332
13393
  ${report.surfaces.map((surface) => `<article class="surface">
13333
13394
  <header><strong>${escapeHtml17(surface.surface)}</strong> <span class="status ${surface.status}">${escapeHtml17(surface.status)}</span></header>
13334
- <p>${String(surface.decisions)} decision(s), ${String(surface.fallbacks)} fallback(s), ${String(surface.errors)} error(s).</p>
13395
+ <p>${String(surface.decisions)} decision(s), ${String(surface.fallbacks)} fallback(s), ${String(surface.degraded)} degraded decision(s), ${String(surface.errors)} error(s).</p>
13335
13396
  <p class="muted">Providers: ${escapeHtml17(surface.providers.join(", ") || "none")}</p>
13336
13397
  <p>${surface.reasons.map((reason) => `<code>${escapeHtml17(reason)}</code>`).join(" ")}</p>
13337
13398
  </article>`).join(`
@@ -26263,6 +26324,29 @@ var toProviderDecision = (event) => {
26263
26324
  turnId: event.turnId
26264
26325
  };
26265
26326
  };
26327
+ var summarizeProviderDecisions = (decisions) => {
26328
+ const providers = uniqueSorted8(decisions.flatMap((decision) => [
26329
+ decision.provider,
26330
+ decision.selectedProvider,
26331
+ decision.fallbackProvider
26332
+ ]));
26333
+ const surfaces = uniqueSorted8(decisions.map((decision) => decision.surface));
26334
+ const degraded = decisions.filter((decision) => decision.status === "degraded").length;
26335
+ const errors = decisions.filter((decision) => decision.status === "error").length;
26336
+ const fallbacks = decisions.filter((decision) => decision.status === "fallback").length;
26337
+ const selected = decisions.filter((decision) => decision.status === "selected" || decision.status === "success").length;
26338
+ const recoveryStatus = errors > 0 ? "failed" : degraded > 0 ? "degraded" : fallbacks > 0 ? "recovered" : selected > 0 ? "selected" : "none";
26339
+ return {
26340
+ degraded,
26341
+ errors,
26342
+ fallbacks,
26343
+ providers,
26344
+ recoveryStatus,
26345
+ selected,
26346
+ surfaces,
26347
+ total: decisions.length
26348
+ };
26349
+ };
26266
26350
  var buildTranscript = (replay) => replay.turns.map((turn) => ({
26267
26351
  assistantReplies: turn.assistantReplies,
26268
26352
  committedText: turn.committedText,
@@ -26305,6 +26389,7 @@ var buildVoiceOperationsRecord = async (options) => {
26305
26389
  const taskIds = new Set(tasks?.map((task) => task.id) ?? []);
26306
26390
  const integrationEvents = options.integrationEvents ? (await options.integrationEvents.list()).filter((event) => hasPayloadValue(event.payload, "sessionId", new Set([options.sessionId])) || hasPayloadValue(event.payload, "reviewId", reviewIds) || hasPayloadValue(event.payload, "taskId", taskIds)) : undefined;
26307
26391
  const sinkDeliveries = integrationEvents?.reduce((total, event) => total + Object.keys(event.sinkDeliveries ?? {}).length, 0) ?? 0;
26392
+ const providerDecisions = traceEvents.map(toProviderDecision).filter((decision) => decision !== undefined);
26308
26393
  return {
26309
26394
  audit: auditEvents ? {
26310
26395
  error: countOutcome(auditEvents, "error"),
@@ -26326,7 +26411,8 @@ var buildVoiceOperationsRecord = async (options) => {
26326
26411
  total: integrationEvents.length
26327
26412
  } : undefined,
26328
26413
  outcome: resolveOutcome4(traceEvents),
26329
- providerDecisions: traceEvents.map(toProviderDecision).filter((decision) => decision !== undefined),
26414
+ providerDecisions,
26415
+ providerDecisionSummary: summarizeProviderDecisions(providerDecisions),
26330
26416
  providers: timelineSession?.providers ?? [],
26331
26417
  replay,
26332
26418
  reviews: reviews ? {
@@ -26448,6 +26534,14 @@ var renderVoiceOperationsRecordIncidentMarkdown = (record) => {
26448
26534
  ].filter((part) => typeof part === "string");
26449
26535
  return `- ${provider}: ${parts.join("; ") || "decision recorded"}`;
26450
26536
  }) : ["- none recorded"];
26537
+ const providerDecisionSummary = record.providerDecisionSummary;
26538
+ const providerRecoveryLine = [
26539
+ `status=${providerDecisionSummary.recoveryStatus}`,
26540
+ `selected=${String(providerDecisionSummary.selected)}`,
26541
+ `fallbacks=${String(providerDecisionSummary.fallbacks)}`,
26542
+ `degraded=${String(providerDecisionSummary.degraded)}`,
26543
+ `errors=${String(providerDecisionSummary.errors)}`
26544
+ ].join("; ");
26451
26545
  return [
26452
26546
  `# Voice incident handoff: ${record.sessionId}`,
26453
26547
  "",
@@ -26460,6 +26554,7 @@ var renderVoiceOperationsRecordIncidentMarkdown = (record) => {
26460
26554
  `- Open tasks: ${openTasks.join("; ") || "none"}`,
26461
26555
  `- Top errors: ${topErrors.join("; ") || "none"}`,
26462
26556
  `- Guardrails: ${String(record.guardrails.blocked)} blocked / ${String(record.guardrails.warned)} warned / ${String(record.guardrails.total)} decisions`,
26557
+ `- Provider recovery: ${providerRecoveryLine}`,
26463
26558
  "",
26464
26559
  "## Provider decisions",
26465
26560
  "",
@@ -26497,6 +26592,7 @@ var renderVoiceOperationsRecordHTML = (record, options = {}) => {
26497
26592
  const providers = record.providers.length ? record.providers.map((provider) => `<article><strong>${escapeHtml42(provider.provider)}</strong><span>${String(provider.eventCount)} events</span><span>${formatMs5(provider.averageElapsedMs)} avg</span><span>${String(provider.errorCount)} errors</span></article>`).join("") : '<p class="muted">No provider events recorded.</p>';
26498
26593
  const transcript = record.transcript.length ? record.transcript.map((turn) => `<li><strong>${escapeHtml42(turn.id)}</strong>${turn.committedText ? `<p><span class="label">Caller</span>${escapeHtml42(turn.committedText)}</p>` : ""}${turn.assistantReplies.map((reply) => `<p><span class="label">Assistant</span>${escapeHtml42(reply)}</p>`).join("")}${turn.errors.map((error) => `<p class="error"><span class="label">Error</span>${escapeHtml42(error)}</p>`).join("")}</li>`).join("") : "<li>No transcript turns recorded.</li>";
26499
26594
  const providerDecisions = record.providerDecisions.length ? record.providerDecisions.map((decision) => `<li><strong>${escapeHtml42(decision.provider ?? decision.selectedProvider ?? decision.fallbackProvider ?? "provider")}</strong> <span>${escapeHtml42(decision.status ?? decision.type)}</span> ${formatMs5(decision.elapsedMs)}${decision.surface ? `<p><span class="label">Surface</span>${escapeHtml42(decision.surface)}</p>` : ""}${decision.kind ? `<p><span class="label">Kind</span>${escapeHtml42(decision.kind)}</p>` : ""}${decision.selectedProvider ? `<p>Selected: ${escapeHtml42(decision.selectedProvider)}</p>` : ""}${decision.fallbackProvider ? `<p>Fallback: ${escapeHtml42(decision.fallbackProvider)}</p>` : ""}${decision.error ? `<p class="error">${escapeHtml42(decision.error)}</p>` : ""}${decision.reason ? `<p>${escapeHtml42(decision.reason)}</p>` : ""}</li>`).join("") : "<li>No provider decisions recorded.</li>";
26595
+ const providerDecisionSummary = record.providerDecisionSummary;
26500
26596
  const handoffs = record.handoffs.length ? record.handoffs.map((handoff) => `<li><strong>${escapeHtml42(handoff.fromAgentId ?? "unknown")}</strong> to <strong>${escapeHtml42(handoff.targetAgentId ?? "unknown")}</strong> <span>${escapeHtml42(handoff.status ?? "")}</span><p>${escapeHtml42(handoff.summary ?? handoff.reason ?? "")}</p></li>`).join("") : "<li>No agent handoffs recorded.</li>";
26501
26597
  const tools = record.tools.length ? record.tools.map((tool) => `<li><strong>${escapeHtml42(tool.toolName ?? "tool")}</strong> <span>${escapeHtml42(tool.status ?? "")}</span> ${formatMs5(tool.elapsedMs)} ${tool.error ? `<p>${escapeHtml42(tool.error)}</p>` : ""}</li>`).join("") : "<li>No tool calls recorded.</li>";
26502
26598
  const reviews = record.reviews?.reviews.length ? record.reviews.reviews.map((review) => `<li><strong>${escapeHtml42(review.title)}</strong> <span>${escapeHtml42(review.summary.outcome ?? "")}</span><p>${escapeHtml42(review.postCall?.summary ?? review.transcript.actual)}</p></li>`).join("") : "<li>No call reviews recorded.</li>";
@@ -26522,7 +26618,7 @@ var renderVoiceOperationsRecordHTML = (record, options = {}) => {
26522
26618
  );`);
26523
26619
  const incidentMarkdown = escapeHtml42(renderVoiceOperationsRecordIncidentMarkdown(record));
26524
26620
  const incidentLink = options.incidentHref ? `<a href="${escapeHtml42(options.incidentHref)}">Download incident.md</a>` : "";
26525
- return `<!doctype html><html lang="en"><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /><title>${escapeHtml42(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>${escapeHtml42(options.title ?? "Voice Operations Record")}</h1><p class="status ${escapeHtml42(record.status)}">${escapeHtml42(record.status)}</p><div class="hero-actions"><a href="#transcript">Transcript</a><a href="#provider-decisions">Provider decisions</a><a href="#guardrails">Guardrails</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>${formatMs5(record.summary.callDurationMs)}</strong></div><div class="card"><span>Guardrails</span><strong>${String(record.guardrails.blocked)}</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="guardrails"><h2>Guardrail Evidence</h2><p class="muted">Live <code>assistant.guardrail</code> decisions attached to this session.</p><ul>${guardrails}</ul></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, guardrails, 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>`;
26621
+ return `<!doctype html><html lang="en"><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /><title>${escapeHtml42(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>${escapeHtml42(options.title ?? "Voice Operations Record")}</h1><p class="status ${escapeHtml42(record.status)}">${escapeHtml42(record.status)}</p><div class="hero-actions"><a href="#transcript">Transcript</a><a href="#provider-decisions">Provider decisions</a><a href="#guardrails">Guardrails</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>${formatMs5(record.summary.callDurationMs)}</strong></div><div class="card"><span>Provider recovery</span><strong>${escapeHtml42(providerDecisionSummary.recoveryStatus)}</strong><span>${String(providerDecisionSummary.fallbacks)} fallback / ${String(providerDecisionSummary.degraded)} degraded / ${String(providerDecisionSummary.errors)} errors</span></div><div class="card"><span>Guardrails</span><strong>${String(record.guardrails.blocked)}</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="guardrails"><h2>Guardrail Evidence</h2><p class="muted">Live <code>assistant.guardrail</code> decisions attached to this session.</p><ul>${guardrails}</ul></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, guardrails, 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>`;
26526
26622
  };
26527
26623
  var createVoiceOperationsRecordRoutes = (options) => {
26528
26624
  const path = options.path ?? "/api/voice-operations/:sessionId";
@@ -54,6 +54,17 @@ export type VoiceOperationsRecordProviderDecision = {
54
54
  type: StoredVoiceTraceEvent['type'];
55
55
  turnId?: string;
56
56
  };
57
+ export type VoiceOperationsRecordProviderDecisionRecoveryStatus = 'degraded' | 'failed' | 'none' | 'recovered' | 'selected';
58
+ export type VoiceOperationsRecordProviderDecisionSummary = {
59
+ degraded: number;
60
+ errors: number;
61
+ fallbacks: number;
62
+ providers: string[];
63
+ recoveryStatus: VoiceOperationsRecordProviderDecisionRecoveryStatus;
64
+ selected: number;
65
+ surfaces: string[];
66
+ total: number;
67
+ };
57
68
  export type VoiceOperationsRecordAuditSummary = {
58
69
  error: number;
59
70
  events: StoredVoiceAuditEvent[];
@@ -139,6 +150,7 @@ export type VoiceOperationsRecord = {
139
150
  integrationEvents?: VoiceOperationsRecordIntegrationEventSummary;
140
151
  outcome: VoiceOperationsRecordOutcome;
141
152
  providerDecisions: VoiceOperationsRecordProviderDecision[];
153
+ providerDecisionSummary: VoiceOperationsRecordProviderDecisionSummary;
142
154
  providers: VoiceTraceTimelineProviderSummary[];
143
155
  replay: VoiceSessionReplay;
144
156
  reviews?: VoiceOperationsRecordReviewSummary;
@@ -1,7 +1,7 @@
1
1
  import { Elysia } from 'elysia';
2
2
  import { type VoiceRoutingEventKind } from './resilienceRoutes';
3
3
  import type { StoredVoiceTraceEvent, VoiceTraceEvent, VoiceTraceEventStore } from './trace';
4
- export type VoiceProviderDecisionStatus = 'error' | 'fallback' | 'selected' | 'skipped' | 'success';
4
+ export type VoiceProviderDecisionStatus = 'degraded' | 'error' | 'fallback' | 'selected' | 'skipped' | 'success';
5
5
  export type VoiceProviderDecisionTrace = {
6
6
  at: number;
7
7
  elapsedMs?: number;
@@ -31,6 +31,7 @@ export type VoiceProviderDecisionTraceIssue = {
31
31
  surface?: string;
32
32
  };
33
33
  export type VoiceProviderDecisionSurfaceReport = {
34
+ degraded: number;
34
35
  decisions: number;
35
36
  errors: number;
36
37
  fallbacks: number;
@@ -48,6 +49,7 @@ export type VoiceProviderDecisionTraceReport = {
48
49
  issues: VoiceProviderDecisionTraceIssue[];
49
50
  status: 'fail' | 'pass' | 'warn';
50
51
  summary: {
52
+ degraded: number;
51
53
  decisions: number;
52
54
  errors: number;
53
55
  fallbacks: number;
@@ -60,9 +62,15 @@ export type VoiceProviderDecisionTraceReport = {
60
62
  export type VoiceProviderDecisionTraceReportOptions = {
61
63
  events?: StoredVoiceTraceEvent[] | VoiceProviderDecisionTrace[];
62
64
  maxAgeMs?: number;
65
+ minDegraded?: number;
63
66
  minDecisions?: number;
67
+ minFallbacks?: number;
64
68
  now?: number;
69
+ requiredFallbackProviders?: readonly string[];
70
+ requiredProviders?: readonly string[];
71
+ requiredReasonIncludes?: readonly string[];
65
72
  requiredSurfaces?: readonly string[];
73
+ requiredStatuses?: readonly VoiceProviderDecisionStatus[];
66
74
  sessionId?: string;
67
75
  store?: VoiceTraceEventStore;
68
76
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@absolutejs/voice",
3
- "version": "0.0.22-beta.297",
3
+ "version": "0.0.22-beta.299",
4
4
  "description": "Voice primitives and Elysia plugin for AbsoluteJS",
5
5
  "repository": {
6
6
  "type": "git",