@absolutejs/voice 0.0.22-beta.244 → 0.0.22-beta.245

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
@@ -37,6 +37,7 @@ These are the primitive-first paths a Vapi-style buyer usually needs. Each path
37
37
  | Phone voice assistant | `createVoicePhoneAgent(...)` | carrier matrix, setup instructions, phone smoke contract, production readiness, operations record | phone setup HTML/JSON, smoke HTML/JSON, framework status UI |
38
38
  | Multi-specialist support flow | `createVoiceAgentSquad(...)` | squad contract, handoff traces, context traces, operations record | Agent Squad status hooks/composables/services/widgets |
39
39
  | Business actions and tools | `createVoiceAgentTool(...)` plus agent tool runtime | tool contracts, audit events, integration events, outcome contracts | operations record, action center, contract routes |
40
+ | Guardrails and policy checks | `createVoiceGuardrailPolicy(...)` plus `createVoiceGuardrailRoutes(...)` | blocking/warning decisions, redacted content, `assistant.guardrail` trace events | guardrail JSON/Markdown routes and operations record traces |
40
41
  | Provider routing and fallback | provider routers, health checks, simulation controls | provider contract matrix, provider-stage traces, latency SLO reports | provider contract hooks/composables/services/widgets |
41
42
  | Production operations | ops status, ops recovery, production readiness, delivery runtime | readiness gates, recovery report, incident Markdown, delivery queues | ops action center, delivery runtime UI, operations record |
42
43
  | Outbound campaigns | `createVoiceCampaignRoutes(...)` | recipient validation, consent/dedupe, carrier dry-run, campaign readiness | campaign routes and operations-record-linked attempt proof |
@@ -113,6 +114,25 @@ app.use(
113
114
  );
