@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 +2 -2
- package/dist/index.js +110 -14
- package/dist/operationsRecord.d.ts +12 -0
- package/dist/providerDecisionTraces.d.ts +9 -1
- package/package.json +1 -1
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:
|
|
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
|
|
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
|
};
|