@absolutejs/voice 0.0.22-beta.247 → 0.0.22-beta.249

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
@@ -2792,7 +2792,18 @@ app.use(
2792
2792
  );
2793
2793
  ```
2794
2794
 
2795
- `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.
2795
+ `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. 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 an `assistant.guardrail` blocked-stage summary when guardrail trace events exist.
2796
+
2797
+ 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:
2798
+
2799
+ ```ts
2800
+ const report = assertVoiceOperationsRecordGuardrails(record, {
2801
+ minBlocked: 1,
2802
+ proofs: ['live-guardrails-runtime'],
2803
+ ruleIds: ['support.no-medical-advice'],
2804
+ stages: ['assistant-output', 'tool-input']
2805
+ });
2806
+ ```
2796
2807
 
2797
2808
  Most proof surfaces can link to the same record by passing an operations-record URL template such as `/voice-operations/:sessionId`. Use that template anywhere a report emits session-level failures: production readiness, ops recovery, trace timelines, session lists, reviews, campaign attempts, eval reports, simulation-suite actions, tool-contract cases, and outcome-contract matched sessions. The goal is that no operator has to guess which trace, review, task, or delivery queue belongs to the failing call.
2798
2809
 
package/dist/index.d.ts CHANGED
@@ -56,7 +56,7 @@ export { buildVoiceProductionReadinessGate, buildVoiceProductionReadinessReport,
56
56
  export { createVoiceReadinessProfile, recommendVoiceReadinessProfile } from './readinessProfiles';
57
57
  export { buildVoiceProviderContractMatrix, createVoiceProviderContractMatrixHTMLHandler, createVoiceProviderContractMatrixJSONHandler, createVoiceProviderContractMatrixPreset, createVoiceProviderContractMatrixRoutes, evaluateVoiceProviderStackGaps, renderVoiceProviderContractMatrixHTML, recommendVoiceProviderStack } from './providerStackRecommendations';
58
58
  export { buildVoiceOpsConsoleReport, createVoiceOpsConsoleRoutes, renderVoiceOpsConsoleHTML } from './opsConsoleRoutes';
59
- export { buildVoiceOperationsRecord, createVoiceOperationsRecordRoutes, renderVoiceOperationsRecordHTML, renderVoiceOperationsRecordIncidentMarkdown } from './operationsRecord';
59
+ export { assertVoiceOperationsRecordGuardrails, buildVoiceOperationsRecord, createVoiceOperationsRecordRoutes, evaluateVoiceOperationsRecordGuardrails, renderVoiceOperationsRecordGuardrailMarkdown, renderVoiceOperationsRecordHTML, renderVoiceOperationsRecordIncidentMarkdown } from './operationsRecord';
60
60
  export { assertVoiceObservabilityExportRecord, buildVoiceObservabilityArtifactIndex, buildVoiceObservabilityExportDeliveryHistory, buildVoiceObservabilityExportReplayReport, buildVoiceObservabilityExport, assertVoiceObservabilityExportSchema, createVoiceObservabilityExportSchema, createVoiceFileObservabilityExportDeliveryReceiptStore, createVoiceMemoryObservabilityExportDeliveryReceiptStore, createVoiceObservabilityExportRoutes, createVoiceObservabilityExportReplayRoutes, deliverVoiceObservabilityExport, loadVoiceObservabilityExportReplaySource, replayVoiceObservabilityExport, renderVoiceObservabilityExportReplayHTML, renderVoiceObservabilityExportMarkdown, validateVoiceObservabilityExportRecord, voiceObservabilityExportSchemaId, voiceObservabilityExportSchemaVersion } from './observabilityExport';
61
61
  export { buildVoiceOpsRecoveryReadinessCheck, buildVoiceOpsRecoveryReport, createVoiceOpsRecoveryRoutes, renderVoiceOpsRecoveryHTML, renderVoiceOpsRecoveryMarkdown } from './opsRecovery';
62
62
  export { buildVoiceIncidentBundle, createStoredVoiceIncidentBundleArtifact, createVoiceIncidentBundleRoutes, createVoiceMemoryIncidentBundleStore, pruneVoiceIncidentBundleArtifacts, saveVoiceIncidentBundleArtifact } from './incidentBundle';
@@ -123,7 +123,7 @@ export type { VoiceOpsStatus, VoiceOpsStatusLink, VoiceOpsStatusOptions, VoiceOp
123
123
  export type { VoiceProductionReadinessAction, VoiceProductionReadinessAuditOptions, VoiceProductionReadinessAuditRequirement, VoiceProductionReadinessAuditSummary, VoiceProductionReadinessCheck, VoiceProductionReadinessGateIssue, VoiceProductionReadinessGateOptions, VoiceProductionReadinessGateProfile, VoiceProductionReadinessGateProfileSurface, VoiceProductionReadinessGateReport, VoiceProductionReadinessOpsActionHistoryOptions, VoiceProductionReadinessOpsActionHistorySummary, VoiceProductionReadinessOperationsRecordLink, VoiceProductionReadinessOperationsRecordLinks, VoiceProductionReadinessProfileExplanation, VoiceProductionReadinessProfileSurface, VoiceProductionReadinessProofSource, VoiceProductionReadinessReport, VoiceProductionReadinessRoutesOptions, VoiceProductionReadinessTraceDeliverySummary, VoiceProductionReadinessAuditDeliveryOptions, VoiceProductionReadinessAuditDeliverySummary, VoiceProductionReadinessTraceDeliveryOptions, VoiceProductionReadinessStatus } from './productionReadiness';
124
124
  export type { VoiceReadinessProfileName, VoiceReadinessProfileOptions, VoiceReadinessProfileRecommendation, VoiceReadinessProfileRecommendationScore, VoiceReadinessProfileRoutesOptions } from './readinessProfiles';
125
125
  export type { VoiceProviderStackChoice, VoiceProviderStackCapabilities, VoiceProviderStackCapabilityGap, VoiceProviderStackCapabilityGapInput, VoiceProviderStackCapabilityGapReport, VoiceProviderContractCheck, VoiceProviderContractCheckStatus, VoiceProviderContractDefinition, VoiceProviderContractMatrixHandlerOptions, VoiceProviderContractMatrixHTMLHandlerOptions, VoiceProviderContractMatrixInput, VoiceProviderContractMatrixPresetOptions, VoiceProviderContractMatrixReport, VoiceProviderContractMatrixRoutesOptions, VoiceProviderContractMatrixRow, VoiceProviderStackInput, VoiceProviderStackKind, VoiceProviderStackRecommendation } from './providerStackRecommendations';
126
- export type { VoiceOperationsRecord, VoiceOperationsRecordAgentHandoff, VoiceOperationsRecordAuditSummary, VoiceOperationsRecordIntegrationEventSummary, VoiceOperationsRecordOptions, VoiceOperationsRecordOutcome, VoiceOperationsRecordProviderDecision, VoiceOperationsRecordReviewSummary, VoiceOperationsRecordRoutesOptions, VoiceOperationsRecordStatus, VoiceOperationsRecordTaskSummary, VoiceOperationsRecordTranscriptTurn, VoiceOperationsRecordTool } from './operationsRecord';
126
+ export type { VoiceOperationsRecord, VoiceOperationsRecordAgentHandoff, VoiceOperationsRecordAuditSummary, VoiceOperationsRecordGuardrailAssertionInput, VoiceOperationsRecordGuardrailAssertionReport, VoiceOperationsRecordGuardrailDecision, VoiceOperationsRecordGuardrailFinding, VoiceOperationsRecordGuardrailSummary, VoiceOperationsRecordIntegrationEventSummary, VoiceOperationsRecordOptions, VoiceOperationsRecordOutcome, VoiceOperationsRecordProviderDecision, VoiceOperationsRecordReviewSummary, VoiceOperationsRecordRoutesOptions, VoiceOperationsRecordStatus, VoiceOperationsRecordTaskSummary, VoiceOperationsRecordTranscriptTurn, VoiceOperationsRecordTool } from './operationsRecord';
127
127
  export type { VoiceObservabilityExportArtifact, VoiceObservabilityExportArtifactChecksum, VoiceObservabilityExportArtifactFreshness, VoiceObservabilityExportArtifactIndex, VoiceObservabilityExportArtifactIndexItem, VoiceObservabilityExportArtifactKind, VoiceObservabilityExportDeliverySummary, VoiceObservabilityExportDeliveryDestination, VoiceObservabilityExportDeliveryDestinationResult, VoiceObservabilityExportDeliveryHistory, VoiceObservabilityExportDeliveryOptions, VoiceObservabilityExportDeliveryReceipt, VoiceObservabilityExportDeliveryReceiptStore, VoiceObservabilityExportDeliveryReport, VoiceObservabilityExportEnvelope, VoiceObservabilityExportIssue, VoiceObservabilityExportIssueCode, VoiceObservabilityExportOptions, VoiceObservabilityExportIngestedRecordKind, VoiceObservabilityExportRedactionSummary, VoiceObservabilityExportRecordValidationOptions, VoiceObservabilityExportReplayIssue, VoiceObservabilityExportReplayIssueCode, VoiceObservabilityExportReplayRecords, VoiceObservabilityExportReplayReport, VoiceObservabilityExportReplayRoutesOptions, VoiceObservabilityExportReplaySource, VoiceObservabilityExportReport, VoiceObservabilityExportRoutesOptions, VoiceObservabilityExportSchema, VoiceObservabilityExportStatus, VoiceObservabilityExportValidationIssue, VoiceObservabilityExportValidationResult } from './observabilityExport';
128
128
  export type { VoiceOpsRecoveryFailedSession, VoiceOpsRecoveryInterventionSummary, VoiceOpsRecoveryIssue, VoiceOpsRecoveryIssueCode, VoiceOpsRecoveryLinks, VoiceOpsRecoveryProviderSummary, VoiceOpsRecoveryReport, VoiceOpsRecoveryReportOptions, VoiceOpsRecoveryRoutesOptions, VoiceOpsRecoveryStatus } from './opsRecovery';
129
129
  export type { StoredVoiceIncidentBundleArtifact, VoiceIncidentBundle, VoiceIncidentBundleArtifactOptions, VoiceIncidentBundleFormat, VoiceIncidentBundleOptions, VoiceIncidentBundleRetentionOptions, VoiceIncidentBundleRetentionReport, VoiceIncidentBundleRoutesOptions, VoiceIncidentBundleStore, VoiceIncidentBundleStoreFilter, VoiceIncidentBundleSummary } from './incidentBundle';
package/dist/index.js CHANGED
@@ -22872,6 +22872,7 @@ var createVoiceTraceTimelineRoutes = (options) => {
22872
22872
  // src/operationsRecord.ts
22873
22873
  var getString16 = (value) => typeof value === "string" ? value : undefined;
22874
22874
  var getNumber9 = (value) => typeof value === "number" && Number.isFinite(value) ? value : undefined;
22875
+ var getBoolean3 = (value) => typeof value === "boolean" ? value : undefined;
22875
22876
  var countOutcome = (events, outcome) => events.filter((event) => event.outcome === outcome).length;
22876
22877
  var matchesSessionScopedId = (id, sessionId) => id === sessionId || id.startsWith(`${sessionId}:`);
22877
22878
  var hasPayloadValue = (payload, key, values) => {
@@ -22879,6 +22880,15 @@ var hasPayloadValue = (payload, key, values) => {
22879
22880
  return typeof value === "string" && values.has(value);
22880
22881
  };
22881
22882
  var countIntegrationDeliveryStatus = (events, status) => events.filter((event) => event.deliveryStatus === status).length;
22883
+ var uniqueSorted = (values) => [
22884
+ ...new Set(values.filter((value) => typeof value === "string"))
22885
+ ].sort();
22886
+ var pushMissingValuesIssue = (input) => {
22887
+ const missing = (input.expected ?? []).filter((value) => !input.actual.includes(value));
22888
+ if (missing.length > 0) {
22889
+ input.issues.push(`Missing guardrail ${input.label}: ${missing.join(", ")}`);
22890
+ }
22891
+ };
22882
22892
  var resolveRoutePath = (path, sessionId) => path.replace(":sessionId", encodeURIComponent(sessionId));
22883
22893
  var toHandoff = (event) => ({
22884
22894
  at: event.at,
@@ -22899,6 +22909,44 @@ var toTool = (event) => ({
22899
22909
  toolName: getString16(event.payload.toolName),
22900
22910
  turnId: event.turnId
22901
22911
  });
22912
+ var toGuardrailFinding = (value) => {
22913
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
22914
+ return;
22915
+ }
22916
+ const record = value;
22917
+ return {
22918
+ action: getString16(record.action),
22919
+ label: getString16(record.label),
22920
+ ruleId: getString16(record.ruleId)
22921
+ };
22922
+ };
22923
+ var toGuardrailDecision = (event) => ({
22924
+ allowed: getBoolean3(event.payload.allowed),
22925
+ at: event.at,
22926
+ findings: Array.isArray(event.payload.findings) ? event.payload.findings.map(toGuardrailFinding).filter((finding) => finding !== undefined) : [],
22927
+ metadata: event.metadata && typeof event.metadata === "object" && !Array.isArray(event.metadata) ? event.metadata : undefined,
22928
+ proof: getString16(event.metadata?.proof),
22929
+ stage: getString16(event.payload.stage),
22930
+ status: getString16(event.payload.status),
22931
+ toolName: getString16(event.payload.toolName),
22932
+ turnId: event.turnId
22933
+ });
22934
+ var summarizeGuardrails = (events) => {
22935
+ const decisions = events.filter((event) => event.type === "assistant.guardrail").map(toGuardrailDecision);
22936
+ const isBlocked = (decision) => decision.allowed === false || decision.status === "fail";
22937
+ const isWarned = (decision) => decision.status === "warn" || decision.findings.some((finding) => finding.action === "warn");
22938
+ const stages = [
22939
+ ...new Set(decisions.map((decision) => decision.stage).filter((stage) => typeof stage === "string"))
22940
+ ].sort();
22941
+ return {
22942
+ blocked: decisions.filter(isBlocked).length,
22943
+ decisions,
22944
+ passed: decisions.filter((decision) => !isBlocked(decision) && !isWarned(decision)).length,
22945
+ stages,
22946
+ total: decisions.length,
22947
+ warned: decisions.filter(isWarned).length
22948
+ };
22949
+ };
22902
22950
  var toProviderDecision = (event) => {
22903
22951
  const provider = getString16(event.payload.provider) ?? getString16(event.payload.selectedProvider) ?? getString16(event.payload.fallbackProvider) ?? getString16(event.payload.variantId);
22904
22952
  const status = getString16(event.payload.providerStatus) ?? getString16(event.payload.status) ?? getString16(event.payload.reason);
@@ -22971,6 +23019,7 @@ var buildVoiceOperationsRecord = async (options) => {
22971
23019
  total: auditEvents.length
22972
23020
  } : undefined,
22973
23021
  checkedAt: Date.now(),
23022
+ guardrails: summarizeGuardrails(traceEvents),
22974
23023
  handoffs: traceEvents.filter((event) => event.type === "agent.handoff").map(toHandoff),
22975
23024
  integrationEvents: integrationEvents ? {
22976
23025
  delivered: countIntegrationDeliveryStatus(integrationEvents, "delivered"),
@@ -23007,6 +23056,78 @@ var buildVoiceOperationsRecord = async (options) => {
23007
23056
  transcript: buildTranscript(replay)
23008
23057
  };
23009
23058
  };
23059
+ var evaluateVoiceOperationsRecordGuardrails = (record, input = {}) => {
23060
+ const issues = [];
23061
+ const decisions = record.guardrails.decisions;
23062
+ const proofs = uniqueSorted(decisions.map((decision) => decision.proof));
23063
+ const ruleIds = uniqueSorted(decisions.flatMap((decision) => decision.findings.map((finding) => finding.ruleId)));
23064
+ const stages = uniqueSorted(decisions.map((decision) => decision.stage));
23065
+ const statuses = uniqueSorted(decisions.map((decision) => decision.status));
23066
+ const toolNames = uniqueSorted(decisions.map((decision) => decision.toolName));
23067
+ const minDecisions = input.minDecisions ?? 1;
23068
+ if (record.guardrails.total < minDecisions) {
23069
+ issues.push(`Expected at least ${String(minDecisions)} guardrail decisions, found ${String(record.guardrails.total)}.`);
23070
+ }
23071
+ if (input.minBlocked !== undefined && record.guardrails.blocked < input.minBlocked) {
23072
+ issues.push(`Expected at least ${String(input.minBlocked)} blocked guardrail decisions, found ${String(record.guardrails.blocked)}.`);
23073
+ }
23074
+ if (input.minWarned !== undefined && record.guardrails.warned < input.minWarned) {
23075
+ issues.push(`Expected at least ${String(input.minWarned)} warned guardrail decisions, found ${String(record.guardrails.warned)}.`);
23076
+ }
23077
+ if (input.minPassed !== undefined && record.guardrails.passed < input.minPassed) {
23078
+ issues.push(`Expected at least ${String(input.minPassed)} passed guardrail decisions, found ${String(record.guardrails.passed)}.`);
23079
+ }
23080
+ pushMissingValuesIssue({
23081
+ actual: proofs,
23082
+ expected: input.proofs,
23083
+ issues,
23084
+ label: "proofs"
23085
+ });
23086
+ pushMissingValuesIssue({
23087
+ actual: ruleIds,
23088
+ expected: input.ruleIds,
23089
+ issues,
23090
+ label: "rule IDs"
23091
+ });
23092
+ pushMissingValuesIssue({
23093
+ actual: stages,
23094
+ expected: input.stages,
23095
+ issues,
23096
+ label: "stages"
23097
+ });
23098
+ pushMissingValuesIssue({
23099
+ actual: statuses,
23100
+ expected: input.statuses,
23101
+ issues,
23102
+ label: "statuses"
23103
+ });
23104
+ pushMissingValuesIssue({
23105
+ actual: toolNames,
23106
+ expected: input.toolNames,
23107
+ issues,
23108
+ label: "tool names"
23109
+ });
23110
+ return {
23111
+ blocked: record.guardrails.blocked,
23112
+ decisions: record.guardrails.total,
23113
+ issues,
23114
+ ok: issues.length === 0,
23115
+ passed: record.guardrails.passed,
23116
+ proofs,
23117
+ ruleIds,
23118
+ stages,
23119
+ statuses,
23120
+ toolNames,
23121
+ warned: record.guardrails.warned
23122
+ };
23123
+ };
23124
+ var assertVoiceOperationsRecordGuardrails = (record, input = {}) => {
23125
+ const report = evaluateVoiceOperationsRecordGuardrails(record, input);
23126
+ if (!report.ok) {
23127
+ throw new Error(`Voice operations record guardrail assertion failed for ${record.sessionId}: ${report.issues.join(" ")}`);
23128
+ }
23129
+ return report;
23130
+ };
23010
23131
  var escapeHtml39 = (value) => value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
23011
23132
  var formatMs4 = (value) => value === undefined ? "n/a" : `${String(value)}ms`;
23012
23133
  var outcomeLabels = (outcome) => [
@@ -23031,6 +23152,9 @@ var renderVoiceOperationsRecordIncidentMarkdown = (record) => {
23031
23152
  `- Providers: ${record.providers.map((provider) => provider.provider).join(", ") || "none recorded"}`,
23032
23153
  `- Open tasks: ${openTasks.join("; ") || "none"}`,
23033
23154
  `- Top errors: ${topErrors.join("; ") || "none"}`,
23155
+ `- Guardrails: ${String(record.guardrails.blocked)} blocked / ${String(record.guardrails.warned)} warned / ${String(record.guardrails.total)} decisions`,
23156
+ "",
23157
+ renderVoiceOperationsRecordGuardrailMarkdown(record),
23034
23158
  "",
23035
23159
  "## Next checks",
23036
23160
  "- Review provider decisions and fallback status.",
@@ -23039,6 +23163,25 @@ var renderVoiceOperationsRecordIncidentMarkdown = (record) => {
23039
23163
  ].join(`