114
115
  ```
115
116
 
117
+ ## Guardrails
118
+
119
+ Use `createVoiceGuardrailRoutes(...)` when you need code-owned policy checks for what an agent may say, what tool payloads may contain, or which transcript content should warn/redact before downstream workflow. The primitive does not force a moderation vendor or hosted dashboard; it returns JSON/Markdown proof and can emit `assistant.guardrail` trace events.
120
+
121
+ ```ts
122
+ import {
123
+ createVoiceGuardrailRoutes,
124
+ voiceGuardrailPolicyPresets
125
+ } from '@absolutejs/voice';
126
+
127
+ app.use(
128
+ createVoiceGuardrailRoutes({
129
+ path: '/api/voice/guardrails',
130
+ policies: [voiceGuardrailPolicyPresets.supportSafeDefaults],
131
+ trace: runtime.traces
132
+ })
133
+ );
134
+ ```
135
+
116
136
  ## Use-Case Recipe: Support Triage
117
137
 
118
138
  Use this path when you want a Vapi-style support assistant that can answer web or phone calls, look up customer context, route billing issues to a specialist, create follow-up work, and leave one debuggable call record. It is a recipe over primitives, not a support app kit.
@@ -0,0 +1,108 @@
1
+ import { Elysia } from 'elysia';
2
+ import type { VoiceTraceEventStore } from './trace';
3
+ export type VoiceGuardrailStage = 'assistant-output' | 'handoff' | 'model-input' | 'tool-input' | 'tool-output' | 'transcript';
4
+ export type VoiceGuardrailSeverity = 'block' | 'warn';
5
+ export type VoiceGuardrailStatus = 'blocked' | 'pass' | 'warn';
6
+ export type VoiceGuardrailRule = {
7
+ action?: VoiceGuardrailSeverity;
8
+ description?: string;
9
+ id: string;
10
+ label?: string;
11
+ match: RegExp | string | ((input: VoiceGuardrailEvaluationInput) => boolean | Promise<boolean>);
12
+ redactWith?: string;
13
+ stages?: VoiceGuardrailStage[];
14
+ };
15
+ export type VoiceGuardrailEvaluationInput = {
16
+ content?: unknown;
17
+ metadata?: Record<string, unknown>;
18
+ sessionId?: string;
19
+ stage: VoiceGuardrailStage;
20
+ turnId?: string;
21
+ };
22
+ export type VoiceGuardrailFinding = {
23
+ action: VoiceGuardrailSeverity;
24
+ description?: string;
25
+ label: string;
26
+ ruleId: string;
27
+ stage: VoiceGuardrailStage;
28
+ };
29
+ export type VoiceGuardrailDecision = {
30
+ allowed: boolean;
31
+ checkedAt: number;
32
+ content?: unknown;
33
+ findings: VoiceGuardrailFinding[];
34
+ redactedContent?: unknown;
35
+ sessionId?: string;
36
+ stage: VoiceGuardrailStage;
37
+ status: VoiceGuardrailStatus;
38
+ turnId?: string;
39
+ };
40
+ export type VoiceGuardrailPolicy = {
41
+ defaultAction?: VoiceGuardrailSeverity;
42
+ id: string;
43
+ label?: string;
44
+ rules: VoiceGuardrailRule[];
45
+ };
46
+ export type VoiceGuardrailReport = {
47
+ checkedAt: number;
48
+ decisions: VoiceGuardrailDecision[];
49
+ failed: number;
50
+ policies: Array<{
51
+ id: string;
52
+ label?: string;
53
+ rules: number;
54
+ }>;
55
+ status: 'fail' | 'pass' | 'warn';
56
+ summary: {
57
+ blocked: number;
58
+ passed: number;
59
+ warned: number;
60
+ };
61
+ total: number;
62
+ };
63
+ export type VoiceGuardrailRoutesOptions = {
64
+ headers?: HeadersInit;
65
+ name?: string;
66
+ path?: string;
67
+ policies?: VoiceGuardrailPolicy[];
68
+ source?: ((input: VoiceGuardrailEvaluationInput) => Promise<VoiceGuardrailDecision | VoiceGuardrailReport> | VoiceGuardrailDecision | VoiceGuardrailReport) | VoiceGuardrailDecision | VoiceGuardrailReport;
69
+ trace?: VoiceTraceEventStore;
70
+ };
71
+ export declare const evaluateVoiceGuardrailPolicy: (policy: VoiceGuardrailPolicy, input: VoiceGuardrailEvaluationInput) => Promise<VoiceGuardrailDecision>;
72
+ export declare const buildVoiceGuardrailReport: (input?: {
73
+ decisions: VoiceGuardrailDecision[];
74
+ policies?: VoiceGuardrailPolicy[];
75
+ }) => VoiceGuardrailReport;
76
+ export declare const createVoiceGuardrailPolicy: (policy: VoiceGuardrailPolicy) => VoiceGuardrailPolicy;
77
+ export declare const voiceGuardrailPolicyPresets: {
78
+ supportSafeDefaults: VoiceGuardrailPolicy;
79
+ };
80
+ export declare const renderVoiceGuardrailMarkdown: (report: VoiceGuardrailReport) => string;
81
+ export declare const createVoiceGuardrailRoutes: (options?: VoiceGuardrailRoutesOptions) => Elysia<"", {
82
+ decorator: {};
83
+ store: {};
84
+ derive: {};
85
+ resolve: {};
86
+ }, {
87
+ typebox: {};
88
+ error: {};
89
+ }, {
90
+ schema: {};
91
+ standaloneSchema: {};
92
+ macro: {};
93
+ macroFn: {};
94
+ parser: {};
95
+ response: {};
96
+ }, {}, {
97
+ derive: {};
98
+ resolve: {};
99
+ schema: {};
100
+ standaloneSchema: {};
101
+ response: {};
102
+ }, {
103
+ derive: {};
104
+ resolve: {};
105
+ schema: {};
106
+ standaloneSchema: {};
107
+ response: {};
108
+ }>;
package/dist/index.d.ts CHANGED
@@ -84,6 +84,7 @@ export { createVoiceOpsRuntime } from './opsRuntime';
84
84
  export { resolveVoiceOpsPreset } from './opsPresets';
85
85
  export { resolveVoiceOutcomeRecipe } from './outcomeRecipes';
86
86
  export { buildVoicePostCallAnalysisReport, createVoicePostCallAnalysisRoutes, renderVoicePostCallAnalysisMarkdown } from './postCallAnalysis';
87
+ export { buildVoiceGuardrailReport, createVoiceGuardrailPolicy, createVoiceGuardrailRoutes, evaluateVoiceGuardrailPolicy, renderVoiceGuardrailMarkdown, voiceGuardrailPolicyPresets } from './guardrails';
87
88
  export { createId, createVoiceSessionRecord } from './store';
88
89
  export { createVoiceSTTRoutingCorrectionHandler, resolveVoiceSTTRoutingStrategy } from './routing';
89
90
  export { applyRiskTieredPhraseHintCorrections, applyPhraseHintCorrections, createDomainLexicon, createDomainPhraseHints, createPhraseHintCorrectionHandler, createRiskyTurnCorrectionHandler } from './correction';
@@ -137,6 +138,7 @@ export type { VoiceOpsRuntime, VoiceOpsRuntimeConfig, VoiceOpsRuntimeSummary, Vo
137
138
  export type { VoiceOpsPresetName, VoiceOpsPresetOverrides, VoiceResolvedOpsPreset } from './opsPresets';
138
139
  export type { VoiceOutcomeRecipe, VoiceOutcomeRecipeName, VoiceOutcomeRecipeOptions } from './outcomeRecipes';
139
140
  export type { VoicePostCallAnalysisFieldRequirement, VoicePostCallAnalysisFieldResult, VoicePostCallAnalysisIssue, VoicePostCallAnalysisIssueCode, VoicePostCallAnalysisOptions, VoicePostCallAnalysisReport, VoicePostCallAnalysisRoutesOptions, VoicePostCallAnalysisStatus } from './postCallAnalysis';
141
+ export type { VoiceGuardrailDecision, VoiceGuardrailEvaluationInput, VoiceGuardrailFinding, VoiceGuardrailPolicy, VoiceGuardrailReport, VoiceGuardrailRoutesOptions, VoiceGuardrailRule, VoiceGuardrailSeverity, VoiceGuardrailStage, VoiceGuardrailStatus } from './guardrails';
140
142
  export type { VoiceCRMActivitySinkOptions, VoiceHubSpotTaskSinkOptions, VoiceHubSpotTaskUpdateSinkOptions, VoiceHelpdeskTicketSinkOptions, VoiceIntegrationHTTPSinkOptions, VoiceIntegrationSink, VoiceIntegrationSinkDeliveryResult, VoiceLinearIssueSinkOptions, VoiceLinearIssueUpdateSinkOptions, VoiceZendeskTicketSinkOptions, VoiceZendeskTicketUpdateSinkOptions } from './opsSinks';
141
143
  export type { VoiceOpsWebhookEnvelope, VoiceOpsWebhookEntity, VoiceOpsWebhookLinkResolver, VoiceOpsWebhookReceiverRoutesOptions, VoiceOpsWebhookSinkOptions, VoiceOpsWebhookVerificationResult } from './opsWebhook';
142
144
  export type { VoiceHandoffDelivery, VoiceHandoffDeliveryRecord, VoiceHandoffDeliveryRecordInput, VoiceHandoffFanoutResult, VoiceQueuedHandoffDeliveryOptions, VoiceTwilioRedirectHandoffAdapterOptions, VoiceWebhookHandoffAdapterOptions } from './handoff';
package/dist/index.js CHANGED
@@ -28931,6 +28931,191 @@ var createVoicePostCallAnalysisRoutes = (options = {}) => {
28931
28931
  });
28932
28932
  return routes;
28933
28933
  };
28934
+ // src/guardrails.ts
28935
+ import { Elysia as Elysia50 } from "elysia";
28936
+ var stringifyContent = (value) => typeof value === "string" ? value : JSON.stringify(value) ?? "";
28937
+ var appliesToStage = (rule, stage) => !rule.stages || rule.stages.length === 0 || rule.stages.includes(stage);
28938
+ var matchesRule = async (rule, input) => {
28939
+ if (!appliesToStage(rule, input.stage)) {
28940
+ return false;
28941
+ }
28942
+ if (typeof rule.match === "function") {
28943
+ return rule.match(input);
28944
+ }
28945
+ const content = stringifyContent(input.content);
28946
+ return typeof rule.match === "string" ? content.toLowerCase().includes(rule.match.toLowerCase()) : rule.match.test(content);
28947
+ };
28948
+ var applyRedactions = (content, rules, findings) => {
28949
+ if (typeof content !== "string") {
28950
+ return content;
28951
+ }
28952
+ return findings.reduce((value, finding) => {
28953
+ const rule = rules.find((candidate) => candidate.id === finding.ruleId);
28954
+ if (!rule || !rule.redactWith) {
28955
+ return value;
28956
+ }
28957
+ if (typeof rule.match === "string") {
28958
+ return value.replaceAll(rule.match, rule.redactWith);
28959
+ }
28960
+ if (rule.match instanceof RegExp) {
28961
+ return value.replace(rule.match, rule.redactWith);
28962
+ }
28963
+ return value;
28964
+ }, content);
28965
+ };
28966
+ var evaluateVoiceGuardrailPolicy = async (policy, input) => {
28967
+ const findings = [];
28968
+ for (const rule of policy.rules) {
28969
+ if (!await matchesRule(rule, input)) {
28970
+ continue;
28971
+ }
28972
+ findings.push({
28973
+ action: rule.action ?? policy.defaultAction ?? "block",
28974
+ description: rule.description,
28975
+ label: rule.label ?? rule.id,
28976
+ ruleId: rule.id,
28977
+ stage: input.stage
28978
+ });
28979
+ }
28980
+ const blocked = findings.some((finding) => finding.action === "block");
28981
+ const status = blocked ? "blocked" : findings.length > 0 ? "warn" : "pass";
28982
+ return {
28983
+ allowed: !blocked,
28984
+ checkedAt: Date.now(),
28985
+ content: input.content,
28986
+ findings,
28987
+ redactedContent: applyRedactions(input.content, policy.rules, findings),
28988
+ sessionId: input.sessionId,
28989
+ stage: input.stage,
28990
+ status,
28991
+ turnId: input.turnId
28992
+ };
28993
+ };
28994
+ var buildVoiceGuardrailReport = (input = { decisions: [] }) => {
28995
+ const blocked = input.decisions.filter((decision) => decision.status === "blocked").length;
28996
+ const warned = input.decisions.filter((decision) => decision.status === "warn").length;
28997
+ const passed = input.decisions.filter((decision) => decision.status === "pass").length;
28998
+ const status = blocked > 0 ? "fail" : warned > 0 ? "warn" : "pass";
28999
+ return {
29000
+ checkedAt: Date.now(),
29001
+ decisions: input.decisions,
29002
+ failed: blocked,
29003
+ policies: (input.policies ?? []).map((policy) => ({
29004
+ id: policy.id,
29005
+ label: policy.label,
29006
+ rules: policy.rules.length
29007
+ })),
29008
+ status,
29009
+ summary: {
29010
+ blocked,
29011
+ passed,
29012
+ warned
29013
+ },
29014
+ total: input.decisions.length
29015
+ };
29016
+ };
29017
+ var createVoiceGuardrailPolicy = (policy) => policy;
29018
+ var voiceGuardrailPolicyPresets = {
29019
+ supportSafeDefaults: createVoiceGuardrailPolicy({
29020
+ id: "support-safe-defaults",
29021
+ label: "Support safe defaults",
29022
+ rules: [
29023
+ {
29024
+ description: "Blocks final legal, medical, or financial advice claims that should route to a human or qualified professional.",
29025
+ id: "regulated-advice",
29026
+ label: "Regulated advice",
29027
+ match: /\b(legal advice|medical advice|financial advice|diagnose|prescribe|guaranteed refund|guaranteed approval)\b/i,
29028
+ stages: ["assistant-output"]
29029
+ },
29030
+ {
29031
+ description: "Warns when payment-card-like data appears in transcripts or tool payloads.",
29032
+ action: "warn",
29033
+ id: "payment-card-like-data",
29034
+ label: "Payment card-like data",
29035
+ match: /\b(?:\d[ -]*?){13,19}\b/,
29036
+ redactWith: "[redacted-card]",
29037
+ stages: ["transcript", "tool-input", "tool-output"]
29038
+ }
29039
+ ]
29040
+ })
29041
+ };
29042
+ var renderVoiceGuardrailMarkdown = (report) => {
29043
+ const lines = [
29044
+ "# Voice Guardrail Report",
29045
+ "",
29046
+ `Status: ${report.status}`,
29047
+ `Checked: ${new Date(report.checkedAt).toISOString()}`,
29048
+ `Decisions: ${report.total}`,
29049
+ `Blocked: ${report.summary.blocked}`,
29050
+ `Warned: ${report.summary.warned}`,
29051
+ "",
29052
+ "## Decisions",
29053
+ ...report.decisions.length > 0 ? report.decisions.map((decision) => `- ${decision.status}: ${decision.stage}${decision.sessionId ? ` session=${decision.sessionId}` : ""} findings=${decision.findings.length}`) : ["- none"]
29054
+ ];
29055
+ return `${lines.join(`
29056
+ `)}
29057
+ `;
29058
+ };
29059
+ var isGuardrailReport = (value) => ("decisions" in value) && ("summary" in value);
29060
+ var normalizeGuardrailRouteInput = async (request) => {
29061
+ if (request.method === "POST") {
29062
+ return await request.json().catch(() => ({}));
29063
+ }
29064
+ const url = new URL(request.url);
29065
+ return {
29066
+ content: url.searchParams.get("content") ?? "",
29067
+ sessionId: url.searchParams.get("sessionId") ?? undefined,
29068
+ stage: url.searchParams.get("stage") ?? "assistant-output",
29069
+ turnId: url.searchParams.get("turnId") ?? undefined
29070
+ };
29071
+ };
29072
+ var resolveGuardrailReport = async (options, input) => {
29073
+ if (options.source !== undefined) {
29074
+ const value = typeof options.source === "function" ? await options.source(input) : options.source;
29075
+ return isGuardrailReport(value) ? value : buildVoiceGuardrailReport({ decisions: [value] });
29076
+ }
29077
+ const decisions = await Promise.all((options.policies ?? []).map((policy) => evaluateVoiceGuardrailPolicy(policy, input)));
29078
+ return buildVoiceGuardrailReport({
29079
+ decisions,
29080
+ policies: options.policies
29081
+ });
29082
+ };
29083
+ var createVoiceGuardrailRoutes = (options = {}) => {
29084
+ const path = options.path ?? "/api/voice/guardrails";
29085
+ const routes = new Elysia50({
29086
+ name: options.name ?? "absolutejs-voice-guardrails"
29087
+ });
29088
+ routes.all(path, async ({ request }) => {
29089
+ const input = await normalizeGuardrailRouteInput(request);
29090
+ const report = await resolveGuardrailReport(options, input);
29091
+ if (options.trace) {
29092
+ await Promise.all(report.decisions.map((decision) => options.trace.append({
29093
+ at: decision.checkedAt,
29094
+ payload: {
29095
+ allowed: decision.allowed,
29096
+ findings: decision.findings,
29097
+ stage: decision.stage,
29098
+ status: decision.status
29099
+ },
29100
+ sessionId: decision.sessionId ?? "guardrail-check",
29101
+ turnId: decision.turnId,
29102
+ type: "assistant.guardrail"
29103
+ })));
29104
+ }
29105
+ return Response.json(report, { headers: options.headers });
29106
+ });
29107
+ routes.all(`${path}.md`, async ({ request }) => {
29108
+ const input = await normalizeGuardrailRouteInput(request);
29109
+ const report = await resolveGuardrailReport(options, input);
29110
+ return new Response(renderVoiceGuardrailMarkdown(report), {
29111
+ headers: {
29112
+ "content-type": "text/markdown; charset=utf-8",
29113
+ ...options.headers
29114
+ }
29115
+ });
29116
+ });
29117
+ return routes;
29118
+ };
28934
29119
  // src/correction.ts
28935
29120
  var escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
28936
29121
  var buildAliasMatcher = (alias) => new RegExp(`(?<![\\p{L}\\p{N}'])${escapeRegExp(alias)}(?![\\p{L}\\p{N}'])`, "giu");
@@ -29323,6 +29508,7 @@ export {
29323
29508
  voiceTelephonyOutcomeToRouteResult,
29324
29509
  voiceObservabilityExportSchemaVersion,
29325
29510
  voiceObservabilityExportSchemaId,
29511
+ voiceGuardrailPolicyPresets,
29326
29512
  voiceComplianceRedactionDefaults,
29327
29513
  voice,
29328
29514
  verifyVoiceTwilioWebhookSignature,
@@ -29438,6 +29624,7 @@ export {
29438
29624
  renderVoiceLiveLatencyHTML,
29439
29625
  renderVoiceLatencySLOMarkdown,
29440
29626
  renderVoiceHandoffHealthHTML,
29627
+ renderVoiceGuardrailMarkdown,
29441
29628
  renderVoiceEvalHTML,
29442
29629
  renderVoiceEvalBaselineHTML,
29443
29630
  renderVoiceDemoReadyHTML,
@@ -29497,6 +29684,7 @@ export {
29497
29684
  evaluateVoiceTelephonyContract,
29498
29685
  evaluateVoiceQuality,
29499
29686
  evaluateVoiceProviderStackGaps,
29687
+ evaluateVoiceGuardrailPolicy,
29500
29688
  encodeTwilioMulawBase64,
29501
29689
  deliverVoiceTraceEventsToSinks,
29502
29690
  deliverVoiceObservabilityExport,
@@ -29676,6 +29864,8 @@ export {
29676
29864
  createVoiceHandoffDeliveryWorkerLoop,
29677
29865
  createVoiceHandoffDeliveryWorker,
29678
29866
  createVoiceHandoffDeliveryRecord,
29867
+ createVoiceGuardrailRoutes,
29868
+ createVoiceGuardrailPolicy,
29679
29869
  createVoiceFileTraceSinkDeliveryStore,
29680
29870
  createVoiceFileTraceEventStore,
29681
29871
  createVoiceFileTaskStore,
@@ -29792,6 +29982,7 @@ export {
29792
29982
  buildVoiceLiveOpsControlState,
29793
29983
  buildVoiceLatencySLOGate,
29794
29984
  buildVoiceIncidentBundle,
29985
+ buildVoiceGuardrailReport,
29795
29986
  buildVoiceDiagnosticsMarkdown,
29796
29987
  buildVoiceDemoReadyReport,
29797
29988
  buildVoiceDeliverySinkReport,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@absolutejs/voice",
3
- "version": "0.0.22-beta.244",
3
+ "version": "0.0.22-beta.245",
4
4
  "description": "Voice primitives and Elysia plugin for AbsoluteJS",
5
5
  "repository": {
6
6
  "type": "git",