@absolutejs/voice 0.0.22-beta.2 → 0.0.22-beta.200
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 +1370 -50
- package/dist/agent.d.ts +62 -0
- package/dist/agentSquadContract.d.ts +69 -0
- package/dist/angular/index.d.ts +13 -0
- package/dist/angular/index.js +3100 -1051
- package/dist/angular/voice-campaign-dialer-proof.service.d.ts +14 -0
- package/dist/angular/voice-controller.service.d.ts +1 -0
- package/dist/angular/voice-delivery-runtime.component.d.ts +17 -0
- package/dist/angular/voice-delivery-runtime.service.d.ts +16 -0
- package/dist/angular/voice-live-ops.service.d.ts +11 -0
- package/dist/angular/voice-ops-action-center.service.d.ts +13 -0
- package/dist/angular/voice-ops-status.component.d.ts +15 -0
- package/dist/angular/voice-ops-status.service.d.ts +12 -0
- package/dist/angular/voice-provider-capabilities.service.d.ts +12 -0
- package/dist/angular/voice-provider-contracts.service.d.ts +12 -0
- package/dist/angular/voice-provider-status.service.d.ts +12 -0
- package/dist/angular/voice-routing-status.service.d.ts +11 -0
- package/dist/angular/voice-stream.service.d.ts +3 -0
- package/dist/angular/voice-trace-timeline.service.d.ts +12 -0
- package/dist/angular/voice-turn-latency.service.d.ts +13 -0
- package/dist/angular/voice-turn-quality.service.d.ts +12 -0
- package/dist/angular/voice-workflow-status.service.d.ts +12 -0
- package/dist/assistant.d.ts +44 -0
- package/dist/assistantHealth.d.ts +81 -0
- package/dist/assistantMemory.d.ts +63 -0
- package/dist/audit.d.ts +128 -0
- package/dist/auditDeliveryRoutes.d.ts +85 -0
- package/dist/auditExport.d.ts +34 -0
- package/dist/auditRoutes.d.ts +66 -0
- package/dist/auditSinks.d.ts +151 -0
- package/dist/bargeInRoutes.d.ts +56 -0
- package/dist/campaign.d.ts +746 -0
- package/dist/campaignDialers.d.ts +90 -0
- package/dist/client/actions.d.ts +105 -0
- package/dist/client/bargeInMonitor.d.ts +7 -0
- package/dist/client/campaignDialerProof.d.ts +23 -0
- package/dist/client/connection.d.ts +3 -0
- package/dist/client/deliveryRuntime.d.ts +34 -0
- package/dist/client/deliveryRuntimeWidget.d.ts +37 -0
- package/dist/client/duplex.d.ts +1 -1
- package/dist/client/htmxBootstrap.js +747 -15
- package/dist/client/index.d.ts +60 -0
- package/dist/client/index.js +4308 -10
- package/dist/client/liveOps.d.ts +22 -0
- package/dist/client/liveOpsWidget.d.ts +23 -0
- package/dist/client/liveTurnLatency.d.ts +41 -0
- package/dist/client/opsActionCenter.d.ts +54 -0
- package/dist/client/opsActionCenterWidget.d.ts +29 -0
- package/dist/client/opsActionHistory.d.ts +19 -0
- package/dist/client/opsActionHistoryWidget.d.ts +11 -0
- package/dist/client/opsStatus.d.ts +19 -0
- package/dist/client/opsStatusWidget.d.ts +40 -0
- package/dist/client/providerCapabilities.d.ts +19 -0
- package/dist/client/providerCapabilitiesWidget.d.ts +32 -0
- package/dist/client/providerContracts.d.ts +19 -0
- package/dist/client/providerContractsWidget.d.ts +37 -0
- package/dist/client/providerSimulationControls.d.ts +33 -0
- package/dist/client/providerSimulationControlsWidget.d.ts +20 -0
- package/dist/client/providerStatus.d.ts +19 -0
- package/dist/client/providerStatusWidget.d.ts +32 -0
- package/dist/client/routingStatus.d.ts +19 -0
- package/dist/client/routingStatusWidget.d.ts +28 -0
- package/dist/client/traceTimeline.d.ts +19 -0
- package/dist/client/traceTimelineWidget.d.ts +36 -0
- package/dist/client/turnLatency.d.ts +22 -0
- package/dist/client/turnLatencyWidget.d.ts +33 -0
- package/dist/client/turnQuality.d.ts +19 -0
- package/dist/client/turnQualityWidget.d.ts +32 -0
- package/dist/client/workflowStatus.d.ts +19 -0
- package/dist/dataControl.d.ts +140 -0
- package/dist/deliveryRuntime.d.ts +158 -0
- package/dist/deliverySinkRoutes.d.ts +117 -0
- package/dist/demoReadyRoutes.d.ts +98 -0
- package/dist/diagnosticsRoutes.d.ts +44 -0
- package/dist/evalRoutes.d.ts +213 -0
- package/dist/fileStore.d.ts +17 -2
- package/dist/handoff.d.ts +54 -0
- package/dist/handoffHealth.d.ts +94 -0
- package/dist/incidentBundle.d.ts +116 -0
- package/dist/index.d.ts +130 -11
- package/dist/index.js +21890 -3707
- package/dist/latencySlo.d.ts +56 -0
- package/dist/liveLatency.d.ts +78 -0
- package/dist/liveOps.d.ts +122 -0
- package/dist/modelAdapters.d.ts +114 -0
- package/dist/openaiRealtime.d.ts +27 -0
- package/dist/openaiTTS.d.ts +18 -0
- package/dist/operationsRecord.d.ts +157 -0
- package/dist/opsActionAuditRoutes.d.ts +99 -0
- package/dist/opsConsoleRoutes.d.ts +80 -0
- package/dist/opsRecovery.d.ts +137 -0
- package/dist/opsStatus.d.ts +76 -0
- package/dist/opsStatusRoutes.d.ts +33 -0
- package/dist/opsWebhook.d.ts +126 -0
- package/dist/outcomeContract.d.ts +112 -0
- package/dist/phoneAgent.d.ts +62 -0
- package/dist/phoneAgentProductionSmoke.d.ts +115 -0
- package/dist/postgresStore.d.ts +13 -2
- package/dist/productionReadiness.d.ts +407 -0
- package/dist/providerAdapters.d.ts +48 -0
- package/dist/providerCapabilities.d.ts +92 -0
- package/dist/providerHealth.d.ts +79 -0
- package/dist/providerRoutingContract.d.ts +38 -0
- package/dist/providerStackRecommendations.d.ts +145 -0
- package/dist/qualityRoutes.d.ts +76 -0
- package/dist/queue.d.ts +61 -0
- package/dist/react/VoiceDeliveryRuntime.d.ts +7 -0
- package/dist/react/VoiceOpsActionCenter.d.ts +5 -0
- package/dist/react/VoiceOpsStatus.d.ts +6 -0
- package/dist/react/VoiceProviderCapabilities.d.ts +6 -0
- package/dist/react/VoiceProviderContracts.d.ts +6 -0
- package/dist/react/VoiceProviderSimulationControls.d.ts +5 -0
- package/dist/react/VoiceProviderStatus.d.ts +6 -0
- package/dist/react/VoiceRoutingStatus.d.ts +6 -0
- package/dist/react/VoiceTraceTimeline.d.ts +6 -0
- package/dist/react/VoiceTurnLatency.d.ts +6 -0
- package/dist/react/VoiceTurnQuality.d.ts +6 -0
- package/dist/react/index.d.ts +25 -0
- package/dist/react/index.js +3903 -14
- package/dist/react/useVoiceCampaignDialerProof.d.ts +10 -0
- package/dist/react/useVoiceController.d.ts +3 -0
- package/dist/react/useVoiceDeliveryRuntime.d.ts +13 -0
- package/dist/react/useVoiceLiveOps.d.ts +9 -0
- package/dist/react/useVoiceOpsActionCenter.d.ts +11 -0
- package/dist/react/useVoiceOpsStatus.d.ts +8 -0
- package/dist/react/useVoiceProviderCapabilities.d.ts +8 -0
- package/dist/react/useVoiceProviderContracts.d.ts +8 -0
- package/dist/react/useVoiceProviderSimulationControls.d.ts +10 -0
- package/dist/react/useVoiceProviderStatus.d.ts +8 -0
- package/dist/react/useVoiceRoutingStatus.d.ts +8 -0
- package/dist/react/useVoiceStream.d.ts +3 -0
- package/dist/react/useVoiceTraceTimeline.d.ts +8 -0
- package/dist/react/useVoiceTurnLatency.d.ts +9 -0
- package/dist/react/useVoiceTurnQuality.d.ts +8 -0
- package/dist/react/useVoiceWorkflowStatus.d.ts +8 -0
- package/dist/readinessProfiles.d.ts +36 -0
- package/dist/reconnectContract.d.ts +87 -0
- package/dist/resilienceRoutes.d.ts +142 -0
- package/dist/sessionReplay.d.ts +187 -0
- package/dist/simulationSuite.d.ts +120 -0
- package/dist/sqliteStore.d.ts +13 -2
- package/dist/svelte/createVoiceCampaignDialerProof.d.ts +9 -0
- package/dist/svelte/createVoiceDeliveryRuntime.d.ts +11 -0
- package/dist/svelte/createVoiceLiveOps.d.ts +13 -0
- package/dist/svelte/createVoiceOpsActionCenter.d.ts +10 -0
- package/dist/svelte/createVoiceOpsStatus.d.ts +9 -0
- package/dist/svelte/createVoiceProviderCapabilities.d.ts +10 -0
- package/dist/svelte/createVoiceProviderContracts.d.ts +10 -0
- package/dist/svelte/createVoiceProviderSimulationControls.d.ts +11 -0
- package/dist/svelte/createVoiceProviderStatus.d.ts +10 -0
- package/dist/svelte/createVoiceRoutingStatus.d.ts +10 -0
- package/dist/svelte/createVoiceTraceTimeline.d.ts +10 -0
- package/dist/svelte/createVoiceTurnLatency.d.ts +11 -0
- package/dist/svelte/createVoiceTurnQuality.d.ts +10 -0
- package/dist/svelte/createVoiceWorkflowStatus.d.ts +8 -0
- package/dist/svelte/index.d.ts +14 -0
- package/dist/svelte/index.js +4446 -406
- package/dist/telephony/contract.d.ts +61 -0
- package/dist/telephony/matrix.d.ts +97 -0
- package/dist/telephony/plivo.d.ts +254 -0
- package/dist/telephony/telnyx.d.ts +247 -0
- package/dist/telephony/twilio.d.ts +135 -2
- package/dist/telephonyOutcome.d.ts +201 -0
- package/dist/testing/index.d.ts +2 -0
- package/dist/testing/index.js +2996 -156
- package/dist/testing/ioProviderSimulator.d.ts +41 -0
- package/dist/testing/providerSimulator.d.ts +44 -0
- package/dist/toolContract.d.ts +130 -0
- package/dist/toolRuntime.d.ts +50 -0
- package/dist/trace.d.ts +19 -1
- package/dist/traceDeliveryRoutes.d.ts +86 -0
- package/dist/traceTimeline.d.ts +96 -0
- package/dist/turnLatency.d.ts +95 -0
- package/dist/turnQuality.d.ts +94 -0
- package/dist/types.d.ts +180 -4
- package/dist/vue/VoiceDeliveryRuntime.d.ts +30 -0
- package/dist/vue/VoiceOpsActionCenter.d.ts +13 -0
- package/dist/vue/VoiceOpsStatus.d.ts +30 -0
- package/dist/vue/VoiceProviderCapabilities.d.ts +51 -0
- package/dist/vue/VoiceProviderContracts.d.ts +21 -0
- package/dist/vue/VoiceProviderSimulationControls.d.ts +88 -0
- package/dist/vue/VoiceProviderStatus.d.ts +51 -0
- package/dist/vue/VoiceRoutingStatus.d.ts +51 -0
- package/dist/vue/VoiceTurnLatency.d.ts +69 -0
- package/dist/vue/VoiceTurnQuality.d.ts +51 -0
- package/dist/vue/index.d.ts +24 -0
- package/dist/vue/index.js +3829 -31
- package/dist/vue/useVoiceCampaignDialerProof.d.ts +11 -0
- package/dist/vue/useVoiceController.d.ts +2 -1
- package/dist/vue/useVoiceDeliveryRuntime.d.ts +13 -0
- package/dist/vue/useVoiceLiveOps.d.ts +9 -0
- package/dist/vue/useVoiceOpsActionCenter.d.ts +11 -0
- package/dist/vue/useVoiceOpsStatus.d.ts +9 -0
- package/dist/vue/useVoiceProviderCapabilities.d.ts +9 -0
- package/dist/vue/useVoiceProviderContracts.d.ts +9 -0
- package/dist/vue/useVoiceProviderSimulationControls.d.ts +24 -0
- package/dist/vue/useVoiceProviderStatus.d.ts +9 -0
- package/dist/vue/useVoiceRoutingStatus.d.ts +8 -0
- package/dist/vue/useVoiceStream.d.ts +4 -1
- package/dist/vue/useVoiceTraceTimeline.d.ts +9 -0
- package/dist/vue/useVoiceTurnLatency.d.ts +10 -0
- package/dist/vue/useVoiceTurnQuality.d.ts +9 -0
- package/dist/vue/useVoiceWorkflowStatus.d.ts +9 -0
- package/dist/workflowContract.d.ts +91 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,11 +1,50 @@
|
|
|
1
1
|
# `@absolutejs/voice`
|
|
2
2
|
|
|
3
|
-
`@absolutejs/voice` is the voice
|
|
3
|
+
`@absolutejs/voice` is the self-hosted voice operations layer for AbsoluteJS.
|
|
4
|
+
|
|
5
|
+
It gives your app the primitives hosted voice platforms usually keep behind their dashboards: browser voice sessions, phone-call routes, provider routing, assistant tools, handoffs, traces, evals, production-readiness checks, latency proof, storage adapters, and framework-native UI helpers.
|
|
6
|
+
|
|
7
|
+
Use it when you want Vapi/Retell/Bland-style voice-agent capability, but you want the orchestration, data, traces, storage, and UI to live inside the AbsoluteJS server you already operate.
|
|
8
|
+
|
|
9
|
+
## Why AbsoluteJS Voice
|
|
10
|
+
|
|
11
|
+
- Self-hosted by default: your app owns sessions, traces, reviews, tasks, handoffs, retention, and provider keys.
|
|
12
|
+
- Provider-neutral: use Deepgram, AssemblyAI, OpenAI, Anthropic, Gemini, ElevenLabs-style TTS, or your own adapters without rewriting app workflow code.
|
|
13
|
+
- Browser and phone surfaces: mount browser WebSocket voice routes plus Twilio, Telnyx, and Plivo telephony routes from the same package.
|
|
14
|
+
- Production proof: ops status, ops recovery, production readiness, operations records, turn quality, turn latency, live browser p50/p95 latency, trace timelines, evals, fixtures, and contracts are package primitives.
|
|
15
|
+
- Framework parity: React, Vue, Svelte, Angular, HTML, HTMX, and plain client entrypoints share the same core behavior.
|
|
16
|
+
- No hosted platform tax: AbsoluteJS Voice does not add a mandatory per-minute orchestration fee between your app and your providers.
|
|
17
|
+
|
|
18
|
+
## Start Here
|
|
19
|
+
|
|
20
|
+
Pick the path that matches what you are building:
|
|
21
|
+
|
|
22
|
+
- Browser voice agent: mount `voice(...)`, choose an STT adapter, and use the React/Vue/Svelte/Angular/HTML/HTMX client helpers for mic, transcript, reconnect, and status UI.
|
|
23
|
+
- Phone voice agent: mount Twilio, Telnyx, or Plivo routes, normalize carrier outcomes, inspect carrier readiness, and persist call lifecycle traces.
|
|
24
|
+
- Outbound campaigns: create self-hosted campaign queues, import CSV/JSON recipients, enforce rate limits/quiet hours/retry backoff, dry-run carrier dialers, and fail production readiness when campaign proof regresses.
|
|
25
|
+
- Production readiness: mount the status and proof primitives you need, such as `createVoiceOpsStatusRoutes(...)`, `createVoiceProductionReadinessRoutes(...)`, quality routes, trace routes, eval routes, and smoke contracts.
|
|
26
|
+
- Support/debug entrypoint: mount `createVoiceOperationsRecordRoutes(...)` so every problematic session has one call-log object linking traces, replay, provider events, tools, handoffs, audit, reviews, tasks, and delivery attempts.
|
|
27
|
+
- Provider routing and fallback: use LLM/STT/TTS provider routers, provider health, provider simulation controls, and cost/latency-aware routing policies.
|
|
28
|
+
- Evals and simulation: mount `createVoiceSimulationSuiteRoutes(...)` to run scenario fixtures, workflow contracts, tool contracts, outcome contracts, baseline comparisons, and saved benchmark artifacts before live traffic.
|
|
29
|
+
|
|
30
|
+
## How This Differs From Hosted Voice Platforms
|
|
31
|
+
|
|
32
|
+
Hosted voice-agent platforms are strongest when you want a managed dashboard, phone-number provisioning, hosted orchestration, and campaign tooling out of the box.
|
|
33
|
+
|
|
34
|
+
AbsoluteJS Voice is strongest when voice is part of your own product and you need code-owned primitives:
|
|
35
|
+
|
|
36
|
+
- Your app stores the call data instead of a vendor dashboard being the source of truth.
|
|
37
|
+
- Your app controls provider routing, fallback, retries, handoffs, and retention.
|
|
38
|
+
- Your team can inspect and extend every primitive.
|
|
39
|
+
- Your framework UI can render first-class voice state without iframe/dashboard handoffs.
|
|
40
|
+
- Your production checks and evals can run in CI, smoke tests, or your own admin UI.
|
|
41
|
+
|
|
42
|
+
The goal is not to clone a hosted platform. The goal is to make AbsoluteJS the best place to build and operate self-hosted voice products.
|
|
4
43
|
|
|
5
44
|
## Install
|
|
6
45
|
|
|
7
46
|
```bash
|
|
8
|
-
bun add @absolutejs/voice
|
|
47
|
+
bun add @absolutejs/voice @absolutejs/voice-deepgram
|
|
9
48
|
```
|
|
10
49
|
|
|
11
50
|
Peer dependencies:
|
|
@@ -21,57 +60,851 @@ Optional framework entrypoints:
|
|
|
21
60
|
- `@absolutejs/voice/angular`
|
|
22
61
|
- `@absolutejs/voice/client`
|
|
23
62
|
|
|
24
|
-
|
|
63
|
+
Common optional adapters:
|
|
64
|
+
|
|
65
|
+
- `@absolutejs/voice-deepgram`
|
|
66
|
+
- `@absolutejs/voice-assemblyai`
|
|
67
|
+
|
|
68
|
+
## Browser Voice Agent
|
|
69
|
+
|
|
70
|
+
```ts
|
|
71
|
+
import { Elysia } from 'elysia';
|
|
72
|
+
import {
|
|
73
|
+
voice,
|
|
74
|
+
createVoiceMemoryStore,
|
|
75
|
+
createPhraseHintCorrectionHandler
|
|
76
|
+
} from '@absolutejs/voice';
|
|
77
|
+
import { deepgram } from '@absolutejs/voice-deepgram';
|
|
78
|
+
|
|
79
|
+
const app = new Elysia()
|
|
80
|
+
.use(
|
|
81
|
+
voice({
|
|
82
|
+
path: '/voice',
|
|
83
|
+
preset: 'guided-intake',
|
|
84
|
+
lexicon: [
|
|
85
|
+
{
|
|
86
|
+
text: 'AbsoluteJS',
|
|
87
|
+
aliases: ['absoloot js'],
|
|
88
|
+
pronunciation: 'ab-so-lute jay ess'
|
|
89
|
+
}
|
|
90
|
+
],
|
|
91
|
+
phraseHints: [
|
|
92
|
+
{ text: 'AbsoluteJS', aliases: ['absolute js'] },
|
|
93
|
+
{ text: 'Joe Johnston', aliases: ['joe johnson'] }
|
|
94
|
+
],
|
|
95
|
+
correctTurn: createPhraseHintCorrectionHandler(),
|
|
96
|
+
onComplete: async ({ session }) => {
|
|
97
|
+
console.log(session.turns);
|
|
98
|
+
},
|
|
99
|
+
async onTurn({ turn }) {
|
|
100
|
+
console.log('turn quality:', {
|
|
101
|
+
source: turn.quality?.source,
|
|
102
|
+
fallbackUsed: turn.quality?.fallbackUsed,
|
|
103
|
+
confidence: turn.quality?.averageConfidence
|
|
104
|
+
});
|
|
105
|
+
return {
|
|
106
|
+
assistantText: `You said: ${turn.text}`
|
|
107
|
+
};
|
|
108
|
+
},
|
|
109
|
+
session: createVoiceMemoryStore(),
|
|
110
|
+
stt: deepgram({
|
|
111
|
+
apiKey: process.env.DEEPGRAM_API_KEY!,
|
|
112
|
+
model: 'nova-3'
|
|
113
|
+
})
|
|
114
|
+
})
|
|
115
|
+
);
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
`createVoiceMemoryStore()` is dev-only. Real deployments should provide a shared store backed by Redis, Postgres, or equivalent.
|
|
119
|
+
|
|
120
|
+
## Production Readiness Path
|
|
121
|
+
|
|
122
|
+
Once the basic route works, mount the proof routes you need. These give you the self-hosted operational surfaces that hosted platforms usually make mandatory, without forcing a bundled app kit:
|
|
123
|
+
|
|
124
|
+
```ts
|
|
125
|
+
import {
|
|
126
|
+
createVoiceAuditDeliveryRoutes,
|
|
127
|
+
createVoiceDemoReadyRoutes,
|
|
128
|
+
createVoiceFileRuntimeStorage,
|
|
129
|
+
createVoiceLiveLatencyRoutes,
|
|
130
|
+
createVoiceOpsStatusRoutes,
|
|
131
|
+
createVoiceProductionReadinessRoutes,
|
|
132
|
+
createVoiceTraceDeliveryRoutes,
|
|
133
|
+
createVoiceTraceTimelineRoutes,
|
|
134
|
+
createVoiceTurnLatencyRoutes,
|
|
135
|
+
createVoiceTurnQualityRoutes
|
|
136
|
+
} from '@absolutejs/voice';
|
|
137
|
+
|
|
138
|
+
const runtime = createVoiceFileRuntimeStorage({
|
|
139
|
+
directory: '.voice-runtime/support'
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
app
|
|
143
|
+
.use(
|
|
144
|
+
createVoiceOpsStatusRoutes({
|
|
145
|
+
store: runtime.traces,
|
|
146
|
+
llmProviders: ['openai', 'anthropic', 'gemini'],
|
|
147
|
+
sttProviders: ['deepgram', 'assemblyai']
|
|
148
|
+
})
|
|
149
|
+
)
|
|
150
|
+
.use(
|
|
151
|
+
createVoiceTurnLatencyRoutes({
|
|
152
|
+
htmlPath: '/turn-latency',
|
|
153
|
+
path: '/api/turn-latency',
|
|
154
|
+
store: runtime.session,
|
|
155
|
+
traceStore: runtime.traces
|
|
156
|
+
})
|
|
157
|
+
)
|
|
158
|
+
.use(
|
|
159
|
+
createVoiceLiveLatencyRoutes({
|
|
160
|
+
htmlPath: '/live-latency',
|
|
161
|
+
path: '/api/live-latency',
|
|
162
|
+
store: runtime.traces
|
|
163
|
+
})
|
|
164
|
+
)
|
|
165
|
+
.use(
|
|
166
|
+
createVoiceTurnQualityRoutes({
|
|
167
|
+
htmlPath: '/turn-quality',
|
|
168
|
+
path: '/api/turn-quality',
|
|
169
|
+
store: runtime.session
|
|
170
|
+
})
|
|
171
|
+
)
|
|
172
|
+
.use(
|
|
173
|
+
createVoiceTraceTimelineRoutes({
|
|
174
|
+
htmlPath: '/traces',
|
|
175
|
+
path: '/api/voice-traces',
|
|
176
|
+
store: runtime.traces
|
|
177
|
+
})
|
|
178
|
+
)
|
|
179
|
+
.use(
|
|
180
|
+
createVoiceProductionReadinessRoutes({
|
|
181
|
+
audit: runtime.audit,
|
|
182
|
+
auditDeliveries: runtime.auditDeliveries,
|
|
183
|
+
htmlPath: '/production-readiness',
|
|
184
|
+
path: '/api/production-readiness',
|
|
185
|
+
store: runtime.traces,
|
|
186
|
+
traceDeliveries: runtime.traceDeliveries
|
|
187
|
+
})
|
|
188
|
+
)
|
|
189
|
+
.use(
|
|
190
|
+
createVoiceAuditDeliveryRoutes({
|
|
191
|
+
htmlPath: '/audit/deliveries',
|
|
192
|
+
path: '/api/voice-audit-deliveries',
|
|
193
|
+
store: runtime.auditDeliveries
|
|
194
|
+
})
|
|
195
|
+
)
|
|
196
|
+
.use(
|
|
197
|
+
createVoiceTraceDeliveryRoutes({
|
|
198
|
+
htmlPath: '/traces/deliveries',
|
|
199
|
+
path: '/api/voice-trace-deliveries',
|
|
200
|
+
store: runtime.traceDeliveries
|
|
201
|
+
})
|
|
202
|
+
);
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
Recommended proof routes:
|
|
206
|
+
|
|
207
|
+
- `/api/voice/ops-status`: compact status for hooks, widgets, and customer-facing demos.
|
|
208
|
+
- `/api/voice/ops-status/html`: HTML status card for quick internal review.
|
|
209
|
+
- `/demo-ready`: customer-facing demo readiness checklist.
|
|
210
|
+
- `/production-readiness`: production gate summary.
|
|
211
|
+
- `/audit/deliveries`: audit sink export queue and failed delivery details.
|
|
212
|
+
- `/voice/phone/smoke-contract`: trace-backed phone-agent production smoke proof.
|
|
213
|
+
- `/traces`: per-session trace timelines.
|
|
214
|
+
- `/traces/deliveries`: trace sink export queue and failed delivery details.
|
|
215
|
+
- `/turn-latency`: server-side turn-stage latency.
|
|
216
|
+
- `/live-latency`: browser-measured speech-to-assistant p50/p95 latency.
|
|
217
|
+
- `/turn-quality`: STT confidence, correction, fallback, and transcript diagnostics.
|
|
218
|
+
|
|
219
|
+
### Readiness Profiles
|
|
220
|
+
|
|
221
|
+
Use `createVoiceReadinessProfile(...)` when you want production-shaped defaults without adopting an app kit. Profiles are just spreadable route-option bundles for `createVoiceProductionReadinessRoutes(...)`; every option remains explicit and overrideable.
|
|
222
|
+
|
|
223
|
+
```ts
|
|
224
|
+
import {
|
|
225
|
+
createVoiceProductionReadinessRoutes,
|
|
226
|
+
createVoiceReadinessProfile
|
|
227
|
+
} from '@absolutejs/voice';
|
|
228
|
+
|
|
229
|
+
app.use(
|
|
230
|
+
createVoiceProductionReadinessRoutes({
|
|
231
|
+
...createVoiceReadinessProfile('meeting-recorder', {
|
|
232
|
+
bargeInReports: async () => [await buildBargeInReport()],
|
|
233
|
+
explain: true,
|
|
234
|
+
providerRoutingContracts: async () => [await runProviderRoutingContract()],
|
|
235
|
+
reconnectContracts: async () => [await runReconnectContract()]
|
|
236
|
+
}),
|
|
237
|
+
store: runtime.traces
|
|
238
|
+
})
|
|
239
|
+
);
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
Built-in profiles:
|
|
243
|
+
|
|
244
|
+
- `meeting-recorder`: live latency, session health, provider fallback, routing contracts, reconnect proof, and barge-in interruption proof.
|
|
245
|
+
- `phone-agent`: carrier readiness, phone-agent smoke proof, campaign readiness proof, handoffs, provider routing contracts, audit/trace delivery health, and delivery runtime proof.
|
|
246
|
+
- `ops-heavy`: audit evidence, operator action history, audit/trace delivery health, delivery runtime proof, and deploy-gate support.
|
|
247
|
+
|
|
248
|
+
Phone-agent fast path:
|
|
249
|
+
|
|
250
|
+
```ts
|
|
251
|
+
app.use(
|
|
252
|
+
createVoiceProductionReadinessRoutes({
|
|
253
|
+
...createVoiceReadinessProfile('phone-agent', {
|
|
254
|
+
auditDeliveries: runtime.auditDeliveries,
|
|
255
|
+
campaignReadiness: () =>
|
|
256
|
+
runVoiceCampaignReadinessProof({
|
|
257
|
+
store: runtime.campaigns
|
|
258
|
+
}),
|
|
259
|
+
carriers: loadCarrierMatrixInputs,
|
|
260
|
+
deliveryRuntime,
|
|
261
|
+
explain: true,
|
|
262
|
+
phoneAgentSmokes: async () => [await runPhoneSmoke()],
|
|
263
|
+
providerRoutingContracts: async () => [await runProviderRoutingContract()],
|
|
264
|
+
traceDeliveries: runtime.traceDeliveries
|
|
265
|
+
}),
|
|
266
|
+
store: runtime.traces
|
|
267
|
+
})
|
|
268
|
+
);
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
Ops-heavy fast path:
|
|
272
|
+
|
|
273
|
+
```ts
|
|
274
|
+
app.use(
|
|
275
|
+
createVoiceProductionReadinessRoutes({
|
|
276
|
+
...createVoiceReadinessProfile('ops-heavy', {
|
|
277
|
+
audit: runtime.audit,
|
|
278
|
+
auditDeliveries: runtime.auditDeliveries,
|
|
279
|
+
deliveryRuntime,
|
|
280
|
+
traceDeliveries: runtime.traceDeliveries
|
|
281
|
+
}),
|
|
282
|
+
gate: {
|
|
283
|
+
failOnWarnings: true
|
|
284
|
+
},
|
|
285
|
+
store: runtime.traces
|
|
286
|
+
})
|
|
287
|
+
);
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
The profile helper intentionally does not mount routes, create storage, start workers, or prescribe a deploy workflow. It only returns readiness options so teams can standardize defaults while keeping control over proof sources and route mounting.
|
|
291
|
+
|
|
292
|
+
Pass `explain: true` when the readiness JSON and HTML should describe the selected profile, its purpose, and which expected proof surfaces are configured or still missing. This is useful for customer demos and internal release reviews where the readiness URL needs to explain what it certifies without sending people to docs.
|
|
293
|
+
|
|
294
|
+
## Delivery Runtime Presets
|
|
295
|
+
|
|
296
|
+
Use `createVoiceDeliveryRuntimePresetConfig(...)` when you want one primitive to create paired audit and trace delivery workers for the same target. The preset returns a normal `VoiceDeliveryRuntimeConfig`, so you can still inspect or override worker options before passing it to `createVoiceDeliveryRuntime(...)`.
|
|
297
|
+
|
|
298
|
+
### File Delivery
|
|
299
|
+
|
|
300
|
+
Use file delivery for local demos, dev environments, or self-hosted deployments that collect exports from disk.
|
|
301
|
+
|
|
302
|
+
```ts
|
|
303
|
+
import {
|
|
304
|
+
createVoiceDeliveryRuntime,
|
|
305
|
+
createVoiceDeliveryRuntimePresetConfig,
|
|
306
|
+
createVoiceDeliveryRuntimeRoutes,
|
|
307
|
+
createVoiceFileRuntimeStorage
|
|
308
|
+
} from '@absolutejs/voice';
|
|
309
|
+
|
|
310
|
+
const runtimeStorage = createVoiceFileRuntimeStorage({
|
|
311
|
+
directory: '.voice-runtime/support'
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
const deliveryRuntime = createVoiceDeliveryRuntime(
|
|
315
|
+
createVoiceDeliveryRuntimePresetConfig({
|
|
316
|
+
auditDeliveries: runtimeStorage.auditDeliveries,
|
|
317
|
+
directory: '.voice-runtime/support/delivery-exports',
|
|
318
|
+
leases: createLeaseCoordinator(),
|
|
319
|
+
mode: 'file',
|
|
320
|
+
traceDeliveries: runtimeStorage.traceDeliveries
|
|
321
|
+
})
|
|
322
|
+
);
|
|
323
|
+
|
|
324
|
+
app.use(
|
|
325
|
+
createVoiceDeliveryRuntimeRoutes({
|
|
326
|
+
runtime: deliveryRuntime
|
|
327
|
+
})
|
|
328
|
+
);
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
### Webhook Delivery
|
|
332
|
+
|
|
333
|
+
Use webhook delivery when audit and trace exports should go to your own ingestion service, SIEM bridge, warehouse collector, or internal ops backend. The built-in HTTP sinks support retries, optional HMAC signing, custom headers, timeouts, and custom envelope bodies.
|
|
334
|
+
|
|
335
|
+
```ts
|
|
336
|
+
const deliveryRuntime = createVoiceDeliveryRuntime(
|
|
337
|
+
createVoiceDeliveryRuntimePresetConfig({
|
|
338
|
+
auditDeliveries: runtimeStorage.auditDeliveries,
|
|
339
|
+
auditSinkId: 'support-audit-webhook',
|
|
340
|
+
body: {
|
|
341
|
+
audit: ({ events }) => ({
|
|
342
|
+
eventCount: events.length,
|
|
343
|
+
events,
|
|
344
|
+
source: 'support-app',
|
|
345
|
+
surface: 'audit-deliveries'
|
|
346
|
+
}),
|
|
347
|
+
trace: ({ events }) => ({
|
|
348
|
+
eventCount: events.length,
|
|
349
|
+
events,
|
|
350
|
+
source: 'support-app',
|
|
351
|
+
surface: 'trace-deliveries'
|
|
352
|
+
})
|
|
353
|
+
},
|
|
354
|
+
failures: {
|
|
355
|
+
maxFailures: 3
|
|
356
|
+
},
|
|
357
|
+
leases: {
|
|
358
|
+
audit: createLeaseCoordinator(),
|
|
359
|
+
trace: createLeaseCoordinator()
|
|
360
|
+
},
|
|
361
|
+
mode: 'webhook',
|
|
362
|
+
signingSecret: process.env.VOICE_DELIVERY_WEBHOOK_SECRET,
|
|
363
|
+
traceDeliveries: runtimeStorage.traceDeliveries,
|
|
364
|
+
traceSinkId: 'support-trace-webhook',
|
|
365
|
+
url: process.env.VOICE_DELIVERY_WEBHOOK_URL!
|
|
366
|
+
})
|
|
367
|
+
);
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
### S3 Delivery
|
|
371
|
+
|
|
372
|
+
Use S3 delivery when exports should land directly in object storage through Bun's native S3 client. Set `bucket` and `keyPrefix`; the preset writes audit and trace exports under separate prefixes.
|
|
373
|
+
|
|
374
|
+
```ts
|
|
375
|
+
const deliveryRuntime = createVoiceDeliveryRuntime(
|
|
376
|
+
createVoiceDeliveryRuntimePresetConfig({
|
|
377
|
+
auditDeliveries: runtimeStorage.auditDeliveries,
|
|
378
|
+
auditSinkId: 'support-audit-s3',
|
|
379
|
+
bucket: process.env.VOICE_DELIVERY_S3_BUCKET,
|
|
380
|
+
failures: {
|
|
381
|
+
maxFailures: 3
|
|
382
|
+
},
|
|
383
|
+
keyPrefix: 'support/voice-deliveries',
|
|
384
|
+
leases: {
|
|
385
|
+
audit: createLeaseCoordinator(),
|
|
386
|
+
trace: createLeaseCoordinator()
|
|
387
|
+
},
|
|
388
|
+
mode: 's3',
|
|
389
|
+
traceDeliveries: runtimeStorage.traceDeliveries,
|
|
390
|
+
traceSinkId: 'support-trace-s3'
|
|
391
|
+
})
|
|
392
|
+
);
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
Mount `createVoiceDeliveryRuntimeRoutes({ runtime: deliveryRuntime })` to expose:
|
|
396
|
+
|
|
397
|
+
- `/api/voice-delivery-runtime`: combined audit and trace worker summary.
|
|
398
|
+
- `/api/voice-delivery-runtime/tick`: manual tick for both workers.
|
|
399
|
+
- `/delivery-runtime`: HTML worker control plane.
|
|
400
|
+
|
|
401
|
+
Pass the same runtime to production readiness so failed, dead-lettered, or pending export queues become deploy-blocking evidence:
|
|
402
|
+
|
|
403
|
+
```ts
|
|
404
|
+
app.use(
|
|
405
|
+
createVoiceProductionReadinessRoutes({
|
|
406
|
+
auditDeliveries: runtimeStorage.auditDeliveries,
|
|
407
|
+
deliveryRuntime,
|
|
408
|
+
links: {
|
|
409
|
+
deliveryRuntime: '/delivery-runtime'
|
|
410
|
+
},
|
|
411
|
+
store: runtimeStorage.traces,
|
|
412
|
+
traceDeliveries: runtimeStorage.traceDeliveries
|
|
413
|
+
})
|
|
414
|
+
);
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
## Simulation Suite Path
|
|
418
|
+
|
|
419
|
+
Use `createVoiceSimulationSuiteRoutes(...)` when you want one pre-production proof surface for the things that usually live in separate dashboards or scripts:
|
|
420
|
+
|
|
421
|
+
```ts
|
|
422
|
+
import {
|
|
423
|
+
createVoiceSimulationSuiteRoutes,
|
|
424
|
+
createVoiceFileRuntimeStorage
|
|
425
|
+
} from '@absolutejs/voice';
|
|
426
|
+
|
|
427
|
+
const runtime = createVoiceFileRuntimeStorage({
|
|
428
|
+
directory: '.voice-runtime/support'
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
app.use(
|
|
432
|
+
createVoiceSimulationSuiteRoutes({
|
|
433
|
+
htmlPath: '/voice/simulations',
|
|
434
|
+
path: '/api/voice/simulations',
|
|
435
|
+
store: runtime.traces,
|
|
436
|
+
scenarios: workflowScenarios,
|
|
437
|
+
fixtureStore: scenarioFixtureStore,
|
|
438
|
+
tools: toolContracts,
|
|
439
|
+
outcomes: {
|
|
440
|
+
contracts: outcomeContracts,
|
|
441
|
+
events: runtime.events,
|
|
442
|
+
reviews: runtime.reviews,
|
|
443
|
+
sessions: runtime.session,
|
|
444
|
+
tasks: runtime.tasks
|
|
445
|
+
}
|
|
446
|
+
})
|
|
447
|
+
);
|
|
448
|
+
```
|
|
449
|
+
|
|
450
|
+
The suite rolls up session quality, scenario evals, fixture simulations, tool contracts, and outcome contracts into one pass/fail report. It is the code-owned equivalent of "test this voice flow before production" without requiring a hosted voice-agent dashboard.
|
|
451
|
+
|
|
452
|
+
## Self-Hosted Campaigns
|
|
453
|
+
|
|
454
|
+
Use `createVoiceCampaignRoutes(...)` when you need Retell/Bland-style outbound campaign primitives without giving a hosted dialer ownership of recipients, attempts, outcomes, or readiness proof.
|
|
455
|
+
|
|
456
|
+
```ts
|
|
457
|
+
import {
|
|
458
|
+
createVoiceCampaignRoutes,
|
|
459
|
+
createVoiceProductionReadinessRoutes,
|
|
460
|
+
createVoiceReadinessProfile,
|
|
461
|
+
createVoiceSQLiteCampaignStore,
|
|
462
|
+
runVoiceCampaignReadinessProof
|
|
463
|
+
} from '@absolutejs/voice';
|
|
464
|
+
|
|
465
|
+
const campaigns = createVoiceSQLiteCampaignStore({
|
|
466
|
+
path: '.voice-runtime/campaigns.sqlite'
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
app.use(
|
|
470
|
+
createVoiceCampaignRoutes({
|
|
471
|
+
htmlPath: '/voice/campaigns',
|
|
472
|
+
path: '/api/voice/campaigns',
|
|
473
|
+
store: campaigns,
|
|
474
|
+
title: 'Outbound Campaigns'
|
|
475
|
+
})
|
|
476
|
+
);
|
|
477
|
+
```
|
|
478
|
+
|
|
479
|
+
The campaign runtime gives you explicit primitives instead of a campaign app kit:
|
|
480
|
+
|
|
481
|
+
- `importVoiceCampaignRecipients(...)`: validates CSV/JSON rows, phone numbers, consent, duplicates, variables, and metadata.
|
|
482
|
+
- `VoiceCampaignRuntime.importRecipients(...)`: persists accepted recipients and returns rejected-row evidence.
|
|
483
|
+
- `tick(...)`: enforces campaign status, max concurrency, attempt windows, quiet hours, rolling rate limits, retry backoff, and `maxAttempts`.
|
|
484
|
+
- `pause(...)`, `resume(...)`, `cancel(...)`: operator-safe campaign controls.
|
|
485
|
+
- `applyVoiceCampaignTelephonyOutcome(...)`: maps Twilio/Telnyx/Plivo webhook decisions back into campaign attempts.
|
|
486
|
+
- `buildVoiceCampaignObservabilityReport(...)`: queue depth, active attempts, leases, attempt rates, failures, and stuck work.
|
|
487
|
+
|
|
488
|
+
Import recipients through the route API:
|
|
489
|
+
|
|
490
|
+
```ts
|
|
491
|
+
await fetch('/api/voice/campaigns/campaign-1/recipients/import', {
|
|
492
|
+
body: JSON.stringify({
|
|
493
|
+
csv: `id,name,phone,consent,segment
|
|
494
|
+
recipient-1,Ada,+15550001001,yes,trial
|
|
495
|
+
recipient-2,Grace,+15550001002,true,enterprise`,
|
|
496
|
+
requireConsent: true,
|
|
497
|
+
variableColumns: ['segment']
|
|
498
|
+
}),
|
|
499
|
+
headers: {
|
|
500
|
+
'content-type': 'application/json'
|
|
501
|
+
},
|
|
502
|
+
method: 'POST'
|
|
503
|
+
});
|
|
504
|
+
```
|
|
505
|
+
|
|
506
|
+
Create campaigns with scheduling controls:
|
|
507
|
+
|
|
508
|
+
```ts
|
|
509
|
+
await runtime.create({
|
|
510
|
+
maxAttempts: 3,
|
|
511
|
+
maxConcurrentAttempts: 10,
|
|
512
|
+
name: 'Renewal outreach',
|
|
513
|
+
schedule: {
|
|
514
|
+
attemptWindow: { startHour: 9, endHour: 17 },
|
|
515
|
+
quietHours: { startHour: 12, endHour: 13 },
|
|
516
|
+
rateLimit: { maxAttempts: 60, windowMs: 60_000 },
|
|
517
|
+
retryPolicy: { backoffMs: [5 * 60_000, 30 * 60_000] }
|
|
518
|
+
}
|
|
519
|
+
});
|
|
520
|
+
```
|
|
521
|
+
|
|
522
|
+
Certify the campaign path without live carrier traffic:
|
|
523
|
+
|
|
524
|
+
```ts
|
|
525
|
+
const campaignReadiness = await runVoiceCampaignReadinessProof({
|
|
526
|
+
store: campaigns
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
if (!campaignReadiness.ok) {
|
|
530
|
+
throw new Error(
|
|
531
|
+
campaignReadiness.checks
|
|
532
|
+
.filter((check) => check.status !== 'pass')
|
|
533
|
+
.map((check) => check.name)
|
|
534
|
+
.join('\n')
|
|
535
|
+
);
|
|
536
|
+
}
|
|
537
|
+
```
|
|
538
|
+
|
|
539
|
+
Pass that proof into production readiness so campaign regressions block deploys:
|
|
540
|
+
|
|
541
|
+
```ts
|
|
542
|
+
app.use(
|
|
543
|
+
createVoiceProductionReadinessRoutes({
|
|
544
|
+
...createVoiceReadinessProfile('phone-agent', {
|
|
545
|
+
campaignReadiness: () =>
|
|
546
|
+
runVoiceCampaignReadinessProof({
|
|
547
|
+
store: campaigns
|
|
548
|
+
}),
|
|
549
|
+
explain: true
|
|
550
|
+
}),
|
|
551
|
+
store: runtime.traces
|
|
552
|
+
})
|
|
553
|
+
);
|
|
554
|
+
```
|
|
555
|
+
|
|
556
|
+
For carrier-specific outbound dialing, use `createVoiceTwilioCampaignDialer(...)`, `createVoiceTelnyxCampaignDialer(...)`, or `createVoicePlivoCampaignDialer(...)` as the campaign `dialer`. `runVoiceCampaignDialerProof(...)` dry-runs those provider requests with intercepted fetch calls and synthetic webhook outcomes, so you can prove metadata and outcome application before a real campaign sends traffic.
|
|
557
|
+
|
|
558
|
+
## Phone Voice Agent In 20 Minutes
|
|
559
|
+
|
|
560
|
+
Use `createVoicePhoneAgent(...)` when the agent needs to answer or place calls through your own Twilio, Telnyx, or Plivo account. This is the self-hosted alternative to a hosted phone-agent dashboard: your app owns the carrier routes, stream URLs, webhooks, traces, readiness checks, and lifecycle outcomes.
|
|
561
|
+
|
|
562
|
+
```ts
|
|
563
|
+
import {
|
|
564
|
+
createVoicePhoneAgent,
|
|
565
|
+
createVoiceTelephonyOutcomePolicy,
|
|
566
|
+
runVoicePhoneAgentProductionSmokeContract
|
|
567
|
+
} from '@absolutejs/voice';
|
|
568
|
+
import { deepgram } from '@absolutejs/voice-deepgram';
|
|
569
|
+
|
|
570
|
+
const outcomePolicy = createVoiceTelephonyOutcomePolicy({
|
|
571
|
+
transferTarget: '+15551234567'
|
|
572
|
+
});
|
|
573
|
+
|
|
574
|
+
app
|
|
575
|
+
.use(
|
|
576
|
+
createVoicePhoneAgent({
|
|
577
|
+
setup: {
|
|
578
|
+
path: '/api/voice/phone/setup',
|
|
579
|
+
title: 'Support Phone Agent'
|
|
580
|
+
},
|
|
581
|
+
matrix: {
|
|
582
|
+
path: '/api/carriers',
|
|
583
|
+
title: 'AbsoluteJS Voice Carrier Matrix'
|
|
584
|
+
},
|
|
585
|
+
productionSmoke: {
|
|
586
|
+
maxAgeMs: 24 * 60 * 60 * 1000,
|
|
587
|
+
required: [
|
|
588
|
+
'carrier-contract',
|
|
589
|
+
'media-started',
|
|
590
|
+
'transcript',
|
|
591
|
+
'assistant-response',
|
|
592
|
+
'lifecycle-outcome',
|
|
593
|
+
'no-session-error',
|
|
594
|
+
'fresh-trace'
|
|
595
|
+
],
|
|
596
|
+
store: runtime.traces
|
|
597
|
+
},
|
|
598
|
+
carriers: [
|
|
599
|
+
{
|
|
600
|
+
provider: 'twilio',
|
|
601
|
+
options: {
|
|
602
|
+
context: {},
|
|
603
|
+
outcomePolicy,
|
|
604
|
+
session: runtime.session,
|
|
605
|
+
stt: deepgram({ apiKey: process.env.DEEPGRAM_API_KEY! }),
|
|
606
|
+
streamPath: '/api/voice/twilio/stream',
|
|
607
|
+
twiml: {
|
|
608
|
+
path: '/api/voice/twilio',
|
|
609
|
+
streamUrl: process.env.TWILIO_STREAM_URL
|
|
610
|
+
},
|
|
611
|
+
webhook: {
|
|
612
|
+
path: '/api/voice/twilio/webhook',
|
|
613
|
+
signingSecret: process.env.TWILIO_AUTH_TOKEN
|
|
614
|
+
},
|
|
615
|
+
async onTurn({ turn }) {
|
|
616
|
+
return { assistantText: `I heard: ${turn.text}` };
|
|
617
|
+
},
|
|
618
|
+
onComplete: async () => {}
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
]
|
|
622
|
+
}).routes
|
|
623
|
+
);
|
|
624
|
+
```
|
|
625
|
+
|
|
626
|
+
The wrapper mounts selected carrier routes plus two proof surfaces:
|
|
627
|
+
|
|
628
|
+
- `/api/voice/phone/setup`: one setup report with carrier URLs, smoke links, lifecycle stages, and readiness.
|
|
629
|
+
- `/api/voice/phone/setup?format=html`: copy/paste setup page for carrier dashboards.
|
|
630
|
+
- `/api/carriers`: carrier matrix JSON for Twilio, Telnyx, and Plivo.
|
|
631
|
+
- `/api/carriers?format=html`: side-by-side carrier readiness matrix.
|
|
632
|
+
- `/api/voice/phone/smoke-contract?sessionId=...`: trace-backed production smoke contract.
|
|
633
|
+
- `/voice/phone/smoke-contract?sessionId=...`: HTML production smoke contract.
|
|
634
|
+
|
|
635
|
+
The setup page tells you exactly what to copy into the carrier dashboard:
|
|
636
|
+
|
|
637
|
+
- Twilio: set the phone number voice webhook/TwiML URL to the reported TwiML URL, set the status callback to the reported webhook URL, and allow the reported `wss://` media stream.
|
|
638
|
+
- Telnyx: set the connection TeXML URL to the reported TeXML URL, set the status webhook to the reported webhook URL, and allow the reported `wss://` media stream.
|
|
639
|
+
- Plivo: set the answer URL to the reported answer URL, set the status callback to the reported webhook URL, and allow the reported `wss://` media stream.
|
|
640
|
+
|
|
641
|
+
Each configured carrier can also expose its own setup and smoke pages, for example:
|
|
642
|
+
|
|
643
|
+
- `/api/voice/twilio/setup?format=html`
|
|
644
|
+
- `/api/voice/twilio/smoke?format=html`
|
|
645
|
+
- `/api/voice/telnyx/setup?format=html`
|
|
646
|
+
- `/api/voice/telnyx/smoke?format=html`
|
|
647
|
+
- `/api/voice/plivo/setup?format=html`
|
|
648
|
+
- `/api/voice/plivo/smoke?format=html`
|
|
649
|
+
|
|
650
|
+
The phone-agent report normalizes the lifecycle schema across carriers:
|
|
651
|
+
|
|
652
|
+
- `ringing`
|
|
653
|
+
- `answered`
|
|
654
|
+
- `media-started`
|
|
655
|
+
- `transcript`
|
|
656
|
+
- `assistant-response`
|
|
657
|
+
- `transfer`
|
|
658
|
+
- `voicemail`
|
|
659
|
+
- `no-answer`
|
|
660
|
+
- `completed`
|
|
661
|
+
- `failed`
|
|
662
|
+
|
|
663
|
+
That is the important Vapi/Retell/Bland gap this primitive closes: a team can mount one phone-agent entrypoint, bring its own carrier account, verify readiness before live calls, and keep call traces and lifecycle outcomes inside its own AbsoluteJS app. Telnyx and Plivo use the same wrapper with `{ provider: 'telnyx', options: ... }` or `{ provider: 'plivo', options: ... }`. The lower-level `createTwilioVoiceRoutes(...)`, `createTelnyxVoiceRoutes(...)`, and `createPlivoVoiceRoutes(...)` helpers remain available when you need carrier-specific control.
|
|
664
|
+
|
|
665
|
+
After running a real smoke call, certify the phone-agent path from traces:
|
|
666
|
+
|
|
667
|
+
```ts
|
|
668
|
+
const smoke = await runVoicePhoneAgentProductionSmokeContract({
|
|
669
|
+
maxAgeMs: 24 * 60 * 60 * 1000,
|
|
670
|
+
required: [
|
|
671
|
+
'media-started',
|
|
672
|
+
'transcript',
|
|
673
|
+
'assistant-response',
|
|
674
|
+
'lifecycle-outcome',
|
|
675
|
+
'no-session-error',
|
|
676
|
+
'fresh-trace'
|
|
677
|
+
],
|
|
678
|
+
sessionId: 'phone-smoke-session',
|
|
679
|
+
store: runtime.traces
|
|
680
|
+
});
|
|
681
|
+
|
|
682
|
+
if (!smoke.pass) {
|
|
683
|
+
throw new Error(smoke.issues.map((issue) => issue.message).join('\n'));
|
|
684
|
+
}
|
|
685
|
+
```
|
|
686
|
+
|
|
687
|
+
Pass those reports into production readiness through `phoneAgentSmokes`. This makes deployment fail when the carrier setup exists but the actual phone-agent call path did not produce media start, transcript, assistant response, terminal lifecycle outcome, and clean trace evidence.
|
|
688
|
+
|
|
689
|
+
When `productionSmoke` is enabled on `createVoicePhoneAgent(...)`, the wrapper mounts `/api/voice/phone/smoke-contract?sessionId=...` for JSON and `/voice/phone/smoke-contract?sessionId=...` for HTML. It also derives carrier contract evidence from the existing carrier matrix unless you provide a custom `getContract`.
|
|
690
|
+
|
|
691
|
+
## Ops Status Hooks And Widgets
|
|
692
|
+
|
|
693
|
+
Use `createVoiceOpsStatusRoutes(...)` when you want a small status endpoint for demos, admin pages, and framework widgets. It is intentionally not a route bundle: mount quality gates, eval routes, provider health, session replay, phone-agent smoke proof, handoff health, and diagnostics explicitly when your app needs them.
|
|
694
|
+
|
|
695
|
+
```ts
|
|
696
|
+
import {
|
|
697
|
+
createVoiceDemoReadyRoutes,
|
|
698
|
+
createVoiceFileRuntimeStorage,
|
|
699
|
+
createVoiceOpsStatusRoutes,
|
|
700
|
+
summarizeVoiceOpsStatus
|
|
701
|
+
} from '@absolutejs/voice';
|
|
702
|
+
|
|
703
|
+
const runtime = createVoiceFileRuntimeStorage({ directory: '.voice-runtime/support' });
|
|
704
|
+
|
|
705
|
+
app.use(
|
|
706
|
+
createVoiceOpsStatusRoutes({
|
|
707
|
+
store: runtime.traces,
|
|
708
|
+
llmProviders: ['openai', 'anthropic', 'gemini'],
|
|
709
|
+
sttProviders: ['deepgram', 'assemblyai']
|
|
710
|
+
})
|
|
711
|
+
);
|
|
712
|
+
```
|
|
713
|
+
|
|
714
|
+
The status endpoint is intentionally small enough for customer-facing demos. It can report fixture-backed workflow readiness while leaving deeper live quality/session failures visible on the proof routes you mount separately.
|
|
715
|
+
|
|
716
|
+
For a single demo page that rolls up ops status, production readiness, phone setup, and phone smoke proof, mount `createVoiceDemoReadyRoutes(...)` with the same reports you already expose elsewhere:
|
|
717
|
+
|
|
718
|
+
```ts
|
|
719
|
+
app.use(
|
|
720
|
+
createVoiceDemoReadyRoutes({
|
|
721
|
+
opsStatus: {
|
|
722
|
+
href: '/api/voice/ops-status',
|
|
723
|
+
load: () => summarizeVoiceOpsStatus(opsStatusOptions)
|
|
724
|
+
},
|
|
725
|
+
phoneSetup: {
|
|
726
|
+
href: '/api/voice/phone/setup?format=html',
|
|
727
|
+
load: () => phoneAgentSetupReport
|
|
728
|
+
},
|
|
729
|
+
phoneSmoke: {
|
|
730
|
+
href: '/voice/phone/smoke-contract',
|
|
731
|
+
load: () => phoneSmokeReport
|
|
732
|
+
},
|
|
733
|
+
productionReadiness: {
|
|
734
|
+
href: '/production-readiness',
|
|
735
|
+
load: () => productionReadinessReport
|
|
736
|
+
}
|
|
737
|
+
})
|
|
738
|
+
);
|
|
739
|
+
```
|
|
740
|
+
|
|
741
|
+
```ts
|
|
742
|
+
app.use(
|
|
743
|
+
createVoiceOpsStatusRoutes({
|
|
744
|
+
include: { quality: false, sessions: false },
|
|
745
|
+
preferFixtureWorkflows: true,
|
|
746
|
+
store: runtime.traces
|
|
747
|
+
})
|
|
748
|
+
);
|
|
749
|
+
```
|
|
750
|
+
|
|
751
|
+
### React Status Widget
|
|
752
|
+
|
|
753
|
+
```tsx
|
|
754
|
+
import { VoiceOpsStatus } from '@absolutejs/voice/react';
|
|
755
|
+
|
|
756
|
+
export function OpsBadge() {
|
|
757
|
+
return <VoiceOpsStatus intervalMs={5000} />;
|
|
758
|
+
}
|
|
759
|
+
```
|
|
760
|
+
|
|
761
|
+
### Vue Status Widget
|
|
762
|
+
|
|
763
|
+
```vue
|
|
764
|
+
<script setup lang="ts">
|
|
765
|
+
import { VoiceOpsStatus } from '@absolutejs/voice/vue';
|
|
766
|
+
</script>
|
|
767
|
+
|
|
768
|
+
<template>
|
|
769
|
+
<VoiceOpsStatus :interval-ms="5000" />
|
|
770
|
+
</template>
|
|
771
|
+
```
|
|
772
|
+
|
|
773
|
+
### Svelte Status Widget
|
|
774
|
+
|
|
775
|
+
```svelte
|
|
776
|
+
<script lang="ts">
|
|
777
|
+
import { onDestroy, onMount } from 'svelte';
|
|
778
|
+
import { createVoiceOpsStatus } from '@absolutejs/voice/svelte';
|
|
779
|
+
|
|
780
|
+
const status = createVoiceOpsStatus('/api/voice/ops-status', { intervalMs: 5000 });
|
|
781
|
+
let html = '';
|
|
782
|
+
onMount(() => status.subscribe(() => (html = status.getHTML())));
|
|
783
|
+
onDestroy(() => status.close());
|
|
784
|
+
</script>
|
|
785
|
+
|
|
786
|
+
{@html html}
|
|
787
|
+
```
|
|
788
|
+
|
|
789
|
+
### Angular Status Widget
|
|
790
|
+
|
|
791
|
+
```ts
|
|
792
|
+
import { VoiceOpsStatusService } from '@absolutejs/voice/angular';
|
|
793
|
+
|
|
794
|
+
status = inject(VoiceOpsStatusService).connect('/api/voice/ops-status', {
|
|
795
|
+
intervalMs: 5000
|
|
796
|
+
});
|
|
797
|
+
```
|
|
798
|
+
|
|
799
|
+
```html
|
|
800
|
+
<h2>{{ status.report()?.status === 'pass' ? 'Passing' : 'Needs attention' }}</h2>
|
|
801
|
+
<p>{{ status.report()?.passed ?? 0 }} passing checks</p>
|
|
802
|
+
```
|
|
803
|
+
|
|
804
|
+
### HTML Or HTMX Status Widget
|
|
805
|
+
|
|
806
|
+
```html
|
|
807
|
+
<div id="voice-ops-status"></div>
|
|
808
|
+
<script type="module">
|
|
809
|
+
import { mountVoiceOpsStatus } from '@absolutejs/voice/client';
|
|
810
|
+
|
|
811
|
+
mountVoiceOpsStatus(document.querySelector('#voice-ops-status'), '/api/voice/ops-status', {
|
|
812
|
+
intervalMs: 5000
|
|
813
|
+
});
|
|
814
|
+
</script>
|
|
815
|
+
```
|
|
816
|
+
|
|
817
|
+
For custom elements:
|
|
818
|
+
|
|
819
|
+
```html
|
|
820
|
+
<absolute-voice-ops-status interval-ms="5000"></absolute-voice-ops-status>
|
|
821
|
+
<script type="module">
|
|
822
|
+
import { defineVoiceOpsStatusElement } from '@absolutejs/voice/client';
|
|
823
|
+
defineVoiceOpsStatusElement();
|
|
824
|
+
</script>
|
|
825
|
+
```
|
|
826
|
+
|
|
827
|
+
## Delivery Runtime Widgets
|
|
828
|
+
|
|
829
|
+
After mounting `createVoiceDeliveryRuntimeRoutes(...)`, apps can expose audit and trace worker health through the same framework-native primitives:
|
|
830
|
+
|
|
831
|
+
```tsx
|
|
832
|
+
import { VoiceDeliveryRuntime } from '@absolutejs/voice/react';
|
|
833
|
+
|
|
834
|
+
export function DeliveryWorkers() {
|
|
835
|
+
return <VoiceDeliveryRuntime intervalMs={5000} />;
|
|
836
|
+
}
|
|
837
|
+
```
|
|
838
|
+
|
|
839
|
+
The widget includes operator actions by default: `Tick workers` drains pending/failed deliveries, and `Requeue dead letters` moves reviewed dead-lettered audit/trace deliveries back into the live queues. Pass `includeActions={false}` when you only want a read-only status card.
|
|
840
|
+
|
|
841
|
+
```ts
|
|
842
|
+
import { VoiceDeliveryRuntime } from '@absolutejs/voice/vue';
|
|
843
|
+
import { createVoiceDeliveryRuntime } from '@absolutejs/voice/svelte';
|
|
844
|
+
import { VoiceDeliveryRuntimeService } from '@absolutejs/voice/angular';
|
|
845
|
+
```
|
|
846
|
+
|
|
847
|
+
For HTML or HTMX pages:
|
|
848
|
+
|
|
849
|
+
```html
|
|
850
|
+
<absolute-voice-delivery-runtime interval-ms="5000"></absolute-voice-delivery-runtime>
|
|
851
|
+
<script type="module">
|
|
852
|
+
import { defineVoiceDeliveryRuntimeElement } from '@absolutejs/voice/client';
|
|
853
|
+
defineVoiceDeliveryRuntimeElement();
|
|
854
|
+
</script>
|
|
855
|
+
```
|
|
856
|
+
|
|
857
|
+
## Voice Ops Action Center
|
|
858
|
+
|
|
859
|
+
Use `VoiceOpsActionCenter` when you want one primitive operator panel for production proofs and recovery actions without building a dashboard. The default action builder can include production readiness refresh, delivery worker ticks, dead-letter requeue, turn-latency proof, and provider failover simulation.
|
|
860
|
+
|
|
861
|
+
```tsx
|
|
862
|
+
import { VoiceOpsActionCenter } from '@absolutejs/voice/react';
|
|
863
|
+
import { createVoiceOpsActionCenterActions } from '@absolutejs/voice/client';
|
|
864
|
+
|
|
865
|
+
export function OperatorPanel() {
|
|
866
|
+
return (
|
|
867
|
+
<VoiceOpsActionCenter
|
|
868
|
+
actions={createVoiceOpsActionCenterActions({
|
|
869
|
+
providers: ['deepgram', 'assemblyai']
|
|
870
|
+
})}
|
|
871
|
+
/>
|
|
872
|
+
);
|
|
873
|
+
}
|
|
874
|
+
```
|
|
875
|
+
|
|
876
|
+
Mount `createVoiceOpsActionAuditRoutes(...)` to make every action-center click auditable. The client posts successful and failed action results to `/api/voice/ops-actions/audit` by default, and the route records both `operator.action` audit events and `operator.action` trace events.
|
|
25
877
|
|
|
26
878
|
```ts
|
|
27
|
-
import {
|
|
28
|
-
import {
|
|
29
|
-
voice,
|
|
30
|
-
createVoiceMemoryStore,
|
|
31
|
-
createPhraseHintCorrectionHandler
|
|
32
|
-
} from '@absolutejs/voice';
|
|
33
|
-
import { deepgram } from '@absolutejs/voice-deepgram';
|
|
879
|
+
import { createVoiceOpsActionAuditRoutes } from '@absolutejs/voice';
|
|
34
880
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
{
|
|
42
|
-
text: 'AbsoluteJS',
|
|
43
|
-
aliases: ['absoloot js'],
|
|
44
|
-
pronunciation: 'ab-so-lute jay ess'
|
|
45
|
-
}
|
|
46
|
-
],
|
|
47
|
-
phraseHints: [
|
|
48
|
-
{ text: 'AbsoluteJS', aliases: ['absolute js'] },
|
|
49
|
-
{ text: 'Joe Johnston', aliases: ['joe johnson'] }
|
|
50
|
-
],
|
|
51
|
-
correctTurn: createPhraseHintCorrectionHandler(),
|
|
52
|
-
onComplete: async ({ session }) => {
|
|
53
|
-
console.log(session.turns);
|
|
54
|
-
},
|
|
55
|
-
async onTurn({ turn }) {
|
|
56
|
-
console.log('turn quality:', {
|
|
57
|
-
source: turn.quality?.source,
|
|
58
|
-
fallbackUsed: turn.quality?.fallbackUsed,
|
|
59
|
-
confidence: turn.quality?.averageConfidence
|
|
60
|
-
});
|
|
61
|
-
return {
|
|
62
|
-
assistantText: `You said: ${turn.text}`
|
|
63
|
-
};
|
|
64
|
-
},
|
|
65
|
-
session: createVoiceMemoryStore(),
|
|
66
|
-
stt: deepgram({
|
|
67
|
-
apiKey: process.env.DEEPGRAM_API_KEY!,
|
|
68
|
-
model: 'nova-3'
|
|
69
|
-
})
|
|
70
|
-
})
|
|
71
|
-
);
|
|
881
|
+
app.use(
|
|
882
|
+
createVoiceOpsActionAuditRoutes({
|
|
883
|
+
audit: runtimeStorage.audit,
|
|
884
|
+
trace: runtimeStorage.traces
|
|
885
|
+
})
|
|
886
|
+
);
|
|
72
887
|
```
|
|
73
888
|
|
|
74
|
-
`
|
|
889
|
+
The same route exposes `GET /api/voice/ops-actions/history` and `/voice/ops-actions` so apps can show recent operator actions beside the action center. For HTML or HTMX pages, use `mountVoiceOpsActionHistory(...)` from `@absolutejs/voice/client`.
|
|
890
|
+
|
|
891
|
+
For HTML or HTMX pages:
|
|
892
|
+
|
|
893
|
+
```html
|
|
894
|
+
<div id="voice-ops-actions"></div>
|
|
895
|
+
<script type="module">
|
|
896
|
+
import {
|
|
897
|
+
createVoiceOpsActionCenterActions,
|
|
898
|
+
mountVoiceOpsActionCenter
|
|
899
|
+
} from '@absolutejs/voice/client';
|
|
900
|
+
|
|
901
|
+
mountVoiceOpsActionCenter(document.querySelector('#voice-ops-actions'), {
|
|
902
|
+
actions: createVoiceOpsActionCenterActions({
|
|
903
|
+
providers: ['deepgram']
|
|
904
|
+
})
|
|
905
|
+
});
|
|
906
|
+
</script>
|
|
907
|
+
```
|
|
75
908
|
|
|
76
909
|
## Voice Assistants
|
|
77
910
|
|
|
@@ -212,7 +1045,34 @@ const billingAgent = createVoiceAgent({
|
|
|
212
1045
|
const frontDesk = createVoiceAgentSquad({
|
|
213
1046
|
id: 'front-desk',
|
|
214
1047
|
defaultAgentId: 'support',
|
|
215
|
-
agents: [supportAgent, billingAgent]
|
|
1048
|
+
agents: [supportAgent, billingAgent],
|
|
1049
|
+
contextPolicy: ({ summaryMessage, turn }) => ({
|
|
1050
|
+
messages: [
|
|
1051
|
+
summaryMessage,
|
|
1052
|
+
{
|
|
1053
|
+
content: turn.text,
|
|
1054
|
+
role: 'user'
|
|
1055
|
+
}
|
|
1056
|
+
],
|
|
1057
|
+
metadata: {
|
|
1058
|
+
contextPolicy: 'handoff-summary-and-current-turn'
|
|
1059
|
+
},
|
|
1060
|
+
system: 'Use only the handoff summary and current caller turn.'
|
|
1061
|
+
}),
|
|
1062
|
+
handoffPolicy: ({ handoff }) => {
|
|
1063
|
+
if (handoff.targetAgentId === 'billing') {
|
|
1064
|
+
return {
|
|
1065
|
+
summary: 'Route verified billing requests to the billing specialist.',
|
|
1066
|
+
metadata: { queue: 'billing' }
|
|
1067
|
+
};
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
return {
|
|
1071
|
+
allow: false,
|
|
1072
|
+
reason: `No approved route for ${handoff.targetAgentId}.`,
|
|
1073
|
+
escalate: { reason: 'unsupported-specialist' }
|
|
1074
|
+
};
|
|
1075
|
+
}
|
|
216
1076
|
});
|
|
217
1077
|
|
|
218
1078
|
voice({
|
|
@@ -226,6 +1086,60 @@ voice({
|
|
|
226
1086
|
|
|
227
1087
|
`createVoiceAgentSquad(...)` gives you squad-style specialization without locking your app into a hosted voice platform. An agent can return `handoff: { targetAgentId: 'billing' }`; the squad records the handoff, runs the target agent on the same turn, and still returns a standard `VoiceRouteResult`.
|
|
228
1088
|
|
|
1089
|
+
For production call centers, pass `handoffPolicy` to keep routing code-owned instead of dashboard-owned. The policy can allow a handoff, reroute it to a different specialist, merge handoff metadata, summarize the reason for the target agent, or block the handoff and return an escalation. Squad traces mark each handoff as `allowed`, `blocked`, `unknown-target`, or `max-exceeded`, so support teams can audit why a caller moved between specialists.
|
|
1090
|
+
|
|
1091
|
+
Pass `contextPolicy` when a specialist should receive a controlled context window. The default behavior preserves the accumulated conversation plus a system handoff summary. A context policy can trim that to a handoff summary and current turn, add a specialist-specific system prompt, or attach metadata that appears in the returned squad state. This is the code-owned equivalent of Vapi Squads context controls: the app decides what each specialist sees, and `agent.context` traces show whether default or custom context was applied.
|
|
1092
|
+
|
|
1093
|
+
Each specialist owns its own `tools`, so tool permissions stay explicit per agent. For example, support can have `lookup_order`, billing can have `refund_invoice`, and scheduling can have `book_appointment`. The squad only routes; it does not give every specialist every tool by default.
|
|
1094
|
+
|
|
1095
|
+
Use `runVoiceAgentSquadContract(...)` in tests or readiness checks when you need proof that a specialist graph still routes correctly:
|
|
1096
|
+
|
|
1097
|
+
```ts
|
|
1098
|
+
import {
|
|
1099
|
+
createVoiceMemoryTraceEventStore,
|
|
1100
|
+
runVoiceAgentSquadContract
|
|
1101
|
+
} from '@absolutejs/voice';
|
|
1102
|
+
|
|
1103
|
+
const trace = createVoiceMemoryTraceEventStore();
|
|
1104
|
+
const frontDesk = createVoiceAgentSquad({
|
|
1105
|
+
id: 'front-desk',
|
|
1106
|
+
defaultAgentId: 'support',
|
|
1107
|
+
agents: [supportAgent, billingAgent],
|
|
1108
|
+
trace
|
|
1109
|
+
});
|
|
1110
|
+
|
|
1111
|
+
const report = await runVoiceAgentSquadContract({
|
|
1112
|
+
context: {},
|
|
1113
|
+
squad: frontDesk,
|
|
1114
|
+
trace,
|
|
1115
|
+
contract: {
|
|
1116
|
+
id: 'billing-route',
|
|
1117
|
+
scenarioId: 'billing-route',
|
|
1118
|
+
turns: [
|
|
1119
|
+
{
|
|
1120
|
+
text: 'I have a billing question.',
|
|
1121
|
+
expect: {
|
|
1122
|
+
finalAgentId: 'billing',
|
|
1123
|
+
outcome: 'assistant',
|
|
1124
|
+
assistantIncludes: ['billing'],
|
|
1125
|
+
handoffs: [
|
|
1126
|
+
{
|
|
1127
|
+
fromAgentId: 'support',
|
|
1128
|
+
targetAgentId: 'billing',
|
|
1129
|
+
status: 'allowed'
|
|
1130
|
+
}
|
|
1131
|
+
]
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
]
|
|
1135
|
+
}
|
|
1136
|
+
});
|
|
1137
|
+
|
|
1138
|
+
if (!report.pass) {
|
|
1139
|
+
throw new Error(report.issues.map((issue) => issue.message).join('\n'));
|
|
1140
|
+
}
|
|
1141
|
+
```
|
|
1142
|
+
|
|
229
1143
|
## Traces And Replay
|
|
230
1144
|
|
|
231
1145
|
Use trace stores when you want every call to be inspectable outside a hosted platform. Trace events are append-only records for model passes, tool calls, handoffs, agent results, call lifecycle, turn timing, errors, and cost telemetry.
|
|
@@ -233,13 +1147,22 @@ Use trace stores when you want every call to be inspectable outside a hosted pla
|
|
|
233
1147
|
```ts
|
|
234
1148
|
import {
|
|
235
1149
|
buildVoiceTraceReplay,
|
|
1150
|
+
buildVoiceAuditExport,
|
|
1151
|
+
createVoiceAuditHTTPSink,
|
|
1152
|
+
createVoiceAuditLogger,
|
|
1153
|
+
createVoiceAuditSinkDeliveryWorker,
|
|
1154
|
+
createVoiceAuditSinkStore,
|
|
1155
|
+
createVoiceAuditTrailRoutes,
|
|
236
1156
|
createVoiceAgent,
|
|
237
1157
|
createVoiceFileRuntimeStorage,
|
|
238
1158
|
createVoiceRedisTaskLeaseCoordinator,
|
|
1159
|
+
createVoiceTraceDeliveryRoutes,
|
|
239
1160
|
createVoiceTraceHTTPSink,
|
|
240
1161
|
createVoiceTraceSinkStore,
|
|
241
1162
|
createVoiceTraceSinkDeliveryWorker,
|
|
1163
|
+
buildVoiceDataRetentionPlan,
|
|
242
1164
|
exportVoiceTrace,
|
|
1165
|
+
applyVoiceDataRetentionPolicy,
|
|
243
1166
|
pruneVoiceTraceEvents,
|
|
244
1167
|
voice
|
|
245
1168
|
} from '@absolutejs/voice';
|
|
@@ -251,6 +1174,30 @@ const runtimeStorage = createVoiceFileRuntimeStorage({
|
|
|
251
1174
|
const redisLeases = createVoiceRedisTaskLeaseCoordinator({
|
|
252
1175
|
url: process.env.REDIS_URL
|
|
253
1176
|
});
|
|
1177
|
+
const auditStore = createVoiceAuditSinkStore({
|
|
1178
|
+
store: runtimeStorage.audit,
|
|
1179
|
+
deliveryQueue: runtimeStorage.auditDeliveries,
|
|
1180
|
+
sinks: [
|
|
1181
|
+
createVoiceAuditHTTPSink({
|
|
1182
|
+
id: 'security-warehouse',
|
|
1183
|
+
signingSecret: process.env.VOICE_AUDIT_SINK_SECRET,
|
|
1184
|
+
url: process.env.VOICE_AUDIT_SINK_URL!
|
|
1185
|
+
})
|
|
1186
|
+
]
|
|
1187
|
+
});
|
|
1188
|
+
const audit = createVoiceAuditLogger(auditStore);
|
|
1189
|
+
const auditSinkWorker = createVoiceAuditSinkDeliveryWorker({
|
|
1190
|
+
deliveries: runtimeStorage.auditDeliveries,
|
|
1191
|
+
leases: redisLeases,
|
|
1192
|
+
sinks: [
|
|
1193
|
+
createVoiceAuditHTTPSink({
|
|
1194
|
+
id: 'security-warehouse',
|
|
1195
|
+
signingSecret: process.env.VOICE_AUDIT_SINK_SECRET,
|
|
1196
|
+
url: process.env.VOICE_AUDIT_SINK_URL!
|
|
1197
|
+
})
|
|
1198
|
+
],
|
|
1199
|
+
workerId: 'audit-sink-worker'
|
|
1200
|
+
});
|
|
254
1201
|
const trace = createVoiceTraceSinkStore({
|
|
255
1202
|
store: runtimeStorage.traces,
|
|
256
1203
|
deliveryQueue: runtimeStorage.traceDeliveries,
|
|
@@ -277,6 +1224,9 @@ const traceSinkWorker = createVoiceTraceSinkDeliveryWorker({
|
|
|
277
1224
|
|
|
278
1225
|
const supportAgent = createVoiceAgent({
|
|
279
1226
|
id: 'support',
|
|
1227
|
+
audit,
|
|
1228
|
+
auditProvider: 'openai',
|
|
1229
|
+
auditModel: 'gpt-4.1',
|
|
280
1230
|
trace,
|
|
281
1231
|
model: {
|
|
282
1232
|
async generate() {
|
|
@@ -293,6 +1243,17 @@ voice({
|
|
|
293
1243
|
onTurn: supportAgent.onTurn,
|
|
294
1244
|
onComplete: async () => {}
|
|
295
1245
|
});
|
|
1246
|
+
app.use(
|
|
1247
|
+
createVoiceAuditTrailRoutes({
|
|
1248
|
+
store: runtimeStorage.audit
|
|
1249
|
+
})
|
|
1250
|
+
);
|
|
1251
|
+
app.use(
|
|
1252
|
+
createVoiceTraceDeliveryRoutes({
|
|
1253
|
+
store: runtimeStorage.traceDeliveries,
|
|
1254
|
+
worker: traceSinkWorker
|
|
1255
|
+
})
|
|
1256
|
+
);
|
|
296
1257
|
|
|
297
1258
|
const replay = await exportVoiceTrace({
|
|
298
1259
|
store: runtimeStorage.traces,
|
|
@@ -314,18 +1275,152 @@ await pruneVoiceTraceEvents({
|
|
|
314
1275
|
store: runtimeStorage.traces,
|
|
315
1276
|
before: Date.now() - 30 * 24 * 60 * 60 * 1000
|
|
316
1277
|
});
|
|
1278
|
+
|
|
1279
|
+
await audit.operatorAction({
|
|
1280
|
+
action: 'review.approve',
|
|
1281
|
+
actor: { id: 'operator-123', kind: 'operator' },
|
|
1282
|
+
resource: { id: 'review-123', type: 'review' }
|
|
1283
|
+
});
|
|
317
1284
|
```
|
|
318
1285
|
|
|
319
1286
|
`createVoiceMemoryTraceEventStore(...)`, `createVoiceFileTraceEventStore(...)`, `createVoiceSQLiteTraceEventStore(...)`, and `createVoicePostgresTraceEventStore(...)` all implement the same `VoiceTraceEventStore` contract. File, SQLite, and Postgres runtime storage expose `runtimeStorage.traces` and `runtimeStorage.traceDeliveries` alongside sessions, reviews, tasks, events, and external object mappings. Passing `trace` to `voice(...)` records session lifecycle, transcript, committed-turn, assistant, cost, and error events; passing it to agents records model passes, tools, results, and handoffs.
|
|
320
1287
|
|
|
321
1288
|
For self-hosted QA and support workflows, use `summarizeVoiceTrace(...)`, `evaluateVoiceTrace(...)`, `renderVoiceTraceMarkdown(...)`, `renderVoiceTraceHTML(...)`, or `buildVoiceTraceReplay(...)`. They turn raw trace events into portable artifacts you can attach to tickets, inspect locally, or fail in CI when a call has missing transcripts, missing turns, tool errors, session errors, or excessive handoffs.
|
|
322
1289
|
|
|
323
|
-
For observability pipelines, wrap any trace store with `createVoiceTraceSinkStore(...)` and pass sinks such as `createVoiceTraceHTTPSink(...)`. The wrapper still writes to your normal file, SQLite, or Postgres store, then fans out appended events to your warehouse, logs, S3 bridge, or analytics endpoint. Use `awaitDelivery: true` only when you want trace delivery to block append completion. For durable delivery, pass `deliveryQueue` and run `createVoiceTraceSinkDeliveryWorker(...)` or `createVoiceTraceSinkDeliveryWorkerLoop(...)`; the worker uses the same Redis lease/idempotency primitives as ops workers and supports retries plus dead-letter stores.
|
|
1290
|
+
For observability pipelines, wrap any trace store with `createVoiceTraceSinkStore(...)` and pass sinks such as `createVoiceTraceHTTPSink(...)`. The wrapper still writes to your normal file, SQLite, or Postgres store, then fans out appended events to your warehouse, logs, S3 bridge, or analytics endpoint. Use `awaitDelivery: true` only when you want trace delivery to block append completion. For durable delivery, pass `deliveryQueue` and run `createVoiceTraceSinkDeliveryWorker(...)` or `createVoiceTraceSinkDeliveryWorkerLoop(...)`; the worker uses the same Redis lease/idempotency primitives as ops workers and supports retries plus dead-letter stores. Mount `createVoiceTraceDeliveryRoutes({ store: runtimeStorage.traceDeliveries, worker })` to expose `/traces/deliveries`, `/api/voice-trace-deliveries`, and an explicit `POST /api/voice-trace-deliveries/drain` retry path.
|
|
324
1291
|
|
|
325
1292
|
When traces may leave your private runtime, pass `redact: true` or a redaction config to `exportVoiceTrace(...)`, `renderVoiceTraceMarkdown(...)`, `renderVoiceTraceHTML(...)`, or `buildVoiceTraceReplay(...)`. The built-in redactor scrubs common email addresses, phone numbers, and sensitive keys like `token`, `secret`, `password`, `apiKey`, `authorization`, `phone`, and `email`; you can pass custom keys or replacement text for stricter policies.
|
|
326
1293
|
|
|
327
1294
|
For retention jobs, `pruneVoiceTraceEvents(...)` works against any trace store. Use `dryRun: true` before deleting, filter by session, trace, scenario, turn, or event type, cap each run with `limit`, or keep only the newest N matching events with `keepNewest`.
|
|
328
1295
|
|
|
1296
|
+
For whole-runtime data control, use `buildVoiceDataRetentionPlan(...)` first and then `applyVoiceDataRetentionPolicy(...)` when the deletion set is correct. The policy works across stores exposed by file, SQLite, or Postgres runtime storage, including sessions, traces, trace deliveries, audit deliveries, reviews, ops tasks, integration events, and campaigns. A cutoff or per-scope `keepNewest` selector is required before anything is deleted, so an empty policy reports skipped scopes instead of wiping data.
|
|
1297
|
+
|
|
1298
|
+
```ts
|
|
1299
|
+
const plan = await buildVoiceDataRetentionPlan({
|
|
1300
|
+
before: Date.now() - 30 * 24 * 60 * 60 * 1000,
|
|
1301
|
+
...runtimeStorage
|
|
1302
|
+
});
|
|
1303
|
+
|
|
1304
|
+
console.log(plan.scopes);
|
|
1305
|
+
|
|
1306
|
+
await applyVoiceDataRetentionPolicy({
|
|
1307
|
+
audit: runtimeStorage.audit,
|
|
1308
|
+
before: Date.now() - 30 * 24 * 60 * 60 * 1000,
|
|
1309
|
+
...runtimeStorage
|
|
1310
|
+
});
|
|
1311
|
+
```
|
|
1312
|
+
|
|
1313
|
+
For a compliance-facing control surface, mount `createVoiceDataControlRoutes(...)`. It packages the same primitives into a self-hosted report for customer-owned storage, retention dry-runs, guarded deletion, redacted audit export, zero-retention mode, and provider-key handling.
|
|
1314
|
+
|
|
1315
|
+
```ts
|
|
1316
|
+
import {
|
|
1317
|
+
createVoiceDataControlRoutes,
|
|
1318
|
+
createVoiceZeroRetentionPolicy,
|
|
1319
|
+
voiceComplianceRedactionDefaults
|
|
1320
|
+
} from '@absolutejs/voice';
|
|
1321
|
+
|
|
1322
|
+
app.use(
|
|
1323
|
+
createVoiceDataControlRoutes({
|
|
1324
|
+
...runtimeStorage,
|
|
1325
|
+
audit: runtimeStorage.audit,
|
|
1326
|
+
auditDeliveries: runtimeStorage.auditDeliveries,
|
|
1327
|
+
path: '/data-control',
|
|
1328
|
+
redact: voiceComplianceRedactionDefaults,
|
|
1329
|
+
traceDeliveries: runtimeStorage.traceDeliveries
|
|
1330
|
+
})
|
|
1331
|
+
);
|
|
1332
|
+
|
|
1333
|
+
const zeroRetentionPlan = await buildVoiceDataRetentionPlan(
|
|
1334
|
+
createVoiceZeroRetentionPolicy({
|
|
1335
|
+
...runtimeStorage,
|
|
1336
|
+
audit: runtimeStorage.audit,
|
|
1337
|
+
auditDeliveries: runtimeStorage.auditDeliveries,
|
|
1338
|
+
traceDeliveries: runtimeStorage.traceDeliveries
|
|
1339
|
+
})
|
|
1340
|
+
);
|
|
1341
|
+
```
|
|
1342
|
+
|
|
1343
|
+
Mounted routes:
|
|
1344
|
+
|
|
1345
|
+
- `GET /data-control`: HTML compliance/data-control report.
|
|
1346
|
+
- `GET /data-control.json`: JSON report with redaction, storage, retention plan, audit export, and provider-key recommendations.
|
|
1347
|
+
- `GET /data-control.md`: Markdown report for release/security reviews.
|
|
1348
|
+
- `POST /data-control/retention/plan`: dry-run deletion proof from a JSON policy body.
|
|
1349
|
+
- `POST /data-control/retention/apply`: applies retention only when the body includes `confirm: "apply-retention-policy"`.
|
|
1350
|
+
- `GET /data-control/audit.json`, `/data-control/audit.md`, `/data-control/audit.html`: redacted audit exports.
|
|
1351
|
+
|
|
1352
|
+
`createVoiceZeroRetentionPolicy(...)` intentionally defaults to `dryRun: true`; callers must explicitly apply the generated policy after reviewing the deletion proof. This gives compliance-sensitive deployments a concrete zero-retention recipe without making accidental deletion easy.
|
|
1353
|
+
|
|
1354
|
+
Use `createVoiceAuditLogger(...)` when you need append-only compliance evidence outside call traces. The logger records provider calls, tool calls, handoffs, retention runs, and operator actions into `runtimeStorage.audit`, so self-hosted teams can prove who changed what, which provider ran, which tool fired, and what data-control policy deleted.
|
|
1355
|
+
|
|
1356
|
+
Pass `audit` directly to `createVoiceAgent(...)` to record model calls as provider-call audit events and tool executions as tool-call audit events. Pass it to `createVoiceAgentSquad(...)` to record squad handoffs automatically. Use `auditProvider` and `auditModel` on agents when you want readiness and compliance reports to show the actual model provider instead of the agent id.
|
|
1357
|
+
|
|
1358
|
+
For compliance pipelines, wrap any audit store with `createVoiceAuditSinkStore(...)` and pass sinks such as `createVoiceAuditHTTPSink(...)`. Audit sinks redact by default, support HMAC signing, retries, event-type filters, optional blocking delivery, durable delivery queues through `runtimeStorage.auditDeliveries`, and background workers through `createVoiceAuditSinkDeliveryWorker(...)` or `createVoiceAuditSinkDeliveryWorkerLoop(...)`. File, SQLite, and Postgres runtime storage all expose `auditDeliveries`, so teams can ship evidence to a SIEM, warehouse, or internal security service without a hosted dashboard. Mount `createVoiceAuditDeliveryRoutes({ store: runtimeStorage.auditDeliveries, worker })` to expose `/audit/deliveries`, `/api/voice-audit-deliveries`, and an explicit `POST /api/voice-audit-deliveries/drain` retry path.
|
|
1359
|
+
|
|
1360
|
+
Pass `audit: runtimeStorage.audit` into production readiness when audit coverage should block deploys. By default readiness requires provider-call, retention-policy, and operator-action audit evidence; retention-policy evidence must be from the last 7 days so a stale one-time audit event does not certify an active retention job. Override required event types or freshness with `audit: { store: runtimeStorage.audit, require: [{ type: 'retention.policy', maxAgeMs: ... }] }` when a deployment has different compliance gates. Pass `auditDeliveries: runtimeStorage.auditDeliveries` and `traceDeliveries: runtimeStorage.traceDeliveries` when sink export health should also block deploys; failed or dead-lettered deliveries fail readiness, pending deliveries warn, and pending deliveries older than the configured fail window fail readiness.
|
|
1361
|
+
|
|
1362
|
+
Mount `createVoiceAuditTrailRoutes(...)` to expose `/api/voice-audit` and `/audit` over the same store. File, SQLite, and Postgres runtime storage all expose `runtimeStorage.audit`. The JSON and HTML surfaces support filters like `type`, `outcome`, `actorId`, `resourceType`, `resourceId`, `sessionId`, `traceId`, and `limit`, so operators can search audit evidence without writing a custom viewer first.
|
|
1363
|
+
|
|
1364
|
+
Use `exportVoiceAuditTrail(...)` or `buildVoiceAuditExport(...)` when audit evidence needs to leave the app. Pass `redact: true` to scrub sensitive keys plus common email and phone patterns from payloads and metadata before generating JSON, Markdown, or HTML. Audit trail routes also expose redacted exports at `/api/voice-audit/export`, `/api/voice-audit/export?format=markdown`, `/api/voice-audit/export?format=html`, and `/audit/export`; export routes redact by default unless `redact=false` is passed.
|
|
1365
|
+
|
|
1366
|
+
## Operations Records And Recovery
|
|
1367
|
+
|
|
1368
|
+
Use operations records as the default support/debug entrypoint. A hosted platform would send an operator to a call log; AbsoluteJS Voice gives the same workflow as a code-owned route:
|
|
1369
|
+
|
|
1370
|
+
```ts
|
|
1371
|
+
app.use(
|
|
1372
|
+
createVoiceOperationsRecordRoutes({
|
|
1373
|
+
audit: runtimeStorage.audit,
|
|
1374
|
+
htmlPath: '/voice-operations/:sessionId',
|
|
1375
|
+
path: '/api/voice-operations/:sessionId',
|
|
1376
|
+
store: runtimeStorage.traces
|
|
1377
|
+
})
|
|
1378
|
+
);
|
|
1379
|
+
```
|
|
1380
|
+
|
|
1381
|
+
`createVoiceOperationsRecordRoutes(...)` links the call/session timeline, replay, provider events, 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.
|
|
1382
|
+
|
|
1383
|
+
Mount `createVoiceOpsRecoveryRoutes(...)` beside it when operators need one deploy-checkable recovery signal:
|
|
1384
|
+
|
|
1385
|
+
```ts
|
|
1386
|
+
app.use(
|
|
1387
|
+
createVoiceOpsRecoveryRoutes({
|
|
1388
|
+
auditDeliveries: runtimeStorage.auditDeliveries,
|
|
1389
|
+
handoffDeliveries,
|
|
1390
|
+
links: {
|
|
1391
|
+
operationsRecords: '/voice-operations/:sessionId',
|
|
1392
|
+
traceDeliveries: '/traces/deliveries'
|
|
1393
|
+
},
|
|
1394
|
+
traceDeliveries: runtimeStorage.traceDeliveries,
|
|
1395
|
+
traces: runtimeStorage.traces
|
|
1396
|
+
})
|
|
1397
|
+
);
|
|
1398
|
+
```
|
|
1399
|
+
|
|
1400
|
+
The recovery report summarizes recovered provider fallback, unresolved provider failures, audit/trace delivery backlog, handoff delivery backlog, operator interventions, failed sessions, and latency SLO issues. When `operationsRecords` is configured, provider and latency recovery issues link directly to the impacted operations record instead of a generic dashboard.
|
|
1401
|
+
|
|
1402
|
+
Pass the same report into production readiness to make recovery issues a deploy gate:
|
|
1403
|
+
|
|
1404
|
+
```ts
|
|
1405
|
+
const opsRecovery = await buildVoiceOpsRecoveryReport({
|
|
1406
|
+
links: { operationsRecords: '/voice-operations/:sessionId' },
|
|
1407
|
+
traces: runtimeStorage.traces
|
|
1408
|
+
});
|
|
1409
|
+
|
|
1410
|
+
app.use(
|
|
1411
|
+
createVoiceProductionReadinessRoutes({
|
|
1412
|
+
links: {
|
|
1413
|
+
operationsRecords: '/voice-operations/:sessionId',
|
|
1414
|
+
opsRecovery: '/ops-recovery'
|
|
1415
|
+
},
|
|
1416
|
+
opsRecovery,
|
|
1417
|
+
store: runtimeStorage.traces
|
|
1418
|
+
})
|
|
1419
|
+
);
|
|
1420
|
+
```
|
|
1421
|
+
|
|
1422
|
+
Readiness emits the stable `voice.readiness.ops_recovery` gate code when unresolved recovery issues remain.
|
|
1423
|
+
|
|
329
1424
|
## Production Voice Ops
|
|
330
1425
|
|
|
331
1426
|
The recommended production pattern is:
|
|
@@ -733,6 +1828,59 @@ app.use(
|
|
|
733
1828
|
|
|
734
1829
|
Client state now exposes `assistantAudio` on the stream/controller helpers, so apps can buffer or play synthesized chunks without inventing a second transport.
|
|
735
1830
|
|
|
1831
|
+
## OpenAI Realtime
|
|
1832
|
+
|
|
1833
|
+
Use `createOpenAIRealtimeAdapter(...)` when you want a direct OpenAI Realtime speech-to-speech output path for live smoke tests, duplex benchmarks, or custom realtime orchestration. It implements the same `RealtimeAdapter` contract used by the benchmark harness, so the provider can stream `response.output_audio.delta` audio chunks into AbsoluteJS voice events while still emitting normalized transcript, error, and close events.
|
|
1834
|
+
|
|
1835
|
+
```ts
|
|
1836
|
+
import { createOpenAIRealtimeAdapter } from '@absolutejs/voice';
|
|
1837
|
+
import { runTTSAdapterFixture } from '@absolutejs/voice/testing';
|
|
1838
|
+
|
|
1839
|
+
const realtime = createOpenAIRealtimeAdapter({
|
|
1840
|
+
apiKey: process.env.OPENAI_API_KEY!,
|
|
1841
|
+
instructions: 'Answer in one concise sentence.',
|
|
1842
|
+
model: 'gpt-realtime',
|
|
1843
|
+
voice: 'marin'
|
|
1844
|
+
});
|
|
1845
|
+
|
|
1846
|
+
app.use(
|
|
1847
|
+
voice({
|
|
1848
|
+
path: '/voice',
|
|
1849
|
+
realtime,
|
|
1850
|
+
realtimeInputFormat: {
|
|
1851
|
+
channels: 1,
|
|
1852
|
+
container: 'raw',
|
|
1853
|
+
encoding: 'pcm_s16le',
|
|
1854
|
+
sampleRateHz: 24000
|
|
1855
|
+
},
|
|
1856
|
+
session,
|
|
1857
|
+
onTurn: async ({ turn }) => ({
|
|
1858
|
+
assistantText: `You said: ${turn.text}`
|
|
1859
|
+
}),
|
|
1860
|
+
onComplete: async () => {}
|
|
1861
|
+
})
|
|
1862
|
+
);
|
|
1863
|
+
|
|
1864
|
+
const report = await runTTSAdapterFixture(
|
|
1865
|
+
realtime,
|
|
1866
|
+
{
|
|
1867
|
+
id: 'openai-realtime-smoke',
|
|
1868
|
+
text: 'Say exactly: AbsoluteJS realtime is online.',
|
|
1869
|
+
title: 'OpenAI Realtime smoke'
|
|
1870
|
+
},
|
|
1871
|
+
{
|
|
1872
|
+
realtimeFormat: {
|
|
1873
|
+
channels: 1,
|
|
1874
|
+
container: 'raw',
|
|
1875
|
+
encoding: 'pcm_s16le',
|
|
1876
|
+
sampleRateHz: 24000
|
|
1877
|
+
}
|
|
1878
|
+
}
|
|
1879
|
+
);
|
|
1880
|
+
```
|
|
1881
|
+
|
|
1882
|
+
For server-to-server use, the adapter opens a WebSocket to OpenAI, sends `session.update`, streams text or base64 PCM input, and emits raw 24kHz mono `pcm_s16le` assistant audio. It requires raw 24kHz mono PCM input because that is the OpenAI Realtime PCM format. The main `voice(...)` route can now run in cascaded mode with `stt` plus optional `tts`, or direct realtime mode with `realtime`. Browser demos should make sure the captured PCM format matches `realtimeInputFormat` or resample before sending audio.
|
|
1883
|
+
|
|
736
1884
|
If you want a minimal browser playback path, use the client audio player:
|
|
737
1885
|
|
|
738
1886
|
```ts
|
|
@@ -1069,6 +2217,176 @@ app.use(
|
|
|
1069
2217
|
- `benchmark-results/sessions-cheap-stt-runs-3.json`
|
|
1070
2218
|
- `benchmark-results/stt-routing-run-manifest.json`
|
|
1071
2219
|
|
|
2220
|
+
## LLM Provider Routing
|
|
2221
|
+
|
|
2222
|
+
Use `createVoiceProviderRouter(...)` when your assistant can run on more than one LLM provider. The router keeps provider choice inside your app: you define the available model adapters, profile each provider, and choose a policy.
|
|
2223
|
+
|
|
2224
|
+
```ts
|
|
2225
|
+
import {
|
|
2226
|
+
createAnthropicVoiceAssistantModel,
|
|
2227
|
+
createGeminiVoiceAssistantModel,
|
|
2228
|
+
createOpenAIVoiceAssistantModel,
|
|
2229
|
+
createVoiceProviderRouter,
|
|
2230
|
+
resolveVoiceProviderRoutingPolicyPreset
|
|
2231
|
+
} from '@absolutejs/voice';
|
|
2232
|
+
|
|
2233
|
+
const model = createVoiceProviderRouter({
|
|
2234
|
+
providers: {
|
|
2235
|
+
openai: createOpenAIVoiceAssistantModel({ apiKey: process.env.OPENAI_API_KEY! }),
|
|
2236
|
+
anthropic: createAnthropicVoiceAssistantModel({ apiKey: process.env.ANTHROPIC_API_KEY! }),
|
|
2237
|
+
gemini: createGeminiVoiceAssistantModel({ apiKey: process.env.GEMINI_API_KEY! })
|
|
2238
|
+
},
|
|
2239
|
+
providerHealth: {
|
|
2240
|
+
failureThreshold: 1,
|
|
2241
|
+
cooldownMs: 30_000,
|
|
2242
|
+
rateLimitCooldownMs: 120_000
|
|
2243
|
+
},
|
|
2244
|
+
providerProfiles: {
|
|
2245
|
+
openai: { cost: 6, latencyMs: 650, quality: 0.92, timeoutMs: 3500 },
|
|
2246
|
+
anthropic: { cost: 7, latencyMs: 850, quality: 0.95, timeoutMs: 4500 },
|
|
2247
|
+
gemini: { cost: 2, latencyMs: 700, quality: 0.86, timeoutMs: 3500 }
|
|
2248
|
+
},
|
|
2249
|
+
policy: resolveVoiceProviderRoutingPolicyPreset('balanced')
|
|
2250
|
+
});
|
|
2251
|
+
```
|
|
2252
|
+
|
|
2253
|
+
Built-in policy presets:
|
|
2254
|
+
|
|
2255
|
+
- `quality-first`: rank by `providerProfiles[provider].quality`, then priority, latency, and cost.
|
|
2256
|
+
- `latency-first`: rank by expected latency.
|
|
2257
|
+
- `cost-first`: rank by expected cost.
|
|
2258
|
+
- `cost-cap`: rank by cost and reject providers above `maxCost`.
|
|
2259
|
+
- `balanced`: weighted score using cost, latency, quality, and priority.
|
|
2260
|
+
|
|
2261
|
+
Budget filters are strict. If you pass `maxCost`, `maxLatencyMs`, or `minQuality`, providers outside those limits are removed before ranking, even if they were selected by the request.
|
|
2262
|
+
|
|
2263
|
+
```ts
|
|
2264
|
+
const policy = resolveVoiceProviderRoutingPolicyPreset('cost-cap', {
|
|
2265
|
+
maxCost: 3,
|
|
2266
|
+
minQuality: 0.82
|
|
2267
|
+
});
|
|
2268
|
+
```
|
|
2269
|
+
|
|
2270
|
+
Use `runVoiceProviderRoutingContract(...)` when provider fallback needs to be certified before production. The contract reads provider routing trace events and verifies the expected selected provider, fallback provider, status, and kind in order.
|
|
2271
|
+
|
|
2272
|
+
```ts
|
|
2273
|
+
import { runVoiceProviderRoutingContract } from '@absolutejs/voice';
|
|
2274
|
+
|
|
2275
|
+
const report = await runVoiceProviderRoutingContract({
|
|
2276
|
+
store: runtime.traces,
|
|
2277
|
+
contract: {
|
|
2278
|
+
id: 'openai-to-anthropic-fallback',
|
|
2279
|
+
expect: [
|
|
2280
|
+
{
|
|
2281
|
+
kind: 'llm',
|
|
2282
|
+
provider: 'openai',
|
|
2283
|
+
selectedProvider: 'openai',
|
|
2284
|
+
fallbackProvider: 'anthropic',
|
|
2285
|
+
status: 'error'
|
|
2286
|
+
},
|
|
2287
|
+
{
|
|
2288
|
+
kind: 'llm',
|
|
2289
|
+
provider: 'anthropic',
|
|
2290
|
+
selectedProvider: 'openai',
|
|
2291
|
+
status: 'fallback'
|
|
2292
|
+
}
|
|
2293
|
+
]
|
|
2294
|
+
}
|
|
2295
|
+
});
|
|
2296
|
+
|
|
2297
|
+
if (!report.pass) {
|
|
2298
|
+
throw new Error(report.issues.map((issue) => issue.message).join('\n'));
|
|
2299
|
+
}
|
|
2300
|
+
```
|
|
2301
|
+
|
|
2302
|
+
Pass provider routing contract reports into production readiness through `providerRoutingContracts`. Readiness fails when a fallback contract fails, so model-routing regressions become deploy blockers instead of dashboard-only surprises.
|
|
2303
|
+
|
|
2304
|
+
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.
|
|
2305
|
+
|
|
2306
|
+
```ts
|
|
2307
|
+
import {
|
|
2308
|
+
buildVoiceProviderContractMatrix,
|
|
2309
|
+
createVoiceProviderContractMatrixPreset,
|
|
2310
|
+
createVoiceProviderContractMatrixRoutes
|
|
2311
|
+
} from '@absolutejs/voice';
|
|
2312
|
+
|
|
2313
|
+
const providerContracts = () =>
|
|
2314
|
+
createVoiceProviderContractMatrixPreset('phone-agent', {
|
|
2315
|
+
env: process.env,
|
|
2316
|
+
providers: {
|
|
2317
|
+
llm: ['openai', 'anthropic', 'gemini'],
|
|
2318
|
+
stt: ['deepgram', 'assemblyai'],
|
|
2319
|
+
tts: ['openai', 'emergency']
|
|
2320
|
+
},
|
|
2321
|
+
selected: {
|
|
2322
|
+
llm: 'openai',
|
|
2323
|
+
stt: 'deepgram',
|
|
2324
|
+
tts: 'openai'
|
|
2325
|
+
},
|
|
2326
|
+
latencyBudgets: {
|
|
2327
|
+
openai: 900,
|
|
2328
|
+
deepgram: 250,
|
|
2329
|
+
assemblyai: 900,
|
|
2330
|
+
emergency: 80
|
|
2331
|
+
},
|
|
2332
|
+
remediationHref: '/provider-contracts'
|
|
2333
|
+
});
|
|
2334
|
+
|
|
2335
|
+
const app = createVoiceProviderContractMatrixRoutes({
|
|
2336
|
+
htmlPath: '/provider-contracts',
|
|
2337
|
+
path: '/api/provider-contracts',
|
|
2338
|
+
load: () => buildVoiceProviderContractMatrix(providerContracts())
|
|
2339
|
+
});
|
|
2340
|
+
```
|
|
2341
|
+
|
|
2342
|
+
The preset maps common provider names to env checks, streaming defaults, fallback rows, and profile-required capabilities. Override `configured`, `capabilities`, `fallbackProviders`, or `streaming` whenever your deployment uses custom adapters or local fallbacks.
|
|
2343
|
+
|
|
2344
|
+
For full control, pass an object policy:
|
|
2345
|
+
|
|
2346
|
+
```ts
|
|
2347
|
+
const model = createVoiceProviderRouter({
|
|
2348
|
+
providers,
|
|
2349
|
+
providerProfiles,
|
|
2350
|
+
policy: {
|
|
2351
|
+
strategy: 'balanced',
|
|
2352
|
+
maxLatencyMs: 1000,
|
|
2353
|
+
weights: { cost: 1, latencyMs: 0.004, quality: 12 }
|
|
2354
|
+
}
|
|
2355
|
+
});
|
|
2356
|
+
```
|
|
2357
|
+
|
|
2358
|
+
The same profile and policy shape also works for STT and TTS provider routers, so a self-hosted app can choose the fastest provider for live calls, cap cost for background work, or require a minimum quality score without hard-coding provider branches.
|
|
2359
|
+
|
|
2360
|
+
```ts
|
|
2361
|
+
const stt = createVoiceSTTProviderRouter({
|
|
2362
|
+
adapters: {
|
|
2363
|
+
deepgram,
|
|
2364
|
+
assemblyai
|
|
2365
|
+
},
|
|
2366
|
+
providerHealth: { cooldownMs: 30_000 },
|
|
2367
|
+
providerProfiles: {
|
|
2368
|
+
deepgram: { cost: 4, latencyMs: 180, quality: 0.93, timeoutMs: 1500 },
|
|
2369
|
+
assemblyai: { cost: 2, latencyMs: 650, quality: 0.88, timeoutMs: 3000 }
|
|
2370
|
+
},
|
|
2371
|
+
policy: resolveVoiceProviderRoutingPolicyPreset('latency-first')
|
|
2372
|
+
});
|
|
2373
|
+
|
|
2374
|
+
const tts = createVoiceTTSProviderRouter({
|
|
2375
|
+
adapters: {
|
|
2376
|
+
elevenlabs,
|
|
2377
|
+
openai
|
|
2378
|
+
},
|
|
2379
|
+
providerProfiles: {
|
|
2380
|
+
elevenlabs: { cost: 5, latencyMs: 220, quality: 0.94 },
|
|
2381
|
+
openai: { cost: 2, latencyMs: 320, quality: 0.87 }
|
|
2382
|
+
},
|
|
2383
|
+
policy: resolveVoiceProviderRoutingPolicyPreset('cost-cap', {
|
|
2384
|
+
maxCost: 3,
|
|
2385
|
+
minQuality: 0.85
|
|
2386
|
+
})
|
|
2387
|
+
});
|
|
2388
|
+
```
|
|
2389
|
+
|
|
1072
2390
|
## Presets
|
|
1073
2391
|
|
|
1074
2392
|
Voice now ships named runtime presets so apps can start from a useful baseline instead of hand-tuning silence and capture settings every time.
|
|
@@ -1566,6 +2884,8 @@ Default reconnect strategy is `resume-last-turn`.
|
|
|
1566
2884
|
|
|
1567
2885
|
If an adapter does not emit native end-of-turn events, core falls back to silence detection with a default `700ms` threshold.
|
|
1568
2886
|
|
|
2887
|
+
For browser/client proof, use `runVoiceReconnectContract(...)` or mount `createVoiceReconnectContractRoutes(...)` with captured reconnect snapshots. The contract verifies that a reconnect was observed, the stream resumed before exhaustion, and replayed state did not duplicate committed turn IDs.
|
|
2888
|
+
|
|
1569
2889
|
## STT Fallback
|
|
1570
2890
|
|
|
1571
2891
|
You can pair a primary vendor with an optional fallback vendor per route when you need extra reliability for accents, edge environments, or short commands.
|