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

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,128 @@
1
+ import { Elysia } from 'elysia';
2
+ import type { VoiceAgentTool } from './agent';
3
+ import type { VoiceTraceEventStore } from './trace';
4
+ import type { VoiceOnTurnObjectHandler, VoiceRouteResult, VoiceSessionRecord } from './types';
5
+ import type { VoiceAssistantGuardrails } from './assistant';
6
+ export type VoiceGuardrailStage = 'assistant-output' | 'handoff' | 'model-input' | 'tool-input' | 'tool-output' | 'transcript';
7
+ export type VoiceGuardrailSeverity = 'block' | 'warn';
8
+ export type VoiceGuardrailStatus = 'blocked' | 'pass' | 'warn';
9
+ export type VoiceGuardrailRule = {
10
+ action?: VoiceGuardrailSeverity;
11
+ description?: string;
12
+ id: string;
13
+ label?: string;
14
+ match: RegExp | string | ((input: VoiceGuardrailEvaluationInput) => boolean | Promise<boolean>);
15
+ redactWith?: string;
16
+ stages?: VoiceGuardrailStage[];
17
+ };
18
+ export type VoiceGuardrailEvaluationInput = {
19
+ content?: unknown;
20
+ metadata?: Record<string, unknown>;
21
+ sessionId?: string;
22
+ stage: VoiceGuardrailStage;
23
+ turnId?: string;
24
+ };
25
+ export type VoiceGuardrailFinding = {
26
+ action: VoiceGuardrailSeverity;
27
+ description?: string;
28
+ label: string;
29
+ ruleId: string;
30
+ stage: VoiceGuardrailStage;
31
+ };
32
+ export type VoiceGuardrailDecision = {
33
+ allowed: boolean;
34
+ checkedAt: number;
35
+ content?: unknown;
36
+ findings: VoiceGuardrailFinding[];
37
+ redactedContent?: unknown;
38
+ sessionId?: string;
39
+ stage: VoiceGuardrailStage;
40
+ status: VoiceGuardrailStatus;
41
+ turnId?: string;
42
+ };
43
+ export type VoiceGuardrailPolicy = {
44
+ defaultAction?: VoiceGuardrailSeverity;
45
+ id: string;
46
+ label?: string;
47
+ rules: VoiceGuardrailRule[];
48
+ };
49
+ export type VoiceGuardrailReport = {
50
+ checkedAt: number;
51
+ decisions: VoiceGuardrailDecision[];
52
+ failed: number;
53
+ policies: Array<{
54
+ id: string;
55
+ label?: string;
56
+ rules: number;
57
+ }>;
58
+ status: 'fail' | 'pass' | 'warn';
59
+ summary: {
60
+ blocked: number;
61
+ passed: number;
62
+ warned: number;
63
+ };
64
+ total: number;
65
+ };
66
+ export type VoiceGuardrailRoutesOptions = {
67
+ headers?: HeadersInit;
68
+ name?: string;
69
+ path?: string;
70
+ policies?: VoiceGuardrailPolicy[];
71
+ source?: ((input: VoiceGuardrailEvaluationInput) => Promise<VoiceGuardrailDecision | VoiceGuardrailReport> | VoiceGuardrailDecision | VoiceGuardrailReport) | VoiceGuardrailDecision | VoiceGuardrailReport;
72
+ trace?: VoiceTraceEventStore;
73
+ };
74
+ export type VoiceGuardrailRuntimeBlockInput<TContext = unknown, TSession extends VoiceSessionRecord = VoiceSessionRecord, TResult = unknown> = Parameters<VoiceOnTurnObjectHandler<TContext, TSession, TResult>>[0] & {
75
+ decision: VoiceGuardrailDecision;
76
+ stage: VoiceGuardrailStage;
77
+ };
78
+ export type VoiceGuardrailRuntimeOptions<TContext = unknown, TSession extends VoiceSessionRecord = VoiceSessionRecord, TResult = unknown> = {
79
+ blockResult?: (input: VoiceGuardrailRuntimeBlockInput<TContext, TSession, TResult>) => Promise<VoiceRouteResult<TResult>> | VoiceRouteResult<TResult>;
80
+ name?: string;
81
+ policies: VoiceGuardrailPolicy[];
82
+ trace?: VoiceTraceEventStore;
83
+ };
84
+ export type VoiceGuardrailRuntime<TContext = unknown, TSession extends VoiceSessionRecord = VoiceSessionRecord, TResult = unknown> = {
85
+ assistantGuardrails: VoiceAssistantGuardrails<TContext, TSession, TResult>;
86
+ evaluate: (input: VoiceGuardrailEvaluationInput) => Promise<VoiceGuardrailDecision>;
87
+ wrapTool: <TArgs extends Record<string, unknown>, TToolResult>(tool: VoiceAgentTool<TContext, TSession, TArgs, TToolResult, TResult>) => VoiceAgentTool<TContext, TSession, TArgs, TToolResult, TResult>;
88
+ wrapTools: (tools: Array<VoiceAgentTool<TContext, TSession, Record<string, unknown>, unknown, TResult>>) => Array<VoiceAgentTool<TContext, TSession, Record<string, unknown>, unknown, TResult>>;
89
+ };
90
+ export declare const evaluateVoiceGuardrailPolicy: (policy: VoiceGuardrailPolicy, input: VoiceGuardrailEvaluationInput) => Promise<VoiceGuardrailDecision>;
91
+ export declare const buildVoiceGuardrailReport: (input?: {
92
+ decisions: VoiceGuardrailDecision[];
93
+ policies?: VoiceGuardrailPolicy[];
94
+ }) => VoiceGuardrailReport;
95
+ export declare const createVoiceGuardrailPolicy: (policy: VoiceGuardrailPolicy) => VoiceGuardrailPolicy;
96
+ export declare const createVoiceGuardrailRuntime: <TContext = unknown, TSession extends VoiceSessionRecord = VoiceSessionRecord, TResult = unknown>(options: VoiceGuardrailRuntimeOptions<TContext, TSession, TResult>) => VoiceGuardrailRuntime<TContext, TSession, TResult>;
97
+ export declare const voiceGuardrailPolicyPresets: {
98
+ supportSafeDefaults: VoiceGuardrailPolicy;
99
+ };
100
+ export declare const renderVoiceGuardrailMarkdown: (report: VoiceGuardrailReport) => string;
101
+ export declare const createVoiceGuardrailRoutes: (options?: VoiceGuardrailRoutesOptions) => Elysia<"", {
102
+ decorator: {};
103
+ store: {};
104
+ derive: {};
105
+ resolve: {};
106
+ }, {
107
+ typebox: {};
108
+ error: {};
109
+ }, {
110
+ schema: {};
111
+ standaloneSchema: {};
112
+ macro: {};
113
+ macroFn: {};
114
+ parser: {};
115
+ response: {};
116
+ }, {}, {
117
+ derive: {};
118
+ resolve: {};
119
+ schema: {};
120
+ standaloneSchema: {};
121
+ response: {};
122
+ }, {
123
+ derive: {};
124
+ resolve: {};
125
+ schema: {};
126
+ standaloneSchema: {};
127
+ response: {};
128
+ }>;
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, createVoiceGuardrailRuntime, 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, VoiceGuardrailRuntime, VoiceGuardrailRuntimeBlockInput, VoiceGuardrailRuntimeOptions, 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,343 @@ 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 appendGuardrailTrace = async (trace, decision, metadata) => {
29019
+ await trace?.append({
29020
+ at: decision.checkedAt,
29021
+ payload: {
29022
+ allowed: decision.allowed,
29023
+ findingCount: decision.findings.length,
29024
+ findings: decision.findings,
29025
+ metadata,
29026
+ redactedContent: decision.redactedContent,
29027
+ stage: decision.stage,
29028
+ status: decision.status
29029
+ },
29030
+ sessionId: decision.sessionId ?? "guardrail-check",
29031
+ turnId: decision.turnId,
29032
+ type: "assistant.guardrail"
29033
+ });
29034
+ };
29035
+ var defaultGuardrailBlockResult = (reason) => ({
29036
+ assistantText: "I cannot safely complete that in the automated flow. I am routing this to a human specialist.",
29037
+ escalate: {
29038
+ metadata: {
29039
+ guardrail: true
29040
+ },
29041
+ reason
29042
+ }
29043
+ });
29044
+ var createVoiceGuardrailRuntime = (options) => {
29045
+ const evaluate = async (input) => {
29046
+ const decisions = await Promise.all(options.policies.map((policy) => evaluateVoiceGuardrailPolicy(policy, input)));
29047
+ await Promise.all(decisions.map((decision) => appendGuardrailTrace(options.trace, decision, input.metadata)));
29048
+ return decisions.find((decision) => !decision.allowed) ?? decisions[0];
29049
+ };
29050
+ const blockResult = async (input) => options.blockResult?.(input) ?? defaultGuardrailBlockResult(`guardrail-blocked-${input.stage}`);
29051
+ const assistantGuardrails = {
29052
+ beforeTurn: async (input) => {
29053
+ const transcriptDecision = await evaluate({
29054
+ content: input.turn.text,
29055
+ metadata: {
29056
+ runtime: options.name,
29057
+ surface: "live-transcript"
29058
+ },
29059
+ sessionId: input.session.id,
29060
+ stage: "transcript",
29061
+ turnId: input.turn.id
29062
+ });
29063
+ if (!transcriptDecision.allowed) {
29064
+ return blockResult({
29065
+ ...input,
29066
+ decision: transcriptDecision,
29067
+ stage: "transcript"
29068
+ });
29069
+ }
29070
+ const modelInputDecision = await evaluate({
29071
+ content: JSON.stringify({
29072
+ scenarioId: input.session.scenarioId,
29073
+ turn: input.turn.text
29074
+ }),
29075
+ metadata: {
29076
+ runtime: options.name,
29077
+ surface: "live-model-input"
29078
+ },
29079
+ sessionId: input.session.id,
29080
+ stage: "model-input",
29081
+ turnId: input.turn.id
29082
+ });
29083
+ if (!modelInputDecision.allowed) {
29084
+ return blockResult({
29085
+ ...input,
29086
+ decision: modelInputDecision,
29087
+ stage: "model-input"
29088
+ });
29089
+ }
29090
+ return;
29091
+ },
29092
+ afterTurn: async (input) => {
29093
+ if (!input.result.assistantText) {
29094
+ return;
29095
+ }
29096
+ const decision = await evaluate({
29097
+ content: input.result.assistantText,
29098
+ metadata: {
29099
+ runtime: options.name,
29100
+ surface: "live-assistant-output"
29101
+ },
29102
+ sessionId: input.session.id,
29103
+ stage: "assistant-output",
29104
+ turnId: input.turn.id
29105
+ });
29106
+ if (!decision.allowed) {
29107
+ return blockResult({
29108
+ ...input,
29109
+ decision,
29110
+ stage: "assistant-output"
29111
+ });
29112
+ }
29113
+ if (typeof decision.redactedContent === "string" && decision.redactedContent !== input.result.assistantText) {
29114
+ return {
29115
+ ...input.result,
29116
+ assistantText: decision.redactedContent
29117
+ };
29118
+ }
29119
+ return;
29120
+ }
29121
+ };
29122
+ const wrapTool = (tool) => ({
29123
+ ...tool,
29124
+ execute: async (input) => {
29125
+ const inputDecision = await evaluate({
29126
+ content: JSON.stringify({
29127
+ args: input.args,
29128
+ toolName: tool.name
29129
+ }),
29130
+ metadata: {
29131
+ runtime: options.name,
29132
+ surface: "live-tool-input",
29133
+ toolName: tool.name
29134
+ },
29135
+ sessionId: input.session.id,
29136
+ stage: "tool-input",
29137
+ turnId: input.turn.id
29138
+ });
29139
+ if (!inputDecision.allowed) {
29140
+ throw new Error(`Guardrail blocked tool input for ${tool.name}.`);
29141
+ }
29142
+ const result = await tool.execute(input);
29143
+ const outputDecision = await evaluate({
29144
+ content: JSON.stringify({
29145
+ result,
29146
+ toolName: tool.name
29147
+ }),
29148
+ metadata: {
29149
+ runtime: options.name,
29150
+ surface: "live-tool-output",
29151
+ toolName: tool.name
29152
+ },
29153
+ sessionId: input.session.id,
29154
+ stage: "tool-output",
29155
+ turnId: input.turn.id
29156
+ });
29157
+ if (!outputDecision.allowed) {
29158
+ throw new Error(`Guardrail blocked tool output for ${tool.name}.`);
29159
+ }
29160
+ return result;
29161
+ }
29162
+ });
29163
+ return {
29164
+ assistantGuardrails,
29165
+ evaluate,
29166
+ wrapTool,
29167
+ wrapTools: (tools) => tools.map((tool) => wrapTool(tool))
29168
+ };
29169
+ };
29170
+ var voiceGuardrailPolicyPresets = {
29171
+ supportSafeDefaults: createVoiceGuardrailPolicy({
29172
+ id: "support-safe-defaults",
29173
+ label: "Support safe defaults",
29174
+ rules: [
29175
+ {
29176
+ description: "Blocks final legal, medical, or financial advice claims that should route to a human or qualified professional.",
29177
+ id: "regulated-advice",
29178
+ label: "Regulated advice",
29179
+ match: /\b(legal advice|medical advice|financial advice|diagnose|prescribe|guaranteed refund|guaranteed approval)\b/i,
29180
+ stages: ["assistant-output"]
29181
+ },
29182
+ {
29183
+ description: "Warns when payment-card-like data appears in transcripts or tool payloads.",
29184
+ action: "warn",
29185
+ id: "payment-card-like-data",
29186
+ label: "Payment card-like data",
29187
+ match: /\b(?:\d[ -]*?){13,19}\b/,
29188
+ redactWith: "[redacted-card]",
29189
+ stages: ["transcript", "tool-input", "tool-output"]
29190
+ }
29191
+ ]
29192
+ })
29193
+ };
29194
+ var renderVoiceGuardrailMarkdown = (report) => {
29195
+ const lines = [
29196
+ "# Voice Guardrail Report",
29197
+ "",
29198
+ `Status: ${report.status}`,
29199
+ `Checked: ${new Date(report.checkedAt).toISOString()}`,
29200
+ `Decisions: ${report.total}`,
29201
+ `Blocked: ${report.summary.blocked}`,
29202
+ `Warned: ${report.summary.warned}`,
29203
+ "",
29204
+ "## Decisions",
29205
+ ...report.decisions.length > 0 ? report.decisions.map((decision) => `- ${decision.status}: ${decision.stage}${decision.sessionId ? ` session=${decision.sessionId}` : ""} findings=${decision.findings.length}`) : ["- none"]
29206
+ ];
29207
+ return `${lines.join(`
29208
+ `)}
29209
+ `;
29210
+ };
29211
+ var isGuardrailReport = (value) => ("decisions" in value) && ("summary" in value);
29212
+ var normalizeGuardrailRouteInput = async (request) => {
29213
+ if (request.method === "POST") {
29214
+ return await request.json().catch(() => ({}));
29215
+ }
29216
+ const url = new URL(request.url);
29217
+ return {
29218
+ content: url.searchParams.get("content") ?? "",
29219
+ sessionId: url.searchParams.get("sessionId") ?? undefined,
29220
+ stage: url.searchParams.get("stage") ?? "assistant-output",
29221
+ turnId: url.searchParams.get("turnId") ?? undefined
29222
+ };
29223
+ };
29224
+ var resolveGuardrailReport = async (options, input) => {
29225
+ if (options.source !== undefined) {
29226
+ const value = typeof options.source === "function" ? await options.source(input) : options.source;
29227
+ return isGuardrailReport(value) ? value : buildVoiceGuardrailReport({ decisions: [value] });
29228
+ }
29229
+ const decisions = await Promise.all((options.policies ?? []).map((policy) => evaluateVoiceGuardrailPolicy(policy, input)));
29230
+ return buildVoiceGuardrailReport({
29231
+ decisions,
29232
+ policies: options.policies
29233
+ });
29234
+ };
29235
+ var createVoiceGuardrailRoutes = (options = {}) => {
29236
+ const path = options.path ?? "/api/voice/guardrails";
29237
+ const routes = new Elysia50({
29238
+ name: options.name ?? "absolutejs-voice-guardrails"
29239
+ });
29240
+ routes.all(path, async ({ request }) => {
29241
+ const input = await normalizeGuardrailRouteInput(request);
29242
+ const report = await resolveGuardrailReport(options, input);
29243
+ if (options.trace) {
29244
+ await Promise.all(report.decisions.map((decision) => options.trace.append({
29245
+ at: decision.checkedAt,
29246
+ payload: {
29247
+ allowed: decision.allowed,
29248
+ findings: decision.findings,
29249
+ stage: decision.stage,
29250
+ status: decision.status
29251
+ },
29252
+ sessionId: decision.sessionId ?? "guardrail-check",
29253
+ turnId: decision.turnId,
29254
+ type: "assistant.guardrail"
29255
+ })));
29256
+ }
29257
+ return Response.json(report, { headers: options.headers });
29258
+ });
29259
+ routes.all(`${path}.md`, async ({ request }) => {
29260
+ const input = await normalizeGuardrailRouteInput(request);
29261
+ const report = await resolveGuardrailReport(options, input);
29262
+ return new Response(renderVoiceGuardrailMarkdown(report), {
29263
+ headers: {
29264
+ "content-type": "text/markdown; charset=utf-8",
29265
+ ...options.headers
29266
+ }
29267
+ });
29268
+ });
29269
+ return routes;
29270
+ };
28934
29271
  // src/correction.ts
