@absolutejs/voice 0.0.22-beta.43 → 0.0.22-beta.430
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 +3690 -55
- package/dist/agent.d.ts +62 -0
- package/dist/agentSquadContract.d.ts +98 -0
- package/dist/angular/index.d.ts +19 -0
- package/dist/angular/index.js +4647 -1140
- package/dist/angular/voice-agent-squad-status.service.d.ts +12 -0
- package/dist/angular/voice-call-debugger.service.d.ts +12 -0
- 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-platform-coverage.service.d.ts +12 -0
- package/dist/angular/voice-profile-comparison.service.d.ts +12 -0
- package/dist/angular/voice-proof-trends.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-readiness-failures.service.d.ts +13 -0
- package/dist/angular/voice-routing-status.service.d.ts +11 -0
- package/dist/angular/voice-session-snapshot.service.d.ts +13 -0
- package/dist/angular/voice-stream.service.d.ts +2 -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/audit.d.ts +131 -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/browserCallProfiles.d.ts +120 -0
- package/dist/browserMediaRoutes.d.ts +62 -0
- package/dist/callDebugger.d.ts +66 -0
- package/dist/campaign.d.ts +794 -0
- package/dist/campaignDialers.d.ts +111 -0
- package/dist/client/actions.d.ts +94 -0
- package/dist/client/agentSquadStatus.d.ts +37 -0
- package/dist/client/agentSquadStatusWidget.d.ts +24 -0
- package/dist/client/bargeInMonitor.d.ts +7 -0
- package/dist/client/browserMedia.d.ts +8 -0
- package/dist/client/callDebugger.d.ts +19 -0
- package/dist/client/callDebuggerWidget.d.ts +30 -0
- package/dist/client/campaignDialerProof.d.ts +23 -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 +950 -14
- package/dist/client/index.d.ts +87 -0
- package/dist/client/index.js +9890 -52
- 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/platformCoverage.d.ts +19 -0
- package/dist/client/platformCoverageWidget.d.ts +37 -0
- package/dist/client/profileComparison.d.ts +19 -0
- package/dist/client/profileComparisonWidget.d.ts +41 -0
- package/dist/client/profileSwitchRecommendation.d.ts +19 -0
- package/dist/client/profileSwitchRecommendationWidget.d.ts +12 -0
- package/dist/client/proofTrends.d.ts +19 -0
- package/dist/client/proofTrendsWidget.d.ts +37 -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/providerStatusWidget.d.ts +32 -0
- package/dist/client/readinessFailures.d.ts +19 -0
- package/dist/client/readinessFailuresWidget.d.ts +42 -0
- package/dist/client/routingStatus.d.ts +19 -0
- package/dist/client/routingStatusWidget.d.ts +32 -0
- package/dist/client/sessionSnapshot.d.ts +21 -0
- package/dist/client/sessionSnapshotWidget.d.ts +33 -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/competitiveCoverage.d.ts +141 -0
- package/dist/dataControl.d.ts +180 -0
- package/dist/deliveryRuntime.d.ts +158 -0
- package/dist/deliverySinkRoutes.d.ts +117 -0
- package/dist/demoReadyRoutes.d.ts +98 -0
- package/dist/evalRoutes.d.ts +96 -0
- package/dist/fileStore.d.ts +14 -2
- package/dist/guardrails.d.ts +128 -0
- package/dist/incidentBundle.d.ts +116 -0
- package/dist/index.d.ts +159 -18
- package/dist/index.js +34814 -6758
- package/dist/latencySlo.d.ts +56 -0
- package/dist/liveLatency.d.ts +78 -0
- package/dist/liveOps.d.ts +190 -0
- package/dist/mediaPipelineRoutes.d.ts +117 -0
- package/dist/modelAdapters.d.ts +54 -2
- package/dist/observabilityExport.d.ts +497 -0
- package/dist/openaiTTS.d.ts +18 -0
- package/dist/operationsRecord.d.ts +351 -0
- package/dist/opsActionAuditRoutes.d.ts +99 -0
- package/dist/opsConsoleRoutes.d.ts +3 -0
- package/dist/opsRecovery.d.ts +137 -0
- package/dist/opsStatus.d.ts +76 -0
- package/dist/opsStatusRoutes.d.ts +33 -0
- package/dist/outcomeContract.d.ts +146 -0
- package/dist/phoneAgent.d.ts +139 -0
- package/dist/phoneAgentProductionSmoke.d.ts +115 -0
- package/dist/platformCoverage.d.ts +91 -0
- package/dist/postCallAnalysis.d.ts +98 -0
- package/dist/postgresStore.d.ts +13 -2
- package/dist/productionReadiness.d.ts +698 -0
- package/dist/profileSwitchRecommendation.d.ts +350 -0
- package/dist/proofAssertions.d.ts +32 -0
- package/dist/proofPack.d.ts +206 -0
- package/dist/proofRunner.d.ts +79 -0
- package/dist/proofTrends.d.ts +715 -0
- package/dist/providerAdapters.d.ts +12 -1
- package/dist/providerCapabilities.d.ts +92 -0
- package/dist/providerDecisionTraces.d.ts +130 -0
- package/dist/providerOrchestration.d.ts +109 -0
- package/dist/providerRouterTraces.d.ts +35 -0
- package/dist/providerRoutingContract.d.ts +71 -0
- package/dist/providerSlo.d.ts +142 -0
- package/dist/providerStackRecommendations.d.ts +187 -0
- package/dist/queue.d.ts +9 -0
- package/dist/react/VoiceAgentSquadStatus.d.ts +5 -0
- package/dist/react/VoiceCallDebuggerLaunch.d.ts +6 -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/VoicePlatformCoverage.d.ts +6 -0
- package/dist/react/VoiceProfileComparison.d.ts +6 -0
- package/dist/react/VoiceProfileSwitchRecommendation.d.ts +6 -0
- package/dist/react/VoiceProofTrends.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/VoiceReadinessFailures.d.ts +6 -0
- package/dist/react/VoiceRoutingStatus.d.ts +6 -0
- package/dist/react/VoiceSessionSnapshot.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 +40 -0
- package/dist/react/index.js +10624 -31
- package/dist/react/useVoiceAgentSquadStatus.d.ts +8 -0
- package/dist/react/useVoiceCallDebugger.d.ts +8 -0
- package/dist/react/useVoiceCampaignDialerProof.d.ts +10 -0
- package/dist/react/useVoiceController.d.ts +2 -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/useVoicePlatformCoverage.d.ts +8 -0
- package/dist/react/useVoiceProfileComparison.d.ts +8 -0
- package/dist/react/useVoiceProfileSwitchRecommendation.d.ts +8 -0
- package/dist/react/useVoiceProofTrends.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/useVoiceReadinessFailures.d.ts +8 -0
- package/dist/react/useVoiceRoutingStatus.d.ts +8 -0
- package/dist/react/useVoiceSessionSnapshot.d.ts +9 -0
- package/dist/react/useVoiceStream.d.ts +2 -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 +38 -0
- package/dist/realtimeChannel.d.ts +136 -0
- package/dist/realtimeProviderContracts.d.ts +133 -0
- package/dist/reconnectContract.d.ts +88 -0
- package/dist/resilienceRoutes.d.ts +40 -0
- package/dist/sessionReplay.d.ts +12 -0
- package/dist/sessionSnapshot.d.ts +109 -0
- package/dist/simulationSuite.d.ts +143 -0
- package/dist/sloCalibration.d.ts +185 -0
- package/dist/sqliteStore.d.ts +13 -2
- package/dist/svelte/createVoiceAgentSquadStatus.d.ts +9 -0
- package/dist/svelte/createVoiceCallDebugger.d.ts +12 -0
- 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/createVoicePlatformCoverage.d.ts +7 -0
- package/dist/svelte/createVoiceProfileComparison.d.ts +7 -0
- package/dist/svelte/createVoiceProofTrends.d.ts +7 -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 +4 -2
- package/dist/svelte/createVoiceReadinessFailures.d.ts +7 -0
- package/dist/svelte/createVoiceRoutingStatus.d.ts +10 -0
- package/dist/svelte/createVoiceSessionSnapshot.d.ts +13 -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 +20 -0
- package/dist/svelte/index.js +6403 -460
- package/dist/telephony/contract.d.ts +61 -0
- package/dist/telephony/matrix.d.ts +97 -0
- package/dist/telephony/plivo.d.ts +303 -0
- package/dist/telephony/security.d.ts +182 -0
- package/dist/telephony/telnyx.d.ts +291 -0
- package/dist/telephony/twilio.d.ts +136 -2
- package/dist/telephonyMediaRoutes.d.ts +72 -0
- package/dist/telephonyOutcome.d.ts +273 -0
- package/dist/testing/index.js +5363 -163
- package/dist/testing/telephony.d.ts +25 -0
- package/dist/toolContract.d.ts +161 -0
- package/dist/toolRuntime.d.ts +50 -0
- package/dist/trace.d.ts +40 -1
- package/dist/traceDeliveryRoutes.d.ts +86 -0
- package/dist/traceTimeline.d.ts +97 -0
- package/dist/turnLatency.d.ts +95 -0
- package/dist/turnQuality.d.ts +94 -0
- package/dist/types.d.ts +157 -3
- package/dist/voiceMonitoring.d.ts +444 -0
- package/dist/vue/VoiceCallDebuggerLaunch.d.ts +68 -0
- 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/VoicePlatformCoverage.d.ts +23 -0
- package/dist/vue/VoiceProofTrends.d.ts +21 -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/VoiceReadinessFailures.d.ts +21 -0
- package/dist/vue/VoiceRoutingStatus.d.ts +51 -0
- package/dist/vue/VoiceSessionSnapshot.d.ts +68 -0
- package/dist/vue/VoiceTurnLatency.d.ts +69 -0
- package/dist/vue/VoiceTurnQuality.d.ts +51 -0
- package/dist/vue/index.d.ts +35 -0
- package/dist/vue/index.js +10069 -56
- package/dist/vue/useVoiceAgentSquadStatus.d.ts +9 -0
- package/dist/vue/useVoiceCallDebugger.d.ts +10 -0
- 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/useVoicePlatformCoverage.d.ts +9 -0
- package/dist/vue/useVoiceProfileComparison.d.ts +9 -0
- package/dist/vue/useVoiceProofTrends.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 +1 -1
- package/dist/vue/useVoiceReadinessFailures.d.ts +899 -0
- package/dist/vue/useVoiceRoutingStatus.d.ts +8 -0
- package/dist/vue/useVoiceSessionSnapshot.d.ts +10 -0
- package/dist/vue/useVoiceStream.d.ts +3 -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 +4 -1
package/README.md
CHANGED
|
@@ -1,77 +1,2538 @@
|
|
|
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
4
|
|
|
5
|
-
|
|
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
6
|
|
|
7
|
-
|
|
8
|
-
|
|
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
|
+
## Buyer Paths
|
|
31
|
+
|
|
32
|
+
These are the primitive-first paths a Vapi-style buyer usually needs. Each path stays inside your AbsoluteJS app; the package gives you route handlers, stores, reports, hooks, composables, services, widgets, and contracts instead of a hosted dashboard.
|
|
33
|
+
|
|
34
|
+
| If you need | Start with | Add proof with | UI entrypoints |
|
|
35
|
+
| --- | --- | --- | --- |
|
|
36
|
+
| Web voice assistant | `voice(...)` or `createVoiceAssistant(...)` | trace timeline, turn quality, live latency, reconnect contract, operations record | React/Vue/Svelte/Angular voice stream helpers, HTML/HTMX/client helpers |
|
|
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
|
+
| Multi-specialist support flow | `createVoiceAgentSquad(...)` | squad contract, handoff traces, context traces, operations record | Agent Squad status hooks/composables/services/widgets |
|
|
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(...)`, `createVoiceGuardrailRuntime(...)`, and `createVoiceGuardrailRoutes(...)` | live assistant/tool enforcement, blocking/warning decisions, redacted content, `assistant.guardrail` trace events | guardrail JSON/Markdown routes and operations record traces |
|
|
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 |
|
|
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 |
|
|
43
|
+
| Outbound campaigns | `createVoiceCampaignRoutes(...)` | recipient validation, consent/dedupe, carrier dry-run, campaign readiness | campaign routes and operations-record-linked attempt proof |
|
|
44
|
+
| Simulation before launch | `createVoiceSimulationSuiteRoutes(...)` | scenarios, evals, tool contracts, outcome contracts, baselines | simulation-suite HTML/JSON and linked operations records |
|
|
45
|
+
| Compliance controls | runtime storage, audit logger, data-control routes | retention dry-run, redacted audit export, zero-retention policy, deploy gate | data-control HTML/JSON and audit/export routes |
|
|
46
|
+
|
|
47
|
+
## Capability Matrix
|
|
48
|
+
|
|
49
|
+
| Surface | Core package | Example/demo role | Not our lane |
|
|
50
|
+
| --- | --- | --- | --- |
|
|
51
|
+
| Browser voice | WebSocket voice route, client stream/controller/store primitives, reconnect, barge-in, latency, framework helpers | Prove the same mic/transcript/status workflow across React, Vue, Svelte, Angular, HTML, and HTMX | Hosted iframe widget that owns app UX |
|
|
52
|
+
| Telephony | Twilio, Telnyx, Plivo route bridges, phone-agent wrapper, setup instructions, smoke contracts, carrier outcomes | Show setup/smoke/proof surfaces and call lifecycle debugging | Buying/provisioning phone numbers for the user |
|
|
53
|
+
| Agents and tools | assistant, agent, tools, squads, handoff/context policies, contracts, audit hooks | Demonstrate realistic support/sales/workflow flows | Dashboard-only visual bot builder |
|
|
54
|
+
| Provider layer | OpenAI/Anthropic/Gemini model paths, STT/TTS adapter seams, routing, fallback, health, simulation | Show provider switching and health in UI | Reselling provider minutes or hiding provider accounts |
|
|
55
|
+
| Observability | traces, timelines, replay, operations records, incident Markdown, ops recovery, readiness | Make every failing proof link to a call/session record | Vendor dashboard as source of truth |
|
|
56
|
+
| Evals and simulation | fixtures, eval routes, simulation suite, workflow/tool/outcome contracts, baselines | Prove flows before live traffic | Opaque hosted test runner |
|
|
57
|
+
| Data and compliance controls | file/SQLite/Postgres/S3 storage paths, redaction, retention, audit exports, guarded deletion | Show customer-owned storage and export proof | Legal certification or compliance attestation |
|
|
58
|
+
|
|
59
|
+
## Proof Pack
|
|
60
|
+
|
|
61
|
+
Use this checklist when a buyer asks, "How do I know this replaces a hosted voice dashboard?" Each artifact is a route, report, contract, or export the app owns. The point is not screenshots; the point is reproducible proof that can live in CI, an internal admin page, or a customer-facing demo.
|
|
62
|
+
|
|
63
|
+
| Buyer question | Proof artifact | What it proves |
|
|
64
|
+
| --- | --- | --- |
|
|
65
|
+
| Can I launch a browser voice agent quickly? | `/voice`, framework mic/transcript UI, `/traces`, `/production-readiness` | Browser voice route, live transcript, trace persistence, readiness gate |
|
|
66
|
+
| Can I run phone agents without hosted orchestration? | `/voice/phone/setup`, `/voice/phone/smoke-contract`, carrier matrix JSON | Carrier setup instructions, webhook/stream URLs, smoke proof, lifecycle traces |
|
|
67
|
+
| Can I debug a bad call like a hosted call log? | `/voice-operations/:sessionId`, `/voice-operations/:sessionId/incident.md` | Transcript, trace timeline, provider decisions, tools, handoffs, reviews, tasks, audit, deliveries |
|
|
68
|
+
| Can I test before production traffic? | `/voice/simulations`, tool contracts, outcome contracts, workflow contracts | Scenario/eval proof, tool idempotency/retry proof, business outcome proof |
|
|
69
|
+
| Can I prove provider fallback and latency? | provider contract matrix, provider status UI, `/turn-latency`, `/live-latency` | Provider choice, fallback behavior, server turn timing, browser p50/p95 timing |
|
|
70
|
+
| Can operators intervene safely? | live-ops routes, action center, ops action audit routes, operations record | Pause/resume/takeover, injected instructions, operator action audit trail |
|
|
71
|
+
| Can I run outbound campaigns? | `/voice/campaigns`, `/voice/campaigns/observability`, `/api/voice/campaigns/readiness-proof` | Recipient import evidence, consent/dedupe checks, scheduling policy, worker-safe attempts |
|
|
72
|
+
| Can I handle post-call workflow? | `createVoicePostCallAnalysisRoutes(...)`, reviews, tasks, integration events, outcome contracts, operations record | Extracted-field proof, task creation, webhook/sink delivery, matched session proof |
|
|
73
|
+
| Can I keep data in my infrastructure? | `/data-control`, `/data-control/audit.md`, retention dry-run/apply routes | Customer-owned storage, redaction, audit export, guarded deletion, zero-retention planning |
|
|
74
|
+
| Can I prove release readiness? | `/production-readiness`, `/ops-recovery`, delivery runtime, readiness profiles | Deploy gates for session health, audits, delivery queues, provider/campaign/phone proof |
|
|
75
|
+
|
|
76
|
+
For a demo, the fastest convincing path is:
|
|
77
|
+
|
|
78
|
+
1. Start a browser or phone session.
|
|
79
|
+
2. Open `/voice-operations/:sessionId` for the session.
|
|
80
|
+
3. Open `/production-readiness` and follow any linked proof surface.
|
|
81
|
+
4. Open `/voice/simulations` or the relevant tool/outcome contract route.
|
|
82
|
+
5. Open `/data-control` to show where retention, redaction, and audit export live.
|
|
83
|
+
|
|
84
|
+
If those five surfaces are green and linked, the buyer can see the core difference from Vapi-style hosted orchestration: the operational proof lives inside the app, not in a vendor dashboard.
|
|
85
|
+
|
|
86
|
+
## Post-Call Analysis Proof
|
|
87
|
+
|
|
88
|
+
Use `createVoicePostCallAnalysisRoutes(...)` when the hosted-platform feature you need is call analysis plus follow-up proof. It validates that required extracted fields exist, expected ops tasks were created, integration/webhook events delivered, and the report links back to `/voice-operations/:sessionId`.
|
|
89
|
+
|
|
90
|
+
```ts
|
|
91
|
+
import { createVoicePostCallAnalysisRoutes } from '@absolutejs/voice';
|
|
92
|
+
|
|
93
|
+
app.use(
|
|
94
|
+
createVoicePostCallAnalysisRoutes({
|
|
95
|
+
path: '/api/voice/post-call-analysis',
|
|
96
|
+
operationRecordBasePath: '/voice-operations/:sessionId',
|
|
97
|
+
reviews: runtime.reviews,
|
|
98
|
+
tasks: runtime.tasks,
|
|
99
|
+
integrationEvents: runtime.events,
|
|
100
|
+
source: ({ reviewId, sessionId }) => ({
|
|
101
|
+
reviewId,
|
|
102
|
+
sessionId,
|
|
103
|
+
// Use your own extractor output here, for example fields persisted from an LLM/tool result.
|
|
104
|
+
extractedFields: loadExtractedPostCallFields(reviewId ?? sessionId)
|
|
105
|
+
}),
|
|
106
|
+
fields: [
|
|
107
|
+
{ path: 'review.postCall.target', label: 'customer target' },
|
|
108
|
+
{ path: 'customerId' },
|
|
109
|
+
{ path: 'category' }
|
|
110
|
+
],
|
|
111
|
+
requiredTaskKinds: ['support-triage'],
|
|
112
|
+
requireDeliveredIntegrationEvent: true
|
|
113
|
+
})
|
|
114
|
+
);
|
|
9
115
|
```
|
|
10
116
|
|
|
11
|
-
|
|
117
|
+
## Guardrails
|
|
12
118
|
|
|
13
|
-
-
|
|
14
|
-
- `elysia`
|
|
119
|
+
Use `createVoiceGuardrailRuntime(...)` when you need code-owned live enforcement for what an agent may say, what tool payloads may contain, or which transcript content should warn/redact before downstream workflow. Use `createVoiceGuardrailRoutes(...)` beside it when you also want JSON/Markdown proof. The primitive does not force a moderation vendor or hosted dashboard; it emits `assistant.guardrail` trace events from the runtime and route surfaces.
|
|
15
120
|
|
|
16
|
-
|
|
121
|
+
```ts
|
|
122
|
+
import {
|
|
123
|
+
createVoiceGuardrailRuntime,
|
|
124
|
+
createVoiceGuardrailRoutes,
|
|
125
|
+
voiceGuardrailPolicyPresets
|
|
126
|
+
} from '@absolutejs/voice';
|
|
17
127
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
128
|
+
const guardrails = createVoiceGuardrailRuntime({
|
|
129
|
+
blockResult: ({ decision }) => ({
|
|
130
|
+
assistantText: 'I need to route this to a human specialist.',
|
|
131
|
+
escalate: {
|
|
132
|
+
reason: `guardrail-blocked-${decision.stage}`
|
|
133
|
+
}
|
|
134
|
+
}),
|
|
135
|
+
policies: [voiceGuardrailPolicyPresets.supportSafeDefaults],
|
|
136
|
+
trace: runtime.traces
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
const assistant = createVoiceAssistant({
|
|
140
|
+
guardrails: guardrails.assistantGuardrails,
|
|
141
|
+
id: 'support',
|
|
142
|
+
model,
|
|
143
|
+
tools: guardrails.wrapTools([lookupCustomerTool, createTicketTool])
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
app.use(
|
|
147
|
+
createVoiceGuardrailRoutes({
|
|
148
|
+
path: '/api/voice/guardrails',
|
|
149
|
+
policies: [voiceGuardrailPolicyPresets.supportSafeDefaults],
|
|
150
|
+
trace: runtime.traces
|
|
151
|
+
})
|
|
152
|
+
);
|
|
153
|
+
```
|
|
23
154
|
|
|
24
|
-
##
|
|
155
|
+
## Use-Case Recipe: Support Triage
|
|
156
|
+
|
|
157
|
+
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.
|
|
158
|
+
|
|
159
|
+
The production shape is:
|
|
160
|
+
|
|
161
|
+
1. Persist sessions, traces, reviews, tasks, integration events, and audit events in app-owned runtime storage.
|
|
162
|
+
2. Define server-side tools with idempotency and contract proof.
|
|
163
|
+
3. Compose support and billing specialists with `createVoiceAgentSquad(...)`.
|
|
164
|
+
4. Mount a browser route with `voice(...)` and optionally a phone route with `createVoicePhoneAgent(...)`.
|
|
165
|
+
5. Add outcome, readiness, simulation, audit, and operations-record routes so every failed proof links back to the call.
|
|
25
166
|
|
|
26
167
|
```ts
|
|
27
168
|
import { Elysia } from 'elysia';
|
|
28
169
|
import {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
170
|
+
createVoiceAgent,
|
|
171
|
+
createVoiceAgentSquad,
|
|
172
|
+
createVoiceAgentTool,
|
|
173
|
+
createVoiceFileRuntimeStorage,
|
|
174
|
+
createVoiceOperationsRecordRoutes,
|
|
175
|
+
createVoiceProductionReadinessRoutes,
|
|
176
|
+
createVoiceSimulationSuiteRoutes,
|
|
177
|
+
createVoiceToolContractRoutes,
|
|
178
|
+
createVoiceToolRuntimeContractDefaults,
|
|
179
|
+
resolveVoiceOutcomeRecipe,
|
|
180
|
+
voice
|
|
32
181
|
} from '@absolutejs/voice';
|
|
33
182
|
import { deepgram } from '@absolutejs/voice-deepgram';
|
|
34
183
|
|
|
184
|
+
const runtime = createVoiceFileRuntimeStorage({
|
|
185
|
+
directory: '.voice-runtime/support-triage'
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
const lookupCustomer = createVoiceAgentTool({
|
|
189
|
+
name: 'lookup_customer',
|
|
190
|
+
description: 'Look up a customer and their open support state.',
|
|
191
|
+
parameters: {
|
|
192
|
+
type: 'object',
|
|
193
|
+
properties: {
|
|
194
|
+
customerId: { type: 'string' }
|
|
195
|
+
},
|
|
196
|
+
required: ['customerId']
|
|
197
|
+
},
|
|
198
|
+
execute: async ({ args }) => ({
|
|
199
|
+
customerId: args.customerId,
|
|
200
|
+
plan: 'business',
|
|
201
|
+
openTickets: 1,
|
|
202
|
+
status: 'active'
|
|
203
|
+
})
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
const support = createVoiceAgent({
|
|
207
|
+
id: 'support',
|
|
208
|
+
system: 'Triage the caller, use tools for account context, and hand billing questions to billing.',
|
|
209
|
+
tools: [lookupCustomer],
|
|
210
|
+
trace: runtime.traces,
|
|
211
|
+
model: {
|
|
212
|
+
async generate({ messages, tools }) {
|
|
213
|
+
const latest = messages.at(-1)?.content.toLowerCase() ?? '';
|
|
214
|
+
if (latest.includes('billing')) {
|
|
215
|
+
return {
|
|
216
|
+
assistantText: 'I am routing you to billing with the context so far.',
|
|
217
|
+
handoff: { reason: 'billing-request', targetAgentId: 'billing' }
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return {
|
|
222
|
+
assistantText: `I can help with that. I can also use ${tools.map((tool) => tool.name).join(', ')} when I need account context.`
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
const billing = createVoiceAgent({
|
|
229
|
+
id: 'billing',
|
|
230
|
+
system: 'Handle billing questions and escalate refund or cancellation risk.',
|
|
231
|
+
trace: runtime.traces,
|
|
232
|
+
model: {
|
|
233
|
+
async generate() {
|
|
234
|
+
return {
|
|
235
|
+
assistantText: 'I can help with billing. I have the handoff context from support.'
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
const supportDesk = createVoiceAgentSquad({
|
|
242
|
+
id: 'support-desk',
|
|
243
|
+
defaultAgentId: 'support',
|
|
244
|
+
agents: [support, billing],
|
|
245
|
+
trace: runtime.traces,
|
|
246
|
+
handoffPolicy: ({ handoff }) =>
|
|
247
|
+
handoff.targetAgentId === 'billing'
|
|
248
|
+
? {
|
|
249
|
+
summary: 'Billing specialist receives the support summary and current caller intent.',
|
|
250
|
+
metadata: { queue: 'billing' }
|
|
251
|
+
}
|
|
252
|
+
: {
|
|
253
|
+
allow: false,
|
|
254
|
+
reason: 'Only billing handoffs are approved in this recipe.',
|
|
255
|
+
escalate: { reason: 'unsupported-specialist' }
|
|
256
|
+
}
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
const toolContractDefinitions = [
|
|
260
|
+
{
|
|
261
|
+
id: 'lookup-customer-contract',
|
|
262
|
+
label: 'Lookup customer returns support state',
|
|
263
|
+
tool: lookupCustomer,
|
|
264
|
+
cases: [
|
|
265
|
+
{
|
|
266
|
+
id: 'active-business-customer',
|
|
267
|
+
args: { customerId: 'cus_123' },
|
|
268
|
+
expect: {
|
|
269
|
+
expectedResult: {
|
|
270
|
+
customerId: 'cus_123',
|
|
271
|
+
openTickets: 1,
|
|
272
|
+
plan: 'business',
|
|
273
|
+
status: 'active'
|
|
274
|
+
},
|
|
275
|
+
expectStatus: 'ok'
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
],
|
|
279
|
+
defaultRuntime: createVoiceToolRuntimeContractDefaults()
|
|
280
|
+
}
|
|
281
|
+
];
|
|
282
|
+
|
|
35
283
|
const app = new Elysia()
|
|
36
284
|
.use(
|
|
37
285
|
voice({
|
|
38
|
-
path: '/voice',
|
|
39
|
-
preset: '
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
286
|
+
path: '/voice/support',
|
|
287
|
+
preset: 'reliability',
|
|
288
|
+
session: runtime.session,
|
|
289
|
+
stt: deepgram({
|
|
290
|
+
apiKey: process.env.DEEPGRAM_API_KEY!,
|
|
291
|
+
model: 'flux-general-en'
|
|
292
|
+
}),
|
|
293
|
+
trace: runtime.traces,
|
|
294
|
+
onTurn: supportDesk.onTurn,
|
|
295
|
+
onComplete: async () => {},
|
|
296
|
+
ops: {
|
|
297
|
+
...resolveVoiceOutcomeRecipe('support-triage', {
|
|
298
|
+
assignee: 'support-oncall',
|
|
299
|
+
queue: 'support-triage'
|
|
300
|
+
}),
|
|
301
|
+
events: runtime.events,
|
|
302
|
+
reviews: runtime.reviews,
|
|
303
|
+
tasks: runtime.tasks
|
|
304
|
+
}
|
|
305
|
+
})
|
|
306
|
+
)
|
|
307
|
+
.use(
|
|
308
|
+
createVoiceToolContractRoutes({
|
|
309
|
+
contracts: toolContractDefinitions,
|
|
310
|
+
htmlPath: '/voice/support/tool-contracts',
|
|
311
|
+
path: '/api/voice/support/tool-contracts'
|
|
312
|
+
})
|
|
313
|
+
)
|
|
314
|
+
.use(
|
|
315
|
+
createVoiceSimulationSuiteRoutes({
|
|
316
|
+
htmlPath: '/voice/support/simulations',
|
|
317
|
+
path: '/api/voice/support/simulations',
|
|
318
|
+
tools: toolContractDefinitions,
|
|
319
|
+
operationsRecordHref: '/voice-operations/:sessionId'
|
|
320
|
+
})
|
|
321
|
+
)
|
|
322
|
+
.use(
|
|
323
|
+
createVoiceOperationsRecordRoutes({
|
|
324
|
+
htmlPath: '/voice-operations/:sessionId',
|
|
325
|
+
path: '/api/voice-operations/:sessionId',
|
|
326
|
+
store: runtime.traces,
|
|
327
|
+
reviews: runtime.reviews,
|
|
328
|
+
tasks: runtime.tasks,
|
|
329
|
+
integrationEvents: runtime.events
|
|
330
|
+
})
|
|
331
|
+
)
|
|
332
|
+
.use(
|
|
333
|
+
createVoiceProductionReadinessRoutes({
|
|
334
|
+
htmlPath: '/production-readiness',
|
|
335
|
+
path: '/api/production-readiness',
|
|
336
|
+
store: runtime.traces,
|
|
337
|
+
links: {
|
|
338
|
+
operationsRecords: '/voice-operations/:sessionId',
|
|
339
|
+
simulations: '/voice/support/simulations'
|
|
340
|
+
}
|
|
341
|
+
})
|
|
342
|
+
);
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
The demo UI should show a mic button, transcript, current specialist badge, readiness link, simulation link, and operations-record link. Use the framework helpers for that surface instead of embedding a dashboard: `VoiceAgentSquadStatus` in React, `useVoiceAgentSquadStatus(...)` in Vue, `createVoiceAgentSquadStatus(...)` in Svelte, `VoiceAgentSquadStatusService` in Angular, or `<absolute-voice-agent-squad-status>` for HTML/HTMX.
|
|
346
|
+
|
|
347
|
+
This recipe covers the hosted-platform expectations that matter for support triage: assistant entrypoint, business tools, specialist handoff, post-call task creation, simulation proof, tool contract proof, production readiness, audit-compatible runtime storage, and a call-log replacement at `/voice-operations/:sessionId`.
|
|
348
|
+
|
|
349
|
+
## Use-Case Recipe: Appointment Scheduling
|
|
350
|
+
|
|
351
|
+
Use this path when the assistant needs to check availability, book a slot, create a confirmation task, and prove the post-call workflow before production traffic. This is the self-hosted version of a hosted scheduling agent: your app owns the calendar tool, booking policy, storage, follow-up tasks, and call evidence.
|
|
352
|
+
|
|
353
|
+
The production shape is:
|
|
354
|
+
|
|
355
|
+
1. Persist sessions, traces, reviews, tasks, integration events, and audit events in app-owned runtime storage.
|
|
356
|
+
2. Define `check_availability` and `book_appointment` as server-side tools with deterministic tool contracts.
|
|
357
|
+
3. Use `resolveVoiceOutcomeRecipe('appointment-booking')` so completed calls create appointment-confirmation work.
|
|
358
|
+
4. Add an outcome contract that requires a completed session, review, task, and integration events.
|
|
359
|
+
5. Mount simulation, outcome-contract, readiness, and operations-record routes so scheduling regressions fail before live calls.
|
|
360
|
+
|
|
361
|
+
```ts
|
|
362
|
+
import { Elysia } from 'elysia';
|
|
363
|
+
import {
|
|
364
|
+
createVoiceAgent,
|
|
365
|
+
createVoiceAgentTool,
|
|
366
|
+
createVoiceFileRuntimeStorage,
|
|
367
|
+
createVoiceOperationsRecordRoutes,
|
|
368
|
+
createVoiceOutcomeContractRoutes,
|
|
369
|
+
createVoiceProductionReadinessRoutes,
|
|
370
|
+
createVoiceSimulationSuiteRoutes,
|
|
371
|
+
createVoiceToolContractRoutes,
|
|
372
|
+
createVoiceToolRuntimeContractDefaults,
|
|
373
|
+
resolveVoiceOutcomeRecipe,
|
|
374
|
+
voice
|
|
375
|
+
} from '@absolutejs/voice';
|
|
376
|
+
import { deepgram } from '@absolutejs/voice-deepgram';
|
|
377
|
+
|
|
378
|
+
const runtime = createVoiceFileRuntimeStorage({
|
|
379
|
+
directory: '.voice-runtime/appointments'
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
const checkAvailability = createVoiceAgentTool({
|
|
383
|
+
name: 'check_availability',
|
|
384
|
+
description: 'Return open appointment slots for a service and date.',
|
|
385
|
+
parameters: {
|
|
386
|
+
type: 'object',
|
|
387
|
+
properties: {
|
|
388
|
+
date: { type: 'string' },
|
|
389
|
+
service: { type: 'string' }
|
|
390
|
+
},
|
|
391
|
+
required: ['date', 'service']
|
|
392
|
+
},
|
|
393
|
+
execute: async ({ args }) => ({
|
|
394
|
+
date: args.date,
|
|
395
|
+
service: args.service,
|
|
396
|
+
slots: ['2026-05-04T15:00:00-04:00', '2026-05-04T16:30:00-04:00']
|
|
397
|
+
})
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
const bookAppointment = createVoiceAgentTool({
|
|
401
|
+
name: 'book_appointment',
|
|
402
|
+
description: 'Book a confirmed appointment slot.',
|
|
403
|
+
parameters: {
|
|
404
|
+
type: 'object',
|
|
405
|
+
properties: {
|
|
406
|
+
customerName: { type: 'string' },
|
|
407
|
+
phone: { type: 'string' },
|
|
408
|
+
service: { type: 'string' },
|
|
409
|
+
startsAt: { type: 'string' }
|
|
410
|
+
},
|
|
411
|
+
required: ['customerName', 'phone', 'service', 'startsAt']
|
|
412
|
+
},
|
|
413
|
+
execute: async ({ args }) => ({
|
|
414
|
+
appointmentId: `appt_${args.startsAt}`,
|
|
415
|
+
customerName: args.customerName,
|
|
416
|
+
phone: args.phone,
|
|
417
|
+
service: args.service,
|
|
418
|
+
startsAt: args.startsAt,
|
|
419
|
+
status: 'confirmed'
|
|
420
|
+
})
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
const scheduler = createVoiceAgent({
|
|
424
|
+
id: 'scheduler',
|
|
425
|
+
system: 'Collect caller details, check availability, book an appointment, and summarize the confirmation.',
|
|
426
|
+
tools: [checkAvailability, bookAppointment],
|
|
427
|
+
trace: runtime.traces,
|
|
428
|
+
model: {
|
|
429
|
+
async generate({ tools }) {
|
|
430
|
+
return {
|
|
431
|
+
assistantText: `I can check times and book the appointment. Available tools: ${tools.map((tool) => tool.name).join(', ')}`
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
const toolContractDefinitions = [
|
|
438
|
+
{
|
|
439
|
+
id: 'check-availability-contract',
|
|
440
|
+
label: 'Availability returns bookable slots',
|
|
441
|
+
tool: checkAvailability,
|
|
442
|
+
cases: [
|
|
443
|
+
{
|
|
444
|
+
id: 'consultation-slots',
|
|
445
|
+
args: { date: '2026-05-04', service: 'consultation' },
|
|
446
|
+
expect: { expectStatus: 'ok' }
|
|
447
|
+
}
|
|
448
|
+
],
|
|
449
|
+
defaultRuntime: createVoiceToolRuntimeContractDefaults()
|
|
450
|
+
},
|
|
451
|
+
{
|
|
452
|
+
id: 'book-appointment-contract',
|
|
453
|
+
label: 'Booking returns a confirmed appointment',
|
|
454
|
+
tool: bookAppointment,
|
|
455
|
+
cases: [
|
|
456
|
+
{
|
|
457
|
+
id: 'confirmed-consultation',
|
|
458
|
+
args: {
|
|
459
|
+
customerName: 'Ada Lovelace',
|
|
460
|
+
phone: '+15551234567',
|
|
461
|
+
service: 'consultation',
|
|
462
|
+
startsAt: '2026-05-04T15:00:00-04:00'
|
|
463
|
+
},
|
|
464
|
+
expect: {
|
|
465
|
+
expectedResult: {
|
|
466
|
+
appointmentId: 'appt_2026-05-04T15:00:00-04:00',
|
|
467
|
+
customerName: 'Ada Lovelace',
|
|
468
|
+
phone: '+15551234567',
|
|
469
|
+
service: 'consultation',
|
|
470
|
+
startsAt: '2026-05-04T15:00:00-04:00',
|
|
471
|
+
status: 'confirmed'
|
|
472
|
+
},
|
|
473
|
+
expectStatus: 'ok'
|
|
45
474
|
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
475
|
+
}
|
|
476
|
+
],
|
|
477
|
+
defaultRuntime: createVoiceToolRuntimeContractDefaults()
|
|
478
|
+
}
|
|
479
|
+
];
|
|
480
|
+
|
|
481
|
+
const outcomeContractDefinitions = [
|
|
482
|
+
{
|
|
483
|
+
id: 'appointment-booked',
|
|
484
|
+
label: 'Completed appointment call produces follow-up work',
|
|
485
|
+
expectedDisposition: 'completed',
|
|
486
|
+
minSessions: 1,
|
|
487
|
+
minTasks: 1,
|
|
488
|
+
requireIntegrationEvents: ['call.completed', 'review.saved', 'task.created'],
|
|
489
|
+
requireReview: true,
|
|
490
|
+
requireTask: true
|
|
491
|
+
}
|
|
492
|
+
];
|
|
493
|
+
|
|
494
|
+
const app = new Elysia()
|
|
495
|
+
.use(
|
|
496
|
+
voice({
|
|
497
|
+
path: '/voice/appointments',
|
|
498
|
+
preset: 'reliability',
|
|
499
|
+
session: runtime.session,
|
|
66
500
|
stt: deepgram({
|
|
67
501
|
apiKey: process.env.DEEPGRAM_API_KEY!,
|
|
68
|
-
model: '
|
|
69
|
-
})
|
|
502
|
+
model: 'flux-general-en'
|
|
503
|
+
}),
|
|
504
|
+
trace: runtime.traces,
|
|
505
|
+
onTurn: scheduler.onTurn,
|
|
506
|
+
onComplete: async () => {},
|
|
507
|
+
ops: {
|
|
508
|
+
...resolveVoiceOutcomeRecipe('appointment-booking', {
|
|
509
|
+
assignee: 'scheduling-oncall',
|
|
510
|
+
queue: 'appointments'
|
|
511
|
+
}),
|
|
512
|
+
events: runtime.events,
|
|
513
|
+
reviews: runtime.reviews,
|
|
514
|
+
tasks: runtime.tasks
|
|
515
|
+
}
|
|
516
|
+
})
|
|
517
|
+
)
|
|
518
|
+
.use(
|
|
519
|
+
createVoiceToolContractRoutes({
|
|
520
|
+
contracts: toolContractDefinitions,
|
|
521
|
+
htmlPath: '/voice/appointments/tool-contracts',
|
|
522
|
+
path: '/api/voice/appointments/tool-contracts'
|
|
523
|
+
})
|
|
524
|
+
)
|
|
525
|
+
.use(
|
|
526
|
+
createVoiceOutcomeContractRoutes({
|
|
527
|
+
contracts: outcomeContractDefinitions,
|
|
528
|
+
events: runtime.events,
|
|
529
|
+
htmlPath: '/voice/appointments/outcome-contracts',
|
|
530
|
+
operationsRecordHref: '/voice-operations/:sessionId',
|
|
531
|
+
path: '/api/voice/appointments/outcome-contracts',
|
|
532
|
+
reviews: runtime.reviews,
|
|
533
|
+
sessions: runtime.session,
|
|
534
|
+
tasks: runtime.tasks
|
|
535
|
+
})
|
|
536
|
+
)
|
|
537
|
+
.use(
|
|
538
|
+
createVoiceSimulationSuiteRoutes({
|
|
539
|
+
htmlPath: '/voice/appointments/simulations',
|
|
540
|
+
operationsRecordHref: '/voice-operations/:sessionId',
|
|
541
|
+
outcomes: {
|
|
542
|
+
contracts: outcomeContractDefinitions,
|
|
543
|
+
events: runtime.events,
|
|
544
|
+
reviews: runtime.reviews,
|
|
545
|
+
sessions: runtime.session,
|
|
546
|
+
tasks: runtime.tasks
|
|
547
|
+
},
|
|
548
|
+
path: '/api/voice/appointments/simulations',
|
|
549
|
+
tools: toolContractDefinitions
|
|
550
|
+
})
|
|
551
|
+
)
|
|
552
|
+
.use(
|
|
553
|
+
createVoiceOperationsRecordRoutes({
|
|
554
|
+
htmlPath: '/voice-operations/:sessionId',
|
|
555
|
+
integrationEvents: runtime.events,
|
|
556
|
+
path: '/api/voice-operations/:sessionId',
|
|
557
|
+
reviews: runtime.reviews,
|
|
558
|
+
store: runtime.traces,
|
|
559
|
+
tasks: runtime.tasks
|
|
560
|
+
})
|
|
561
|
+
)
|
|
562
|
+
.use(
|
|
563
|
+
createVoiceProductionReadinessRoutes({
|
|
564
|
+
htmlPath: '/production-readiness',
|
|
565
|
+
links: {
|
|
566
|
+
operationsRecords: '/voice-operations/:sessionId',
|
|
567
|
+
simulations: '/voice/appointments/simulations'
|
|
568
|
+
},
|
|
569
|
+
path: '/api/production-readiness',
|
|
570
|
+
store: runtime.traces
|
|
70
571
|
})
|
|
71
572
|
);
|
|
72
573
|
```
|
|
73
574
|
|
|
74
|
-
|
|
575
|
+
The UI should keep the scheduling flow simple: microphone, transcript, selected slot, booking status, confirmation task link, `/voice/appointments/simulations`, `/voice/appointments/outcome-contracts`, `/production-readiness`, and `/voice-operations/:sessionId`. If the booking or confirmation proof fails, the operator should start at the outcome contract and follow the linked operations record.
|
|
576
|
+
|
|
577
|
+
This recipe covers the hosted-platform expectations that matter for appointment scheduling: scheduling tools, deterministic tool proof, post-call confirmation work, outcome validation, simulation proof, production readiness, and one call-log replacement for debugging.
|
|
578
|
+
|
|
579
|
+
## Use-Case Recipe: Campaign Outreach
|
|
580
|
+
|
|
581
|
+
Use this path when you need Retell/Bland-style outbound outreach without handing recipients, consent proof, attempt policy, carrier outcomes, or debugging records to a hosted campaign dashboard. The package gives you campaign primitives; your app decides who can upload recipients, when workers run, which carrier dials, and how campaign results sync back to your product.
|
|
582
|
+
|
|
583
|
+
The production shape is:
|
|
584
|
+
|
|
585
|
+
1. Store campaigns in app-owned storage.
|
|
586
|
+
2. Import recipients with consent checks, phone validation, dedupe, variables, and rejected-row evidence.
|
|
587
|
+
3. Configure campaign policy for max attempts, concurrency, attempt windows, quiet hours, rate limits, and retry backoff.
|
|
588
|
+
4. Use a carrier dialer or your own dialer function, then apply Twilio/Telnyx/Plivo webhook outcomes back to attempts.
|
|
589
|
+
5. Expose campaign routes, observability, readiness proof, production readiness, and operations-record links for every attempted call.
|
|
590
|
+
|
|
591
|
+
```ts
|
|
592
|
+
import { Elysia } from 'elysia';
|
|
593
|
+
import {
|
|
594
|
+
createVoiceCampaignRoutes,
|
|
595
|
+
createVoiceFileRuntimeStorage,
|
|
596
|
+
createVoiceOperationsRecordRoutes,
|
|
597
|
+
createVoiceProductionReadinessRoutes,
|
|
598
|
+
createVoiceReadinessProfile,
|
|
599
|
+
createVoiceSQLiteCampaignStore,
|
|
600
|
+
runVoiceCampaignReadinessProof
|
|
601
|
+
} from '@absolutejs/voice';
|
|
602
|
+
|
|
603
|
+
const runtime = createVoiceFileRuntimeStorage({
|
|
604
|
+
directory: '.voice-runtime/campaign-outreach'
|
|
605
|
+
});
|
|
606
|
+
|
|
607
|
+
const campaigns = createVoiceSQLiteCampaignStore({
|
|
608
|
+
path: '.voice-runtime/campaigns.sqlite'
|
|
609
|
+
});
|
|
610
|
+
|
|
611
|
+
const app = new Elysia()
|
|
612
|
+
.use(
|
|
613
|
+
createVoiceCampaignRoutes({
|
|
614
|
+
htmlPath: '/voice/campaigns',
|
|
615
|
+
operationsRecordHref: '/voice-operations/:sessionId',
|
|
616
|
+
path: '/api/voice/campaigns',
|
|
617
|
+
store: campaigns,
|
|
618
|
+
title: 'Renewal Outreach'
|
|
619
|
+
})
|
|
620
|
+
)
|
|
621
|
+
.use(
|
|
622
|
+
createVoiceOperationsRecordRoutes({
|
|
623
|
+
htmlPath: '/voice-operations/:sessionId',
|
|
624
|
+
path: '/api/voice-operations/:sessionId',
|
|
625
|
+
store: runtime.traces
|
|
626
|
+
})
|
|
627
|
+
)
|
|
628
|
+
.use(
|
|
629
|
+
createVoiceProductionReadinessRoutes({
|
|
630
|
+
...createVoiceReadinessProfile('phone-agent', {
|
|
631
|
+
campaignReadiness: () =>
|
|
632
|
+
runVoiceCampaignReadinessProof({
|
|
633
|
+
store: campaigns
|
|
634
|
+
}),
|
|
635
|
+
explain: true
|
|
636
|
+
}),
|
|
637
|
+
links: {
|
|
638
|
+
campaigns: '/voice/campaigns',
|
|
639
|
+
operationsRecords: '/voice-operations/:sessionId'
|
|
640
|
+
},
|
|
641
|
+
store: runtime.traces
|
|
642
|
+
})
|
|
643
|
+
);
|
|
644
|
+
|
|
645
|
+
await fetch('/api/voice/campaigns', {
|
|
646
|
+
body: JSON.stringify({
|
|
647
|
+
maxAttempts: 3,
|
|
648
|
+
maxConcurrentAttempts: 10,
|
|
649
|
+
name: 'Renewal outreach',
|
|
650
|
+
schedule: {
|
|
651
|
+
attemptWindow: { startHour: 9, endHour: 17 },
|
|
652
|
+
quietHours: { startHour: 12, endHour: 13 },
|
|
653
|
+
rateLimit: { maxAttempts: 60, windowMs: 60_000 },
|
|
654
|
+
retryPolicy: { backoffMs: [5 * 60_000, 30 * 60_000] }
|
|
655
|
+
}
|
|
656
|
+
}),
|
|
657
|
+
headers: { 'content-type': 'application/json' },
|
|
658
|
+
method: 'POST'
|
|
659
|
+
});
|
|
660
|
+
|
|
661
|
+
await fetch('/api/voice/campaigns/campaign-1/recipients/import', {
|
|
662
|
+
body: JSON.stringify({
|
|
663
|
+
csv: `id,name,phone,consent,segment
|
|
664
|
+
recipient-1,Ada,+15550001001,yes,trial
|
|
665
|
+
recipient-2,Grace,+15550001002,true,enterprise
|
|
666
|
+
recipient-3,Linus,not-a-phone,yes,partner
|
|
667
|
+
recipient-4,Barbara,+15550001004,no,trial`,
|
|
668
|
+
metadataColumns: ['segment'],
|
|
669
|
+
requireConsent: true,
|
|
670
|
+
variableColumns: ['segment']
|
|
671
|
+
}),
|
|
672
|
+
headers: { 'content-type': 'application/json' },
|
|
673
|
+
method: 'POST'
|
|
674
|
+
});
|
|
675
|
+
|
|
676
|
+
await fetch('/api/voice/campaigns/campaign-1/enqueue', {
|
|
677
|
+
method: 'POST'
|
|
678
|
+
});
|
|
679
|
+
```
|
|
680
|
+
|
|
681
|
+
Use `/api/voice/campaigns/campaign-1/tick` for manual workers or `createVoiceCampaignWorkerLoop(...)` when the app should continuously drain eligible recipients. The runtime enforces the campaign policy on each tick, so parallel workers do not double-start recipients and attempts respect quiet hours, rate limits, retry backoff, and max attempts.
|
|
682
|
+
|
|
683
|
+
For production carrier dialing, pass a `dialer` to `createVoiceCampaignRoutes(...)`: `createVoiceTwilioCampaignDialer(...)`, `createVoiceTelnyxCampaignDialer(...)`, `createVoicePlivoCampaignDialer(...)`, or a custom dialer that starts the call and returns an external call id. Run `runVoiceCampaignDialerProof(...)` before live traffic to dry-run carrier request metadata and webhook outcome application.
|
|
684
|
+
|
|
685
|
+
The UI should show `/voice/campaigns`, `/voice/campaigns/observability`, `/api/voice/campaigns/readiness-proof`, `/production-readiness`, and operations-record links for recent attempts. If a recipient failed, the operator should open the campaign attempt, follow `/voice-operations/:sessionId`, and see the same trace/review/task/audit context used by support and phone-agent flows.
|
|
686
|
+
|
|
687
|
+
This recipe covers the hosted-platform expectations that matter for campaign outreach: recipient import evidence, consent/dedupe checks, scheduling policy, worker-safe attempts, carrier dry-run proof, webhook outcome mapping, queue observability, readiness gating, and call-log replacement links.
|
|
688
|
+
|
|
689
|
+
## Use-Case Recipe: Meeting Recorder
|
|
690
|
+
|
|
691
|
+
Use this path when the product needs a browser recorder for meetings, interviews, demos, or internal calls: capture microphone audio, persist transcripts and traces, generate a post-call review, expose a replayable operations record, and keep retention/export controls inside the app. This is not a hosted meeting bot; it is a set of recorder primitives your AbsoluteJS UI can own.
|
|
692
|
+
|
|
693
|
+
The production shape is:
|
|
694
|
+
|
|
695
|
+
1. Mount a browser voice route with persistent session, trace, review, task, and audit-capable storage.
|
|
696
|
+
2. Use framework stream/controller helpers for the microphone, transcript, reconnect state, and recording status.
|
|
697
|
+
3. Persist a review artifact on completion with transcript, summary, latency, outcome, and recommended follow-up.
|
|
698
|
+
4. Mount trace timelines, operations records, production readiness, and data-control routes.
|
|
699
|
+
5. Gate release with the `meeting-recorder` readiness profile so reconnect, barge-in/interruption, provider routing, latency, and session-health proof stay visible.
|
|
700
|
+
|
|
701
|
+
```ts
|
|
702
|
+
import { Elysia } from 'elysia';
|
|
703
|
+
import {
|
|
704
|
+
createVoiceDataControlRoutes,
|
|
705
|
+
createVoiceFileRuntimeStorage,
|
|
706
|
+
createVoiceOperationsRecordRoutes,
|
|
707
|
+
createVoiceProductionReadinessRoutes,
|
|
708
|
+
createVoiceReadinessProfile,
|
|
709
|
+
createVoiceTraceTimelineRoutes,
|
|
710
|
+
voice,
|
|
711
|
+
voiceComplianceRedactionDefaults
|
|
712
|
+
} from '@absolutejs/voice';
|
|
713
|
+
import { deepgram } from '@absolutejs/voice-deepgram';
|
|
714
|
+
|
|
715
|
+
const runtime = createVoiceFileRuntimeStorage({
|
|
716
|
+
directory: '.voice-runtime/meeting-recorder'
|
|
717
|
+
});
|
|
718
|
+
|
|
719
|
+
const app = new Elysia()
|
|
720
|
+
.use(
|
|
721
|
+
voice({
|
|
722
|
+
path: '/voice/meeting-recorder',
|
|
723
|
+
preset: 'reliability',
|
|
724
|
+
session: runtime.session,
|
|
725
|
+
stt: deepgram({
|
|
726
|
+
apiKey: process.env.DEEPGRAM_API_KEY!,
|
|
727
|
+
model: 'flux-general-en'
|
|
728
|
+
}),
|
|
729
|
+
trace: runtime.traces,
|
|
730
|
+
async onTurn({ turn }) {
|
|
731
|
+
return {
|
|
732
|
+
assistantText: '',
|
|
733
|
+
metadata: {
|
|
734
|
+
recorder: true,
|
|
735
|
+
transcript: turn.text
|
|
736
|
+
}
|
|
737
|
+
};
|
|
738
|
+
},
|
|
739
|
+
onComplete: async () => {},
|
|
740
|
+
ops: {
|
|
741
|
+
events: runtime.events,
|
|
742
|
+
reviews: runtime.reviews,
|
|
743
|
+
tasks: runtime.tasks,
|
|
744
|
+
buildReview: ({ session }) => ({
|
|
745
|
+
errors: [],
|
|
746
|
+
latencyBreakdown: [],
|
|
747
|
+
notes: ['Generated by the self-hosted meeting recorder path.'],
|
|
748
|
+
postCall: {
|
|
749
|
+
label: 'Meeting summary',
|
|
750
|
+
recommendedAction: 'Review the transcript and share action items.',
|
|
751
|
+
summary: 'Review transcript, decisions, and follow-up owners.'
|
|
752
|
+
},
|
|
753
|
+
summary: {
|
|
754
|
+
outcome: 'completed',
|
|
755
|
+
pass: true,
|
|
756
|
+
turnCount: session.turns.length
|
|
757
|
+
},
|
|
758
|
+
title: `Meeting recorder review for ${session.id}`,
|
|
759
|
+
timeline: [],
|
|
760
|
+
transcript: {
|
|
761
|
+
actual: session.turns
|
|
762
|
+
.map((turn) => turn.text)
|
|
763
|
+
.filter(Boolean)
|
|
764
|
+
.join('\n')
|
|
765
|
+
}
|
|
766
|
+
})
|
|
767
|
+
}
|
|
768
|
+
})
|
|
769
|
+
)
|
|
770
|
+
.use(
|
|
771
|
+
createVoiceTraceTimelineRoutes({
|
|
772
|
+
htmlPath: '/traces',
|
|
773
|
+
path: '/api/voice-traces',
|
|
774
|
+
store: runtime.traces
|
|
775
|
+
})
|
|
776
|
+
)
|
|
777
|
+
.use(
|
|
778
|
+
createVoiceOperationsRecordRoutes({
|
|
779
|
+
htmlPath: '/voice-operations/:sessionId',
|
|
780
|
+
integrationEvents: runtime.events,
|
|
781
|
+
path: '/api/voice-operations/:sessionId',
|
|
782
|
+
reviews: runtime.reviews,
|
|
783
|
+
store: runtime.traces,
|
|
784
|
+
tasks: runtime.tasks
|
|
785
|
+
})
|
|
786
|
+
)
|
|
787
|
+
.use(
|
|
788
|
+
createVoiceDataControlRoutes({
|
|
789
|
+
...runtime,
|
|
790
|
+
audit: runtime.audit,
|
|
791
|
+
auditDeliveries: runtime.auditDeliveries,
|
|
792
|
+
redact: voiceComplianceRedactionDefaults,
|
|
793
|
+
traceDeliveries: runtime.traceDeliveries
|
|
794
|
+
})
|
|
795
|
+
)
|
|
796
|
+
.use(
|
|
797
|
+
createVoiceProductionReadinessRoutes({
|
|
798
|
+
...createVoiceReadinessProfile('meeting-recorder', {
|
|
799
|
+
explain: true
|
|
800
|
+
}),
|
|
801
|
+
links: {
|
|
802
|
+
dataControl: '/data-control',
|
|
803
|
+
operationsRecords: '/voice-operations/:sessionId',
|
|
804
|
+
traces: '/traces'
|
|
805
|
+
},
|
|
806
|
+
store: runtime.traces
|
|
807
|
+
})
|
|
808
|
+
);
|
|
809
|
+
```
|
|
810
|
+
|
|
811
|
+
The UI should show a clear recording button, elapsed time, live transcript, reconnect state, recording status, a stop/finalize action, and links to `/voice-operations/:sessionId`, `/traces`, `/production-readiness`, and `/data-control`. Use `useVoiceStream(...)`, `useVoiceController(...)`, `createVoiceStream(...)`, `VoiceStreamService`, or the HTML/HTMX client helpers depending on the framework; the route and trace/review stores stay the same.
|
|
812
|
+
|
|
813
|
+
For customer-facing exports, use the same redaction/export primitives as support workflows: render trace Markdown or audit Markdown with `voiceComplianceRedactionDefaults`, then deliver it through file, webhook, or S3 delivery runtimes. For sensitive recordings, start with a retention dry run through `/data-control/retention/plan` before applying deletion.
|
|
814
|
+
|
|
815
|
+
This recipe covers the hosted-platform expectations that matter for meeting recorders: browser mic capture, live transcript UI, reconnect visibility, post-call review, transcript/debug record, readiness proof, customer-owned storage, retention controls, and redacted export paths.
|
|
816
|
+
|
|
817
|
+
## Use-Case Recipe: Compliance-Sensitive Calls
|
|
818
|
+
|
|
819
|
+
Use this path when the voice app handles sensitive customer support, healthcare-adjacent intake, financial workflows, internal investigations, or regulated customer data. AbsoluteJS Voice can provide self-hosted controls and evidence: customer-owned storage, provider-key ownership, redaction defaults, audit trails, guarded deletion, zero-retention policy helpers, redacted exports, and deploy gates. It does not certify the app for HIPAA, SOC 2, GDPR, or any other legal regime by itself.
|
|
820
|
+
|
|
821
|
+
The production shape is:
|
|
822
|
+
|
|
823
|
+
1. Use customer-owned runtime storage, preferably Postgres for production records and S3/webhook delivery for exported evidence.
|
|
824
|
+
2. Keep provider keys in the app owner environment, not in a hosted voice dashboard.
|
|
825
|
+
3. Pass `audit` into agents/tools/squads so provider calls, tool executions, handoffs, retention runs, and operator actions are recorded.
|
|
826
|
+
4. Mount data-control routes for redacted audit export, retention dry-runs, guarded deletion, zero-retention planning, and provider-key recommendations.
|
|
827
|
+
5. Make audit evidence, recent retention-policy evidence, and audit/trace delivery health part of production readiness.
|
|
828
|
+
|
|
829
|
+
```ts
|
|
830
|
+
import { Elysia } from 'elysia';
|
|
831
|
+
import {
|
|
832
|
+
applyVoiceDataRetentionPolicy,
|
|
833
|
+
buildVoiceDataRetentionPlan,
|
|
834
|
+
createVoiceAuditLogger,
|
|
835
|
+
createVoiceDataControlRoutes,
|
|
836
|
+
createVoiceOperationsRecordRoutes,
|
|
837
|
+
createVoicePostgresRuntimeStorage,
|
|
838
|
+
createVoiceProductionReadinessRoutes,
|
|
839
|
+
createVoiceZeroRetentionPolicy,
|
|
840
|
+
exportVoiceAuditTrail,
|
|
841
|
+
renderVoiceAuditMarkdown,
|
|
842
|
+
voiceComplianceRedactionDefaults
|
|
843
|
+
} from '@absolutejs/voice';
|
|
844
|
+
|
|
845
|
+
const runtime = createVoicePostgresRuntimeStorage({
|
|
846
|
+
connectionString: process.env.DATABASE_URL!,
|
|
847
|
+
schemaName: 'voice_ops',
|
|
848
|
+
tablePrefix: 'sensitive'
|
|
849
|
+
});
|
|
850
|
+
|
|
851
|
+
const audit = createVoiceAuditLogger(runtime.audit);
|
|
852
|
+
|
|
853
|
+
const app = new Elysia()
|
|
854
|
+
.use(
|
|
855
|
+
createVoiceDataControlRoutes({
|
|
856
|
+
...runtime,
|
|
857
|
+
audit: runtime.audit,
|
|
858
|
+
auditDeliveries: runtime.auditDeliveries,
|
|
859
|
+
path: '/data-control',
|
|
860
|
+
redact: voiceComplianceRedactionDefaults,
|
|
861
|
+
title: 'Sensitive Voice Data Control',
|
|
862
|
+
traceDeliveries: runtime.traceDeliveries
|
|
863
|
+
})
|
|
864
|
+
)
|
|
865
|
+
.use(
|
|
866
|
+
createVoiceOperationsRecordRoutes({
|
|
867
|
+
audit: runtime.audit,
|
|
868
|
+
htmlPath: '/voice-operations/:sessionId',
|
|
869
|
+
integrationEvents: runtime.events,
|
|
870
|
+
path: '/api/voice-operations/:sessionId',
|
|
871
|
+
reviews: runtime.reviews,
|
|
872
|
+
store: runtime.traces,
|
|
873
|
+
tasks: runtime.tasks
|
|
874
|
+
})
|
|
875
|
+
)
|
|
876
|
+
.use(
|
|
877
|
+
createVoiceProductionReadinessRoutes({
|
|
878
|
+
audit: {
|
|
879
|
+
require: [
|
|
880
|
+
{ type: 'provider.call' },
|
|
881
|
+
{ type: 'operator.action' },
|
|
882
|
+
{ type: 'retention.policy', maxAgeMs: 7 * 24 * 60 * 60 * 1000 }
|
|
883
|
+
],
|
|
884
|
+
store: runtime.audit
|
|
885
|
+
},
|
|
886
|
+
auditDeliveries: runtime.auditDeliveries,
|
|
887
|
+
links: {
|
|
888
|
+
audit: '/audit',
|
|
889
|
+
auditDeliveries: '/audit/deliveries',
|
|
890
|
+
dataControl: '/data-control',
|
|
891
|
+
operationsRecords: '/voice-operations/:sessionId',
|
|
892
|
+
traceDeliveries: '/traces/deliveries'
|
|
893
|
+
},
|
|
894
|
+
store: runtime.traces,
|
|
895
|
+
traceDeliveries: runtime.traceDeliveries
|
|
896
|
+
})
|
|
897
|
+
);
|
|
898
|
+
|
|
899
|
+
await audit.operatorAction({
|
|
900
|
+
action: 'retention.policy.reviewed',
|
|
901
|
+
actor: { id: 'ops-admin', type: 'operator' },
|
|
902
|
+
outcome: 'ok',
|
|
903
|
+
resource: { id: 'zero-retention-policy', type: 'voice.retention' }
|
|
904
|
+
});
|
|
905
|
+
|
|
906
|
+
const zeroRetentionPolicy = createVoiceZeroRetentionPolicy({
|
|
907
|
+
...runtime,
|
|
908
|
+
audit: runtime.audit,
|
|
909
|
+
auditDeliveries: runtime.auditDeliveries,
|
|
910
|
+
traceDeliveries: runtime.traceDeliveries
|
|
911
|
+
});
|
|
912
|
+
|
|
913
|
+
const retentionPlan = await buildVoiceDataRetentionPlan(zeroRetentionPolicy);
|
|
914
|
+
|
|
915
|
+
if (retentionPlan.deletedCount > 0) {
|
|
916
|
+
await applyVoiceDataRetentionPolicy({
|
|
917
|
+
...zeroRetentionPolicy,
|
|
918
|
+
dryRun: false
|
|
919
|
+
});
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
const auditExport = await exportVoiceAuditTrail({
|
|
923
|
+
redact: voiceComplianceRedactionDefaults,
|
|
924
|
+
store: runtime.audit
|
|
925
|
+
});
|
|
926
|
+
|
|
927
|
+
const redactedAuditMarkdown = renderVoiceAuditMarkdown(auditExport.events, {
|
|
928
|
+
title: 'Sensitive Voice Audit Export'
|
|
929
|
+
});
|
|
930
|
+
```
|
|
931
|
+
|
|
932
|
+
For the actual agent or squad, pass the same `audit` logger into `createVoiceAgent(...)`, `createVoiceAgentSquad(...)`, or `createVoiceAssistant(...)` with explicit `auditProvider` and `auditModel` labels. That makes provider usage and tool execution visible in `/data-control/audit.md`, `/audit`, readiness checks, and operations records.
|
|
933
|
+
|
|
934
|
+
The UI should expose `/data-control`, `/data-control.json`, `/data-control/audit.md`, `/data-control/retention/plan`, `/production-readiness`, and `/voice-operations/:sessionId`. Destructive retention application should remain a server-side operator action that first reviews the dry-run plan and then posts `confirm: "apply-retention-policy"`.
|
|
935
|
+
|
|
936
|
+
This recipe covers the hosted-platform expectations that matter for compliance-sensitive voice apps: customer-owned records, provider-key ownership, redacted exports, audit evidence, guarded retention, zero-retention planning, deploy gates, and a clear boundary that the package supplies controls and proof artifacts, not legal certification.
|
|
937
|
+
|
|
938
|
+
## How This Differs From Hosted Voice Platforms
|
|
939
|
+
|
|
940
|
+
Hosted voice-agent platforms are strongest when you want a managed dashboard, phone-number provisioning, hosted orchestration, and campaign tooling out of the box.
|
|
941
|
+
|
|
942
|
+
AbsoluteJS Voice is strongest when voice is part of your own product and you need code-owned primitives:
|
|
943
|
+
|
|
944
|
+
- Your app stores the call data instead of a vendor dashboard being the source of truth.
|
|
945
|
+
- Your app controls provider routing, fallback, retries, handoffs, and retention.
|
|
946
|
+
- Your team can inspect and extend every primitive.
|
|
947
|
+
- Your framework UI can render first-class voice state without iframe/dashboard handoffs.
|
|
948
|
+
- Your production checks and evals can run in CI, smoke tests, or your own admin UI.
|
|
949
|
+
|
|
950
|
+
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.
|
|
951
|
+
|
|
952
|
+
## Default Debug Path
|
|
953
|
+
|
|
954
|
+
Hosted platforms usually make the call log the center of debugging. AbsoluteJS Voice makes the operations record that center, while keeping the data and routes inside your app.
|
|
955
|
+
|
|
956
|
+
Mount `createVoiceOperationsRecordRoutes(...)` early in any serious voice app and use `/voice-operations/:sessionId` as the first support link for failed calls, bad transcripts, provider fallback, slow turns, handoff failures, campaign attempts, and post-call workflow issues.
|
|
957
|
+
|
|
958
|
+
The recommended investigation path is:
|
|
959
|
+
|
|
960
|
+
1. Open `/production-readiness`, `/ops-recovery`, `/voice/simulations`, a tool contract report, or an outcome contract report.
|
|
961
|
+
2. Follow the linked `/voice-operations/:sessionId` record for the impacted call or session.
|
|
962
|
+
3. Inspect the transcript, trace timeline, replay links, provider decisions, tool calls, handoffs, reviews, tasks, audit events, integration events, and sink delivery attempts from one page.
|
|
963
|
+
4. Use `/voice-operations/:sessionId/incident.md` when support, engineering, or a customer-facing handoff needs copyable incident context.
|
|
964
|
+
|
|
965
|
+
That is the Vapi-style call-log workflow without a vendor dashboard becoming the source of truth.
|
|
966
|
+
|
|
967
|
+
## Switching From Vapi
|
|
968
|
+
|
|
969
|
+
If a team is already evaluating Vapi, map the dashboard concepts to AbsoluteJS primitives this way:
|
|
970
|
+
|
|
971
|
+
| Hosted voice-platform concept | AbsoluteJS Voice primitive |
|
|
972
|
+
| --- | --- |
|
|
973
|
+
| Assistant | `createVoiceAssistant(...)`, `createVoiceAgent(...)`, or `voice({ onTurn })` |
|
|
974
|
+
| Web call | `voice(...)` plus React, Vue, Svelte, Angular, HTML, HTMX, or client helpers |
|
|
975
|
+
| Phone call | `createVoicePhoneAgent(...)` with Twilio, Telnyx, or Plivo routes |
|
|
976
|
+
| Squads / multi-assistant routing | `createVoiceAgentSquad(...)` with `handoffPolicy`, `contextPolicy`, specialist tools, traces, and squad contracts |
|
|
977
|
+
| Tools / functions | Agent tools, tool runtime, `runVoiceToolContract(...)`, audit events, and integration events |
|
|
978
|
+
| Call logs | `/voice-operations/:sessionId`, trace timelines, replay links, reviews, tasks, audit, provider decisions, and delivery queues |
|
|
979
|
+
| Post-call analysis | reviews, outcomes, ops tasks, handoff deliveries, integration events, webhook/audit sinks, and outcome contracts |
|
|
980
|
+
| Simulation testing | `createVoiceSimulationSuiteRoutes(...)` with scenarios, fixtures, tool contracts, outcome contracts, and baseline comparison |
|
|
981
|
+
| Production monitoring | `createVoiceProductionReadinessRoutes(...)`, `createVoiceOpsRecoveryRoutes(...)`, ops status, latency SLO gates, provider health, and delivery runtime proof |
|
|
982
|
+
| Campaigns | `createVoiceCampaignRoutes(...)`, recipient import, scheduling controls, carrier dialer proof, and campaign readiness |
|
|
983
|
+
| Compliance controls | self-hosted storage, redaction defaults, retention plans, audit exports, data-control routes, and provider-key ownership |
|
|
984
|
+
|
|
985
|
+
The practical difference is ownership. In Vapi-style systems, the assistant, call log, tool execution, and operational dashboard live primarily in the vendor platform. With AbsoluteJS Voice, those same surfaces are route handlers, stores, reports, hooks, and contracts inside the AbsoluteJS app.
|
|
986
|
+
|
|
987
|
+
Use `createVoiceAssistant(...)` when you want a product-level assistant surface with tools, guardrails, experiments, tracing, reviews, tasks, and ops recipes. Drop down to `createVoiceAgent(...)` when you want a provider-neutral model/tool loop. Use raw `voice({ onTurn })` when you want the smallest possible browser voice route.
|
|
988
|
+
|
|
989
|
+
Use `createVoiceAgentSquad(...)` for Vapi Squads-style specialist routing without moving routing policy into a hosted dashboard. Each specialist owns its tools, `handoffPolicy` decides whether to allow, reroute, block, or escalate transfers, and `contextPolicy` decides what conversation context the next specialist receives. Squad traces and contracts make the handoff graph testable before production.
|
|
990
|
+
|
|
991
|
+
Use `createVoicePhoneAgent(...)` when the hosted-platform feature you need is "call this assistant by phone." The wrapper mounts carrier routes, setup pages, carrier matrix proof, and smoke-contract routes while still letting your app own Twilio, Telnyx, or Plivo credentials, webhooks, stream URLs, traces, and lifecycle outcomes.
|
|
992
|
+
|
|
993
|
+
Use operations records instead of hosted call logs. A proof failure should link to `/voice-operations/:sessionId`; the record then links the transcript, replay, provider choices, tool calls, handoffs, audit events, reviews, tasks, integration events, and delivery attempts. When someone needs a support handoff, send `/voice-operations/:sessionId/incident.md`.
|
|
994
|
+
|
|
995
|
+
Use simulation and contracts before live traffic. The simulation suite, tool contracts, outcome contracts, provider routing contracts, phone-agent smoke contracts, and production-readiness gates turn dashboard-only confidence into code-owned deploy evidence.
|
|
996
|
+
|
|
997
|
+
### Vapi Migration Checklist
|
|
998
|
+
|
|
999
|
+
Use this checklist when a buyer asks whether AbsoluteJS Voice covers the practical Vapi surface area without becoming a hosted platform:
|
|
1000
|
+
|
|
1001
|
+
| Vapi evaluation question | AbsoluteJS proof to show |
|
|
1002
|
+
| --- | --- |
|
|
1003
|
+
| Can I make a web voice assistant? | Framework page using `voice(...)`, then `/traces` and `/production-readiness` |
|
|
1004
|
+
| Can I make phone calls? | `/phone-agent`, `/api/voice/phone/setup`, carrier matrix, and phone smoke proof |
|
|
1005
|
+
| Can I use multiple assistants? | `createVoiceAgentSquad(...)`, `/agent-squad-contract`, current-specialist framework helpers, and handoff traces |
|
|
1006
|
+
| Can I call tools/functions? | Tool definitions, `/tool-contracts`, audit events, integration events, and operations records |
|
|
1007
|
+
| Can I debug a bad call? | `/voice-operations/:sessionId`, session replay, trace timeline, incident Markdown, delivery attempts, and provider decisions |
|
|
1008
|
+
| Can I monitor production health? | `/production-readiness`, `/ops-recovery`, `/api/production-readiness/gate`, provider SLOs, and delivery runtime proof |
|
|
1009
|
+
| Can I test before live traffic? | `/voice/simulations`, scenario fixtures, tool contracts, outcome contracts, provider routing contracts, and eval baselines |
|
|
1010
|
+
| Can I run outbound campaigns? | `createVoiceCampaignRoutes(...)`, campaign readiness proof, carrier dry-run proof, retry/quiet-hours/rate-limit evidence |
|
|
1011
|
+
| Can operators intervene? | Live-ops routes, action-center helpers, pause/resume/takeover runtime controls, and operator action audit history |
|
|
1012
|
+
| Can I own compliance evidence? | `/data-control`, redacted audit export, retention dry-run/apply routes, provider-key recommendations, and customer-owned storage |
|
|
1013
|
+
| Can I export logs to my infrastructure? | `/voice/observability-export`, delivery receipts, artifact index, replay proof, S3/SQLite/Postgres/file/webhook destinations |
|
|
1014
|
+
|
|
1015
|
+
The migration path should start by replacing hosted-dashboard concepts with mounted primitives and proof routes. Do not start by copying a hosted dashboard. Start with the voice route, operations record, readiness gate, provider contracts, and customer-owned observability export; then add campaigns, live-ops, or compliance controls only when the app needs those surfaces.
|
|
1016
|
+
|
|
1017
|
+
## Install
|
|
1018
|
+
|
|
1019
|
+
```bash
|
|
1020
|
+
bun add @absolutejs/voice @absolutejs/voice-deepgram
|
|
1021
|
+
```
|
|
1022
|
+
|
|
1023
|
+
Peer dependencies:
|
|
1024
|
+
|
|
1025
|
+
- `@absolutejs/absolute`
|
|
1026
|
+
- `elysia`
|
|
1027
|
+
|
|
1028
|
+
Optional framework entrypoints:
|
|
1029
|
+
|
|
1030
|
+
- `@absolutejs/voice/react`
|
|
1031
|
+
- `@absolutejs/voice/vue`
|
|
1032
|
+
- `@absolutejs/voice/svelte`
|
|
1033
|
+
- `@absolutejs/voice/angular`
|
|
1034
|
+
- `@absolutejs/voice/client`
|
|
1035
|
+
|
|
1036
|
+
Common optional adapters:
|
|
1037
|
+
|
|
1038
|
+
- `@absolutejs/voice-deepgram`
|
|
1039
|
+
- `@absolutejs/voice-assemblyai`
|
|
1040
|
+
|
|
1041
|
+
## Fastest First Success
|
|
1042
|
+
|
|
1043
|
+
Use these paths when you want the smallest useful setup that still proves the app is production-shaped. The point is not to hide primitives; it is to mount the voice route plus the debug surfaces a real team needs immediately.
|
|
1044
|
+
|
|
1045
|
+
### Browser Agent In 10 Minutes
|
|
1046
|
+
|
|
1047
|
+
```ts
|
|
1048
|
+
import { Elysia } from 'elysia';
|
|
1049
|
+
import {
|
|
1050
|
+
createVoiceFileRuntimeStorage,
|
|
1051
|
+
createVoiceOperationsRecordRoutes,
|
|
1052
|
+
createVoiceOpsStatusRoutes,
|
|
1053
|
+
createVoiceProductionReadinessRoutes,
|
|
1054
|
+
createVoiceTraceTimelineRoutes,
|
|
1055
|
+
voice
|
|
1056
|
+
} from '@absolutejs/voice';
|
|
1057
|
+
import { deepgram } from '@absolutejs/voice-deepgram';
|
|
1058
|
+
|
|
1059
|
+
const runtime = createVoiceFileRuntimeStorage({
|
|
1060
|
+
directory: '.voice-runtime/support'
|
|
1061
|
+
});
|
|
1062
|
+
|
|
1063
|
+
export const app = new Elysia()
|
|
1064
|
+
.use(
|
|
1065
|
+
voice({
|
|
1066
|
+
path: '/voice',
|
|
1067
|
+
session: runtime.session,
|
|
1068
|
+
trace: runtime.traces,
|
|
1069
|
+
stt: deepgram({ apiKey: process.env.DEEPGRAM_API_KEY! }),
|
|
1070
|
+
async onTurn({ turn }) {
|
|
1071
|
+
return { assistantText: `I heard: ${turn.text}` };
|
|
1072
|
+
},
|
|
1073
|
+
onComplete: async () => {}
|
|
1074
|
+
})
|
|
1075
|
+
)
|
|
1076
|
+
.use(
|
|
1077
|
+
createVoiceOpsStatusRoutes({
|
|
1078
|
+
path: '/api/voice/ops-status',
|
|
1079
|
+
store: runtime.traces,
|
|
1080
|
+
sttProviders: ['deepgram']
|
|
1081
|
+
})
|
|
1082
|
+
)
|
|
1083
|
+
.use(
|
|
1084
|
+
createVoiceTraceTimelineRoutes({
|
|
1085
|
+
htmlPath: '/traces',
|
|
1086
|
+
path: '/api/voice-traces',
|
|
1087
|
+
store: runtime.traces
|
|
1088
|
+
})
|
|
1089
|
+
)
|
|
1090
|
+
.use(
|
|
1091
|
+
createVoiceOperationsRecordRoutes({
|
|
1092
|
+
audit: runtime.audit,
|
|
1093
|
+
htmlPath: '/voice-operations/:sessionId',
|
|
1094
|
+
path: '/api/voice-operations/:sessionId',
|
|
1095
|
+
store: runtime.traces
|
|
1096
|
+
})
|
|
1097
|
+
)
|
|
1098
|
+
.use(
|
|
1099
|
+
createVoiceProductionReadinessRoutes({
|
|
1100
|
+
links: {
|
|
1101
|
+
operationsRecords: '/voice-operations/:sessionId'
|
|
1102
|
+
},
|
|
1103
|
+
path: '/api/production-readiness',
|
|
1104
|
+
htmlPath: '/production-readiness',
|
|
1105
|
+
store: runtime.traces
|
|
1106
|
+
})
|
|
1107
|
+
);
|
|
1108
|
+
```
|
|
1109
|
+
|
|
1110
|
+
After one browser call, open:
|
|
1111
|
+
|
|
1112
|
+
- `/api/voice/ops-status`: compact health signal for UI/widgets.
|
|
1113
|
+
- `/traces`: trace timeline by session.
|
|
1114
|
+
- `/voice-operations/:sessionId`: call-log/debug record for the session.
|
|
1115
|
+
- `/voice-operations/:sessionId/incident.md`: copyable incident handoff.
|
|
1116
|
+
- `/production-readiness`: deploy gate summary.
|
|
1117
|
+
|
|
1118
|
+
### Phone Agent In 20 Minutes
|
|
1119
|
+
|
|
1120
|
+
```ts
|
|
1121
|
+
import { Elysia } from 'elysia';
|
|
1122
|
+
import {
|
|
1123
|
+
createVoiceFileRuntimeStorage,
|
|
1124
|
+
createVoiceOperationsRecordRoutes,
|
|
1125
|
+
createVoicePhoneAgent,
|
|
1126
|
+
createVoiceProductionReadinessRoutes,
|
|
1127
|
+
createVoiceReadinessProfile,
|
|
1128
|
+
createVoiceTelephonyOutcomePolicy
|
|
1129
|
+
} from '@absolutejs/voice';
|
|
1130
|
+
import { deepgram } from '@absolutejs/voice-deepgram';
|
|
1131
|
+
|
|
1132
|
+
const runtime = createVoiceFileRuntimeStorage({
|
|
1133
|
+
directory: '.voice-runtime/support'
|
|
1134
|
+
});
|
|
1135
|
+
const outcomePolicy = createVoiceTelephonyOutcomePolicy({
|
|
1136
|
+
transferTarget: process.env.VOICE_TRANSFER_TARGET
|
|
1137
|
+
});
|
|
1138
|
+
|
|
1139
|
+
export const app = new Elysia()
|
|
1140
|
+
.use(
|
|
1141
|
+
createVoicePhoneAgent({
|
|
1142
|
+
setup: { path: '/api/voice/phone/setup' },
|
|
1143
|
+
matrix: { path: '/api/carriers' },
|
|
1144
|
+
productionSmoke: {
|
|
1145
|
+
maxAgeMs: 24 * 60 * 60 * 1000,
|
|
1146
|
+
required: [
|
|
1147
|
+
'carrier-contract',
|
|
1148
|
+
'media-started',
|
|
1149
|
+
'transcript',
|
|
1150
|
+
'assistant-response',
|
|
1151
|
+
'lifecycle-outcome',
|
|
1152
|
+
'no-session-error',
|
|
1153
|
+
'fresh-trace'
|
|
1154
|
+
],
|
|
1155
|
+
store: runtime.traces
|
|
1156
|
+
},
|
|
1157
|
+
carriers: [
|
|
1158
|
+
{
|
|
1159
|
+
provider: 'twilio',
|
|
1160
|
+
options: {
|
|
1161
|
+
context: {},
|
|
1162
|
+
outcomePolicy,
|
|
1163
|
+
session: runtime.session,
|
|
1164
|
+
stt: deepgram({ apiKey: process.env.DEEPGRAM_API_KEY! }),
|
|
1165
|
+
streamPath: '/api/voice/twilio/stream',
|
|
1166
|
+
twiml: {
|
|
1167
|
+
path: '/api/voice/twilio',
|
|
1168
|
+
streamUrl: process.env.TWILIO_STREAM_URL
|
|
1169
|
+
},
|
|
1170
|
+
webhook: {
|
|
1171
|
+
path: '/api/voice/twilio/webhook',
|
|
1172
|
+
signingSecret: process.env.TWILIO_AUTH_TOKEN
|
|
1173
|
+
},
|
|
1174
|
+
async onTurn({ turn }) {
|
|
1175
|
+
return { assistantText: `I heard: ${turn.text}` };
|
|
1176
|
+
},
|
|
1177
|
+
onComplete: async () => {}
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
1180
|
+
]
|
|
1181
|
+
}).routes
|
|
1182
|
+
)
|
|
1183
|
+
.use(
|
|
1184
|
+
createVoiceOperationsRecordRoutes({
|
|
1185
|
+
audit: runtime.audit,
|
|
1186
|
+
htmlPath: '/voice-operations/:sessionId',
|
|
1187
|
+
path: '/api/voice-operations/:sessionId',
|
|
1188
|
+
store: runtime.traces
|
|
1189
|
+
})
|
|
1190
|
+
)
|
|
1191
|
+
.use(
|
|
1192
|
+
createVoiceProductionReadinessRoutes({
|
|
1193
|
+
...createVoiceReadinessProfile('phone-agent', {
|
|
1194
|
+
explain: true
|
|
1195
|
+
}),
|
|
1196
|
+
links: {
|
|
1197
|
+
operationsRecords: '/voice-operations/:sessionId'
|
|
1198
|
+
},
|
|
1199
|
+
path: '/api/production-readiness',
|
|
1200
|
+
htmlPath: '/production-readiness',
|
|
1201
|
+
store: runtime.traces
|
|
1202
|
+
})
|
|
1203
|
+
);
|
|
1204
|
+
```
|
|
1205
|
+
|
|
1206
|
+
Open `/api/voice/phone/setup?format=html`, copy the reported Twilio URLs into the carrier dashboard, run one smoke call, then inspect:
|
|
1207
|
+
|
|
1208
|
+
- `/api/carriers?format=html`: carrier setup matrix.
|
|
1209
|
+
- `/voice/phone/smoke-contract?sessionId=...`: trace-backed phone smoke proof.
|
|
1210
|
+
- `/voice-operations/:sessionId`: call-log/debug record.
|
|
1211
|
+
- `/production-readiness`: phone-agent readiness gate.
|
|
1212
|
+
|
|
1213
|
+
## Browser Voice Agent
|
|
1214
|
+
|
|
1215
|
+
```ts
|
|
1216
|
+
import { Elysia } from 'elysia';
|
|
1217
|
+
import {
|
|
1218
|
+
voice,
|
|
1219
|
+
createVoiceMemoryStore,
|
|
1220
|
+
createPhraseHintCorrectionHandler
|
|
1221
|
+
} from '@absolutejs/voice';
|
|
1222
|
+
import { deepgram } from '@absolutejs/voice-deepgram';
|
|
1223
|
+
|
|
1224
|
+
const app = new Elysia()
|
|
1225
|
+
.use(
|
|
1226
|
+
voice({
|
|
1227
|
+
path: '/voice',
|
|
1228
|
+
preset: 'guided-intake',
|
|
1229
|
+
lexicon: [
|
|
1230
|
+
{
|
|
1231
|
+
text: 'AbsoluteJS',
|
|
1232
|
+
aliases: ['absoloot js'],
|
|
1233
|
+
pronunciation: 'ab-so-lute jay ess'
|
|
1234
|
+
}
|
|
1235
|
+
],
|
|
1236
|
+
phraseHints: [
|
|
1237
|
+
{ text: 'AbsoluteJS', aliases: ['absolute js'] },
|
|
1238
|
+
{ text: 'Joe Johnston', aliases: ['joe johnson'] }
|
|
1239
|
+
],
|
|
1240
|
+
correctTurn: createPhraseHintCorrectionHandler(),
|
|
1241
|
+
onComplete: async ({ session }) => {
|
|
1242
|
+
console.log(session.turns);
|
|
1243
|
+
},
|
|
1244
|
+
async onTurn({ turn }) {
|
|
1245
|
+
console.log('turn quality:', {
|
|
1246
|
+
source: turn.quality?.source,
|
|
1247
|
+
fallbackUsed: turn.quality?.fallbackUsed,
|
|
1248
|
+
confidence: turn.quality?.averageConfidence
|
|
1249
|
+
});
|
|
1250
|
+
return {
|
|
1251
|
+
assistantText: `You said: ${turn.text}`
|
|
1252
|
+
};
|
|
1253
|
+
},
|
|
1254
|
+
session: createVoiceMemoryStore(),
|
|
1255
|
+
stt: deepgram({
|
|
1256
|
+
apiKey: process.env.DEEPGRAM_API_KEY!,
|
|
1257
|
+
model: 'nova-3'
|
|
1258
|
+
})
|
|
1259
|
+
})
|
|
1260
|
+
);
|
|
1261
|
+
```
|
|
1262
|
+
|
|
1263
|
+
`createVoiceMemoryStore()` is dev-only. Real deployments should provide a shared store backed by Redis, Postgres, or equivalent.
|
|
1264
|
+
|
|
1265
|
+
## Production Readiness Path
|
|
1266
|
+
|
|
1267
|
+
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:
|
|
1268
|
+
|
|
1269
|
+
```ts
|
|
1270
|
+
import {
|
|
1271
|
+
createVoiceAuditDeliveryRoutes,
|
|
1272
|
+
createVoiceDemoReadyRoutes,
|
|
1273
|
+
createVoiceFileRuntimeStorage,
|
|
1274
|
+
createVoiceLiveLatencyRoutes,
|
|
1275
|
+
createVoiceOpsStatusRoutes,
|
|
1276
|
+
createVoiceProductionReadinessRoutes,
|
|
1277
|
+
createVoiceTraceDeliveryRoutes,
|
|
1278
|
+
createVoiceTraceTimelineRoutes,
|
|
1279
|
+
createVoiceTurnLatencyRoutes,
|
|
1280
|
+
createVoiceTurnQualityRoutes
|
|
1281
|
+
} from '@absolutejs/voice';
|
|
1282
|
+
|
|
1283
|
+
const runtime = createVoiceFileRuntimeStorage({
|
|
1284
|
+
directory: '.voice-runtime/support'
|
|
1285
|
+
});
|
|
1286
|
+
|
|
1287
|
+
app
|
|
1288
|
+
.use(
|
|
1289
|
+
createVoiceOpsStatusRoutes({
|
|
1290
|
+
store: runtime.traces,
|
|
1291
|
+
llmProviders: ['openai', 'anthropic', 'gemini'],
|
|
1292
|
+
sttProviders: ['deepgram', 'assemblyai']
|
|
1293
|
+
})
|
|
1294
|
+
)
|
|
1295
|
+
.use(
|
|
1296
|
+
createVoiceTurnLatencyRoutes({
|
|
1297
|
+
htmlPath: '/turn-latency',
|
|
1298
|
+
path: '/api/turn-latency',
|
|
1299
|
+
store: runtime.session,
|
|
1300
|
+
traceStore: runtime.traces
|
|
1301
|
+
})
|
|
1302
|
+
)
|
|
1303
|
+
.use(
|
|
1304
|
+
createVoiceLiveLatencyRoutes({
|
|
1305
|
+
htmlPath: '/live-latency',
|
|
1306
|
+
path: '/api/live-latency',
|
|
1307
|
+
store: runtime.traces
|
|
1308
|
+
})
|
|
1309
|
+
)
|
|
1310
|
+
.use(
|
|
1311
|
+
createVoiceTurnQualityRoutes({
|
|
1312
|
+
htmlPath: '/turn-quality',
|
|
1313
|
+
path: '/api/turn-quality',
|
|
1314
|
+
store: runtime.session
|
|
1315
|
+
})
|
|
1316
|
+
)
|
|
1317
|
+
.use(
|
|
1318
|
+
createVoiceTraceTimelineRoutes({
|
|
1319
|
+
htmlPath: '/traces',
|
|
1320
|
+
path: '/api/voice-traces',
|
|
1321
|
+
store: runtime.traces
|
|
1322
|
+
})
|
|
1323
|
+
)
|
|
1324
|
+
.use(
|
|
1325
|
+
createVoiceProductionReadinessRoutes({
|
|
1326
|
+
audit: runtime.audit,
|
|
1327
|
+
auditDeliveries: runtime.auditDeliveries,
|
|
1328
|
+
htmlPath: '/production-readiness',
|
|
1329
|
+
path: '/api/production-readiness',
|
|
1330
|
+
store: runtime.traces,
|
|
1331
|
+
traceDeliveries: runtime.traceDeliveries
|
|
1332
|
+
})
|
|
1333
|
+
)
|
|
1334
|
+
.use(
|
|
1335
|
+
createVoiceAuditDeliveryRoutes({
|
|
1336
|
+
htmlPath: '/audit/deliveries',
|
|
1337
|
+
path: '/api/voice-audit-deliveries',
|
|
1338
|
+
store: runtime.auditDeliveries
|
|
1339
|
+
})
|
|
1340
|
+
)
|
|
1341
|
+
.use(
|
|
1342
|
+
createVoiceTraceDeliveryRoutes({
|
|
1343
|
+
htmlPath: '/traces/deliveries',
|
|
1344
|
+
path: '/api/voice-trace-deliveries',
|
|
1345
|
+
store: runtime.traceDeliveries
|
|
1346
|
+
})
|
|
1347
|
+
);
|
|
1348
|
+
```
|
|
1349
|
+
|
|
1350
|
+
Recommended proof routes:
|
|
1351
|
+
|
|
1352
|
+
- `/api/voice/ops-status`: compact status for hooks, widgets, and customer-facing demos.
|
|
1353
|
+
- `/api/voice/ops-status/html`: HTML status card for quick internal review.
|
|
1354
|
+
- `/demo-ready`: customer-facing demo readiness checklist.
|
|
1355
|
+
- `/production-readiness`: production gate summary.
|
|
1356
|
+
- `/voice-operations/:sessionId`: default call-log/debug record for one problematic session.
|
|
1357
|
+
- `/voice-operations/:sessionId/incident.md`: copyable incident handoff for support and engineering.
|
|
1358
|
+
- `/audit/deliveries`: audit sink export queue and failed delivery details.
|
|
1359
|
+
- `/voice/phone/smoke-contract`: trace-backed phone-agent production smoke proof.
|
|
1360
|
+
- `/traces`: per-session trace timelines.
|
|
1361
|
+
- `/traces/deliveries`: trace sink export queue and failed delivery details.
|
|
1362
|
+
- `/turn-latency`: server-side turn-stage latency.
|
|
1363
|
+
- `/live-latency`: browser-measured speech-to-assistant p50/p95 latency.
|
|
1364
|
+
- `/turn-quality`: STT confidence, correction, fallback, and transcript diagnostics.
|
|
1365
|
+
|
|
1366
|
+
### Readiness Profiles
|
|
1367
|
+
|
|
1368
|
+
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.
|
|
1369
|
+
|
|
1370
|
+
```ts
|
|
1371
|
+
import {
|
|
1372
|
+
createVoiceProductionReadinessRoutes,
|
|
1373
|
+
createVoiceReadinessProfile
|
|
1374
|
+
} from '@absolutejs/voice';
|
|
1375
|
+
|
|
1376
|
+
app.use(
|
|
1377
|
+
createVoiceProductionReadinessRoutes({
|
|
1378
|
+
...createVoiceReadinessProfile('meeting-recorder', {
|
|
1379
|
+
bargeInReports: async () => [await buildBargeInReport()],
|
|
1380
|
+
explain: true,
|
|
1381
|
+
providerRoutingContracts: async () => [await runProviderRoutingContract()],
|
|
1382
|
+
reconnectContracts: async () => [await runReconnectContract()]
|
|
1383
|
+
}),
|
|
1384
|
+
store: runtime.traces
|
|
1385
|
+
})
|
|
1386
|
+
);
|
|
1387
|
+
```
|
|
1388
|
+
|
|
1389
|
+
Use `evaluateVoiceProductionReadinessEvidence(...)` or `assertVoiceProductionReadinessEvidence(...)` when a proof pack should check the readiness JSON directly. This keeps release gates tied to structured evidence instead of route text:
|
|
1390
|
+
|
|
1391
|
+
```ts
|
|
1392
|
+
const readiness = await buildVoiceProductionReadinessReport({
|
|
1393
|
+
store: runtime.traces,
|
|
1394
|
+
providerSlo,
|
|
1395
|
+
opsRecovery
|
|
1396
|
+
});
|
|
1397
|
+
|
|
1398
|
+
assertVoiceProductionReadinessEvidence(readiness, {
|
|
1399
|
+
requireStatus: 'pass',
|
|
1400
|
+
requiredChecks: ['Provider SLO gates', 'Session health', 'Turn quality']
|
|
1401
|
+
});
|
|
1402
|
+
```
|
|
1403
|
+
|
|
1404
|
+
Use `createVoiceProductionReadinessProofRuntime(...)` when the app needs a fresh, isolated proof window instead of letting stale local traces certify a deploy. The runtime is intentionally small: it owns a bounded in-memory trace store, route-cache defaults, a reusable TTL cache, a proof-freshness check, and optional synthetic provider/live-latency seed events. Your app still mounts routes, writes artifacts, and decides which proof sources matter.
|
|
1405
|
+
|
|
1406
|
+
```ts
|
|
1407
|
+
import {
|
|
1408
|
+
createVoiceProductionReadinessProofRuntime,
|
|
1409
|
+
createVoiceProductionReadinessRoutes,
|
|
1410
|
+
createVoiceReadinessProfile
|
|
1411
|
+
} from '@absolutejs/voice';
|
|
1412
|
+
|
|
1413
|
+
const readinessProof = createVoiceProductionReadinessProofRuntime({
|
|
1414
|
+
cacheMs: 10_000,
|
|
1415
|
+
traceMaxAgeMs: 30 * 60_000
|
|
1416
|
+
});
|
|
1417
|
+
|
|
1418
|
+
const refreshReadinessProof = () =>
|
|
1419
|
+
readinessProof.refresh(async (metadata) => {
|
|
1420
|
+
await readinessProof.seedTraceProof({
|
|
1421
|
+
llmProvider: 'openai',
|
|
1422
|
+
scenarioId: 'provider-slo-proof',
|
|
1423
|
+
sttProvider: 'deepgram',
|
|
1424
|
+
ttsProvider: 'openai'
|
|
1425
|
+
});
|
|
1426
|
+
|
|
1427
|
+
await writeProofPack({
|
|
1428
|
+
generatedAt: metadata.generatedAt,
|
|
1429
|
+
runId: metadata.runId
|
|
1430
|
+
});
|
|
1431
|
+
});
|
|
1432
|
+
|
|
1433
|
+
app.use(
|
|
1434
|
+
createVoiceProductionReadinessRoutes({
|
|
1435
|
+
...createVoiceReadinessProfile('phone-agent', {
|
|
1436
|
+
explain: true
|
|
1437
|
+
}),
|
|
1438
|
+
additionalChecks: async () => [
|
|
1439
|
+
await readinessProof.buildFreshnessCheck()
|
|
1440
|
+
],
|
|
1441
|
+
cacheMs: readinessProof.options.cacheMs,
|
|
1442
|
+
providerSlo: async () => {
|
|
1443
|
+
await refreshReadinessProof();
|
|
1444
|
+
return {
|
|
1445
|
+
events: await readinessProof.store.list(),
|
|
1446
|
+
requiredKinds: ['llm', 'stt', 'tts']
|
|
1447
|
+
};
|
|
1448
|
+
},
|
|
1449
|
+
resolveOptions: async () => {
|
|
1450
|
+
await refreshReadinessProof();
|
|
1451
|
+
return {};
|
|
1452
|
+
},
|
|
1453
|
+
store: readinessProof.store,
|
|
1454
|
+
traceMaxAgeMs: readinessProof.options.traceMaxAgeMs
|
|
1455
|
+
})
|
|
1456
|
+
);
|
|
1457
|
+
```
|
|
1458
|
+
|
|
1459
|
+
Use `buildVoiceProofPack(...)` and `writeVoiceProofPack(...)` when the app needs a customer-owned proof artifact instead of demo-only file glue. Proof packs render JSON and Markdown, expose route helpers, and can be converted into observability export artifacts:
|
|
1460
|
+
|
|
1461
|
+
```ts
|
|
1462
|
+
import {
|
|
1463
|
+
buildVoiceProofPackFromObservabilityExport,
|
|
1464
|
+
createVoiceProofPackRoutes,
|
|
1465
|
+
writeVoiceProofPack
|
|
1466
|
+
} from '@absolutejs/voice';
|
|
1467
|
+
|
|
1468
|
+
const exportReport = await buildVoiceObservabilityExport({
|
|
1469
|
+
store: runtime.traces,
|
|
1470
|
+
audit: runtime.audit
|
|
1471
|
+
});
|
|
1472
|
+
const proofPack = buildVoiceProofPackFromObservabilityExport(exportReport, {
|
|
1473
|
+
runId: 'release-proof'
|
|
1474
|
+
});
|
|
1475
|
+
const written = await writeVoiceProofPack(proofPack, {
|
|
1476
|
+
outputDir: '.voice-runtime/proof-pack'
|
|
1477
|
+
});
|
|
1478
|
+
|
|
1479
|
+
app.use(createVoiceProofPackRoutes({ source: proofPack }));
|
|
1480
|
+
|
|
1481
|
+
const exportWithProofPack = await buildVoiceObservabilityExport({
|
|
1482
|
+
artifacts: written.artifacts,
|
|
1483
|
+
store: runtime.traces
|
|
1484
|
+
});
|
|
1485
|
+
```
|
|
1486
|
+
|
|
1487
|
+
This primitive does not start workers, create persistent storage, mount a dashboard, or prescribe a deploy workflow. It only gives self-hosted apps one clean readiness-proof runtime so JSON, HTML, gate checks, proof packs, and trend artifacts agree on the same fresh evidence window.
|
|
1488
|
+
|
|
1489
|
+
Use `buildVoiceRealCallProfileEvidenceFromTraceEvents(...)` or `loadVoiceRealCallProfileEvidenceFromTraceStore(...)` when repeated real browser/phone sessions should drive profile defaults and provider/runtime recommendations. These helpers read ordinary trace events such as `session.error`, `provider.decision`, `client.live_latency`, `client.browser_media`, `client.telephony_media`, `client.barge_in`, and `turn_latency.stage`, then emit `VoiceProofTrendRealCallProfileEvidence[]` for `buildVoiceRealCallProfileHistoryReport(...)`.
|
|
1490
|
+
|
|
1491
|
+
```ts
|
|
1492
|
+
import {
|
|
1493
|
+
buildVoiceRealCallProfileHistoryReport,
|
|
1494
|
+
createVoiceRealCallProfileHistoryRoutes,
|
|
1495
|
+
loadVoiceRealCallProfileEvidenceFromTraceStore
|
|
1496
|
+
} from '@absolutejs/voice';
|
|
1497
|
+
|
|
1498
|
+
const buildRealCallHistory = async () =>
|
|
1499
|
+
buildVoiceRealCallProfileHistoryReport({
|
|
1500
|
+
evidence: await loadVoiceRealCallProfileEvidenceFromTraceStore({
|
|
1501
|
+
defaultProfileId: 'meeting-recorder',
|
|
1502
|
+
defaultProfileLabel: 'Meeting recorder',
|
|
1503
|
+
store: runtime.traces
|
|
1504
|
+
}),
|
|
1505
|
+
source: 'runtime.traces'
|
|
1506
|
+
});
|
|
1507
|
+
|
|
1508
|
+
app.use(
|
|
1509
|
+
createVoiceRealCallProfileHistoryRoutes({
|
|
1510
|
+
source: buildRealCallHistory
|
|
1511
|
+
})
|
|
1512
|
+
);
|
|
1513
|
+
```
|
|
1514
|
+
|
|
1515
|
+
The point is not to benchmark a fake demo once. The point is to let every real call add profile evidence so `/api/voice/real-call-profile-history`, provider recommendations, profile-switch readiness, and operations records can explain which provider/runtime path is winning for each call shape.
|
|
1516
|
+
|
|
1517
|
+
Use `buildVoiceRealCallProfileReadinessCheck(...)` to make that history deploy-blocking through `createVoiceProductionReadinessRoutes(...)`:
|
|
1518
|
+
|
|
1519
|
+
```ts
|
|
1520
|
+
createVoiceProductionReadinessRoutes({
|
|
1521
|
+
additionalChecks: async () => [
|
|
1522
|
+
buildVoiceRealCallProfileReadinessCheck(await buildRealCallHistory(), {
|
|
1523
|
+
minActionableProfiles: 2,
|
|
1524
|
+
minCycles: 10,
|
|
1525
|
+
requiredProfileIds: ['meeting-recorder', 'support-agent'],
|
|
1526
|
+
requiredProviderRoles: ['llm', 'stt', 'tts']
|
|
1527
|
+
})
|
|
1528
|
+
],
|
|
1529
|
+
store: runtime.traces
|
|
1530
|
+
});
|
|
1531
|
+
```
|
|
1532
|
+
|
|
1533
|
+
The readiness check includes recovery actions from `buildVoiceRealCallProfileRecoveryActions(...)`, so failed gates can point operators at the profile history report, browser/phone proof, missing provider-role evidence, operations records, and production-readiness refresh instead of only saying "failed."
|
|
1534
|
+
|
|
1535
|
+
Mount `createVoiceRealCallProfileRecoveryActionRoutes(...)` when those actions should be executable. The package owns the route contract and result shape; the app supplies safe handlers:
|
|
1536
|
+
|
|
1537
|
+
```ts
|
|
1538
|
+
app.use(
|
|
1539
|
+
createVoiceRealCallProfileRecoveryActionRoutes({
|
|
1540
|
+
handlers: {
|
|
1541
|
+
'collect-browser-proof': async () => runBrowserProfileProof(),
|
|
1542
|
+
'collect-phone-proof': async () => runPhoneProfileProof(),
|
|
1543
|
+
refresh: async () => refreshReadinessProof()
|
|
1544
|
+
},
|
|
1545
|
+
source: buildRealCallHistory
|
|
1546
|
+
})
|
|
1547
|
+
);
|
|
1548
|
+
```
|
|
1549
|
+
|
|
1550
|
+
For longer-running proof collection, add a job store and mark selected actions async. The POST returns a `jobId` with `jobStatus: "queued"`, and the app can poll `${path}/actions/:jobId`:
|
|
1551
|
+
|
|
1552
|
+
```ts
|
|
1553
|
+
const recoveryJobs = createVoiceInMemoryRealCallProfileRecoveryJobStore();
|
|
1554
|
+
|
|
1555
|
+
app.use(
|
|
1556
|
+
createVoiceRealCallProfileRecoveryActionRoutes({
|
|
1557
|
+
asyncActionIds: ['collect-browser-proof', 'collect-phone-proof'],
|
|
1558
|
+
handlers: {
|
|
1559
|
+
'collect-browser-proof': async () => runBrowserProfileProof(),
|
|
1560
|
+
'collect-phone-proof': async () => runPhoneProfileProof()
|
|
1561
|
+
},
|
|
1562
|
+
jobStore: recoveryJobs,
|
|
1563
|
+
source: buildRealCallHistory
|
|
1564
|
+
})
|
|
1565
|
+
);
|
|
1566
|
+
```
|
|
1567
|
+
|
|
1568
|
+
Use the SQLite job store when recovery jobs should survive restarts:
|
|
1569
|
+
|
|
1570
|
+
```ts
|
|
1571
|
+
const recoveryJobs = createVoiceSQLiteRealCallProfileRecoveryJobStore({
|
|
1572
|
+
path: '.voice-runtime/real-call-recovery/jobs.sqlite'
|
|
1573
|
+
});
|
|
1574
|
+
```
|
|
1575
|
+
|
|
1576
|
+
Recovery routes expose recent persisted jobs at `${path}/actions/jobs`. Stores can implement `list({ limit, actionId, status })`; the bundled memory and SQLite stores both support it.
|
|
1577
|
+
|
|
1578
|
+
Use `buildVoiceRealCallProfileRecoveryJobHistoryCheck(...)` in production readiness to prove recovery jobs are persisted and inspectable:
|
|
1579
|
+
|
|
1580
|
+
```ts
|
|
1581
|
+
additionalChecks: async () => [
|
|
1582
|
+
await buildVoiceRealCallProfileRecoveryJobHistoryCheck(recoveryJobs)
|
|
1583
|
+
];
|
|
1584
|
+
```
|
|
1585
|
+
|
|
1586
|
+
Use `buildVoiceReadinessRecoveryActions(...)` when a UI or API needs a compact plan of actions attached to failed or warning readiness checks:
|
|
1587
|
+
|
|
1588
|
+
```ts
|
|
1589
|
+
const recoveryPlan = buildVoiceReadinessRecoveryActions(readinessReport);
|
|
1590
|
+
```
|
|
1591
|
+
|
|
1592
|
+
Use `createVoiceProfileTraceTagger(...)` when the app already has a trace store and needs every appended trace to carry a benchmark profile label. It wraps any `VoiceTraceEventStore`, preserves the underlying store behavior, and adds `profileId`/`benchmarkProfileId` metadata and payload fields that real-call profile history can ingest later.
|
|
1593
|
+
|
|
1594
|
+
```ts
|
|
1595
|
+
import { createVoiceProfileTraceTagger } from '@absolutejs/voice';
|
|
1596
|
+
|
|
1597
|
+
const trace = createVoiceProfileTraceTagger({
|
|
1598
|
+
defaultProfile: {
|
|
1599
|
+
id: 'meeting-recorder',
|
|
1600
|
+
label: 'Meeting recorder'
|
|
1601
|
+
},
|
|
1602
|
+
resolveProfile: (event) =>
|
|
1603
|
+
event.sessionId.startsWith('support-') ? 'support-agent' : undefined,
|
|
1604
|
+
store: runtime.traces
|
|
1605
|
+
});
|
|
1606
|
+
```
|
|
1607
|
+
|
|
1608
|
+
Built-in profiles:
|
|
1609
|
+
|
|
1610
|
+
- `meeting-recorder`: live latency, session health, provider fallback, routing contracts, reconnect proof, and barge-in interruption proof.
|
|
1611
|
+
- `phone-agent`: carrier readiness, phone-agent smoke proof, campaign readiness proof, handoffs, provider routing contracts, audit/trace delivery health, customer-owned observability export delivery history, and delivery runtime proof.
|
|
1612
|
+
- `ops-heavy`: audit evidence, operator action history, audit/trace delivery health, customer-owned observability export delivery history, delivery runtime proof, and deploy-gate support.
|
|
1613
|
+
|
|
1614
|
+
Phone-agent fast path:
|
|
1615
|
+
|
|
1616
|
+
```ts
|
|
1617
|
+
app.use(
|
|
1618
|
+
createVoiceProductionReadinessRoutes({
|
|
1619
|
+
...createVoiceReadinessProfile('phone-agent', {
|
|
1620
|
+
auditDeliveries: runtime.auditDeliveries,
|
|
1621
|
+
campaignReadiness: () =>
|
|
1622
|
+
runVoiceCampaignReadinessProof({
|
|
1623
|
+
store: runtime.campaigns
|
|
1624
|
+
}),
|
|
1625
|
+
carriers: loadCarrierMatrixInputs,
|
|
1626
|
+
deliveryRuntime,
|
|
1627
|
+
explain: true,
|
|
1628
|
+
observabilityExportDeliveryHistory: {
|
|
1629
|
+
store: observabilityExportDeliveryReceipts,
|
|
1630
|
+
maxAgeMs: 60 * 60 * 1000,
|
|
1631
|
+
failOnStale: true
|
|
1632
|
+
},
|
|
1633
|
+
phoneAgentSmokes: async () => [await runPhoneSmoke()],
|
|
1634
|
+
providerRoutingContracts: async () => [await runProviderRoutingContract()],
|
|
1635
|
+
traceDeliveries: runtime.traceDeliveries
|
|
1636
|
+
}),
|
|
1637
|
+
store: runtime.traces
|
|
1638
|
+
})
|
|
1639
|
+
);
|
|
1640
|
+
```
|
|
1641
|
+
|
|
1642
|
+
Ops-heavy fast path:
|
|
1643
|
+
|
|
1644
|
+
```ts
|
|
1645
|
+
app.use(
|
|
1646
|
+
createVoiceProductionReadinessRoutes({
|
|
1647
|
+
...createVoiceReadinessProfile('ops-heavy', {
|
|
1648
|
+
audit: runtime.audit,
|
|
1649
|
+
auditDeliveries: runtime.auditDeliveries,
|
|
1650
|
+
deliveryRuntime,
|
|
1651
|
+
observabilityExportDeliveryHistory: {
|
|
1652
|
+
store: observabilityExportDeliveryReceipts
|
|
1653
|
+
},
|
|
1654
|
+
traceDeliveries: runtime.traceDeliveries
|
|
1655
|
+
}),
|
|
1656
|
+
gate: {
|
|
1657
|
+
failOnWarnings: true
|
|
1658
|
+
},
|
|
1659
|
+
store: runtime.traces
|
|
1660
|
+
})
|
|
1661
|
+
);
|
|
1662
|
+
```
|
|
1663
|
+
|
|
1664
|
+
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.
|
|
1665
|
+
|
|
1666
|
+
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.
|
|
1667
|
+
|
|
1668
|
+
## Delivery Runtime Presets
|
|
1669
|
+
|
|
1670
|
+
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(...)`.
|
|
1671
|
+
|
|
1672
|
+
### File Delivery
|
|
1673
|
+
|
|
1674
|
+
Use file delivery for local demos, dev environments, or self-hosted deployments that collect exports from disk.
|
|
1675
|
+
|
|
1676
|
+
```ts
|
|
1677
|
+
import {
|
|
1678
|
+
createVoiceDeliveryRuntime,
|
|
1679
|
+
createVoiceDeliveryRuntimePresetConfig,
|
|
1680
|
+
createVoiceDeliveryRuntimeRoutes,
|
|
1681
|
+
createVoiceFileRuntimeStorage
|
|
1682
|
+
} from '@absolutejs/voice';
|
|
1683
|
+
|
|
1684
|
+
const runtimeStorage = createVoiceFileRuntimeStorage({
|
|
1685
|
+
directory: '.voice-runtime/support'
|
|
1686
|
+
});
|
|
1687
|
+
|
|
1688
|
+
const deliveryRuntime = createVoiceDeliveryRuntime(
|
|
1689
|
+
createVoiceDeliveryRuntimePresetConfig({
|
|
1690
|
+
auditDeliveries: runtimeStorage.auditDeliveries,
|
|
1691
|
+
directory: '.voice-runtime/support/delivery-exports',
|
|
1692
|
+
leases: createLeaseCoordinator(),
|
|
1693
|
+
mode: 'file',
|
|
1694
|
+
traceDeliveries: runtimeStorage.traceDeliveries
|
|
1695
|
+
})
|
|
1696
|
+
);
|
|
1697
|
+
|
|
1698
|
+
app.use(
|
|
1699
|
+
createVoiceDeliveryRuntimeRoutes({
|
|
1700
|
+
runtime: deliveryRuntime
|
|
1701
|
+
})
|
|
1702
|
+
);
|
|
1703
|
+
```
|
|
1704
|
+
|
|
1705
|
+
### Webhook Delivery
|
|
1706
|
+
|
|
1707
|
+
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.
|
|
1708
|
+
|
|
1709
|
+
```ts
|
|
1710
|
+
const deliveryRuntime = createVoiceDeliveryRuntime(
|
|
1711
|
+
createVoiceDeliveryRuntimePresetConfig({
|
|
1712
|
+
auditDeliveries: runtimeStorage.auditDeliveries,
|
|
1713
|
+
auditSinkId: 'support-audit-webhook',
|
|
1714
|
+
body: {
|
|
1715
|
+
audit: ({ events }) => ({
|
|
1716
|
+
eventCount: events.length,
|
|
1717
|
+
events,
|
|
1718
|
+
source: 'support-app',
|
|
1719
|
+
surface: 'audit-deliveries'
|
|
1720
|
+
}),
|
|
1721
|
+
trace: ({ events }) => ({
|
|
1722
|
+
eventCount: events.length,
|
|
1723
|
+
events,
|
|
1724
|
+
source: 'support-app',
|
|
1725
|
+
surface: 'trace-deliveries'
|
|
1726
|
+
})
|
|
1727
|
+
},
|
|
1728
|
+
failures: {
|
|
1729
|
+
maxFailures: 3
|
|
1730
|
+
},
|
|
1731
|
+
leases: {
|
|
1732
|
+
audit: createLeaseCoordinator(),
|
|
1733
|
+
trace: createLeaseCoordinator()
|
|
1734
|
+
},
|
|
1735
|
+
mode: 'webhook',
|
|
1736
|
+
signingSecret: process.env.VOICE_DELIVERY_WEBHOOK_SECRET,
|
|
1737
|
+
traceDeliveries: runtimeStorage.traceDeliveries,
|
|
1738
|
+
traceSinkId: 'support-trace-webhook',
|
|
1739
|
+
url: process.env.VOICE_DELIVERY_WEBHOOK_URL!
|
|
1740
|
+
})
|
|
1741
|
+
);
|
|
1742
|
+
```
|
|
1743
|
+
|
|
1744
|
+
### S3 Delivery
|
|
1745
|
+
|
|
1746
|
+
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.
|
|
1747
|
+
|
|
1748
|
+
```ts
|
|
1749
|
+
const deliveryRuntime = createVoiceDeliveryRuntime(
|
|
1750
|
+
createVoiceDeliveryRuntimePresetConfig({
|
|
1751
|
+
auditDeliveries: runtimeStorage.auditDeliveries,
|
|
1752
|
+
auditSinkId: 'support-audit-s3',
|
|
1753
|
+
bucket: process.env.VOICE_DELIVERY_S3_BUCKET,
|
|
1754
|
+
failures: {
|
|
1755
|
+
maxFailures: 3
|
|
1756
|
+
},
|
|
1757
|
+
keyPrefix: 'support/voice-deliveries',
|
|
1758
|
+
leases: {
|
|
1759
|
+
audit: createLeaseCoordinator(),
|
|
1760
|
+
trace: createLeaseCoordinator()
|
|
1761
|
+
},
|
|
1762
|
+
mode: 's3',
|
|
1763
|
+
traceDeliveries: runtimeStorage.traceDeliveries,
|
|
1764
|
+
traceSinkId: 'support-trace-s3'
|
|
1765
|
+
})
|
|
1766
|
+
);
|
|
1767
|
+
```
|
|
1768
|
+
|
|
1769
|
+
Mount `createVoiceDeliveryRuntimeRoutes({ runtime: deliveryRuntime })` to expose:
|
|
1770
|
+
|
|
1771
|
+
- `/api/voice-delivery-runtime`: combined audit and trace worker summary.
|
|
1772
|
+
- `/api/voice-delivery-runtime/tick`: manual tick for both workers.
|
|
1773
|
+
- `/delivery-runtime`: HTML worker control plane.
|
|
1774
|
+
|
|
1775
|
+
Pass the same runtime to production readiness so failed, dead-lettered, or pending export queues become deploy-blocking evidence:
|
|
1776
|
+
|
|
1777
|
+
```ts
|
|
1778
|
+
app.use(
|
|
1779
|
+
createVoiceProductionReadinessRoutes({
|
|
1780
|
+
auditDeliveries: runtimeStorage.auditDeliveries,
|
|
1781
|
+
deliveryRuntime,
|
|
1782
|
+
links: {
|
|
1783
|
+
deliveryRuntime: '/delivery-runtime'
|
|
1784
|
+
},
|
|
1785
|
+
store: runtimeStorage.traces,
|
|
1786
|
+
traceDeliveries: runtimeStorage.traceDeliveries
|
|
1787
|
+
})
|
|
1788
|
+
);
|
|
1789
|
+
```
|
|
1790
|
+
|
|
1791
|
+
## Simulation Suite Path
|
|
1792
|
+
|
|
1793
|
+
Use `createVoiceSimulationSuiteRoutes(...)` when you want one pre-production proof surface for the things that usually live in separate dashboards or scripts:
|
|
1794
|
+
|
|
1795
|
+
```ts
|
|
1796
|
+
import {
|
|
1797
|
+
createVoiceSimulationSuiteRoutes,
|
|
1798
|
+
createVoiceFileRuntimeStorage
|
|
1799
|
+
} from '@absolutejs/voice';
|
|
1800
|
+
|
|
1801
|
+
const runtime = createVoiceFileRuntimeStorage({
|
|
1802
|
+
directory: '.voice-runtime/support'
|
|
1803
|
+
});
|
|
1804
|
+
|
|
1805
|
+
app.use(
|
|
1806
|
+
createVoiceSimulationSuiteRoutes({
|
|
1807
|
+
htmlPath: '/voice/simulations',
|
|
1808
|
+
path: '/api/voice/simulations',
|
|
1809
|
+
store: runtime.traces,
|
|
1810
|
+
scenarios: workflowScenarios,
|
|
1811
|
+
fixtureStore: scenarioFixtureStore,
|
|
1812
|
+
tools: toolContracts,
|
|
1813
|
+
outcomes: {
|
|
1814
|
+
contracts: outcomeContracts,
|
|
1815
|
+
events: runtime.events,
|
|
1816
|
+
reviews: runtime.reviews,
|
|
1817
|
+
sessions: runtime.session,
|
|
1818
|
+
tasks: runtime.tasks
|
|
1819
|
+
}
|
|
1820
|
+
})
|
|
1821
|
+
);
|
|
1822
|
+
```
|
|
1823
|
+
|
|
1824
|
+
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.
|
|
1825
|
+
|
|
1826
|
+
## Self-Hosted Campaigns
|
|
1827
|
+
|
|
1828
|
+
Use `createVoiceCampaignRoutes(...)` when you need Retell/Bland-style outbound campaign primitives without giving a hosted dialer ownership of recipients, attempts, outcomes, or readiness proof.
|
|
1829
|
+
|
|
1830
|
+
```ts
|
|
1831
|
+
import {
|
|
1832
|
+
createVoiceCampaignRoutes,
|
|
1833
|
+
createVoiceProductionReadinessRoutes,
|
|
1834
|
+
createVoiceReadinessProfile,
|
|
1835
|
+
createVoiceSQLiteCampaignStore,
|
|
1836
|
+
runVoiceCampaignReadinessProof
|
|
1837
|
+
} from '@absolutejs/voice';
|
|
1838
|
+
|
|
1839
|
+
const campaigns = createVoiceSQLiteCampaignStore({
|
|
1840
|
+
path: '.voice-runtime/campaigns.sqlite'
|
|
1841
|
+
});
|
|
1842
|
+
|
|
1843
|
+
app.use(
|
|
1844
|
+
createVoiceCampaignRoutes({
|
|
1845
|
+
htmlPath: '/voice/campaigns',
|
|
1846
|
+
path: '/api/voice/campaigns',
|
|
1847
|
+
store: campaigns,
|
|
1848
|
+
title: 'Outbound Campaigns'
|
|
1849
|
+
})
|
|
1850
|
+
);
|
|
1851
|
+
```
|
|
1852
|
+
|
|
1853
|
+
The campaign runtime gives you explicit primitives instead of a campaign app kit:
|
|
1854
|
+
|
|
1855
|
+
- `importVoiceCampaignRecipients(...)`: validates CSV/JSON rows, phone numbers, consent, duplicates, variables, and metadata.
|
|
1856
|
+
- `VoiceCampaignRuntime.importRecipients(...)`: persists accepted recipients and returns rejected-row evidence.
|
|
1857
|
+
- `tick(...)`: enforces campaign status, max concurrency, attempt windows, quiet hours, rolling rate limits, retry backoff, and `maxAttempts`.
|
|
1858
|
+
- `pause(...)`, `resume(...)`, `cancel(...)`: operator-safe campaign controls.
|
|
1859
|
+
- `applyVoiceCampaignTelephonyOutcome(...)`: maps Twilio/Telnyx/Plivo webhook decisions back into campaign attempts.
|
|
1860
|
+
- `buildVoiceCampaignObservabilityReport(...)`: queue depth, active attempts, leases, attempt rates, failures, and stuck work.
|
|
1861
|
+
|
|
1862
|
+
Import recipients through the route API:
|
|
1863
|
+
|
|
1864
|
+
```ts
|
|
1865
|
+
await fetch('/api/voice/campaigns/campaign-1/recipients/import', {
|
|
1866
|
+
body: JSON.stringify({
|
|
1867
|
+
csv: `id,name,phone,consent,segment
|
|
1868
|
+
recipient-1,Ada,+15550001001,yes,trial
|
|
1869
|
+
recipient-2,Grace,+15550001002,true,enterprise`,
|
|
1870
|
+
requireConsent: true,
|
|
1871
|
+
variableColumns: ['segment']
|
|
1872
|
+
}),
|
|
1873
|
+
headers: {
|
|
1874
|
+
'content-type': 'application/json'
|
|
1875
|
+
},
|
|
1876
|
+
method: 'POST'
|
|
1877
|
+
});
|
|
1878
|
+
```
|
|
1879
|
+
|
|
1880
|
+
Create campaigns with scheduling controls:
|
|
1881
|
+
|
|
1882
|
+
```ts
|
|
1883
|
+
await runtime.create({
|
|
1884
|
+
maxAttempts: 3,
|
|
1885
|
+
maxConcurrentAttempts: 10,
|
|
1886
|
+
name: 'Renewal outreach',
|
|
1887
|
+
schedule: {
|
|
1888
|
+
attemptWindow: { startHour: 9, endHour: 17 },
|
|
1889
|
+
quietHours: { startHour: 12, endHour: 13 },
|
|
1890
|
+
rateLimit: { maxAttempts: 60, windowMs: 60_000 },
|
|
1891
|
+
retryPolicy: { backoffMs: [5 * 60_000, 30 * 60_000] }
|
|
1892
|
+
}
|
|
1893
|
+
});
|
|
1894
|
+
```
|
|
1895
|
+
|
|
1896
|
+
Certify the campaign path without live carrier traffic:
|
|
1897
|
+
|
|
1898
|
+
```ts
|
|
1899
|
+
const campaignReadiness = await runVoiceCampaignReadinessProof({
|
|
1900
|
+
store: campaigns
|
|
1901
|
+
});
|
|
1902
|
+
|
|
1903
|
+
if (!campaignReadiness.ok) {
|
|
1904
|
+
throw new Error(
|
|
1905
|
+
campaignReadiness.checks
|
|
1906
|
+
.filter((check) => check.status !== 'pass')
|
|
1907
|
+
.map((check) => check.name)
|
|
1908
|
+
.join('\n')
|
|
1909
|
+
);
|
|
1910
|
+
}
|
|
1911
|
+
```
|
|
1912
|
+
|
|
1913
|
+
Pass that proof into production readiness so campaign regressions block deploys:
|
|
1914
|
+
|
|
1915
|
+
```ts
|
|
1916
|
+
app.use(
|
|
1917
|
+
createVoiceProductionReadinessRoutes({
|
|
1918
|
+
...createVoiceReadinessProfile('phone-agent', {
|
|
1919
|
+
campaignReadiness: () =>
|
|
1920
|
+
runVoiceCampaignReadinessProof({
|
|
1921
|
+
store: campaigns
|
|
1922
|
+
}),
|
|
1923
|
+
explain: true
|
|
1924
|
+
}),
|
|
1925
|
+
store: runtime.traces
|
|
1926
|
+
})
|
|
1927
|
+
);
|
|
1928
|
+
```
|
|
1929
|
+
|
|
1930
|
+
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.
|
|
1931
|
+
|
|
1932
|
+
## Phone Voice Agent In 20 Minutes
|
|
1933
|
+
|
|
1934
|
+
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.
|
|
1935
|
+
|
|
1936
|
+
```ts
|
|
1937
|
+
import {
|
|
1938
|
+
createVoicePhoneAgent,
|
|
1939
|
+
createVoiceTelephonyOutcomePolicy,
|
|
1940
|
+
runVoicePhoneAgentProductionSmokeContract
|
|
1941
|
+
} from '@absolutejs/voice';
|
|
1942
|
+
import { deepgram } from '@absolutejs/voice-deepgram';
|
|
1943
|
+
|
|
1944
|
+
const outcomePolicy = createVoiceTelephonyOutcomePolicy({
|
|
1945
|
+
transferTarget: '+15551234567'
|
|
1946
|
+
});
|
|
1947
|
+
|
|
1948
|
+
app
|
|
1949
|
+
.use(
|
|
1950
|
+
createVoicePhoneAgent({
|
|
1951
|
+
setup: {
|
|
1952
|
+
path: '/api/voice/phone/setup',
|
|
1953
|
+
title: 'Support Phone Agent'
|
|
1954
|
+
},
|
|
1955
|
+
matrix: {
|
|
1956
|
+
path: '/api/carriers',
|
|
1957
|
+
title: 'AbsoluteJS Voice Carrier Matrix'
|
|
1958
|
+
},
|
|
1959
|
+
productionSmoke: {
|
|
1960
|
+
maxAgeMs: 24 * 60 * 60 * 1000,
|
|
1961
|
+
required: [
|
|
1962
|
+
'carrier-contract',
|
|
1963
|
+
'media-started',
|
|
1964
|
+
'transcript',
|
|
1965
|
+
'assistant-response',
|
|
1966
|
+
'lifecycle-outcome',
|
|
1967
|
+
'no-session-error',
|
|
1968
|
+
'fresh-trace'
|
|
1969
|
+
],
|
|
1970
|
+
store: runtime.traces
|
|
1971
|
+
},
|
|
1972
|
+
carriers: [
|
|
1973
|
+
{
|
|
1974
|
+
provider: 'twilio',
|
|
1975
|
+
options: {
|
|
1976
|
+
context: {},
|
|
1977
|
+
outcomePolicy,
|
|
1978
|
+
session: runtime.session,
|
|
1979
|
+
stt: deepgram({ apiKey: process.env.DEEPGRAM_API_KEY! }),
|
|
1980
|
+
streamPath: '/api/voice/twilio/stream',
|
|
1981
|
+
twiml: {
|
|
1982
|
+
path: '/api/voice/twilio',
|
|
1983
|
+
streamUrl: process.env.TWILIO_STREAM_URL
|
|
1984
|
+
},
|
|
1985
|
+
webhook: {
|
|
1986
|
+
path: '/api/voice/twilio/webhook',
|
|
1987
|
+
signingSecret: process.env.TWILIO_AUTH_TOKEN
|
|
1988
|
+
},
|
|
1989
|
+
async onTurn({ turn }) {
|
|
1990
|
+
return { assistantText: `I heard: ${turn.text}` };
|
|
1991
|
+
},
|
|
1992
|
+
onComplete: async () => {}
|
|
1993
|
+
}
|
|
1994
|
+
}
|
|
1995
|
+
]
|
|
1996
|
+
}).routes
|
|
1997
|
+
);
|
|
1998
|
+
```
|
|
1999
|
+
|
|
2000
|
+
The wrapper mounts selected carrier routes plus two proof surfaces:
|
|
2001
|
+
|
|
2002
|
+
- `/api/voice/phone/setup`: one setup report with carrier URLs, smoke links, lifecycle stages, and readiness.
|
|
2003
|
+
- `/api/voice/phone/setup?format=html`: copy/paste setup page for carrier dashboards.
|
|
2004
|
+
- `/api/carriers`: carrier matrix JSON for Twilio, Telnyx, and Plivo.
|
|
2005
|
+
- `/api/carriers?format=html`: side-by-side carrier readiness matrix.
|
|
2006
|
+
- `/api/voice/phone/smoke-contract?sessionId=...`: trace-backed production smoke contract.
|
|
2007
|
+
- `/voice/phone/smoke-contract?sessionId=...`: HTML production smoke contract.
|
|
2008
|
+
|
|
2009
|
+
The setup JSON includes `setupInstructions`, so your own admin UI can render copy-ready carrier fields without scraping HTML:
|
|
2010
|
+
|
|
2011
|
+
```ts
|
|
2012
|
+
const setup = await fetch('/api/voice/phone/setup').then((response) =>
|
|
2013
|
+
response.json()
|
|
2014
|
+
);
|
|
2015
|
+
|
|
2016
|
+
for (const carrier of setup.setupInstructions) {
|
|
2017
|
+
console.log(carrier.carrierName, carrier.status);
|
|
2018
|
+
console.log(carrier.steps.join('\n'));
|
|
2019
|
+
}
|
|
2020
|
+
```
|
|
2021
|
+
|
|
2022
|
+
Each instruction includes:
|
|
2023
|
+
|
|
2024
|
+
- `answerLabel`: `TwiML URL`, `TeXML URL`, or `Answer URL`.
|
|
2025
|
+
- `answerUrl`: the URL to paste into the carrier's inbound voice/answer field.
|
|
2026
|
+
- `webhookUrl`: the status callback or webhook URL.
|
|
2027
|
+
- `streamUrl`: the `wss://` media stream URL the carrier must reach.
|
|
2028
|
+
- `setupPath` and `smokePath`: package-mounted proof pages for that carrier.
|
|
2029
|
+
- `steps`: ordered copy/paste guidance for the carrier dashboard.
|
|
2030
|
+
- `issues`: contract errors or warnings from the carrier matrix.
|
|
2031
|
+
|
|
2032
|
+
The setup page renders the same instructions and tells you exactly what to copy into the carrier dashboard:
|
|
2033
|
+
|
|
2034
|
+
- 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.
|
|
2035
|
+
- 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.
|
|
2036
|
+
- 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.
|
|
2037
|
+
|
|
2038
|
+
Each configured carrier can also expose its own setup and smoke pages, for example:
|
|
2039
|
+
|
|
2040
|
+
- `/api/voice/twilio/setup?format=html`
|
|
2041
|
+
- `/api/voice/twilio/smoke?format=html`
|
|
2042
|
+
- `/api/voice/telnyx/setup?format=html`
|
|
2043
|
+
- `/api/voice/telnyx/smoke?format=html`
|
|
2044
|
+
- `/api/voice/plivo/setup?format=html`
|
|
2045
|
+
- `/api/voice/plivo/smoke?format=html`
|
|
2046
|
+
|
|
2047
|
+
The phone-agent report normalizes the lifecycle schema across carriers:
|
|
2048
|
+
|
|
2049
|
+
- `ringing`
|
|
2050
|
+
- `answered`
|
|
2051
|
+
- `media-started`
|
|
2052
|
+
- `transcript`
|
|
2053
|
+
- `assistant-response`
|
|
2054
|
+
- `transfer`
|
|
2055
|
+
- `voicemail`
|
|
2056
|
+
- `no-answer`
|
|
2057
|
+
- `completed`
|
|
2058
|
+
- `failed`
|
|
2059
|
+
|
|
2060
|
+
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.
|
|
2061
|
+
|
|
2062
|
+
After running a real smoke call, certify the phone-agent path from traces:
|
|
2063
|
+
|
|
2064
|
+
```ts
|
|
2065
|
+
const smoke = await runVoicePhoneAgentProductionSmokeContract({
|
|
2066
|
+
maxAgeMs: 24 * 60 * 60 * 1000,
|
|
2067
|
+
required: [
|
|
2068
|
+
'media-started',
|
|
2069
|
+
'transcript',
|
|
2070
|
+
'assistant-response',
|
|
2071
|
+
'lifecycle-outcome',
|
|
2072
|
+
'no-session-error',
|
|
2073
|
+
'fresh-trace'
|
|
2074
|
+
],
|
|
2075
|
+
sessionId: 'phone-smoke-session',
|
|
2076
|
+
store: runtime.traces
|
|
2077
|
+
});
|
|
2078
|
+
|
|
2079
|
+
if (!smoke.pass) {
|
|
2080
|
+
throw new Error(smoke.issues.map((issue) => issue.message).join('\n'));
|
|
2081
|
+
}
|
|
2082
|
+
```
|
|
2083
|
+
|
|
2084
|
+
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.
|
|
2085
|
+
|
|
2086
|
+
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`.
|
|
2087
|
+
|
|
2088
|
+
## Ops Status Hooks And Widgets
|
|
2089
|
+
|
|
2090
|
+
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.
|
|
2091
|
+
|
|
2092
|
+
```ts
|
|
2093
|
+
import {
|
|
2094
|
+
createVoiceDemoReadyRoutes,
|
|
2095
|
+
createVoiceFileRuntimeStorage,
|
|
2096
|
+
createVoiceOpsStatusRoutes,
|
|
2097
|
+
summarizeVoiceOpsStatus
|
|
2098
|
+
} from '@absolutejs/voice';
|
|
2099
|
+
|
|
2100
|
+
const runtime = createVoiceFileRuntimeStorage({ directory: '.voice-runtime/support' });
|
|
2101
|
+
|
|
2102
|
+
app.use(
|
|
2103
|
+
createVoiceOpsStatusRoutes({
|
|
2104
|
+
store: runtime.traces,
|
|
2105
|
+
llmProviders: ['openai', 'anthropic', 'gemini'],
|
|
2106
|
+
sttProviders: ['deepgram', 'assemblyai']
|
|
2107
|
+
})
|
|
2108
|
+
);
|
|
2109
|
+
```
|
|
2110
|
+
|
|
2111
|
+
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.
|
|
2112
|
+
|
|
2113
|
+
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:
|
|
2114
|
+
|
|
2115
|
+
```ts
|
|
2116
|
+
app.use(
|
|
2117
|
+
createVoiceDemoReadyRoutes({
|
|
2118
|
+
opsStatus: {
|
|
2119
|
+
href: '/api/voice/ops-status',
|
|
2120
|
+
load: () => summarizeVoiceOpsStatus(opsStatusOptions)
|
|
2121
|
+
},
|
|
2122
|
+
phoneSetup: {
|
|
2123
|
+
href: '/api/voice/phone/setup?format=html',
|
|
2124
|
+
load: () => phoneAgentSetupReport
|
|
2125
|
+
},
|
|
2126
|
+
phoneSmoke: {
|
|
2127
|
+
href: '/voice/phone/smoke-contract',
|
|
2128
|
+
load: () => phoneSmokeReport
|
|
2129
|
+
},
|
|
2130
|
+
productionReadiness: {
|
|
2131
|
+
href: '/production-readiness',
|
|
2132
|
+
load: () => productionReadinessReport
|
|
2133
|
+
}
|
|
2134
|
+
})
|
|
2135
|
+
);
|
|
2136
|
+
```
|
|
2137
|
+
|
|
2138
|
+
```ts
|
|
2139
|
+
app.use(
|
|
2140
|
+
createVoiceOpsStatusRoutes({
|
|
2141
|
+
include: { quality: false, sessions: false },
|
|
2142
|
+
preferFixtureWorkflows: true,
|
|
2143
|
+
store: runtime.traces
|
|
2144
|
+
})
|
|
2145
|
+
);
|
|
2146
|
+
```
|
|
2147
|
+
|
|
2148
|
+
### React Status Widget
|
|
2149
|
+
|
|
2150
|
+
```tsx
|
|
2151
|
+
import { VoiceOpsStatus } from '@absolutejs/voice/react';
|
|
2152
|
+
|
|
2153
|
+
export function OpsBadge() {
|
|
2154
|
+
return <VoiceOpsStatus intervalMs={5000} />;
|
|
2155
|
+
}
|
|
2156
|
+
```
|
|
2157
|
+
|
|
2158
|
+
### Vue Status Widget
|
|
2159
|
+
|
|
2160
|
+
```vue
|
|
2161
|
+
<script setup lang="ts">
|
|
2162
|
+
import { VoiceOpsStatus } from '@absolutejs/voice/vue';
|
|
2163
|
+
</script>
|
|
2164
|
+
|
|
2165
|
+
<template>
|
|
2166
|
+
<VoiceOpsStatus :interval-ms="5000" />
|
|
2167
|
+
</template>
|
|
2168
|
+
```
|
|
2169
|
+
|
|
2170
|
+
### Svelte Status Widget
|
|
2171
|
+
|
|
2172
|
+
```svelte
|
|
2173
|
+
<script lang="ts">
|
|
2174
|
+
import { onDestroy, onMount } from 'svelte';
|
|
2175
|
+
import { createVoiceOpsStatus } from '@absolutejs/voice/svelte';
|
|
2176
|
+
|
|
2177
|
+
const status = createVoiceOpsStatus('/api/voice/ops-status', { intervalMs: 5000 });
|
|
2178
|
+
let html = '';
|
|
2179
|
+
onMount(() => status.subscribe(() => (html = status.getHTML())));
|
|
2180
|
+
onDestroy(() => status.close());
|
|
2181
|
+
</script>
|
|
2182
|
+
|
|
2183
|
+
{@html html}
|
|
2184
|
+
```
|
|
2185
|
+
|
|
2186
|
+
### Angular Status Widget
|
|
2187
|
+
|
|
2188
|
+
```ts
|
|
2189
|
+
import { VoiceOpsStatusService } from '@absolutejs/voice/angular';
|
|
2190
|
+
|
|
2191
|
+
status = inject(VoiceOpsStatusService).connect('/api/voice/ops-status', {
|
|
2192
|
+
intervalMs: 5000
|
|
2193
|
+
});
|
|
2194
|
+
```
|
|
2195
|
+
|
|
2196
|
+
```html
|
|
2197
|
+
<h2>{{ status.report()?.status === 'pass' ? 'Passing' : 'Needs attention' }}</h2>
|
|
2198
|
+
<p>{{ status.report()?.passed ?? 0 }} passing checks</p>
|
|
2199
|
+
```
|
|
2200
|
+
|
|
2201
|
+
### HTML Or HTMX Status Widget
|
|
2202
|
+
|
|
2203
|
+
```html
|
|
2204
|
+
<div id="voice-ops-status"></div>
|
|
2205
|
+
<script type="module">
|
|
2206
|
+
import { mountVoiceOpsStatus } from '@absolutejs/voice/client';
|
|
2207
|
+
|
|
2208
|
+
mountVoiceOpsStatus(document.querySelector('#voice-ops-status'), '/api/voice/ops-status', {
|
|
2209
|
+
intervalMs: 5000
|
|
2210
|
+
});
|
|
2211
|
+
</script>
|
|
2212
|
+
```
|
|
2213
|
+
|
|
2214
|
+
For custom elements:
|
|
2215
|
+
|
|
2216
|
+
```html
|
|
2217
|
+
<absolute-voice-ops-status interval-ms="5000"></absolute-voice-ops-status>
|
|
2218
|
+
<script type="module">
|
|
2219
|
+
import { defineVoiceOpsStatusElement } from '@absolutejs/voice/client';
|
|
2220
|
+
defineVoiceOpsStatusElement();
|
|
2221
|
+
</script>
|
|
2222
|
+
```
|
|
2223
|
+
|
|
2224
|
+
### Call Debugger Launch Widgets
|
|
2225
|
+
|
|
2226
|
+
Mount `createVoiceCallDebuggerRoutes(...)` on the server, then expose a small framework-native launcher anywhere operators or developers need the latest support artifact. The launcher opens the full debugger with session snapshot, operations record, failure replay, provider path, transcript, user-heard output, linked artifacts, and incident markdown.
|
|
2227
|
+
|
|
2228
|
+
React:
|
|
2229
|
+
|
|
2230
|
+
```tsx
|
|
2231
|
+
import { VoiceCallDebuggerLaunch } from '@absolutejs/voice/react';
|
|
2232
|
+
|
|
2233
|
+
export function DebugLatestCall() {
|
|
2234
|
+
return (
|
|
2235
|
+
<VoiceCallDebuggerLaunch
|
|
2236
|
+
description="Open snapshot, replay, provider path, transcript, and incident markdown."
|
|
2237
|
+
intervalMs={5000}
|
|
2238
|
+
path="/api/voice-call-debugger/latest"
|
|
2239
|
+
title="Debug Latest Call"
|
|
2240
|
+
/>
|
|
2241
|
+
);
|
|
2242
|
+
}
|
|
2243
|
+
```
|
|
2244
|
+
|
|
2245
|
+
Vue:
|
|
2246
|
+
|
|
2247
|
+
```vue
|
|
2248
|
+
<script setup lang="ts">
|
|
2249
|
+
import { VoiceCallDebuggerLaunch } from '@absolutejs/voice/vue';
|
|
2250
|
+
</script>
|
|
2251
|
+
|
|
2252
|
+
<template>
|
|
2253
|
+
<VoiceCallDebuggerLaunch
|
|
2254
|
+
description="Open snapshot, replay, provider path, transcript, and incident markdown."
|
|
2255
|
+
:interval-ms="5000"
|
|
2256
|
+
path="/api/voice-call-debugger/latest"
|
|
2257
|
+
title="Debug Latest Call"
|
|
2258
|
+
/>
|
|
2259
|
+
</template>
|
|
2260
|
+
```
|
|
2261
|
+
|
|
2262
|
+
Svelte:
|
|
2263
|
+
|
|
2264
|
+
```svelte
|
|
2265
|
+
<script lang="ts">
|
|
2266
|
+
import { onDestroy, onMount } from 'svelte';
|
|
2267
|
+
import { createVoiceCallDebugger } from '@absolutejs/voice/svelte';
|
|
2268
|
+
|
|
2269
|
+
const debuggerLaunch = createVoiceCallDebugger('/api/voice-call-debugger/latest', {
|
|
2270
|
+
description: 'Open snapshot, replay, provider path, transcript, and incident markdown.',
|
|
2271
|
+
intervalMs: 5000,
|
|
2272
|
+
title: 'Debug Latest Call'
|
|
2273
|
+
});
|
|
2274
|
+
let html = debuggerLaunch.getHTML();
|
|
2275
|
+
let unsubscribe = () => {};
|
|
2276
|
+
|
|
2277
|
+
onMount(() => {
|
|
2278
|
+
unsubscribe = debuggerLaunch.subscribe(() => {
|
|
2279
|
+
html = debuggerLaunch.getHTML();
|
|
2280
|
+
});
|
|
2281
|
+
void debuggerLaunch.refresh().catch(() => {});
|
|
2282
|
+
});
|
|
2283
|
+
onDestroy(() => {
|
|
2284
|
+
unsubscribe();
|
|
2285
|
+
debuggerLaunch.close();
|
|
2286
|
+
});
|
|
2287
|
+
</script>
|
|
2288
|
+
|
|
2289
|
+
{@html html}
|
|
2290
|
+
```
|
|
2291
|
+
|
|
2292
|
+
Angular:
|
|
2293
|
+
|
|
2294
|
+
```ts
|
|
2295
|
+
import { Component, computed, inject } from '@angular/core';
|
|
2296
|
+
import { createVoiceCallDebuggerLaunchViewModel } from '@absolutejs/voice/client';
|
|
2297
|
+
import { VoiceCallDebuggerService } from '@absolutejs/voice/angular';
|
|
2298
|
+
|
|
2299
|
+
@Component({
|
|
2300
|
+
selector: 'app-debug-latest-call',
|
|
2301
|
+
template: `
|
|
2302
|
+
<a [href]="model().href">{{ model().label }}</a>
|
|
2303
|
+
<p>{{ model().description }}</p>
|
|
2304
|
+
`
|
|
2305
|
+
})
|
|
2306
|
+
export class DebugLatestCallComponent {
|
|
2307
|
+
private readonly callDebugger = inject(VoiceCallDebuggerService).connect(
|
|
2308
|
+
'/api/voice-call-debugger/latest',
|
|
2309
|
+
{ intervalMs: 5000 }
|
|
2310
|
+
);
|
|
2311
|
+
readonly model = computed(() =>
|
|
2312
|
+
createVoiceCallDebuggerLaunchViewModel(
|
|
2313
|
+
'/api/voice-call-debugger/latest',
|
|
2314
|
+
{
|
|
2315
|
+
error: this.callDebugger.error(),
|
|
2316
|
+
isLoading: this.callDebugger.isLoading(),
|
|
2317
|
+
report: this.callDebugger.report(),
|
|
2318
|
+
updatedAt: this.callDebugger.updatedAt()
|
|
2319
|
+
},
|
|
2320
|
+
{ title: 'Debug Latest Call' }
|
|
2321
|
+
)
|
|
2322
|
+
);
|
|
2323
|
+
}
|
|
2324
|
+
```
|
|
2325
|
+
|
|
2326
|
+
HTML or HTMX:
|
|
2327
|
+
|
|
2328
|
+
```html
|
|
2329
|
+
<absolute-voice-call-debugger-launch
|
|
2330
|
+
interval-ms="5000"
|
|
2331
|
+
path="/api/voice-call-debugger/latest"
|
|
2332
|
+
title="Debug Latest Call"
|
|
2333
|
+
></absolute-voice-call-debugger-launch>
|
|
2334
|
+
|
|
2335
|
+
<script type="module">
|
|
2336
|
+
import { defineVoiceCallDebuggerLaunchElement } from '@absolutejs/voice/client';
|
|
2337
|
+
|
|
2338
|
+
defineVoiceCallDebuggerLaunchElement();
|
|
2339
|
+
</script>
|
|
2340
|
+
```
|
|
2341
|
+
|
|
2342
|
+
## Delivery Runtime Widgets
|
|
2343
|
+
|
|
2344
|
+
After mounting `createVoiceDeliveryRuntimeRoutes(...)`, apps can expose audit and trace worker health through the same framework-native primitives:
|
|
2345
|
+
|
|
2346
|
+
```tsx
|
|
2347
|
+
import { VoiceDeliveryRuntime } from '@absolutejs/voice/react';
|
|
2348
|
+
|
|
2349
|
+
export function DeliveryWorkers() {
|
|
2350
|
+
return <VoiceDeliveryRuntime intervalMs={5000} />;
|
|
2351
|
+
}
|
|
2352
|
+
```
|
|
2353
|
+
|
|
2354
|
+
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.
|
|
2355
|
+
|
|
2356
|
+
```ts
|
|
2357
|
+
import { VoiceDeliveryRuntime } from '@absolutejs/voice/vue';
|
|
2358
|
+
import { createVoiceDeliveryRuntime } from '@absolutejs/voice/svelte';
|
|
2359
|
+
import { VoiceDeliveryRuntimeService } from '@absolutejs/voice/angular';
|
|
2360
|
+
```
|
|
2361
|
+
|
|
2362
|
+
For HTML or HTMX pages:
|
|
2363
|
+
|
|
2364
|
+
```html
|
|
2365
|
+
<absolute-voice-delivery-runtime interval-ms="5000"></absolute-voice-delivery-runtime>
|
|
2366
|
+
<script type="module">
|
|
2367
|
+
import { defineVoiceDeliveryRuntimeElement } from '@absolutejs/voice/client';
|
|
2368
|
+
defineVoiceDeliveryRuntimeElement();
|
|
2369
|
+
</script>
|
|
2370
|
+
```
|
|
2371
|
+
|
|
2372
|
+
## Voice Ops Action Center
|
|
2373
|
+
|
|
2374
|
+
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.
|
|
2375
|
+
|
|
2376
|
+
```tsx
|
|
2377
|
+
import { VoiceOpsActionCenter } from '@absolutejs/voice/react';
|
|
2378
|
+
import { createVoiceOpsActionCenterActions } from '@absolutejs/voice/client';
|
|
2379
|
+
|
|
2380
|
+
export function OperatorPanel() {
|
|
2381
|
+
return (
|
|
2382
|
+
<VoiceOpsActionCenter
|
|
2383
|
+
actions={createVoiceOpsActionCenterActions({
|
|
2384
|
+
providers: ['deepgram', 'assemblyai']
|
|
2385
|
+
})}
|
|
2386
|
+
/>
|
|
2387
|
+
);
|
|
2388
|
+
}
|
|
2389
|
+
```
|
|
2390
|
+
|
|
2391
|
+
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.
|
|
2392
|
+
|
|
2393
|
+
```ts
|
|
2394
|
+
import { createVoiceOpsActionAuditRoutes } from '@absolutejs/voice';
|
|
2395
|
+
|
|
2396
|
+
app.use(
|
|
2397
|
+
createVoiceOpsActionAuditRoutes({
|
|
2398
|
+
audit: runtimeStorage.audit,
|
|
2399
|
+
trace: runtimeStorage.traces
|
|
2400
|
+
})
|
|
2401
|
+
);
|
|
2402
|
+
```
|
|
2403
|
+
|
|
2404
|
+
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`.
|
|
2405
|
+
|
|
2406
|
+
For HTML or HTMX pages:
|
|
2407
|
+
|
|
2408
|
+
```html
|
|
2409
|
+
<div id="voice-ops-actions"></div>
|
|
2410
|
+
<script type="module">
|
|
2411
|
+
import {
|
|
2412
|
+
createVoiceOpsActionCenterActions,
|
|
2413
|
+
mountVoiceOpsActionCenter
|
|
2414
|
+
} from '@absolutejs/voice/client';
|
|
2415
|
+
|
|
2416
|
+
mountVoiceOpsActionCenter(document.querySelector('#voice-ops-actions'), {
|
|
2417
|
+
actions: createVoiceOpsActionCenterActions({
|
|
2418
|
+
providers: ['deepgram']
|
|
2419
|
+
})
|
|
2420
|
+
});
|
|
2421
|
+
</script>
|
|
2422
|
+
```
|
|
2423
|
+
|
|
2424
|
+
## Live Operator Workflows
|
|
2425
|
+
|
|
2426
|
+
Use live-ops primitives when an operator needs to intervene during an active session without taking voice orchestration out of your app. The supported actions are:
|
|
2427
|
+
|
|
2428
|
+
- `pause-assistant`: keep committing caller turns, but skip assistant generation.
|
|
2429
|
+
- `resume-assistant`: let automation continue.
|
|
2430
|
+
- `operator-takeover`: keep the session open while a human handles the caller.
|
|
2431
|
+
- `inject-instruction`: add an operator instruction to the next assistant turn.
|
|
2432
|
+
- `force-handoff`: mark the session for a specific handoff target.
|
|
2433
|
+
- `escalate`, `assign`, `tag`, and `create-task`: record operational intent and make it auditable.
|
|
2434
|
+
|
|
2435
|
+
Mount the live-ops control routes beside your voice route:
|
|
2436
|
+
|
|
2437
|
+
```ts
|
|
2438
|
+
import {
|
|
2439
|
+
createVoiceLiveOpsRoutes,
|
|
2440
|
+
createVoiceMemoryLiveOpsControlStore,
|
|
2441
|
+
voice
|
|
2442
|
+
} from '@absolutejs/voice';
|
|
2443
|
+
import { deepgram } from '@absolutejs/voice-deepgram';
|
|
2444
|
+
|
|
2445
|
+
const liveOps = createVoiceMemoryLiveOpsControlStore();
|
|
2446
|
+
|
|
2447
|
+
app
|
|
2448
|
+
.use(
|
|
2449
|
+
createVoiceLiveOpsRoutes({
|
|
2450
|
+
audit: runtimeStorage.audit,
|
|
2451
|
+
store: liveOps,
|
|
2452
|
+
trace: runtimeStorage.traces
|
|
2453
|
+
})
|
|
2454
|
+
)
|
|
2455
|
+
.use(
|
|
2456
|
+
voice({
|
|
2457
|
+
path: '/voice',
|
|
2458
|
+
liveOps: {
|
|
2459
|
+
getControl: (sessionId) => liveOps.get(sessionId)
|
|
2460
|
+
},
|
|
2461
|
+
session: runtimeStorage.session,
|
|
2462
|
+
stt: deepgram({ apiKey: process.env.DEEPGRAM_API_KEY! }),
|
|
2463
|
+
async onTurn({ turn }) {
|
|
2464
|
+
return { assistantText: `I heard: ${turn.text}` };
|
|
2465
|
+
},
|
|
2466
|
+
onComplete: async () => {}
|
|
2467
|
+
})
|
|
2468
|
+
);
|
|
2469
|
+
```
|
|
2470
|
+
|
|
2471
|
+
The default route accepts `POST /api/voice/live-ops/action`:
|
|
2472
|
+
|
|
2473
|
+
```ts
|
|
2474
|
+
await fetch('/api/voice/live-ops/action', {
|
|
2475
|
+
body: JSON.stringify({
|
|
2476
|
+
action: 'pause-assistant',
|
|
2477
|
+
assignee: 'operator-123',
|
|
2478
|
+
detail: 'Caller is upset; pause automation while support reviews.',
|
|
2479
|
+
sessionId: 'session-123',
|
|
2480
|
+
tag: 'priority-support'
|
|
2481
|
+
}),
|
|
2482
|
+
headers: { 'content-type': 'application/json' },
|
|
2483
|
+
method: 'POST'
|
|
2484
|
+
});
|
|
2485
|
+
```
|
|
2486
|
+
|
|
2487
|
+
Every action updates the control store and can write both `operator.action` audit events and `operator.action` trace events. The voice runtime checks the control state before assistant generation. When `assistantPaused` or `operatorTakeover` is active, it commits the user transcript, records a skipped-turn trace, and does not call the assistant. When `injectedInstruction` is present, the next assistant turn receives that instruction.
|
|
2488
|
+
|
|
2489
|
+
The safe operator runbook is:
|
|
2490
|
+
|
|
2491
|
+
1. Open the session's operations record or trace timeline before intervening.
|
|
2492
|
+
2. Use `pause-assistant` when the bot should stop responding but the transcript should continue.
|
|
2493
|
+
3. Use `inject-instruction` when the bot should continue with human guidance, such as "apologize and offer transfer."
|
|
2494
|
+
4. Use `operator-takeover` when a human is now handling the caller and automation must stay silent.
|
|
2495
|
+
5. Use `force-handoff` or `escalate` when the session needs a specialist, supervisor, or external queue.
|
|
2496
|
+
6. Use `resume-assistant` only after the operator has verified the session state and any handoff context.
|
|
2497
|
+
7. Review `/api/voice/live-ops/control/:sessionId`, `/voice-operations/:sessionId`, `/api/voice/ops-actions/history`, or `/audit` when you need proof of who intervened and why.
|
|
2498
|
+
|
|
2499
|
+
Framework and HTML clients can run the same actions without a custom dashboard:
|
|
2500
|
+
|
|
2501
|
+
```tsx
|
|
2502
|
+
import { useVoiceLiveOps } from '@absolutejs/voice/react';
|
|
2503
|
+
|
|
2504
|
+
export function LiveOperatorPanel({ sessionId }: { sessionId: string }) {
|
|
2505
|
+
const liveOps = useVoiceLiveOps();
|
|
2506
|
+
|
|
2507
|
+
return (
|
|
2508
|
+
<button
|
|
2509
|
+
onClick={() =>
|
|
2510
|
+
liveOps.run({
|
|
2511
|
+
action: 'operator-takeover',
|
|
2512
|
+
assignee: 'operator-123',
|
|
2513
|
+
detail: 'Human support took over the call.',
|
|
2514
|
+
sessionId,
|
|
2515
|
+
tag: 'human-takeover'
|
|
2516
|
+
})
|
|
2517
|
+
}
|
|
2518
|
+
>
|
|
2519
|
+
Take over
|
|
2520
|
+
</button>
|
|
2521
|
+
);
|
|
2522
|
+
}
|
|
2523
|
+
```
|
|
2524
|
+
|
|
2525
|
+
For HTML or HTMX pages:
|
|
2526
|
+
|
|
2527
|
+
```html
|
|
2528
|
+
<absolute-voice-live-ops session-id="session-123"></absolute-voice-live-ops>
|
|
2529
|
+
<script type="module">
|
|
2530
|
+
import { defineVoiceLiveOpsElement } from '@absolutejs/voice/client';
|
|
2531
|
+
defineVoiceLiveOpsElement();
|
|
2532
|
+
</script>
|
|
2533
|
+
```
|
|
2534
|
+
|
|
2535
|
+
Live-ops is intentionally a primitive layer: the package records controls, audit evidence, and trace evidence, while your app decides which operators are allowed to run actions and how those controls appear in the product UI.
|
|
75
2536
|
|
|
76
2537
|
## Voice Assistants
|
|
77
2538
|
|
|
@@ -212,7 +2673,34 @@ const billingAgent = createVoiceAgent({
|
|
|
212
2673
|
const frontDesk = createVoiceAgentSquad({
|
|
213
2674
|
id: 'front-desk',
|
|
214
2675
|
defaultAgentId: 'support',
|
|
215
|
-
agents: [supportAgent, billingAgent]
|
|
2676
|
+
agents: [supportAgent, billingAgent],
|
|
2677
|
+
contextPolicy: ({ summaryMessage, turn }) => ({
|
|
2678
|
+
messages: [
|
|
2679
|
+
summaryMessage,
|
|
2680
|
+
{
|
|
2681
|
+
content: turn.text,
|
|
2682
|
+
role: 'user'
|
|
2683
|
+
}
|
|
2684
|
+
],
|
|
2685
|
+
metadata: {
|
|
2686
|
+
contextPolicy: 'handoff-summary-and-current-turn'
|
|
2687
|
+
},
|
|
2688
|
+
system: 'Use only the handoff summary and current caller turn.'
|
|
2689
|
+
}),
|
|
2690
|
+
handoffPolicy: ({ handoff }) => {
|
|
2691
|
+
if (handoff.targetAgentId === 'billing') {
|
|
2692
|
+
return {
|
|
2693
|
+
summary: 'Route verified billing requests to the billing specialist.',
|
|
2694
|
+
metadata: { queue: 'billing' }
|
|
2695
|
+
};
|
|
2696
|
+
}
|
|
2697
|
+
|
|
2698
|
+
return {
|
|
2699
|
+
allow: false,
|
|
2700
|
+
reason: `No approved route for ${handoff.targetAgentId}.`,
|
|
2701
|
+
escalate: { reason: 'unsupported-specialist' }
|
|
2702
|
+
};
|
|
2703
|
+
}
|
|
216
2704
|
});
|
|
217
2705
|
|
|
218
2706
|
voice({
|
|
@@ -226,6 +2714,98 @@ voice({
|
|
|
226
2714
|
|
|
227
2715
|
`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
2716
|
|
|
2717
|
+
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.
|
|
2718
|
+
|
|
2719
|
+
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.
|
|
2720
|
+
|
|
2721
|
+
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.
|
|
2722
|
+
|
|
2723
|
+
Make the current specialist visible in your UI by mounting trace timelines and using the squad status primitives. They derive current specialist state from `agent.handoff`, `agent.context`, `agent.model`, and `agent.result` traces, so the UI stays tied to the same proof source used by readiness and operations records.
|
|
2724
|
+
|
|
2725
|
+
```tsx
|
|
2726
|
+
import { VoiceAgentSquadStatus } from '@absolutejs/voice/react';
|
|
2727
|
+
|
|
2728
|
+
export function SpecialistBadge({ sessionId }: { sessionId: string }) {
|
|
2729
|
+
return (
|
|
2730
|
+
<VoiceAgentSquadStatus
|
|
2731
|
+
path="/api/voice-traces"
|
|
2732
|
+
sessionId={sessionId}
|
|
2733
|
+
title="Current specialist"
|
|
2734
|
+
/>
|
|
2735
|
+
);
|
|
2736
|
+
}
|
|
2737
|
+
```
|
|
2738
|
+
|
|
2739
|
+
Framework equivalents are available without a dashboard:
|
|
2740
|
+
|
|
2741
|
+
```ts
|
|
2742
|
+
import { useVoiceAgentSquadStatus } from '@absolutejs/voice/vue';
|
|
2743
|
+
import { createVoiceAgentSquadStatus } from '@absolutejs/voice/svelte';
|
|
2744
|
+
import { VoiceAgentSquadStatusService } from '@absolutejs/voice/angular';
|
|
2745
|
+
```
|
|
2746
|
+
|
|
2747
|
+
For HTML or HTMX pages:
|
|
2748
|
+
|
|
2749
|
+
```html
|
|
2750
|
+
<absolute-voice-agent-squad-status
|
|
2751
|
+
path="/api/voice-traces"
|
|
2752
|
+
session-id="session-123"
|
|
2753
|
+
title="Current specialist"
|
|
2754
|
+
></absolute-voice-agent-squad-status>
|
|
2755
|
+
<script type="module">
|
|
2756
|
+
import { defineVoiceAgentSquadStatusElement } from '@absolutejs/voice/client';
|
|
2757
|
+
defineVoiceAgentSquadStatusElement();
|
|
2758
|
+
</script>
|
|
2759
|
+
```
|
|
2760
|
+
|
|
2761
|
+
Use `runVoiceAgentSquadContract(...)` in tests or readiness checks when you need proof that a specialist graph still routes correctly:
|
|
2762
|
+
|
|
2763
|
+
```ts
|
|
2764
|
+
import {
|
|
2765
|
+
createVoiceMemoryTraceEventStore,
|
|
2766
|
+
runVoiceAgentSquadContract
|
|
2767
|
+
} from '@absolutejs/voice';
|
|
2768
|
+
|
|
2769
|
+
const trace = createVoiceMemoryTraceEventStore();
|
|
2770
|
+
const frontDesk = createVoiceAgentSquad({
|
|
2771
|
+
id: 'front-desk',
|
|
2772
|
+
defaultAgentId: 'support',
|
|
2773
|
+
agents: [supportAgent, billingAgent],
|
|
2774
|
+
trace
|
|
2775
|
+
});
|
|
2776
|
+
|
|
2777
|
+
const report = await runVoiceAgentSquadContract({
|
|
2778
|
+
context: {},
|
|
2779
|
+
squad: frontDesk,
|
|
2780
|
+
trace,
|
|
2781
|
+
contract: {
|
|
2782
|
+
id: 'billing-route',
|
|
2783
|
+
scenarioId: 'billing-route',
|
|
2784
|
+
turns: [
|
|
2785
|
+
{
|
|
2786
|
+
text: 'I have a billing question.',
|
|
2787
|
+
expect: {
|
|
2788
|
+
finalAgentId: 'billing',
|
|
2789
|
+
outcome: 'assistant',
|
|
2790
|
+
assistantIncludes: ['billing'],
|
|
2791
|
+
handoffs: [
|
|
2792
|
+
{
|
|
2793
|
+
fromAgentId: 'support',
|
|
2794
|
+
targetAgentId: 'billing',
|
|
2795
|
+
status: 'allowed'
|
|
2796
|
+
}
|
|
2797
|
+
]
|
|
2798
|
+
}
|
|
2799
|
+
}
|
|
2800
|
+
]
|
|
2801
|
+
}
|
|
2802
|
+
});
|
|
2803
|
+
|
|
2804
|
+
if (!report.pass) {
|
|
2805
|
+
throw new Error(report.issues.map((issue) => issue.message).join('\n'));
|
|
2806
|
+
}
|
|
2807
|
+
```
|
|
2808
|
+
|
|
229
2809
|
## Traces And Replay
|
|
230
2810
|
|
|
231
2811
|
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 +2813,22 @@ Use trace stores when you want every call to be inspectable outside a hosted pla
|
|
|
233
2813
|
```ts
|
|
234
2814
|
import {
|
|
235
2815
|
buildVoiceTraceReplay,
|
|
2816
|
+
buildVoiceAuditExport,
|
|
2817
|
+
createVoiceAuditHTTPSink,
|
|
2818
|
+
createVoiceAuditLogger,
|
|
2819
|
+
createVoiceAuditSinkDeliveryWorker,
|
|
2820
|
+
createVoiceAuditSinkStore,
|
|
2821
|
+
createVoiceAuditTrailRoutes,
|
|
236
2822
|
createVoiceAgent,
|
|
237
2823
|
createVoiceFileRuntimeStorage,
|
|
238
2824
|
createVoiceRedisTaskLeaseCoordinator,
|
|
2825
|
+
createVoiceTraceDeliveryRoutes,
|
|
239
2826
|
createVoiceTraceHTTPSink,
|
|
240
2827
|
createVoiceTraceSinkStore,
|
|
241
2828
|
createVoiceTraceSinkDeliveryWorker,
|
|
2829
|
+
buildVoiceDataRetentionPlan,
|
|
242
2830
|
exportVoiceTrace,
|
|
2831
|
+
applyVoiceDataRetentionPolicy,
|
|
243
2832
|
pruneVoiceTraceEvents,
|
|
244
2833
|
voice
|
|
245
2834
|
} from '@absolutejs/voice';
|
|
@@ -251,6 +2840,30 @@ const runtimeStorage = createVoiceFileRuntimeStorage({
|
|
|
251
2840
|
const redisLeases = createVoiceRedisTaskLeaseCoordinator({
|
|
252
2841
|
url: process.env.REDIS_URL
|
|
253
2842
|
});
|
|
2843
|
+
const auditStore = createVoiceAuditSinkStore({
|
|
2844
|
+
store: runtimeStorage.audit,
|
|
2845
|
+
deliveryQueue: runtimeStorage.auditDeliveries,
|
|
2846
|
+
sinks: [
|
|
2847
|
+
createVoiceAuditHTTPSink({
|
|
2848
|
+
id: 'security-warehouse',
|
|
2849
|
+
signingSecret: process.env.VOICE_AUDIT_SINK_SECRET,
|
|
2850
|
+
url: process.env.VOICE_AUDIT_SINK_URL!
|
|
2851
|
+
})
|
|
2852
|
+
]
|
|
2853
|
+
});
|
|
2854
|
+
const audit = createVoiceAuditLogger(auditStore);
|
|
2855
|
+
const auditSinkWorker = createVoiceAuditSinkDeliveryWorker({
|
|
2856
|
+
deliveries: runtimeStorage.auditDeliveries,
|
|
2857
|
+
leases: redisLeases,
|
|
2858
|
+
sinks: [
|
|
2859
|
+
createVoiceAuditHTTPSink({
|
|
2860
|
+
id: 'security-warehouse',
|
|
2861
|
+
signingSecret: process.env.VOICE_AUDIT_SINK_SECRET,
|
|
2862
|
+
url: process.env.VOICE_AUDIT_SINK_URL!
|
|
2863
|
+
})
|
|
2864
|
+
],
|
|
2865
|
+
workerId: 'audit-sink-worker'
|
|
2866
|
+
});
|
|
254
2867
|
const trace = createVoiceTraceSinkStore({
|
|
255
2868
|
store: runtimeStorage.traces,
|
|
256
2869
|
deliveryQueue: runtimeStorage.traceDeliveries,
|
|
@@ -277,6 +2890,9 @@ const traceSinkWorker = createVoiceTraceSinkDeliveryWorker({
|
|
|
277
2890
|
|
|
278
2891
|
const supportAgent = createVoiceAgent({
|
|
279
2892
|
id: 'support',
|
|
2893
|
+
audit,
|
|
2894
|
+
auditProvider: 'openai',
|
|
2895
|
+
auditModel: 'gpt-4.1',
|
|
280
2896
|
trace,
|
|
281
2897
|
model: {
|
|
282
2898
|
async generate() {
|
|
@@ -293,6 +2909,17 @@ voice({
|
|
|
293
2909
|
onTurn: supportAgent.onTurn,
|
|
294
2910
|
onComplete: async () => {}
|
|
295
2911
|
});
|
|
2912
|
+
app.use(
|
|
2913
|
+
createVoiceAuditTrailRoutes({
|
|
2914
|
+
store: runtimeStorage.audit
|
|
2915
|
+
})
|
|
2916
|
+
);
|
|
2917
|
+
app.use(
|
|
2918
|
+
createVoiceTraceDeliveryRoutes({
|
|
2919
|
+
store: runtimeStorage.traceDeliveries,
|
|
2920
|
+
worker: traceSinkWorker
|
|
2921
|
+
})
|
|
2922
|
+
);
|
|
296
2923
|
|
|
297
2924
|
const replay = await exportVoiceTrace({
|
|
298
2925
|
store: runtimeStorage.traces,
|
|
@@ -310,21 +2937,461 @@ console.log(report.summary);
|
|
|
310
2937
|
console.log(report.evaluation.pass);
|
|
311
2938
|
await Bun.write('trace.html', report.html);
|
|
312
2939
|
|
|
313
|
-
await pruneVoiceTraceEvents({
|
|
2940
|
+
await pruneVoiceTraceEvents({
|
|
2941
|
+
store: runtimeStorage.traces,
|
|
2942
|
+
before: Date.now() - 30 * 24 * 60 * 60 * 1000
|
|
2943
|
+
});
|
|
2944
|
+
|
|
2945
|
+
await audit.operatorAction({
|
|
2946
|
+
action: 'review.approve',
|
|
2947
|
+
actor: { id: 'operator-123', kind: 'operator' },
|
|
2948
|
+
resource: { id: 'review-123', type: 'review' }
|
|
2949
|
+
});
|
|
2950
|
+
```
|
|
2951
|
+
|
|
2952
|
+
`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.
|
|
2953
|
+
|
|
2954
|
+
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.
|
|
2955
|
+
|
|
2956
|
+
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.
|
|
2957
|
+
|
|
2958
|
+
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.
|
|
2959
|
+
|
|
2960
|
+
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`.
|
|
2961
|
+
|
|
2962
|
+
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.
|
|
2963
|
+
|
|
2964
|
+
```ts
|
|
2965
|
+
const plan = await buildVoiceDataRetentionPlan({
|
|
2966
|
+
before: Date.now() - 30 * 24 * 60 * 60 * 1000,
|
|
2967
|
+
...runtimeStorage
|
|
2968
|
+
});
|
|
2969
|
+
|
|
2970
|
+
console.log(plan.scopes);
|
|
2971
|
+
|
|
2972
|
+
await applyVoiceDataRetentionPolicy({
|
|
2973
|
+
audit: runtimeStorage.audit,
|
|
2974
|
+
before: Date.now() - 30 * 24 * 60 * 60 * 1000,
|
|
2975
|
+
...runtimeStorage
|
|
2976
|
+
});
|
|
2977
|
+
```
|
|
2978
|
+
|
|
2979
|
+
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.
|
|
2980
|
+
|
|
2981
|
+
```ts
|
|
2982
|
+
import {
|
|
2983
|
+
createVoiceDataControlRoutes,
|
|
2984
|
+
createVoiceZeroRetentionPolicy,
|
|
2985
|
+
voiceComplianceRedactionDefaults
|
|
2986
|
+
} from '@absolutejs/voice';
|
|
2987
|
+
|
|
2988
|
+
app.use(
|
|
2989
|
+
createVoiceDataControlRoutes({
|
|
2990
|
+
...runtimeStorage,
|
|
2991
|
+
audit: runtimeStorage.audit,
|
|
2992
|
+
auditDeliveries: runtimeStorage.auditDeliveries,
|
|
2993
|
+
path: '/data-control',
|
|
2994
|
+
redact: voiceComplianceRedactionDefaults,
|
|
2995
|
+
traceDeliveries: runtimeStorage.traceDeliveries
|
|
2996
|
+
})
|
|
2997
|
+
);
|
|
2998
|
+
|
|
2999
|
+
const zeroRetentionPlan = await buildVoiceDataRetentionPlan(
|
|
3000
|
+
createVoiceZeroRetentionPolicy({
|
|
3001
|
+
...runtimeStorage,
|
|
3002
|
+
audit: runtimeStorage.audit,
|
|
3003
|
+
auditDeliveries: runtimeStorage.auditDeliveries,
|
|
3004
|
+
traceDeliveries: runtimeStorage.traceDeliveries
|
|
3005
|
+
})
|
|
3006
|
+
);
|
|
3007
|
+
```
|
|
3008
|
+
|
|
3009
|
+
Mounted routes:
|
|
3010
|
+
|
|
3011
|
+
- `GET /data-control`: HTML compliance/data-control report.
|
|
3012
|
+
- `GET /data-control.json`: JSON report with redaction, storage, retention plan, audit export, and provider-key recommendations.
|
|
3013
|
+
- `GET /data-control.md`: Markdown report for release/security reviews.
|
|
3014
|
+
- `POST /data-control/retention/plan`: dry-run deletion proof from a JSON policy body.
|
|
3015
|
+
- `POST /data-control/retention/apply`: applies retention only when the body includes `confirm: "apply-retention-policy"`.
|
|
3016
|
+
- `GET /data-control/audit.json`, `/data-control/audit.md`, `/data-control/audit.html`: redacted audit exports.
|
|
3017
|
+
|
|
3018
|
+
`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.
|
|
3019
|
+
|
|
3020
|
+
### Compliance Recipes
|
|
3021
|
+
|
|
3022
|
+
These are recipes, not compliance certifications. AbsoluteJS Voice gives you the self-hosted controls and proof surfaces; your legal/security team still owns the actual HIPAA, SOC 2, GDPR, or customer-contract process.
|
|
3023
|
+
|
|
3024
|
+
Zero-retention sensitive call:
|
|
3025
|
+
|
|
3026
|
+
```ts
|
|
3027
|
+
const policy = createVoiceZeroRetentionPolicy({
|
|
3028
|
+
...runtimeStorage,
|
|
3029
|
+
audit: runtimeStorage.audit,
|
|
3030
|
+
auditDeliveries: runtimeStorage.auditDeliveries,
|
|
3031
|
+
traceDeliveries: runtimeStorage.traceDeliveries
|
|
3032
|
+
});
|
|
3033
|
+
|
|
3034
|
+
const dryRun = await buildVoiceDataRetentionPlan(policy);
|
|
3035
|
+
if (dryRun.deletedCount > 0) {
|
|
3036
|
+
await applyVoiceDataRetentionPolicy({
|
|
3037
|
+
...policy,
|
|
3038
|
+
dryRun: false
|
|
3039
|
+
});
|
|
3040
|
+
}
|
|
3041
|
+
```
|
|
3042
|
+
|
|
3043
|
+
This removes sessions, traces, reviews, tasks, integration events, campaigns, incident bundles, and delivery queues that match the policy selectors. The generated policy starts as a dry run so a zero-retention mode cannot accidentally wipe data without explicit application.
|
|
3044
|
+
|
|
3045
|
+
Redacted support export:
|
|
3046
|
+
|
|
3047
|
+
```ts
|
|
3048
|
+
const auditExport = await exportVoiceAuditTrail({
|
|
3049
|
+
redact: voiceComplianceRedactionDefaults,
|
|
3050
|
+
store: runtimeStorage.audit
|
|
3051
|
+
});
|
|
3052
|
+
const auditMarkdown = renderVoiceAuditMarkdown(auditExport.events);
|
|
3053
|
+
|
|
3054
|
+
const traceMarkdown = renderVoiceTraceMarkdown(events, {
|
|
3055
|
+
redact: voiceComplianceRedactionDefaults
|
|
3056
|
+
});
|
|
3057
|
+
```
|
|
3058
|
+
|
|
3059
|
+
Use this for support tickets, customer escalations, incident reviews, or vendor handoffs where transcripts, tool payloads, provider metadata, or audit events may contain personal data.
|
|
3060
|
+
|
|
3061
|
+
Customer-owned storage:
|
|
3062
|
+
|
|
3063
|
+
```ts
|
|
3064
|
+
const runtimeStorage = createVoicePostgresRuntimeStorage({
|
|
3065
|
+
connectionString: process.env.DATABASE_URL!,
|
|
3066
|
+
schemaName: 'voice_ops',
|
|
3067
|
+
tablePrefix: 'support'
|
|
3068
|
+
});
|
|
3069
|
+
|
|
3070
|
+
app.use(
|
|
3071
|
+
createVoiceDataControlRoutes({
|
|
3072
|
+
...runtimeStorage,
|
|
3073
|
+
audit: runtimeStorage.audit,
|
|
3074
|
+
auditDeliveries: runtimeStorage.auditDeliveries,
|
|
3075
|
+
redact: voiceComplianceRedactionDefaults,
|
|
3076
|
+
traceDeliveries: runtimeStorage.traceDeliveries
|
|
3077
|
+
})
|
|
3078
|
+
);
|
|
3079
|
+
```
|
|
3080
|
+
|
|
3081
|
+
Use file storage for local demos, SQLite for small self-hosted installs, Postgres for production app-owned records, and S3 delivery for exported audit/trace evidence. The important point is that sessions, traces, reviews, tasks, campaigns, audit, and delivery queues remain in infrastructure the app owner controls.
|
|
3082
|
+
|
|
3083
|
+
Deploy gate for compliance evidence:
|
|
3084
|
+
|
|
3085
|
+
```ts
|
|
3086
|
+
app.use(
|
|
3087
|
+
createVoiceProductionReadinessRoutes({
|
|
3088
|
+
audit: {
|
|
3089
|
+
require: [
|
|
3090
|
+
{ type: 'provider.call' },
|
|
3091
|
+
{ type: 'operator.action' },
|
|
3092
|
+
{ type: 'retention.policy', maxAgeMs: 7 * 24 * 60 * 60 * 1000 }
|
|
3093
|
+
],
|
|
3094
|
+
store: runtimeStorage.audit
|
|
3095
|
+
},
|
|
3096
|
+
auditDeliveries: runtimeStorage.auditDeliveries,
|
|
3097
|
+
traceDeliveries: runtimeStorage.traceDeliveries,
|
|
3098
|
+
store: runtimeStorage.traces
|
|
3099
|
+
})
|
|
3100
|
+
);
|
|
3101
|
+
```
|
|
3102
|
+
|
|
3103
|
+
This makes provider-call audit evidence, operator interventions, recent retention-policy proof, and export-queue health part of release readiness instead of a manual dashboard check.
|
|
3104
|
+
|
|
3105
|
+
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.
|
|
3106
|
+
|
|
3107
|
+
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.
|
|
3108
|
+
|
|
3109
|
+
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.
|
|
3110
|
+
|
|
3111
|
+
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.
|
|
3112
|
+
|
|
3113
|
+
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.
|
|
3114
|
+
|
|
3115
|
+
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.
|
|
3116
|
+
|
|
3117
|
+
## Operations Records And Recovery
|
|
3118
|
+
|
|
3119
|
+
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:
|
|
3120
|
+
|
|
3121
|
+
```ts
|
|
3122
|
+
app.use(
|
|
3123
|
+
createVoiceOperationsRecordRoutes({
|
|
3124
|
+
audit: runtimeStorage.audit,
|
|
3125
|
+
htmlPath: '/voice-operations/:sessionId',
|
|
3126
|
+
path: '/api/voice-operations/:sessionId',
|
|
3127
|
+
store: runtimeStorage.traces
|
|
3128
|
+
})
|
|
3129
|
+
);
|
|
3130
|
+
```
|
|
3131
|
+
|
|
3132
|
+
`createVoiceOperationsRecordRoutes(...)` links the call/session timeline, transcript, replay, provider decisions, tools, handoffs, guardrail decisions, audit, reviews, ops tasks, integration events, and sink delivery attempts into one debuggable object. Provider decisions include both older provider-routing events and explicit `provider.decision` traces, so the call log can show the surface, selected provider, fallback provider, recovery status, fallback/degradation counts, and human-readable reason for each runtime choice. Use `/voice-operations/:sessionId` as the first place to investigate failed calls, blocked assistant output, blocked tool payloads, provider failures, handoff failures, slow turns, and campaign attempts. The same mount also exposes incident handoff Markdown at `/voice-operations/:sessionId/incident.md` and `/api/voice-operations/:sessionId/incident.md` for support tooling, including provider-decision recovery summaries and an `assistant.guardrail` blocked-stage summary when those trace events exist.
|
|
3133
|
+
|
|
3134
|
+
Use `evaluateVoiceOperationsRecordProviderRecovery(...)` or `assertVoiceOperationsRecordProviderRecovery(...)` when proof packs should fail unless the operation record contains concrete provider recovery evidence:
|
|
3135
|
+
|
|
3136
|
+
```ts
|
|
3137
|
+
assertVoiceOperationsRecordProviderRecovery(record, {
|
|
3138
|
+
recoveryStatus: 'degraded',
|
|
3139
|
+
minFallbacks: 1,
|
|
3140
|
+
minDegraded: 1,
|
|
3141
|
+
requiredStatuses: ['fallback', 'degraded'],
|
|
3142
|
+
requiredSurfaces: ['live-call'],
|
|
3143
|
+
requiredReasonIncludes: ['latency budget']
|
|
3144
|
+
});
|
|
3145
|
+
```
|
|
3146
|
+
|
|
3147
|
+
Use `evaluateVoiceOperationsRecordGuardrails(...)` when a proof pack or deploy gate needs JSON evidence that guardrails actually ran, blocked the expected stages, and produced named proofs/rule IDs. Use `assertVoiceOperationsRecordGuardrails(...)` in tests or smoke scripts when missing guardrail evidence should fail fast:
|
|
3148
|
+
|
|
3149
|
+
```ts
|
|
3150
|
+
const report = assertVoiceOperationsRecordGuardrails(record, {
|
|
3151
|
+
minBlocked: 1,
|
|
3152
|
+
proofs: ['live-guardrails-runtime'],
|
|
3153
|
+
ruleIds: ['support.no-medical-advice'],
|
|
3154
|
+
stages: ['assistant-output', 'tool-input']
|
|
3155
|
+
});
|
|
3156
|
+
```
|
|
3157
|
+
|
|
3158
|
+
Most proof surfaces can link to the same record by passing an operations-record URL template such as `/voice-operations/:sessionId`. Use that template anywhere a report emits session-level failures: production readiness, ops recovery, trace timelines, session lists, reviews, campaign attempts, eval reports, simulation-suite actions, tool-contract cases, and outcome-contract matched sessions. The goal is that no operator has to guess which trace, review, task, or delivery queue belongs to the failing call.
|
|
3159
|
+
|
|
3160
|
+
If a customer asks for "the call log," send the operations-record URL. If engineering needs reproducible context, send the incident Markdown URL. If a deploy gate fails, start at readiness or ops recovery and follow the linked operations record instead of searching storage manually.
|
|
3161
|
+
|
|
3162
|
+
Mount `createVoiceOpsRecoveryRoutes(...)` beside it when operators need one deploy-checkable recovery signal:
|
|
3163
|
+
|
|
3164
|
+
```ts
|
|
3165
|
+
app.use(
|
|
3166
|
+
createVoiceOpsRecoveryRoutes({
|
|
3167
|
+
auditDeliveries: runtimeStorage.auditDeliveries,
|
|
3168
|
+
handoffDeliveries,
|
|
3169
|
+
links: {
|
|
3170
|
+
operationsRecords: '/voice-operations/:sessionId',
|
|
3171
|
+
traceDeliveries: '/traces/deliveries'
|
|
3172
|
+
},
|
|
3173
|
+
traceDeliveries: runtimeStorage.traceDeliveries,
|
|
3174
|
+
traces: runtimeStorage.traces
|
|
3175
|
+
})
|
|
3176
|
+
);
|
|
3177
|
+
```
|
|
3178
|
+
|
|
3179
|
+
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.
|
|
3180
|
+
|
|
3181
|
+
Pass the same report into production readiness to make recovery issues a deploy gate:
|
|
3182
|
+
|
|
3183
|
+
```ts
|
|
3184
|
+
const opsRecovery = await buildVoiceOpsRecoveryReport({
|
|
3185
|
+
links: { operationsRecords: '/voice-operations/:sessionId' },
|
|
3186
|
+
traces: runtimeStorage.traces
|
|
3187
|
+
});
|
|
3188
|
+
|
|
3189
|
+
app.use(
|
|
3190
|
+
createVoiceProductionReadinessRoutes({
|
|
3191
|
+
links: {
|
|
3192
|
+
operationsRecords: '/voice-operations/:sessionId',
|
|
3193
|
+
opsRecovery: '/ops-recovery'
|
|
3194
|
+
},
|
|
3195
|
+
opsRecovery,
|
|
3196
|
+
store: runtimeStorage.traces
|
|
3197
|
+
})
|
|
3198
|
+
);
|
|
3199
|
+
```
|
|
3200
|
+
|
|
3201
|
+
Readiness emits the stable `voice.readiness.ops_recovery` gate code when unresolved recovery issues remain.
|
|
3202
|
+
|
|
3203
|
+
## Customer-Owned Observability Export
|
|
3204
|
+
|
|
3205
|
+
Use observability exports when a buyer wants the hosted-dashboard evidence graph, but inside their own storage, warehouse, SIEM, incident flow, or release notes. The export manifest links traces, audits, operations records, session snapshots, call-debugger reports, delivery queues, provider SLOs, readiness reports, screenshots, and proof-pack artifacts without making AbsoluteJS Voice the dashboard.
|
|
3206
|
+
|
|
3207
|
+
Every export manifest and artifact index includes a stable schema contract:
|
|
3208
|
+
|
|
3209
|
+
```ts
|
|
3210
|
+
import {
|
|
3211
|
+
assertVoiceObservabilityExportSchema,
|
|
3212
|
+
validateVoiceObservabilityExportRecord,
|
|
3213
|
+
voiceObservabilityExportSchemaId,
|
|
3214
|
+
voiceObservabilityExportSchemaVersion
|
|
3215
|
+
} from '@absolutejs/voice';
|
|
3216
|
+
|
|
3217
|
+
assertVoiceObservabilityExportSchema(exportReport);
|
|
3218
|
+
const validation = validateVoiceObservabilityExportRecord(exportReport);
|
|
3219
|
+
if (!validation.ok) {
|
|
3220
|
+
throw new Error(validation.issues.map((issue) => issue.message).join('\n'));
|
|
3221
|
+
}
|
|
3222
|
+
console.log(voiceObservabilityExportSchemaId, voiceObservabilityExportSchemaVersion);
|
|
3223
|
+
```
|
|
3224
|
+
|
|
3225
|
+
Use `validateVoiceObservabilityExportRecord(...)` or `assertVoiceObservabilityExportRecord(...)` when reading customer-owned records back from SQLite, Postgres, S3, a webhook collector, a warehouse, or a SIEM. The validator accepts manifests, artifact indexes, delivery reports, delivery receipts, delivery histories, and database payload records, then checks the stable schema id/version plus the minimum shape required for safe ingestion.
|
|
3226
|
+
|
|
3227
|
+
Use `evaluateVoicePlatformCoverage(...)` or `assertVoicePlatformCoverage(...)` when the product needs a structured "Vapi replacement surface coverage" gate. The assertion checks required buyer surfaces, evidence artifact names, total surface count, and failed-surface count:
|
|
3228
|
+
|
|
3229
|
+
```ts
|
|
3230
|
+
const coverage = buildVoicePlatformCoverageSummary({
|
|
3231
|
+
coverage: latestProofPack.vapiCoverage,
|
|
3232
|
+
runId: latestProofPack.runId
|
|
3233
|
+
});
|
|
3234
|
+
|
|
3235
|
+
assertVoicePlatformCoverage(coverage, {
|
|
3236
|
+
minSurfaces: 12,
|
|
3237
|
+
requiredEvidence: ['productionReadiness', 'operationsRecord', 'providerSlo'],
|
|
3238
|
+
requiredSurfaces: ['Web voice assistant', 'Call logs and incident handoff']
|
|
3239
|
+
});
|
|
3240
|
+
```
|
|
3241
|
+
|
|
3242
|
+
Use `replayVoiceObservabilityExport(...)` when you need to prove an already-delivered evidence bundle is still usable:
|
|
3243
|
+
|
|
3244
|
+
```ts
|
|
3245
|
+
import { replayVoiceObservabilityExport } from '@absolutejs/voice';
|
|
3246
|
+
|
|
3247
|
+
const replay = await replayVoiceObservabilityExport({
|
|
3248
|
+
kind: 'sqlite',
|
|
3249
|
+
path: '.voice-runtime/observability-exports.sqlite',
|
|
3250
|
+
runId: '2026-04-29T17-20-51.032Z',
|
|
3251
|
+
tableName: 'voice_observability_exports'
|
|
3252
|
+
});
|
|
3253
|
+
|
|
3254
|
+
if (replay.status !== 'pass') {
|
|
3255
|
+
console.error(replay.issues);
|
|
3256
|
+
}
|
|
3257
|
+
```
|
|
3258
|
+
|
|
3259
|
+
Replay sources support supplied records plus file, S3, SQLite, and Postgres delivery targets. The replay report re-validates the manifest/index/database payload, counts artifacts and delivery destinations, flags failed artifacts or destinations, and gives a readiness-style `pass`, `warn`, or `fail` result for customer-owned evidence pipelines.
|
|
3260
|
+
|
|
3261
|
+
```ts
|
|
3262
|
+
import {
|
|
3263
|
+
buildVoiceObservabilityExport,
|
|
3264
|
+
createVoiceFileObservabilityExportDeliveryReceiptStore,
|
|
3265
|
+
createVoiceObservabilityExportRoutes,
|
|
3266
|
+
createVoiceObservabilityExportReplayRoutes
|
|
3267
|
+
} from '@absolutejs/voice';
|
|
3268
|
+
|
|
3269
|
+
const observabilityReceipts =
|
|
3270
|
+
createVoiceFileObservabilityExportDeliveryReceiptStore({
|
|
3271
|
+
directory: '.voice-runtime/observability-export-receipts'
|
|
3272
|
+
});
|
|
3273
|
+
|
|
3274
|
+
app.use(
|
|
3275
|
+
createVoiceObservabilityExportRoutes({
|
|
3276
|
+
artifactIntegrity: {
|
|
3277
|
+
maxAgeMs: 15 * 60 * 1000
|
|
3278
|
+
},
|
|
3279
|
+
deliveryDestinations: [
|
|
3280
|
+
{
|
|
3281
|
+
directory: '.voice-runtime/observability-exports',
|
|
3282
|
+
kind: 'file',
|
|
3283
|
+
label: 'Local customer-owned observability archive'
|
|
3284
|
+
},
|
|
3285
|
+
{
|
|
3286
|
+
bucket: process.env.VOICE_OBSERVABILITY_EXPORT_S3_BUCKET,
|
|
3287
|
+
keyPrefix: 'voice/observability-exports',
|
|
3288
|
+
kind: 's3',
|
|
3289
|
+
label: 'S3 customer-owned observability archive'
|
|
3290
|
+
},
|
|
3291
|
+
{
|
|
3292
|
+
kind: 'sqlite',
|
|
3293
|
+
path: '.voice-runtime/observability-exports.sqlite',
|
|
3294
|
+
tableName: 'voice_observability_exports',
|
|
3295
|
+
label: 'SQLite customer-owned observability warehouse'
|
|
3296
|
+
},
|
|
3297
|
+
{
|
|
3298
|
+
connectionString: process.env.VOICE_OBSERVABILITY_EXPORT_POSTGRES_URL,
|
|
3299
|
+
kind: 'postgres',
|
|
3300
|
+
schemaName: 'voice',
|
|
3301
|
+
tableName: 'observability_exports',
|
|
3302
|
+
label: 'Postgres customer-owned observability warehouse'
|
|
3303
|
+
}
|
|
3304
|
+
],
|
|
3305
|
+
deliveryReceipts: observabilityReceipts,
|
|
3306
|
+
artifacts: [
|
|
3307
|
+
{
|
|
3308
|
+
id: 'latest-proof-pack',
|
|
3309
|
+
kind: 'proof-pack',
|
|
3310
|
+
label: 'Latest proof pack',
|
|
3311
|
+
path: '.voice-runtime/proof-pack/latest.md',
|
|
3312
|
+
required: true
|
|
3313
|
+
}
|
|
3314
|
+
],
|
|
3315
|
+
audit: runtimeStorage.audit,
|
|
3316
|
+
auditDeliveries: runtimeStorage.auditDeliveries,
|
|
3317
|
+
links: {
|
|
3318
|
+
callDebugger: (sessionId) => `/voice-call-debugger/${sessionId}`,
|
|
3319
|
+
operationsRecord: (sessionId) => `/voice-operations/${sessionId}`,
|
|
3320
|
+
sessionSnapshot: (sessionId) =>
|
|
3321
|
+
`/api/voice/session-snapshot/${sessionId}`
|
|
3322
|
+
},
|
|
3323
|
+
redact: true,
|
|
3324
|
+
store: runtimeStorage.traces,
|
|
3325
|
+
traceDeliveries: runtimeStorage.traceDeliveries
|
|
3326
|
+
})
|
|
3327
|
+
);
|
|
3328
|
+
|
|
3329
|
+
app.use(
|
|
3330
|
+
createVoiceObservabilityExportReplayRoutes({
|
|
3331
|
+
source: async () => ({
|
|
3332
|
+
kind: 'sqlite',
|
|
3333
|
+
path: '.voice-runtime/observability-exports.sqlite',
|
|
3334
|
+
runId: 'latest-proof-pack',
|
|
3335
|
+
tableName: 'voice_observability_exports'
|
|
3336
|
+
})
|
|
3337
|
+
})
|
|
3338
|
+
);
|
|
3339
|
+
|
|
3340
|
+
const exportReport = await buildVoiceObservabilityExport({
|
|
3341
|
+
artifactIntegrity: {
|
|
3342
|
+
maxAgeMs: 15 * 60 * 1000
|
|
3343
|
+
},
|
|
3344
|
+
audit: runtimeStorage.audit,
|
|
3345
|
+
auditDeliveries: runtimeStorage.auditDeliveries,
|
|
3346
|
+
callDebuggerReports: [latestCallDebuggerReport],
|
|
3347
|
+
links: {
|
|
3348
|
+
callDebugger: (sessionId) => `/voice-call-debugger/${sessionId}`,
|
|
3349
|
+
operationsRecord: (sessionId) => `/voice-operations/${sessionId}`,
|
|
3350
|
+
sessionSnapshot: (sessionId) => `/api/voice/session-snapshot/${sessionId}`
|
|
3351
|
+
},
|
|
3352
|
+
redact: true,
|
|
3353
|
+
sessionSnapshots: [latestSessionSnapshot],
|
|
314
3354
|
store: runtimeStorage.traces,
|
|
315
|
-
|
|
3355
|
+
traceDeliveries: runtimeStorage.traceDeliveries
|
|
316
3356
|
});
|
|
317
3357
|
```
|
|
318
3358
|
|
|
319
|
-
|
|
3359
|
+
The route helper exposes JSON at `/api/voice/observability-export`, an artifact index at `/api/voice/observability-export/artifacts`, per-artifact downloads at `/api/voice/observability-export/artifacts/:artifactId`, delivery at `POST /api/voice/observability-export/deliveries`, delivery history at `GET /api/voice/observability-export/deliveries`, Markdown at `/voice/observability-export.md`, and HTML at `/voice/observability-export`. `createVoiceObservabilityExportReplayRoutes(...)` adds JSON replay proof at `/api/voice/observability-export/replay` and a readable replay proof page at `/voice/observability-export/replay`. Path-backed artifacts are hashed with SHA-256 by default, include byte size and freshness metadata, and can fail the export when required evidence is missing or stale. File delivery writes `manifest.json`, `artifact-index.json`, and artifact files into a customer-owned archive directory; webhook delivery posts the manifest and artifact index to a buyer-owned collector, SIEM bridge, or warehouse endpoint; S3 delivery writes the same manifest, index, and artifact files through Bun's native S3 client; SQLite and Postgres delivery persist the schema id/version, manifest, artifact index, checksum metadata, status, run id, and timestamps into buyer-owned database tables. Delivery receipt stores persist run id, destinations, status, schema, and target history so operators can prove exports have been continuously healthy. Failed trace/audit deliveries fail the export report, pending deliveries warn, and every trace/audit envelope includes the linked operations-record URL when one is configured. Session snapshots and call-debugger reports become first-class artifact-index rows when passed through `sessionSnapshots` and `callDebuggerReports`, so support bundles, incident handoffs, SIEM records, and warehouse exports share one customer-owned evidence graph. This is the primitive to use when customers ask how voice evidence leaves the app without going through a hosted vendor dashboard.
|
|
320
3360
|
|
|
321
|
-
|
|
3361
|
+
Pass the same report into production readiness when export health should block deploys:
|
|
322
3362
|
|
|
323
|
-
|
|
3363
|
+
```ts
|
|
3364
|
+
const observabilityExportDeliveryReceipts =
|
|
3365
|
+
createVoiceFileObservabilityExportDeliveryReceiptStore({
|
|
3366
|
+
directory: '.voice-runtime/observability-export-receipts'
|
|
3367
|
+
});
|
|
324
3368
|
|
|
325
|
-
|
|
3369
|
+
app.use(
|
|
3370
|
+
createVoiceProductionReadinessRoutes({
|
|
3371
|
+
links: {
|
|
3372
|
+
observabilityExport: '/voice/observability-export',
|
|
3373
|
+
observabilityExportDeliveries:
|
|
3374
|
+
'/api/voice/observability-export/deliveries'
|
|
3375
|
+
},
|
|
3376
|
+
observabilityExport: exportReport,
|
|
3377
|
+
observabilityExportDeliveryHistory: {
|
|
3378
|
+
failOnMissing: true,
|
|
3379
|
+
failOnStale: true,
|
|
3380
|
+
maxAgeMs: 60 * 60 * 1000,
|
|
3381
|
+
store: observabilityExportDeliveryReceipts
|
|
3382
|
+
},
|
|
3383
|
+
observabilityExportReplay: {
|
|
3384
|
+
kind: 'sqlite',
|
|
3385
|
+
path: '.voice-runtime/observability-exports.sqlite',
|
|
3386
|
+
runId: 'latest-proof-pack',
|
|
3387
|
+
tableName: 'voice_observability_exports'
|
|
3388
|
+
},
|
|
3389
|
+
store: runtimeStorage.traces
|
|
3390
|
+
})
|
|
3391
|
+
);
|
|
3392
|
+
```
|
|
326
3393
|
|
|
327
|
-
|
|
3394
|
+
Readiness adds `Observability export`, `Observability export delivery`, and `Observability export replay` checks. Failed export manifests fail the deploy gate, delivery receipt history can fail or warn when no successful delivery exists or the latest success is older than your configured freshness window, and replay health can fail the gate when customer-owned evidence cannot be read back cleanly from file, S3, SQLite, or Postgres.
|
|
328
3395
|
|
|
329
3396
|
## Production Voice Ops
|
|
330
3397
|
|
|
@@ -733,6 +3800,86 @@ app.use(
|
|
|
733
3800
|
|
|
734
3801
|
Client state now exposes `assistantAudio` on the stream/controller helpers, so apps can buffer or play synthesized chunks without inventing a second transport.
|
|
735
3802
|
|
|
3803
|
+
## Realtime Adapter Packages
|
|
3804
|
+
|
|
3805
|
+
Use realtime adapter packages when you want direct speech-to-speech output paths for live smoke tests, duplex benchmarks, or custom realtime orchestration. Core owns the `RealtimeAdapter` contract and `voice({ realtime })` orchestration path; provider protocol code lives in adapter packages such as `@absolutejs/voice-openai` and `@absolutejs/voice-gemini`.
|
|
3806
|
+
|
|
3807
|
+
```ts
|
|
3808
|
+
import { voice } from '@absolutejs/voice';
|
|
3809
|
+
import { openai } from '@absolutejs/voice-openai';
|
|
3810
|
+
import { runTTSAdapterFixture } from '@absolutejs/voice/testing';
|
|
3811
|
+
|
|
3812
|
+
const realtime = openai({
|
|
3813
|
+
apiKey: process.env.OPENAI_API_KEY!,
|
|
3814
|
+
instructions: 'Answer in one concise sentence.',
|
|
3815
|
+
model: 'gpt-realtime',
|
|
3816
|
+
voice: 'marin'
|
|
3817
|
+
});
|
|
3818
|
+
|
|
3819
|
+
app.use(
|
|
3820
|
+
voice({
|
|
3821
|
+
path: '/voice',
|
|
3822
|
+
realtime,
|
|
3823
|
+
realtimeInputFormat: {
|
|
3824
|
+
channels: 1,
|
|
3825
|
+
container: 'raw',
|
|
3826
|
+
encoding: 'pcm_s16le',
|
|
3827
|
+
sampleRateHz: 24000
|
|
3828
|
+
},
|
|
3829
|
+
session,
|
|
3830
|
+
onTurn: async ({ turn }) => ({
|
|
3831
|
+
assistantText: `You said: ${turn.text}`
|
|
3832
|
+
}),
|
|
3833
|
+
onComplete: async () => {}
|
|
3834
|
+
})
|
|
3835
|
+
);
|
|
3836
|
+
|
|
3837
|
+
const report = await runTTSAdapterFixture(
|
|
3838
|
+
realtime,
|
|
3839
|
+
{
|
|
3840
|
+
id: 'openai-realtime-smoke',
|
|
3841
|
+
text: 'Say exactly: AbsoluteJS realtime is online.',
|
|
3842
|
+
title: 'OpenAI Realtime smoke'
|
|
3843
|
+
},
|
|
3844
|
+
{
|
|
3845
|
+
realtimeFormat: {
|
|
3846
|
+
channels: 1,
|
|
3847
|
+
container: 'raw',
|
|
3848
|
+
encoding: 'pcm_s16le',
|
|
3849
|
+
sampleRateHz: 24000
|
|
3850
|
+
}
|
|
3851
|
+
}
|
|
3852
|
+
);
|
|
3853
|
+
```
|
|
3854
|
+
|
|
3855
|
+
For server-to-server use, realtime adapters open provider-specific streaming connections, send session configuration, stream text or PCM input, and emit normalized transcript/audio/error/close events. OpenAI Realtime uses raw 24kHz mono `pcm_s16le` audio. The main `voice(...)` route can 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.
|
|
3856
|
+
|
|
3857
|
+
Use `createVoiceRealtimeProviderContractMatrixPreset(...)` to prove which realtime providers are production-ready. Native media-pipeline primitives such as `VoiceMediaFrame` and `buildVoiceMediaPipelineCalibrationReport(...)` are the path for advanced pipeline behavior in AbsoluteJS apps.
|
|
3858
|
+
|
|
3859
|
+
```ts
|
|
3860
|
+
import {
|
|
3861
|
+
createVoiceRealtimeProviderContractMatrixPreset,
|
|
3862
|
+
createVoiceRealtimeProviderContractRoutes
|
|
3863
|
+
} from '@absolutejs/voice';
|
|
3864
|
+
|
|
3865
|
+
app.use(
|
|
3866
|
+
createVoiceRealtimeProviderContractRoutes({
|
|
3867
|
+
matrix: createVoiceRealtimeProviderContractMatrixPreset({
|
|
3868
|
+
env: process.env,
|
|
3869
|
+
fallbackProviders: {
|
|
3870
|
+
'gemini-live': ['openai-realtime'],
|
|
3871
|
+
'openai-realtime': ['gemini-live']
|
|
3872
|
+
},
|
|
3873
|
+
latencyBudgets: {
|
|
3874
|
+
'gemini-live': 900,
|
|
3875
|
+
'openai-realtime': 800
|
|
3876
|
+
},
|
|
3877
|
+
selected: 'openai-realtime'
|
|
3878
|
+
})
|
|
3879
|
+
})
|
|
3880
|
+
);
|
|
3881
|
+
```
|
|
3882
|
+
|
|
736
3883
|
If you want a minimal browser playback path, use the client audio player:
|
|
737
3884
|
|
|
738
3885
|
```ts
|
|
@@ -1069,6 +4216,363 @@ app.use(
|
|
|
1069
4216
|
- `benchmark-results/sessions-cheap-stt-runs-3.json`
|
|
1070
4217
|
- `benchmark-results/stt-routing-run-manifest.json`
|
|
1071
4218
|
|
|
4219
|
+
## LLM Provider Routing
|
|
4220
|
+
|
|
4221
|
+
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.
|
|
4222
|
+
|
|
4223
|
+
```ts
|
|
4224
|
+
import {
|
|
4225
|
+
createAnthropicVoiceAssistantModel,
|
|
4226
|
+
createGeminiVoiceAssistantModel,
|
|
4227
|
+
createOpenAIVoiceAssistantModel,
|
|
4228
|
+
createVoiceProviderRouter,
|
|
4229
|
+
resolveVoiceProviderRoutingPolicyPreset
|
|
4230
|
+
} from '@absolutejs/voice';
|
|
4231
|
+
|
|
4232
|
+
const model = createVoiceProviderRouter({
|
|
4233
|
+
providers: {
|
|
4234
|
+
openai: createOpenAIVoiceAssistantModel({ apiKey: process.env.OPENAI_API_KEY! }),
|
|
4235
|
+
anthropic: createAnthropicVoiceAssistantModel({ apiKey: process.env.ANTHROPIC_API_KEY! }),
|
|
4236
|
+
gemini: createGeminiVoiceAssistantModel({ apiKey: process.env.GEMINI_API_KEY! })
|
|
4237
|
+
},
|
|
4238
|
+
providerHealth: {
|
|
4239
|
+
failureThreshold: 1,
|
|
4240
|
+
cooldownMs: 30_000,
|
|
4241
|
+
rateLimitCooldownMs: 120_000
|
|
4242
|
+
},
|
|
4243
|
+
providerProfiles: {
|
|
4244
|
+
openai: { cost: 6, latencyMs: 650, quality: 0.92, timeoutMs: 3500 },
|
|
4245
|
+
anthropic: { cost: 7, latencyMs: 850, quality: 0.95, timeoutMs: 4500 },
|
|
4246
|
+
gemini: { cost: 2, latencyMs: 700, quality: 0.86, timeoutMs: 3500 }
|
|
4247
|
+
},
|
|
4248
|
+
policy: resolveVoiceProviderRoutingPolicyPreset('balanced')
|
|
4249
|
+
});
|
|
4250
|
+
```
|
|
4251
|
+
|
|
4252
|
+
Built-in policy presets:
|
|
4253
|
+
|
|
4254
|
+
- `quality-first`: rank by `providerProfiles[provider].quality`, then priority, latency, and cost.
|
|
4255
|
+
- `latency-first`: rank by expected latency.
|
|
4256
|
+
- `cost-first`: rank by expected cost.
|
|
4257
|
+
- `cost-cap`: rank by cost and reject providers above `maxCost`.
|
|
4258
|
+
- `balanced`: weighted score using cost, latency, quality, and priority.
|
|
4259
|
+
|
|
4260
|
+
Use `createVoiceProviderOrchestrationProfile(...)` when one app has multiple provider surfaces with different tradeoffs. This is the Vapi-style "choose providers without glue" layer, but still code-owned: a live call can prefer low latency with a circuit breaker, while a background summary can prefer lower cost and stricter budget caps.
|
|
4261
|
+
|
|
4262
|
+
```ts
|
|
4263
|
+
import {
|
|
4264
|
+
createVoiceProviderOrchestrationProfile,
|
|
4265
|
+
createVoiceProviderRouter
|
|
4266
|
+
} from '@absolutejs/voice';
|
|
4267
|
+
|
|
4268
|
+
const providerProfile = createVoiceProviderOrchestrationProfile({
|
|
4269
|
+
id: 'support-agent-providers',
|
|
4270
|
+
defaultSurface: 'live-call',
|
|
4271
|
+
surfaces: {
|
|
4272
|
+
'live-call': {
|
|
4273
|
+
policy: 'latency-first',
|
|
4274
|
+
fallback: ['openai', 'anthropic', 'gemini'],
|
|
4275
|
+
maxLatencyMs: 900,
|
|
4276
|
+
providerHealth: {
|
|
4277
|
+
failureThreshold: 1,
|
|
4278
|
+
cooldownMs: 30_000,
|
|
4279
|
+
rateLimitCooldownMs: 120_000
|
|
4280
|
+
},
|
|
4281
|
+
providerProfiles: {
|
|
4282
|
+
openai: { cost: 6, latencyMs: 650, quality: 0.92, timeoutMs: 3500 },
|
|
4283
|
+
anthropic: { cost: 7, latencyMs: 850, quality: 0.95, timeoutMs: 4500 },
|
|
4284
|
+
gemini: { cost: 2, latencyMs: 700, quality: 0.86, timeoutMs: 3500 }
|
|
4285
|
+
}
|
|
4286
|
+
},
|
|
4287
|
+
'background-summary': {
|
|
4288
|
+
policy: 'cost-cap',
|
|
4289
|
+
fallback: ['gemini', 'openai'],
|
|
4290
|
+
maxCost: 3,
|
|
4291
|
+
minQuality: 0.82,
|
|
4292
|
+
providerProfiles: {
|
|
4293
|
+
openai: { cost: 6, latencyMs: 650, quality: 0.92 },
|
|
4294
|
+
gemini: { cost: 2, latencyMs: 700, quality: 0.86 }
|
|
4295
|
+
}
|
|
4296
|
+
}
|
|
4297
|
+
}
|
|
4298
|
+
});
|
|
4299
|
+
|
|
4300
|
+
const liveModel = createVoiceProviderRouter({
|
|
4301
|
+
providers,
|
|
4302
|
+
orchestrationProfile: providerProfile,
|
|
4303
|
+
orchestrationSurface: 'live-call'
|
|
4304
|
+
});
|
|
4305
|
+
|
|
4306
|
+
const summaryModel = createVoiceProviderRouter({
|
|
4307
|
+
providers,
|
|
4308
|
+
orchestrationProfile: providerProfile,
|
|
4309
|
+
orchestrationSurface: 'background-summary'
|
|
4310
|
+
});
|
|
4311
|
+
```
|
|
4312
|
+
|
|
4313
|
+
Mount `createVoiceProviderOrchestrationRoutes(...)` and pass the report into production readiness when provider policy should be deploy-gated. This proves that required surfaces have enough providers, explicit fallback order, circuit-breaker settings, timeout budgets, and cost/latency/quality bounds.
|
|
4314
|
+
|
|
4315
|
+
```ts
|
|
4316
|
+
import {
|
|
4317
|
+
buildVoiceProviderOrchestrationReport,
|
|
4318
|
+
createVoiceProviderOrchestrationRoutes,
|
|
4319
|
+
createVoiceProductionReadinessRoutes
|
|
4320
|
+
} from '@absolutejs/voice';
|
|
4321
|
+
|
|
4322
|
+
const providerOrchestration = () =>
|
|
4323
|
+
buildVoiceProviderOrchestrationReport({
|
|
4324
|
+
profile: providerProfile,
|
|
4325
|
+
requirements: {
|
|
4326
|
+
'live-call': {
|
|
4327
|
+
minProviders: 2,
|
|
4328
|
+
requireBudgetPolicy: true,
|
|
4329
|
+
requireCircuitBreaker: true,
|
|
4330
|
+
requireFallback: true,
|
|
4331
|
+
requireTimeoutBudget: true
|
|
4332
|
+
}
|
|
4333
|
+
}
|
|
4334
|
+
});
|
|
4335
|
+
|
|
4336
|
+
app
|
|
4337
|
+
.use(
|
|
4338
|
+
createVoiceProviderOrchestrationRoutes({
|
|
4339
|
+
profile: providerProfile,
|
|
4340
|
+
requirements: {
|
|
4341
|
+
'live-call': {
|
|
4342
|
+
minProviders: 2,
|
|
4343
|
+
requireBudgetPolicy: true,
|
|
4344
|
+
requireCircuitBreaker: true,
|
|
4345
|
+
requireFallback: true,
|
|
4346
|
+
requireTimeoutBudget: true
|
|
4347
|
+
}
|
|
4348
|
+
}
|
|
4349
|
+
})
|
|
4350
|
+
)
|
|
4351
|
+
.use(
|
|
4352
|
+
createVoiceProductionReadinessRoutes({
|
|
4353
|
+
store: runtime.traces,
|
|
4354
|
+
providerOrchestration,
|
|
4355
|
+
links: {
|
|
4356
|
+
providerOrchestration: '/voice/provider-orchestration'
|
|
4357
|
+
}
|
|
4358
|
+
})
|
|
4359
|
+
);
|
|
4360
|
+
```
|
|
4361
|
+
|
|
4362
|
+
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.
|
|
4363
|
+
|
|
4364
|
+
```ts
|
|
4365
|
+
const policy = resolveVoiceProviderRoutingPolicyPreset('cost-cap', {
|
|
4366
|
+
maxCost: 3,
|
|
4367
|
+
minQuality: 0.82
|
|
4368
|
+
});
|
|
4369
|
+
```
|
|
4370
|
+
|
|
4371
|
+
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.
|
|
4372
|
+
|
|
4373
|
+
```ts
|
|
4374
|
+
import { runVoiceProviderRoutingContract } from '@absolutejs/voice';
|
|
4375
|
+
|
|
4376
|
+
const report = await runVoiceProviderRoutingContract({
|
|
4377
|
+
store: runtime.traces,
|
|
4378
|
+
contract: {
|
|
4379
|
+
id: 'openai-to-anthropic-fallback',
|
|
4380
|
+
expect: [
|
|
4381
|
+
{
|
|
4382
|
+
kind: 'llm',
|
|
4383
|
+
provider: 'openai',
|
|
4384
|
+
selectedProvider: 'openai',
|
|
4385
|
+
fallbackProvider: 'anthropic',
|
|
4386
|
+
status: 'error'
|
|
4387
|
+
},
|
|
4388
|
+
{
|
|
4389
|
+
kind: 'llm',
|
|
4390
|
+
provider: 'anthropic',
|
|
4391
|
+
selectedProvider: 'openai',
|
|
4392
|
+
status: 'fallback'
|
|
4393
|
+
}
|
|
4394
|
+
]
|
|
4395
|
+
}
|
|
4396
|
+
});
|
|
4397
|
+
|
|
4398
|
+
if (!report.pass) {
|
|
4399
|
+
throw new Error(report.issues.map((issue) => issue.message).join('\n'));
|
|
4400
|
+
}
|
|
4401
|
+
```
|
|
4402
|
+
|
|
4403
|
+
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.
|
|
4404
|
+
|
|
4405
|
+
Use `createVoiceProviderSloRoutes(...)` when provider speed needs to be release evidence instead of a dashboard claim. The report reads the same provider routing trace events and checks LLM, STT, and TTS latency, p95 latency, timeout rate, fallback rate, and unresolved provider error rate.
|
|
4406
|
+
|
|
4407
|
+
```ts
|
|
4408
|
+
import {
|
|
4409
|
+
createVoiceProviderSloRoutes,
|
|
4410
|
+
createVoiceProductionReadinessRoutes
|
|
4411
|
+
} from '@absolutejs/voice';
|
|
4412
|
+
|
|
4413
|
+
const providerSlo = {
|
|
4414
|
+
requiredKinds: ['llm', 'stt', 'tts'],
|
|
4415
|
+
thresholds: {
|
|
4416
|
+
llm: { maxAverageElapsedMs: 2500, maxP95ElapsedMs: 4500 },
|
|
4417
|
+
stt: { maxAverageElapsedMs: 800, maxP95ElapsedMs: 1500 },
|
|
4418
|
+
tts: { maxAverageElapsedMs: 1200, maxP95ElapsedMs: 2200 }
|
|
4419
|
+
}
|
|
4420
|
+
} as const;
|
|
4421
|
+
|
|
4422
|
+
app
|
|
4423
|
+
.use(
|
|
4424
|
+
createVoiceProviderSloRoutes({
|
|
4425
|
+
store: runtime.traces,
|
|
4426
|
+
...providerSlo
|
|
4427
|
+
})
|
|
4428
|
+
)
|
|
4429
|
+
.use(
|
|
4430
|
+
createVoiceProductionReadinessRoutes({
|
|
4431
|
+
store: runtime.traces,
|
|
4432
|
+
providerSlo,
|
|
4433
|
+
links: {
|
|
4434
|
+
providerSlo: '/voice/provider-slos'
|
|
4435
|
+
}
|
|
4436
|
+
})
|
|
4437
|
+
);
|
|
4438
|
+
```
|
|
4439
|
+
|
|
4440
|
+
The provider SLO routes expose JSON at `/api/voice/provider-slos`, HTML at `/voice/provider-slos`, and Markdown at `/voice/provider-slos.md`. Readiness adds a `Provider SLO gates` check when `providerSlo` is configured; failing latency, timeout, fallback, or unresolved-error budgets close the deploy gate.
|
|
4441
|
+
|
|
4442
|
+
Use `evaluateVoiceProviderSloEvidence(...)` or `assertVoiceProviderSloEvidence(...)` when a proof pack needs to verify the JSON directly instead of scraping rendered HTML. The assertion can require LLM/STT/TTS evidence, latency samples, fallback events, named providers, and per-kind latency ceilings:
|
|
4443
|
+
|
|
4444
|
+
```ts
|
|
4445
|
+
const providerReport = await buildVoiceProviderSloReport({
|
|
4446
|
+
requiredKinds: ['llm', 'stt', 'tts'],
|
|
4447
|
+
store: runtime.traces
|
|
4448
|
+
});
|
|
4449
|
+
|
|
4450
|
+
assertVoiceProviderSloEvidence(providerReport, {
|
|
4451
|
+
fallbackKinds: ['llm'],
|
|
4452
|
+
maxP95ElapsedMs: { llm: 4500, stt: 1500, tts: 2200 },
|
|
4453
|
+
maxStatus: 'pass',
|
|
4454
|
+
minFallbacks: 1,
|
|
4455
|
+
minLatencySamples: 3,
|
|
4456
|
+
requiredKinds: ['llm', 'stt', 'tts'],
|
|
4457
|
+
requiredProviders: ['openai', 'anthropic', 'deepgram']
|
|
4458
|
+
});
|
|
4459
|
+
```
|
|
4460
|
+
|
|
4461
|
+
Use `createVoiceProviderDecisionTraceEvent(...)` and `createVoiceProviderDecisionTraceRoutes(...)` when you need runtime proof for why a provider won, failed, was skipped, or recovered by fallback. This is the per-call decision trail behind provider orchestration: it can read explicit `provider.decision` trace events or normalize existing provider routing events.
|
|
4462
|
+
|
|
4463
|
+
```ts
|
|
4464
|
+
import {
|
|
4465
|
+
createVoiceProviderDecisionTraceEvent,
|
|
4466
|
+
createVoiceProviderDecisionTraceRoutes
|
|
4467
|
+
} from '@absolutejs/voice';
|
|
4468
|
+
|
|
4469
|
+
await traces.append(
|
|
4470
|
+
createVoiceProviderDecisionTraceEvent({
|
|
4471
|
+
provider: 'deepgram',
|
|
4472
|
+
selectedProvider: 'assemblyai',
|
|
4473
|
+
fallbackProvider: 'assemblyai',
|
|
4474
|
+
status: 'fallback',
|
|
4475
|
+
surface: 'live-stt',
|
|
4476
|
+
reason: 'Deepgram timed out, AssemblyAI recovered the live STT turn.'
|
|
4477
|
+
})
|
|
4478
|
+
);
|
|
4479
|
+
|
|
4480
|
+
app.use(
|
|
4481
|
+
createVoiceProviderDecisionTraceRoutes({
|
|
4482
|
+
store: traces,
|
|
4483
|
+
requiredSurfaces: ['live-call', 'live-stt', 'telephony-tts']
|
|
4484
|
+
})
|
|
4485
|
+
);
|
|
4486
|
+
```
|
|
4487
|
+
|
|
4488
|
+
The routes expose JSON at `/api/voice/provider-decisions`, HTML at `/voice/provider-decisions`, and Markdown at `/voice/provider-decisions.md`. Use this next to provider SLOs when a customer asks not just "is fallback working?" but "why did the system choose this provider for this call?". For proof packs, gate fallback and degradation directly with `minFallbacks`, `minDegraded`, `requiredStatuses`, `requiredFallbackProviders`, and `requiredReasonIncludes` so deploy evidence fails when fallback behavior is missing or unexplained.
|
|
4489
|
+
|
|
4490
|
+
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.
|
|
4491
|
+
|
|
4492
|
+
```ts
|
|
4493
|
+
import {
|
|
4494
|
+
buildVoiceProviderContractMatrix,
|
|
4495
|
+
createVoiceProviderContractMatrixPreset,
|
|
4496
|
+
createVoiceProviderContractMatrixRoutes
|
|
4497
|
+
} from '@absolutejs/voice';
|
|
4498
|
+
|
|
4499
|
+
const providerContracts = () =>
|
|
4500
|
+
createVoiceProviderContractMatrixPreset('phone-agent', {
|
|
4501
|
+
env: process.env,
|
|
4502
|
+
providers: {
|
|
4503
|
+
llm: ['openai', 'anthropic', 'gemini'],
|
|
4504
|
+
stt: ['deepgram', 'assemblyai'],
|
|
4505
|
+
tts: ['openai', 'emergency']
|
|
4506
|
+
},
|
|
4507
|
+
selected: {
|
|
4508
|
+
llm: 'openai',
|
|
4509
|
+
stt: 'deepgram',
|
|
4510
|
+
tts: 'openai'
|
|
4511
|
+
},
|
|
4512
|
+
latencyBudgets: {
|
|
4513
|
+
openai: 900,
|
|
4514
|
+
deepgram: 250,
|
|
4515
|
+
assemblyai: 900,
|
|
4516
|
+
emergency: 80
|
|
4517
|
+
},
|
|
4518
|
+
remediationHref: '/provider-contracts'
|
|
4519
|
+
});
|
|
4520
|
+
|
|
4521
|
+
const app = createVoiceProviderContractMatrixRoutes({
|
|
4522
|
+
htmlPath: '/provider-contracts',
|
|
4523
|
+
path: '/api/provider-contracts',
|
|
4524
|
+
load: () => buildVoiceProviderContractMatrix(providerContracts())
|
|
4525
|
+
});
|
|
4526
|
+
```
|
|
4527
|
+
|
|
4528
|
+
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.
|
|
4529
|
+
|
|
4530
|
+
For full control, pass an object policy:
|
|
4531
|
+
|
|
4532
|
+
```ts
|
|
4533
|
+
const model = createVoiceProviderRouter({
|
|
4534
|
+
providers,
|
|
4535
|
+
providerProfiles,
|
|
4536
|
+
policy: {
|
|
4537
|
+
strategy: 'balanced',
|
|
4538
|
+
maxLatencyMs: 1000,
|
|
4539
|
+
weights: { cost: 1, latencyMs: 0.004, quality: 12 }
|
|
4540
|
+
}
|
|
4541
|
+
});
|
|
4542
|
+
```
|
|
4543
|
+
|
|
4544
|
+
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.
|
|
4545
|
+
|
|
4546
|
+
```ts
|
|
4547
|
+
const stt = createVoiceSTTProviderRouter({
|
|
4548
|
+
adapters: {
|
|
4549
|
+
deepgram,
|
|
4550
|
+
assemblyai
|
|
4551
|
+
},
|
|
4552
|
+
providerHealth: { cooldownMs: 30_000 },
|
|
4553
|
+
providerProfiles: {
|
|
4554
|
+
deepgram: { cost: 4, latencyMs: 180, quality: 0.93, timeoutMs: 1500 },
|
|
4555
|
+
assemblyai: { cost: 2, latencyMs: 650, quality: 0.88, timeoutMs: 3000 }
|
|
4556
|
+
},
|
|
4557
|
+
policy: resolveVoiceProviderRoutingPolicyPreset('latency-first')
|
|
4558
|
+
});
|
|
4559
|
+
|
|
4560
|
+
const tts = createVoiceTTSProviderRouter({
|
|
4561
|
+
adapters: {
|
|
4562
|
+
elevenlabs,
|
|
4563
|
+
openai
|
|
4564
|
+
},
|
|
4565
|
+
providerProfiles: {
|
|
4566
|
+
elevenlabs: { cost: 5, latencyMs: 220, quality: 0.94 },
|
|
4567
|
+
openai: { cost: 2, latencyMs: 320, quality: 0.87 }
|
|
4568
|
+
},
|
|
4569
|
+
policy: resolveVoiceProviderRoutingPolicyPreset('cost-cap', {
|
|
4570
|
+
maxCost: 3,
|
|
4571
|
+
minQuality: 0.85
|
|
4572
|
+
})
|
|
4573
|
+
});
|
|
4574
|
+
```
|
|
4575
|
+
|
|
1072
4576
|
## Presets
|
|
1073
4577
|
|
|
1074
4578
|
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 +5070,8 @@ Default reconnect strategy is `resume-last-turn`.
|
|
|
1566
5070
|
|
|
1567
5071
|
If an adapter does not emit native end-of-turn events, core falls back to silence detection with a default `700ms` threshold.
|
|
1568
5072
|
|
|
5073
|
+
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.
|
|
5074
|
+
|
|
1569
5075
|
## STT Fallback
|
|
1570
5076
|
|
|
1571
5077
|
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.
|
|
@@ -1705,3 +5211,132 @@ Browser and framework helpers sit on top of the same connection core:
|
|
|
1705
5211
|
- `VoiceStreamService` in `@absolutejs/voice/angular`
|
|
1706
5212
|
|
|
1707
5213
|
For plain HTML or HTMX flows, use `@absolutejs/voice/client` directly.
|
|
5214
|
+
|
|
5215
|
+
### Browser Media Proof
|
|
5216
|
+
|
|
5217
|
+
If your app owns a browser `RTCPeerConnection`, pass it through `browserMedia` so AbsoluteJS can persist real `RTCPeerConnection.getStats()` evidence and feed production readiness. The default WebSocket microphone flow does not require this; this is for WebRTC voice surfaces where browser transport quality matters.
|
|
5218
|
+
|
|
5219
|
+
Server route setup:
|
|
5220
|
+
|
|
5221
|
+
```ts
|
|
5222
|
+
import {
|
|
5223
|
+
createVoiceBrowserMediaRoutes,
|
|
5224
|
+
createVoiceProductionReadinessRoutes,
|
|
5225
|
+
getLatestVoiceBrowserMediaReport
|
|
5226
|
+
} from '@absolutejs/voice';
|
|
5227
|
+
|
|
5228
|
+
app
|
|
5229
|
+
.use(createVoiceBrowserMediaRoutes({ store: runtime.traces }))
|
|
5230
|
+
.use(
|
|
5231
|
+
createVoiceProductionReadinessRoutes({
|
|
5232
|
+
browserMedia: () =>
|
|
5233
|
+
getLatestVoiceBrowserMediaReport({ store: runtime.traces }),
|
|
5234
|
+
links: {
|
|
5235
|
+
browserMedia: '/voice/browser-media'
|
|
5236
|
+
}
|
|
5237
|
+
})
|
|
5238
|
+
);
|
|
5239
|
+
```
|
|
5240
|
+
|
|
5241
|
+
Shared stream options:
|
|
5242
|
+
|
|
5243
|
+
```ts
|
|
5244
|
+
const browserMedia = {
|
|
5245
|
+
continuity: {
|
|
5246
|
+
maxGapMs: 7000,
|
|
5247
|
+
maxInboundPacketStallMs: 7000,
|
|
5248
|
+
maxOutboundPacketStallMs: 7000,
|
|
5249
|
+
requireInboundAudio: true,
|
|
5250
|
+
requireOutboundAudio: true
|
|
5251
|
+
},
|
|
5252
|
+
getPeerConnection: () => peerConnection,
|
|
5253
|
+
maxJitterMs: 30,
|
|
5254
|
+
maxPacketLossRatio: 0.02,
|
|
5255
|
+
maxRoundTripTimeMs: 250,
|
|
5256
|
+
requireConnectedCandidatePair: true,
|
|
5257
|
+
requireLiveAudioTrack: true
|
|
5258
|
+
};
|
|
5259
|
+
```
|
|
5260
|
+
|
|
5261
|
+
React:
|
|
5262
|
+
|
|
5263
|
+
```tsx
|
|
5264
|
+
import { useRef } from 'react';
|
|
5265
|
+
import { useVoiceStream } from '@absolutejs/voice/react';
|
|
5266
|
+
|
|
5267
|
+
export function WebRTCVoice() {
|
|
5268
|
+
const peerConnection = useRef<RTCPeerConnection | null>(null);
|
|
5269
|
+
const voice = useVoiceStream('/voice/support', {
|
|
5270
|
+
browserMedia: {
|
|
5271
|
+
...browserMedia,
|
|
5272
|
+
getPeerConnection: () => peerConnection.current
|
|
5273
|
+
}
|
|
5274
|
+
});
|
|
5275
|
+
|
|
5276
|
+
return <button onClick={() => voice.close()}>End call</button>;
|
|
5277
|
+
}
|
|
5278
|
+
```
|
|
5279
|
+
|
|
5280
|
+
Vue:
|
|
5281
|
+
|
|
5282
|
+
```ts
|
|
5283
|
+
import { shallowRef } from 'vue';
|
|
5284
|
+
import { useVoiceStream } from '@absolutejs/voice/vue';
|
|
5285
|
+
|
|
5286
|
+
const peerConnection = shallowRef<RTCPeerConnection | null>(null);
|
|
5287
|
+
const voice = useVoiceStream('/voice/support', {
|
|
5288
|
+
browserMedia: {
|
|
5289
|
+
...browserMedia,
|
|
5290
|
+
getPeerConnection: () => peerConnection.value
|
|
5291
|
+
}
|
|
5292
|
+
});
|
|
5293
|
+
```
|
|
5294
|
+
|
|
5295
|
+
Svelte:
|
|
5296
|
+
|
|
5297
|
+
```ts
|
|
5298
|
+
import { createVoiceStream } from '@absolutejs/voice/svelte';
|
|
5299
|
+
|
|
5300
|
+
let peerConnection: RTCPeerConnection | null = null;
|
|
5301
|
+
const voice = createVoiceStream('/voice/support', {
|
|
5302
|
+
browserMedia: {
|
|
5303
|
+
...browserMedia,
|
|
5304
|
+
getPeerConnection: () => peerConnection
|
|
5305
|
+
}
|
|
5306
|
+
});
|
|
5307
|
+
```
|
|
5308
|
+
|
|
5309
|
+
Angular:
|
|
5310
|
+
|
|
5311
|
+
```ts
|
|
5312
|
+
import { Component, inject } from '@angular/core';
|
|
5313
|
+
import { VoiceStreamService } from '@absolutejs/voice/angular';
|
|
5314
|
+
|
|
5315
|
+
@Component({
|
|
5316
|
+
selector: 'app-webrtc-voice',
|
|
5317
|
+
template: `<button type="button" (click)="stream.close()">End call</button>`
|
|
5318
|
+
})
|
|
5319
|
+
export class WebRTCVoiceComponent {
|
|
5320
|
+
private readonly voice = inject(VoiceStreamService);
|
|
5321
|
+
private peerConnection: RTCPeerConnection | null = null;
|
|
5322
|
+
|
|
5323
|
+
readonly stream = this.voice.connect('/voice/support', {
|
|
5324
|
+
browserMedia: {
|
|
5325
|
+
...browserMedia,
|
|
5326
|
+
getPeerConnection: () => this.peerConnection
|
|
5327
|
+
}
|
|
5328
|
+
});
|
|
5329
|
+
}
|
|
5330
|
+
```
|
|
5331
|
+
|
|
5332
|
+
HTMX/plain browser:
|
|
5333
|
+
|
|
5334
|
+
```ts
|
|
5335
|
+
import { createVoiceController } from '@absolutejs/voice/client';
|
|
5336
|
+
|
|
5337
|
+
const voice = createVoiceController('/voice/support', {
|
|
5338
|
+
browserMedia
|
|
5339
|
+
});
|
|
5340
|
+
|
|
5341
|
+
voice.bindHTMX({ element: '#voice-htmx-sync' });
|
|
5342
|
+
```
|