23040
23164
  `);
23041
23165
  };
23166
+ var renderVoiceOperationsRecordGuardrailMarkdown = (record) => {
23167
+ if (record.guardrails.total === 0) {
23168
+ return [
23169
+ "## Guardrail evidence",
23170
+ "",
23171
+ "- No assistant.guardrail events were recorded for this session."
23172
+ ].join(`
23173
+ `);
23174
+ }
23175
+ return [
23176
+ "## Guardrail evidence",
23177
+ "",
23178
+ ...record.guardrails.decisions.map((decision) => {
23179
+ const findings = decision.findings.map((finding) => [finding.action, finding.ruleId, finding.label].filter((value) => typeof value === "string").join(":")).filter(Boolean).join(", ");
23180
+ return `- assistant.guardrail ${decision.stage ?? "unknown"}: ${decision.status ?? "unknown"}; allowed=${String(decision.allowed ?? "unknown")}; proof=${decision.proof ?? "runtime"}; findings=${findings || "none"}`;
23181
+ })
23182
+ ].join(`
23183
+ `);
23184
+ };
23042
23185
  var renderVoiceOperationsRecordHTML = (record, options = {}) => {
23043
23186
  const providers = record.providers.length ? record.providers.map((provider) => `<article><strong>${escapeHtml39(provider.provider)}</strong><span>${String(provider.eventCount)} events</span><span>${formatMs4(provider.averageElapsedMs)} avg</span><span>${String(provider.errorCount)} errors</span></article>`).join("") : '<p class="muted">No provider events recorded.</p>';
23044
23187
  const transcript = record.transcript.length ? record.transcript.map((turn) => `<li><strong>${escapeHtml39(turn.id)}</strong>${turn.committedText ? `<p><span class="label">Caller</span>${escapeHtml39(turn.committedText)}</p>` : ""}${turn.assistantReplies.map((reply) => `<p><span class="label">Assistant</span>${escapeHtml39(reply)}</p>`).join("")}${turn.errors.map((error) => `<p class="error"><span class="label">Error</span>${escapeHtml39(error)}</p>`).join("")}</li>`).join("") : "<li>No transcript turns recorded.</li>";
@@ -23048,6 +23191,10 @@ var renderVoiceOperationsRecordHTML = (record, options = {}) => {
23048
23191
  const reviews = record.reviews?.reviews.length ? record.reviews.reviews.map((review) => `<li><strong>${escapeHtml39(review.title)}</strong> <span>${escapeHtml39(review.summary.outcome ?? "")}</span><p>${escapeHtml39(review.postCall?.summary ?? review.transcript.actual)}</p></li>`).join("") : "<li>No call reviews recorded.</li>";
23049
23192
  const tasks = record.tasks?.tasks.length ? record.tasks.tasks.map((task) => `<li><strong>${escapeHtml39(task.title)}</strong> <span>${escapeHtml39(task.status)}</span><p>${escapeHtml39(task.recommendedAction)}</p></li>`).join("") : "<li>No ops tasks recorded.</li>";
23050
23193
  const integrationEvents = record.integrationEvents?.events.length ? record.integrationEvents.events.map((event) => `<li><strong>${escapeHtml39(event.type)}</strong> <span>${escapeHtml39(event.deliveryStatus ?? "local")}</span><p>${escapeHtml39(event.deliveryError ?? event.deliveredTo ?? "")}</p></li>`).join("") : "<li>No integration events recorded.</li>";
23194
+ const guardrails = record.guardrails.total ? record.guardrails.decisions.map((decision) => {
23195
+ const findings = decision.findings.map((finding) => finding.label ?? finding.ruleId ?? finding.action).filter((value) => typeof value === "string").join(", ") || "none";
23196
+ return `<li><strong>assistant.guardrail ${escapeHtml39(decision.stage ?? "unknown")}</strong> <span>${escapeHtml39(decision.status ?? "")}</span><p>Allowed: ${escapeHtml39(String(decision.allowed ?? "unknown"))} \xB7 Proof: ${escapeHtml39(decision.proof ?? "runtime")}${decision.turnId ? ` \xB7 Turn: ${escapeHtml39(decision.turnId)}` : ""}</p><p>${escapeHtml39(findings)}</p></li>`;
23197
+ }).join("") : "<li>No assistant.guardrail events recorded.</li>";
23051
23198
  const snippet = escapeHtml39(`app.use(
23052
23199
  createVoiceOperationsRecordRoutes({
23053
23200
  audit: auditStore,
@@ -23064,7 +23211,7 @@ var renderVoiceOperationsRecordHTML = (record, options = {}) => {
23064
23211
  );`);
23065
23212
  const incidentMarkdown = escapeHtml39(renderVoiceOperationsRecordIncidentMarkdown(record));
23066
23213
  const incidentLink = options.incidentHref ? `<a href="${escapeHtml39(options.incidentHref)}">Download incident.md</a>` : "";
23067
- return `<!doctype html><html lang="en"><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /><title>${escapeHtml39(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>${escapeHtml39(options.title ?? "Voice Operations Record")}</h1><p class="status ${escapeHtml39(record.status)}">${escapeHtml39(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>`;
23214
+ return `<!doctype html><html lang="en"><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /><title>${escapeHtml39(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>${escapeHtml39(options.title ?? "Voice Operations Record")}</h1><p class="status ${escapeHtml39(record.status)}">${escapeHtml39(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>${formatMs4(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>`;
23068
23215
  };
23069
23216
  var createVoiceOperationsRecordRoutes = (options) => {
23070
23217
  const path = options.path ?? "/api/voice-operations/:sessionId";
@@ -26686,6 +26833,8 @@ var renderIncidentMarkdown = (input) => {
26686
26833
  "",
26687
26834
  ...input.record.tools.length ? input.record.tools.map((tool) => `- ${tool.toolName ?? "tool"} ${tool.status ?? ""} ${tool.elapsedMs === undefined ? "" : `${tool.elapsedMs}ms`} ${tool.error ?? ""}`.trim()) : ["- none"],
26688
26835
  "",
26836
+ renderVoiceOperationsRecordGuardrailMarkdown(input.record),
26837
+ "",
26689
26838
  "## Trace Evidence",
26690
26839
  "",
26691
26840
  input.traceMarkdown,
@@ -29771,6 +29920,7 @@ export {
29771
29920
  renderVoiceOpsActionHistoryHTML,
29772
29921
  renderVoiceOperationsRecordIncidentMarkdown,
29773
29922
  renderVoiceOperationsRecordHTML,
29923
+ renderVoiceOperationsRecordGuardrailMarkdown,
29774
29924
  renderVoiceObservabilityExportReplayHTML,
29775
29925
  renderVoiceObservabilityExportMarkdown,
29776
29926
  renderVoiceLiveLatencyHTML,
@@ -29836,6 +29986,7 @@ export {
29836
29986
  evaluateVoiceTelephonyContract,
29837
29987
  evaluateVoiceQuality,
29838
29988
  evaluateVoiceProviderStackGaps,
29989
+ evaluateVoiceOperationsRecordGuardrails,
29839
29990
  evaluateVoiceGuardrailPolicy,
29840
29991
  encodeTwilioMulawBase64,
29841
29992
  deliverVoiceTraceEventsToSinks,
@@ -30149,6 +30300,7 @@ export {
30149
30300
  buildEmptyVoiceProofTrendReport,
30150
30301
  assignVoiceOpsTask,
30151
30302
  assertVoiceProviderRoutingContract,
30303
+ assertVoiceOperationsRecordGuardrails,
30152
30304
  assertVoiceObservabilityExportSchema,
30153
30305
  assertVoiceObservabilityExportRecord,
30154
30306
  assertVoiceLatencySLOGate,
@@ -81,9 +81,58 @@ export type VoiceOperationsRecordIntegrationEventSummary = {
81
81
  skipped: number;
82
82
  total: number;
83
83
  };
84
+ export type VoiceOperationsRecordGuardrailFinding = {
85
+ action?: string;
86
+ label?: string;
87
+ ruleId?: string;
88
+ };
89
+ export type VoiceOperationsRecordGuardrailDecision = {
90
+ allowed?: boolean;
91
+ at: number;
92
+ findings: VoiceOperationsRecordGuardrailFinding[];
93
+ metadata?: Record<string, unknown>;
94
+ proof?: string;
95
+ stage?: string;
96
+ status?: string;
97
+ toolName?: string;
98
+ turnId?: string;
99
+ };
100
+ export type VoiceOperationsRecordGuardrailSummary = {
101
+ blocked: number;
102
+ decisions: VoiceOperationsRecordGuardrailDecision[];
103
+ passed: number;
104
+ stages: string[];
105
+ total: number;
106
+ warned: number;
107
+ };
108
+ export type VoiceOperationsRecordGuardrailAssertionInput = {
109
+ minBlocked?: number;
110
+ minDecisions?: number;
111
+ minPassed?: number;
112
+ minWarned?: number;
113
+ proofs?: string[];
114
+ ruleIds?: string[];
115
+ stages?: string[];
116
+ statuses?: string[];
117
+ toolNames?: string[];
118
+ };
119
+ export type VoiceOperationsRecordGuardrailAssertionReport = {
120
+ blocked: number;
121
+ decisions: number;
122
+ issues: string[];
123
+ ok: boolean;
124
+ passed: number;
125
+ proofs: string[];
126
+ ruleIds: string[];
127
+ stages: string[];
128
+ statuses: string[];
129
+ toolNames: string[];
130
+ warned: number;
131
+ };
84
132
  export type VoiceOperationsRecord = {
85
133
  audit?: VoiceOperationsRecordAuditSummary;
86
134
  checkedAt: number;
135
+ guardrails: VoiceOperationsRecordGuardrailSummary;
87
136
  handoffs: VoiceOperationsRecordAgentHandoff[];
88
137
  integrationEvents?: VoiceOperationsRecordIntegrationEventSummary;
89
138
  outcome: VoiceOperationsRecordOutcome;
@@ -123,7 +172,10 @@ export type VoiceOperationsRecordRoutesOptions = Omit<VoiceOperationsRecordOptio
123
172
  title?: string;
124
173
  };
125
174
  export declare const buildVoiceOperationsRecord: (options: VoiceOperationsRecordOptions) => Promise<VoiceOperationsRecord>;
175
+ export declare const evaluateVoiceOperationsRecordGuardrails: (record: VoiceOperationsRecord, input?: VoiceOperationsRecordGuardrailAssertionInput) => VoiceOperationsRecordGuardrailAssertionReport;
176
+ export declare const assertVoiceOperationsRecordGuardrails: (record: VoiceOperationsRecord, input?: VoiceOperationsRecordGuardrailAssertionInput) => VoiceOperationsRecordGuardrailAssertionReport;
126
177
  export declare const renderVoiceOperationsRecordIncidentMarkdown: (record: VoiceOperationsRecord) => string;
178
+ export declare const renderVoiceOperationsRecordGuardrailMarkdown: (record: VoiceOperationsRecord) => string;
127
179
  export declare const renderVoiceOperationsRecordHTML: (record: VoiceOperationsRecord, options?: {
128
180
  incidentHref?: string;
129
181
  title?: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@absolutejs/voice",
3
- "version": "0.0.22-beta.247",
3
+ "version": "0.0.22-beta.249",
4
4
  "description": "Voice primitives and Elysia plugin for AbsoluteJS",
5
5
  "repository": {
6
6
  "type": "git",