28935
29272
  var escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
28936
29273
  var buildAliasMatcher = (alias) => new RegExp(`(?<![\\p{L}\\p{N}'])${escapeRegExp(alias)}(?![\\p{L}\\p{N}'])`, "giu");
@@ -29323,6 +29660,7 @@ export {
29323
29660
  voiceTelephonyOutcomeToRouteResult,
29324
29661
  voiceObservabilityExportSchemaVersion,
29325
29662
  voiceObservabilityExportSchemaId,
29663
+ voiceGuardrailPolicyPresets,
29326
29664
  voiceComplianceRedactionDefaults,
29327
29665
  voice,
29328
29666
  verifyVoiceTwilioWebhookSignature,
@@ -29438,6 +29776,7 @@ export {
29438
29776
  renderVoiceLiveLatencyHTML,
29439
29777
  renderVoiceLatencySLOMarkdown,
29440
29778
  renderVoiceHandoffHealthHTML,
29779
+ renderVoiceGuardrailMarkdown,
29441
29780
  renderVoiceEvalHTML,
29442
29781
  renderVoiceEvalBaselineHTML,
29443
29782
  renderVoiceDemoReadyHTML,
@@ -29497,6 +29836,7 @@ export {
29497
29836
  evaluateVoiceTelephonyContract,
29498
29837
  evaluateVoiceQuality,
29499
29838
  evaluateVoiceProviderStackGaps,
29839
+ evaluateVoiceGuardrailPolicy,
29500
29840
  encodeTwilioMulawBase64,
29501
29841
  deliverVoiceTraceEventsToSinks,
29502
29842
  deliverVoiceObservabilityExport,
@@ -29676,6 +30016,9 @@ export {
29676
30016
  createVoiceHandoffDeliveryWorkerLoop,
29677
30017
  createVoiceHandoffDeliveryWorker,
29678
30018
  createVoiceHandoffDeliveryRecord,
30019
+ createVoiceGuardrailRuntime,
30020
+ createVoiceGuardrailRoutes,
30021
+ createVoiceGuardrailPolicy,
29679
30022
  createVoiceFileTraceSinkDeliveryStore,
29680
30023
  createVoiceFileTraceEventStore,
29681
30024
  createVoiceFileTaskStore,
@@ -29792,6 +30135,7 @@ export {
29792
30135
  buildVoiceLiveOpsControlState,
29793
30136
  buildVoiceLatencySLOGate,
29794
30137
  buildVoiceIncidentBundle,
30138
+ buildVoiceGuardrailReport,
29795
30139
  buildVoiceDiagnosticsMarkdown,
29796
30140
  buildVoiceDemoReadyReport,
29797
30141
  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.246",
4
4
  "description": "Voice primitives and Elysia plugin for AbsoluteJS",
5
5
  "repository": {
6
6
  "type": "git",