@absolutejs/voice
v0.0.22-beta.616
Published
Voice primitives and Elysia plugin for AbsoluteJS
Readme
@absolutejs/voice
@absolutejs/voice is the self-hosted voice operations layer for AbsoluteJS.
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.
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.
What's new
0.0.22-beta.474 · Phase 5 runtime hookup — auto-wire monitor sockets to live sessions
The Phase 5 monitor primitive is now first-class in the voice runtime. Pass a monitor binding to voice({...}) and every session opened against the voice plugin auto-registers in the monitor registry, outbound TTS audio auto-fans-out to all listeners, and the close/superseded/session-switch paths deregister automatically. Supervisors connecting to the listen route get a live audio stream without any manual record.emit() calls.
import { Elysia } from "elysia";
import {
createVoiceInMemoryMonitorRegistry,
createVoiceLiveMonitorRoutes,
createVoiceMonitorRuntimeBinding,
voice,
} from "@absolutejs/voice";
const monitorRegistry = createVoiceInMemoryMonitorRegistry();
const app = new Elysia()
.use(
voice({
path: "/voice/realtime",
stt: deepgram({ apiKey: process.env.DEEPGRAM_API_KEY! }),
tts: elevenlabs({ apiKey: process.env.ELEVENLABS_API_KEY! }),
onTurn: async (session, turn, api) => {
// your business logic
},
session: sessionStore,
monitor: createVoiceMonitorRuntimeBinding(monitorRegistry, {
audioFormat: { channels: 1, container: "raw", encoding: "pcm_s16le", sampleRateHz: 24_000 },
}),
}),
)
.use(
createVoiceLiveMonitorRoutes({
registry: monitorRegistry,
authenticate: async ({ request }) => await verifySupervisorJWT(request),
}),
);Surface additions:
createVoiceMonitorRuntimeBinding(registry, { audioFormat?, defaultSource? })— returns aVoiceMonitorRuntimeBindingyou pass to the voice plugin's newmonitoroption. Internally, each session open callsregisterSession({ handle, sessionId }), audio fans out viaemitAudio()on every binarysocket.send, and the close/superseded/session-switch paths callderegister(reason).VoicePluginConfig.monitor?: VoiceMonitorRuntimeBinding— new optional field on the main voice plugin config.VoiceMonitorMutableRegistry.deregister(sessionId, reason?)— explicit deregister path so the runtime binding can tear down stale records on re-register without throwing. Theregister()-returned deregister fn now also accepts an optional reason.- The runtime binding is opt-in — if you don't pass
monitor, voice behaves exactly as before. No overhead on the audio-send hot path beyond a singleMap.get.
4 new tests cover the runtime binding's register/emit/deregister cycle, no-op-after-deregister, re-registration tear-down, and the audioFormat + defaultSource option threading. Full voice suite now 963 pass / 1 pre-existing fail.
0.0.22-beta.473 · Phase 5 — live listen + control monitor sockets (Vapi monitorPlan parity)
Two new WebSocket routes per session that mirror Vapi's monitorPlan.listenUrl + monitorPlan.controlUrl. Supervisors can subscribe to a live call's outbound audio and send control commands (transfer, hangup, escalate, voicemail, no-answer, plus caller-defined mute/say/inject) without touching the call itself.
import { Elysia } from "elysia";
import {
buildVoiceMonitorPlan,
createVoiceInMemoryMonitorRegistry,
createVoiceLiveMonitorRoutes,
createVoiceMonitorSession,
} from "@absolutejs/voice";
const registry = createVoiceInMemoryMonitorRegistry();
// In your runtime: when a session opens, register it so supervisors can listen.
const record = createVoiceMonitorSession({ handle, sessionId: handle.id });
const deregister = registry.register(record);
// When audio leaves the assistant, fan it out:
// record.emit({ at: Date.now(), chunk, format, source: 'assistant' });
// On call end:
// deregister();
const app = new Elysia()
.use(
createVoiceLiveMonitorRoutes({
authenticate: async ({ sessionId, route, request }) =>
await verifySupervisorJWT(request),
controlHandlers: {
say: async ({ message, session }) => {
await yourTtsRuntime.sayInSession(session.sessionId, message.text);
return { detail: `Said: ${message.text}`, ok: true, type: "say" };
},
},
htmlPath: "/voice/monitor",
registry,
}),
);
const plan = buildVoiceMonitorPlan({
baseUrl: "wss://api.example.com",
sessionId: handle.id,
});
// plan.listenUrl → wss://api.example.com/api/voice/monitor/<sessionId>/listen
// plan.controlUrl → wss://api.example.com/api/voice/monitor/<sessionId>/controlSurface summary:
createVoiceInMemoryMonitorRegistry()—{ register, get, list, emit, emitClose }. Voice's session runtime (or any caller) wiresregister()on open + the returned deregister on close;emit()fans out outbound audio frames;emitClose()notifies listeners that the call ended.createVoiceLiveMonitorRoutes(options)— Elysia plugin that mounts two.ws()routes per session::sessionId/listen(read-only outbound audio as binary frames) and:sessionId/control(JSON control messages). Default handlers maptransfer/hangup/escalate/voicemail/no-answerontoVoiceSessionHandleverbs;mute/say/injectrequire caller-supplied handlers viacontrolHandlers.buildVoiceMonitorPlan(input)— Vapi-shaped helper returning{ listenUrl, controlUrl }for inclusion in your call-create API response.- Auth hook —
authenticate?: ({ sessionId, route, request }) => Promise<boolean>runs at the start of both routes; rejected connections get a 4401 close.
This is a primitive: wiring voice's existing activeSessions runtime into the registry (so audio actually flows from a real call without manual record.emit() calls) is the next step and intentionally left out to keep this change additive. Until then, registries built by hand (e.g. from a custom telephony adapter where you control the outbound audio buffer) work out of the box.
0.0.22-beta.472 · Phase 6 — multilingual STT proof gate
runVoiceMultilingualProof(...) turns the voice-fixtures-multilingual corpus (FLEURS + BSC Catalan-Spanish code-switch + CoSHE Hindi-English code-switch) into a gateable readiness/proof artifact. Buyers evaluating Vapi-replacement can now run any combination of STT adapters against the multilingual corpus and assert per-language WER / pass-rate / term-recall budgets in CI.
import {
buildVoiceMultilingualProofReadinessCheck,
renderVoiceMultilingualProofMarkdown,
runVoiceMultilingualProof,
} from "@absolutejs/voice";
import { deepgram } from "@absolutejs/voice-deepgram";
import { speechmatics } from "@absolutejs/voice-speechmatics";
import { soniox } from "@absolutejs/voice-soniox";
const report = await runVoiceMultilingualProof({
adapters: [
{ adapter: deepgram({ apiKey, model: "nova-3" }), adapterId: "deepgram-nova3" },
{ adapter: speechmatics({ apiKey, region: "eu2" }), adapterId: "speechmatics-enhanced" },
{ adapter: soniox({ apiKey, enableLanguageIdentification: true }), adapterId: "soniox" },
],
defaultThresholds: { maxAverageWordErrorRate: 0.30, minPassRate: 0.7 },
perLanguage: [
{ language: "ca-es", label: "Catalan-Spanish code-switch", maxAverageWordErrorRate: 0.45 },
{ language: "hi-en", label: "Hindi-English code-switch", maxAverageWordErrorRate: 0.50 },
],
});
const readiness = buildVoiceMultilingualProofReadinessCheck(report, {
baseHref: "/voice/multilingual-proof",
});
// drop `readiness` into your VoiceProductionReadinessReport.checks array
await Bun.write("docs/multilingual-proof.md", renderVoiceMultilingualProofMarkdown(report));Highlights:
- Loads fixtures from
VOICE_FIXTURE_DIR(or any caller-supplied directory list / pre-loadedVoiceTestFixture[]), filters by an optional predicate, and runs each adapter through the existingrunSTTAdapterBenchmarkharness — no new STT plumbing required. - Buckets fixture results by
fixture.languageand applies per-language thresholds (maxAverageWordErrorRate,minAverageWordAccuracyRate,minPassRate,minTermRecall) layered over caller-provideddefaultThresholds. - Returns a structured report (
adapters[].languageReports[]with metrics + failures, plus per-adapter and globalpassesflags) plus a Markdown renderer for human review and aVoiceProductionReadinessCheck-shaped helper for drop-in readiness wiring. - Pairs naturally with every STT adapter shipped in
voice-adapters(deepgram, assemblyai, azure streaming, speechmatics, gladia, soniox, google-speech streaming + buffered, openai-whisper buffered).
0.0.22-beta.471 · Vapi parity — fromVapiAssistantConfig adapter
Mechanical migration from a Vapi Assistant JSON to a voice assistant. Pass the JSON dump (or a typed subset), provide a modelFactory that maps Vapi's model.provider+model.model to a voice VoiceAgentModel, and get back { assistant, tools, routeHints, unsupported }.
import {
createOpenAIVoiceAssistantModel,
fromVapiAssistantConfig,
type VapiAssistantConfig,
} from "@absolutejs/voice";
const config: VapiAssistantConfig = JSON.parse(vapiDump);
const { assistant, tools, routeHints, unsupported } = fromVapiAssistantConfig(config, {
assistantId: "support",
fetch,
knowledgeBase: { collection: ragCollection },
modelFactory: ({ provider, model, temperature }) => {
if (provider === "openai") {
return createOpenAIVoiceAssistantModel({
apiKey: process.env.OPENAI_API_KEY!,
model: model ?? "gpt-4.1-mini",
temperature,
});
}
throw new Error(`Unsupported provider: ${provider}`);
},
variableResolver: (path, { context }) => {
if (path === "customer.name") return context.customerName;
return undefined;
},
dtmfSendFactory: () => ({ args, api }) =>
api.transfer({ target: "ivr", metadata: { digits: args.digits } }),
});
console.log("Migrated", tools.length, "tools;", unsupported.length, "fields need manual review:");
for (const reason of unsupported) console.log(` - ${reason.field}: ${reason.detail}`);Mappings:
model.provider+model.model+temperature→ yourmodelFactory(you control which provider adapter to use; the adapter doesn't bake in API keys).messages[].role === "system"(orsystemPrompt) → assistantsystemfield;{{var}}bare expansions auto-compile to a function-form prompt that resolves built-ins ({{now}},{{date}},{{time}}) and yourvariableResolverfor everything else. LiquidJS filters are reported inunsupported.tools[].type→ named-tool factories from beta.470:endCall→createVoiceEndCallTool,transferCall→createVoiceTransferCallTool(numbers/SIP URIs auto-mapped),voicemail→createVoiceVoicemailDetectionTool,apiRequest/function(withserver.url) →createVoiceApiRequestTool,query(or top-levelknowledgeBaseId) →createVoiceRAGToolif you pass aknowledgeBaseoption.dtmfrequires adtmfSendFactoryso it can reach your telephony adapter.functiontools withoutserver.urlare forwarded to your optionalcustomToolFactory.voice.*(TTS) +transcriber.*(STT) +firstMessage/firstMessageMode/silenceTimeoutSeconds/maxDurationSeconds/endCall*/recordingEnabled/compliancePlan.*/backgroundSound/voicemailMessage→routeHintsyou pass into yourvoice()plugin / carrier route at mount time.monitorPlan,startSpeakingPlan,stopSpeakingPlan,voicemailDetection.provider,model.toolIds,firstMessageMode→ reported inunsupportedwith concrete migration instructions.
0.0.22-beta.470 · Vapi parity — named tool catalog
Five named-tool factories that mirror Vapi's built-in tools[].type entries 1:1. Drop them straight into createVoiceAssistant({ tools: [...] }) to get the same buyer ergonomics without writing the wiring yourself.
import {
createVoiceAssistant,
createVoiceApiRequestTool,
createVoiceDTMFTool,
createVoiceEndCallTool,
createVoiceTransferCallTool,
createVoiceVoicemailDetectionTool,
} from "@absolutejs/voice";
const tools = [
createVoiceEndCallTool({ farewell: "Thanks for calling, goodbye." }),
createVoiceTransferCallTool({
destinations: [
{ id: "billing", target: "+15551234567", message: "Connecting you to billing." },
{ id: "supervisor", target: "sip:supervisor@pbx", metadata: { queue: "vip" } },
],
}),
createVoiceDTMFTool({
send: async ({ args, api }) => api.transfer({ target: "ivr", metadata: { digits: args.digits } }),
}),
createVoiceVoicemailDetectionTool({ completeAfterMarking: true }),
createVoiceApiRequestTool({
description: "Look up a customer record",
name: "lookupCustomer",
method: "GET",
url: "https://api.example.com/customers",
buildQuery: ({ args }) => ({ email: String(args.email) }),
parameters: { type: "object", properties: { email: { type: "string" } }, required: ["email"] },
}),
];Per-tool highlights:
createVoiceEndCallTool— callsapi.complete(result)with an optional resolved result. JSON-schema{ reason?: string }. Customfarewell(static or({args, context, session}) => string).createVoiceTransferCallTool— multi-destination router, exposes anenumof destination ids to the LLM; callsapi.transfer({ target, metadata, reason, result })with the matching destination. Pairs withcreateVoiceTwilioRedirectHandoffAdapterfor live carrier transfer.createVoiceDTMFTool— caller provides asendhook (typically forwarding to your telephony adapter); validates digits againstallowedDigitsandmaxDigits. Default allowed set0123456789*#.createVoiceVoicemailDetectionTool— LLM-callable companion to ML-based voicemail detection; callsapi.markVoicemail({ metadata })and (by default)api.complete(). Use when the model hears a beep / "leave a message" prompt.createVoiceApiRequestTool— generic HTTP-call tool withbuildBody/buildQuery/buildHeaders/parseResponsehooks. Supports DI offetchfor tests.
0.0.22-beta.469 · Vapi parity — RAG-as-a-tool helper
createVoiceRAGTool(collection, options?) wraps any @absolutejs/rag RAGCollection (or any object that satisfies VoiceRAGCollectionLike) into a VoiceAgentTool shaped like Vapi's built-in query / knowledgeBaseId flow.
import { createInMemoryRAGStore, createRAGCollection } from "@absolutejs/rag";
import { createVoiceAssistant, createVoiceRAGTool } from "@absolutejs/voice";
const collection = createRAGCollection(
createInMemoryRAGStore(),
{ embedding: openaiEmbeddings("text-embedding-3-small") },
);
const knowledgeBase = createVoiceRAGTool(collection, {
allowedFilterKeys: ["productLine"],
fixedFilter: ({ context }) => ({ tenantId: context.tenantId }),
maxChunkChars: 320,
scoreThreshold: 0.65,
});
const assistant = createVoiceAssistant({
id: "support",
model: openaiVoiceAssistantModel,
tools: [knowledgeBase],
system: "You are a support agent. Always cite the knowledge base.",
});Highlights:
- Default tool name
searchKnowledgeBasematches the conventional Vapi knowledge-base tool name. - Generated JSON-schema parameters expose
query(required),topK(clamped tomaxTopK), and an optionalfilterobject whose keys are gated byallowedFilterKeys. fixedFiltercan be a static record or a(context) => filterfunction (use for tenant scoping, locale, etc.). Fixed values always win over LLM-supplied filters.- Default formatter returns numbered citations (
1. <title> (score 0.910): <chunk>); override viaformatResultorresultToMessage. - No hard dependency on
@absolutejs/rag—VoiceRAGCollectionLikeis structurally compatible withRAGCollection, so install rag (or any duck-typed retrieval backend) as a peer.
0.0.22-beta.464 → 468 · Media pipeline issue codes across product surfaces
@absolutejs/voice now projects compact @absolutejs/media reports into every buyer-facing voice surface so media health is gateable, linkable, and visible without buying a hosted dashboard.
Proof pack (beta.464–465) — summarizeVoiceMediaPipelineReport(report, { artifacts }) returns a compact VoiceMediaPipelineProofSummary with aggregated issueCodes and per-section status. writeVoiceMediaPipelineArtifacts({ dir, report, hrefBase }) persists media-quality.{json,md}, media-transport.{json,md}, and media-processor-graph.{json,md} into a proof-pack run directory and returns href links to wire into the summary. Together these shrink the mediaPipelineCalibrationAssertion summary in voice proof packs from ~35 KB to ~1.7 KB (95% reduction) and trim the total proof JSON from ~170 KB to ~114 KB.
Production readiness (beta.464) — buildVoiceMediaPipelineReadinessChecks(report, { baseHref, label }) returns drop-in VoiceProductionReadinessCheck[] entries (overall, media quality, transport, processor graph, interruption). Spread them into additionalChecks for granular gating alongside the existing aggregate "Media pipeline quality" check.
Operations record (beta.466–467) — VoiceOperationsRecord.mediaPipeline is a new optional VoiceOperationsRecordMediaPipelineSummary (status, qualityStatus, transportStatus, processorGraphStatus, issueCodes, jitterMs, frames, surface). Populate it by passing mediaPipeline: VoiceMediaPipelineReport to buildVoiceOperationsRecord, or pass mediaPipeline (a report or sessionId-keyed resolver) to createVoiceOperationsRecordRoutes so every /api/voice-operations/:sessionId response carries it automatically.
Failure replay (beta.466) — buildVoiceFailureReplay reads record.mediaPipeline and adds media.pipelineIssueCodes + media.pipelineStatus to the report, appends "Media pipeline issue: <code>" entries to summary.issues, and demotes failure-replay status to failed/degraded when the pipeline is fail/warn. The Markdown incident artifact gets a new "Media Pipeline" section.
Incident timeline (beta.466) — VoiceIncidentTimelineOptions.extraEvents accepts any custom event source. Feed buildVoiceMediaPipelineIncidentEvents(report, { now, source, category }) into it to surface media-pipeline issues as category: "monitor" entries in /api/voice/incident-timeline alongside operational and failure-replay events.
Ops record renderers (beta.468) — renderVoiceOperationsRecordIncidentMarkdown adds a one-line Media pipeline summary and an issue-code section. renderVoiceOperationsRecordHTML adds a nav anchor, a summary-grid card, and a #media-pipeline section listing surface, per-section status, frame/jitter, and codes.
Requires @absolutejs/[email protected]+. The voice bundle externalizes @absolutejs/media to keep browser bundles parseable; install media as a peer dependency.
Why AbsoluteJS Voice
- Self-hosted by default: your app owns sessions, traces, reviews, tasks, handoffs, retention, and provider keys.
- Provider-neutral: use Deepgram, AssemblyAI, OpenAI, Anthropic, Gemini, ElevenLabs-style TTS, or your own adapters without rewriting app workflow code.
- Browser and phone surfaces: mount browser WebSocket voice routes plus Twilio, Telnyx, and Plivo telephony routes from the same package.
- 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.
- Framework parity: React, Vue, Svelte, Angular, HTML, HTMX, and plain client entrypoints share the same core behavior.
- No hosted platform tax: AbsoluteJS Voice does not add a mandatory per-minute orchestration fee between your app and your providers.
Start Here
Pick the path that matches what you are building:
- 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. - Phone voice agent: mount Twilio, Telnyx, or Plivo routes, normalize carrier outcomes, inspect carrier readiness, and persist call lifecycle traces.
- 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.
- Production readiness: mount the status and proof primitives you need, such as
createVoiceOpsStatusRoutes(...),createVoiceProductionReadinessRoutes(...), quality routes, trace routes, eval routes, and smoke contracts. - 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. - Provider routing and fallback: use LLM/STT/TTS provider routers, provider health, provider simulation controls, and cost/latency-aware routing policies.
- Evals and simulation: mount
createVoiceSimulationSuiteRoutes(...)to run scenario fixtures, workflow contracts, tool contracts, outcome contracts, baseline comparisons, and saved benchmark artifacts before live traffic.
Buyer Paths
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.
| If you need | Start with | Add proof with | UI entrypoints |
| ----------------------------- | ------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------- |
| 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 |
| 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 |
| Multi-specialist support flow | createVoiceAgentSquad(...) | squad contract, handoff traces, context traces, operations record | Agent Squad status hooks/composables/services/widgets |
| Business actions and tools | createVoiceAgentTool(...) plus agent tool runtime | tool contracts, audit events, integration events, outcome contracts | operations record, action center, contract routes |
| 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 |
| 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 |
| 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 |
| Outbound campaigns | createVoiceCampaignRoutes(...) | recipient validation, consent/dedupe, carrier dry-run, campaign readiness | campaign routes and operations-record-linked attempt proof |
| Simulation before launch | createVoiceSimulationSuiteRoutes(...) | scenarios, evals, tool contracts, outcome contracts, baselines | simulation-suite HTML/JSON and linked operations records |
| 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 |
Capability Matrix
| Surface | Core package | Example/demo role | Not our lane | | ---------------------------- | ----------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ | ------------------------------------------------------ | | 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 | | 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 | | Agents and tools | assistant, agent, tools, squads, handoff/context policies, contracts, audit hooks | Demonstrate realistic support/sales/workflow flows | Dashboard-only visual bot builder | | 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 | | 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 | | Evals and simulation | fixtures, eval routes, simulation suite, workflow/tool/outcome contracts, baselines | Prove flows before live traffic | Opaque hosted test runner | | 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 |
Proof Pack
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.
| Buyer question | Proof artifact | What it proves |
| ---------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------- |
| 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 |
| 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 |
| 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 |
| Can I test before production traffic? | /voice/simulations, tool contracts, outcome contracts, workflow contracts | Scenario/eval proof, tool idempotency/retry proof, business outcome proof |
| 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 |
| Can operators intervene safely? | live-ops routes, action center, ops action audit routes, operations record | Pause/resume/takeover, injected instructions, operator action audit trail |
| 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 |
| 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 |
| 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 |
| 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 |
For a demo, the fastest convincing path is:
- Start a browser or phone session.
- Open
/voice-operations/:sessionIdfor the session. - Open
/production-readinessand follow any linked proof surface. - Open
/voice/simulationsor the relevant tool/outcome contract route. - Open
/data-controlto show where retention, redaction, and audit export live.
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.
Post-Call Analysis Proof
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.
import { createVoicePostCallAnalysisRoutes } from "@absolutejs/voice";
app.use(
createVoicePostCallAnalysisRoutes({
path: "/api/voice/post-call-analysis",
operationRecordBasePath: "/voice-operations/:sessionId",
reviews: runtime.reviews,
tasks: runtime.tasks,
integrationEvents: runtime.events,
source: ({ reviewId, sessionId }) => ({
reviewId,
sessionId,
// Use your own extractor output here, for example fields persisted from an LLM/tool result.
extractedFields: loadExtractedPostCallFields(reviewId ?? sessionId),
}),
fields: [
{ path: "review.postCall.target", label: "customer target" },
{ path: "customerId" },
{ path: "category" },
],
requiredTaskKinds: ["support-triage"],
requireDeliveredIntegrationEvent: true,
}),
);Guardrails
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.
import {
createVoiceGuardrailRuntime,
createVoiceGuardrailRoutes,
voiceGuardrailPolicyPresets,
} from "@absolutejs/voice";
const guardrails = createVoiceGuardrailRuntime({
blockResult: ({ decision }) => ({
assistantText: "I need to route this to a human specialist.",
escalate: {
reason: `guardrail-blocked-${decision.stage}`,
},
}),
policies: [voiceGuardrailPolicyPresets.supportSafeDefaults],
trace: runtime.traces,
});
const assistant = createVoiceAssistant({
guardrails: guardrails.assistantGuardrails,
id: "support",
model,
tools: guardrails.wrapTools([lookupCustomerTool, createTicketTool]),
});
app.use(
createVoiceGuardrailRoutes({
path: "/api/voice/guardrails",
policies: [voiceGuardrailPolicyPresets.supportSafeDefaults],
trace: runtime.traces,
}),
);Use-Case Recipe: Support Triage
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.
The production shape is:
- Persist sessions, traces, reviews, tasks, integration events, and audit events in app-owned runtime storage.
- Define server-side tools with idempotency and contract proof.
- Compose support and billing specialists with
createVoiceAgentSquad(...). - Mount a browser route with
voice(...)and optionally a phone route withcreateVoicePhoneAgent(...). - Add outcome, readiness, simulation, audit, and operations-record routes so every failed proof links back to the call.
import { Elysia } from "elysia";
import {
createVoiceAgent,
createVoiceAgentSquad,
createVoiceAgentTool,
createVoiceFileRuntimeStorage,
createVoiceOperationsRecordRoutes,
createVoiceProductionReadinessRoutes,
createVoiceSimulationSuiteRoutes,
createVoiceToolContractRoutes,
createVoiceToolRuntimeContractDefaults,
resolveVoiceOutcomeRecipe,
voice,
} from "@absolutejs/voice";
import { deepgram } from "@absolutejs/voice-deepgram";
const runtime = createVoiceFileRuntimeStorage({
directory: ".voice-runtime/support-triage",
});
const lookupCustomer = createVoiceAgentTool({
name: "lookup_customer",
description: "Look up a customer and their open support state.",
parameters: {
type: "object",
properties: {
customerId: { type: "string" },
},
required: ["customerId"],
},
execute: async ({ args }) => ({
customerId: args.customerId,
plan: "business",
openTickets: 1,
status: "active",
}),
});
const support = createVoiceAgent({
id: "support",
system:
"Triage the caller, use tools for account context, and hand billing questions to billing.",
tools: [lookupCustomer],
trace: runtime.traces,
model: {
async generate({ messages, tools }) {
const latest = messages.at(-1)?.content.toLowerCase() ?? "";
if (latest.includes("billing")) {
return {
assistantText: "I am routing you to billing with the context so far.",
handoff: { reason: "billing-request", targetAgentId: "billing" },
};
}
return {
assistantText: `I can help with that. I can also use ${tools.map((tool) => tool.name).join(", ")} when I need account context.`,
};
},
},
});
const billing = createVoiceAgent({
id: "billing",
system: "Handle billing questions and escalate refund or cancellation risk.",
trace: runtime.traces,
model: {
async generate() {
return {
assistantText:
"I can help with billing. I have the handoff context from support.",
};
},
},
});
const supportDesk = createVoiceAgentSquad({
id: "support-desk",
defaultAgentId: "support",
agents: [support, billing],
trace: runtime.traces,
handoffPolicy: ({ handoff }) =>
handoff.targetAgentId === "billing"
? {
summary:
"Billing specialist receives the support summary and current caller intent.",
metadata: { queue: "billing" },
}
: {
allow: false,
reason: "Only billing handoffs are approved in this recipe.",
escalate: { reason: "unsupported-specialist" },
},
});
const toolContractDefinitions = [
{
id: "lookup-customer-contract",
label: "Lookup customer returns support state",
tool: lookupCustomer,
cases: [
{
id: "active-business-customer",
args: { customerId: "cus_123" },
expect: {
expectedResult: {
customerId: "cus_123",
openTickets: 1,
plan: "business",
status: "active",
},
expectStatus: "ok",
},
},
],
defaultRuntime: createVoiceToolRuntimeContractDefaults(),
},
];
const app = new Elysia()
.use(
voice({
path: "/voice/support",
preset: "reliability",
session: runtime.session,
stt: deepgram({
apiKey: process.env.DEEPGRAM_API_KEY!,
model: "flux-general-en",
}),
trace: runtime.traces,
onTurn: supportDesk.onTurn,
onComplete: async () => {},
ops: {
...resolveVoiceOutcomeRecipe("support-triage", {
assignee: "support-oncall",
queue: "support-triage",
}),
events: runtime.events,
reviews: runtime.reviews,
tasks: runtime.tasks,
},
}),
)
.use(
createVoiceToolContractRoutes({
contracts: toolContractDefinitions,
htmlPath: "/voice/support/tool-contracts",
path: "/api/voice/support/tool-contracts",
}),
)
.use(
createVoiceSimulationSuiteRoutes({
htmlPath: "/voice/support/simulations",
path: "/api/voice/support/simulations",
tools: toolContractDefinitions,
operationsRecordHref: "/voice-operations/:sessionId",
}),
)
.use(
createVoiceOperationsRecordRoutes({
htmlPath: "/voice-operations/:sessionId",
path: "/api/voice-operations/:sessionId",
store: runtime.traces,
reviews: runtime.reviews,
tasks: runtime.tasks,
integrationEvents: runtime.events,
}),
)
.use(
createVoiceProductionReadinessRoutes({
htmlPath: "/production-readiness",
path: "/api/production-readiness",
store: runtime.traces,
links: {
operationsRecords: "/voice-operations/:sessionId",
simulations: "/voice/support/simulations",
},
}),
);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.
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.
Use-Case Recipe: Appointment Scheduling
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.
The production shape is:
- Persist sessions, traces, reviews, tasks, integration events, and audit events in app-owned runtime storage.
- Define
check_availabilityandbook_appointmentas server-side tools with deterministic tool contracts. - Use
resolveVoiceOutcomeRecipe('appointment-booking')so completed calls create appointment-confirmation work. - Add an outcome contract that requires a completed session, review, task, and integration events.
- Mount simulation, outcome-contract, readiness, and operations-record routes so scheduling regressions fail before live calls.
import { Elysia } from "elysia";
import {
createVoiceAgent,
createVoiceAgentTool,
createVoiceFileRuntimeStorage,
createVoiceOperationsRecordRoutes,
createVoiceOutcomeContractRoutes,
createVoiceProductionReadinessRoutes,
createVoiceSimulationSuiteRoutes,
createVoiceToolContractRoutes,
createVoiceToolRuntimeContractDefaults,
resolveVoiceOutcomeRecipe,
voice,
} from "@absolutejs/voice";
import { deepgram } from "@absolutejs/voice-deepgram";
const runtime = createVoiceFileRuntimeStorage({
directory: ".voice-runtime/appointments",
});
const checkAvailability = createVoiceAgentTool({
name: "check_availability",
description: "Return open appointment slots for a service and date.",
parameters: {
type: "object",
properties: {
date: { type: "string" },
service: { type: "string" },
},
required: ["date", "service"],
},
execute: async ({ args }) => ({
date: args.date,
service: args.service,
slots: ["2026-05-04T15:00:00-04:00", "2026-05-04T16:30:00-04:00"],
}),
});
const bookAppointment = createVoiceAgentTool({
name: "book_appointment",
description: "Book a confirmed appointment slot.",
parameters: {
type: "object",
properties: {
customerName: { type: "string" },
phone: { type: "string" },
service: { type: "string" },
startsAt: { type: "string" },
},
required: ["customerName", "phone", "service", "startsAt"],
},
execute: async ({ args }) => ({
appointmentId: `appt_${args.startsAt}`,
customerName: args.customerName,
phone: args.phone,
service: args.service,
startsAt: args.startsAt,
status: "confirmed",
}),
});
const scheduler = createVoiceAgent({
id: "scheduler",
system:
"Collect caller details, check availability, book an appointment, and summarize the confirmation.",
tools: [checkAvailability, bookAppointment],
trace: runtime.traces,
model: {
async generate({ tools }) {
return {
assistantText: `I can check times and book the appointment. Available tools: ${tools.map((tool) => tool.name).join(", ")}`,
};
},
},
});
const toolContractDefinitions = [
{
id: "check-availability-contract",
label: "Availability returns bookable slots",
tool: checkAvailability,
cases: [
{
id: "consultation-slots",
args: { date: "2026-05-04", service: "consultation" },
expect: { expectStatus: "ok" },
},
],
defaultRuntime: createVoiceToolRuntimeContractDefaults(),
},
{
id: "book-appointment-contract",
label: "Booking returns a confirmed appointment",
tool: bookAppointment,
cases: [
{
id: "confirmed-consultation",
args: {
customerName: "Ada Lovelace",
phone: "+15551234567",
service: "consultation",
startsAt: "2026-05-04T15:00:00-04:00",
},
expect: {
expectedResult: {
appointmentId: "appt_2026-05-04T15:00:00-04:00",
customerName: "Ada Lovelace",
phone: "+15551234567",
service: "consultation",
startsAt: "2026-05-04T15:00:00-04:00",
status: "confirmed",
},
expectStatus: "ok",
},
},
],
defaultRuntime: createVoiceToolRuntimeContractDefaults(),
},
];
const outcomeContractDefinitions = [
{
id: "appointment-booked",
label: "Completed appointment call produces follow-up work",
expectedDisposition: "completed",
minSessions: 1,
minTasks: 1,
requireIntegrationEvents: [
"call.completed",
"review.saved",
"task.created",
],
requireReview: true,
requireTask: true,
},
];
const app = new Elysia()
.use(
voice({
path: "/voice/appointments",
preset: "reliability",
session: runtime.session,
stt: deepgram({
apiKey: process.env.DEEPGRAM_API_KEY!,
model: "flux-general-en",
}),
trace: runtime.traces,
onTurn: scheduler.onTurn,
onComplete: async () => {},
ops: {
...resolveVoiceOutcomeRecipe("appointment-booking", {
assignee: "scheduling-oncall",
queue: "appointments",
}),
events: runtime.events,
reviews: runtime.reviews,
tasks: runtime.tasks,
},
}),
)
.use(
createVoiceToolContractRoutes({
contracts: toolContractDefinitions,
htmlPath: "/voice/appointments/tool-contracts",
path: "/api/voice/appointments/tool-contracts",
}),
)
.use(
createVoiceOutcomeContractRoutes({
contracts: outcomeContractDefinitions,
events: runtime.events,
htmlPath: "/voice/appointments/outcome-contracts",
operationsRecordHref: "/voice-operations/:sessionId",
path: "/api/voice/appointments/outcome-contracts",
reviews: runtime.reviews,
sessions: runtime.session,
tasks: runtime.tasks,
}),
)
.use(
createVoiceSimulationSuiteRoutes({
htmlPath: "/voice/appointments/simulations",
operationsRecordHref: "/voice-operations/:sessionId",
outcomes: {
contracts: outcomeContractDefinitions,
events: runtime.events,
reviews: runtime.reviews,
sessions: runtime.session,
tasks: runtime.tasks,
},
path: "/api/voice/appointments/simulations",
tools: toolContractDefinitions,
}),
)
.use(
createVoiceOperationsRecordRoutes({
htmlPath: "/voice-operations/:sessionId",
integrationEvents: runtime.events,
path: "/api/voice-operations/:sessionId",
reviews: runtime.reviews,
store: runtime.traces,
tasks: runtime.tasks,
}),
)
.use(
createVoiceProductionReadinessRoutes({
htmlPath: "/production-readiness",
links: {
operationsRecords: "/voice-operations/:sessionId",
simulations: "/voice/appointments/simulations",
},
path: "/api/production-readiness",
store: runtime.traces,
}),
);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.
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.
Use-Case Recipe: Campaign Outreach
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.
The production shape is:
- Store campaigns in app-owned storage.
- Import recipients with consent checks, phone validation, dedupe, variables, and rejected-row evidence.
- Configure campaign policy for max attempts, concurrency, attempt windows, quiet hours, rate limits, and retry backoff.
- Use a carrier dialer or your own dialer function, then apply Twilio/Telnyx/Plivo webhook outcomes back to attempts.
- Expose campaign routes, observability, readiness proof, production readiness, and operations-record links for every attempted call.
import { Elysia } from "elysia";
import {
createVoiceCampaignRoutes,
createVoiceFileRuntimeStorage,
createVoiceOperationsRecordRoutes,
createVoiceProductionReadinessRoutes,
createVoiceReadinessProfile,
createVoiceSQLiteCampaignStore,
runVoiceCampaignReadinessProof,
} from "@absolutejs/voice";
const runtime = createVoiceFileRuntimeStorage({
directory: ".voice-runtime/campaign-outreach",
});
const campaigns = createVoiceSQLiteCampaignStore({
path: ".voice-runtime/campaigns.sqlite",
});
const app = new Elysia()
.use(
createVoiceCampaignRoutes({
htmlPath: "/voice/campaigns",
operationsRecordHref: "/voice-operations/:sessionId",
path: "/api/voice/campaigns",
store: campaigns,
title: "Renewal Outreach",
}),
)
.use(
createVoiceOperationsRecordRoutes({
htmlPath: "/voice-operations/:sessionId",
path: "/api/voice-operations/:sessionId",
store: runtime.traces,
}),
)
.use(
createVoiceProductionReadinessRoutes({
...createVoiceReadinessProfile("phone-agent", {
campaignReadiness: () =>
runVoiceCampaignReadinessProof({
store: campaigns,
}),
explain: true,
}),
links: {
campaigns: "/voice/campaigns",
operationsRecords: "/voice-operations/:sessionId",
},
store: runtime.traces,
}),
);
await fetch("/api/voice/campaigns", {
body: JSON.stringify({
maxAttempts: 3,
maxConcurrentAttempts: 10,
name: "Renewal outreach",
schedule: {
attemptWindow: { startHour: 9, endHour: 17 },
quietHours: { startHour: 12, endHour: 13 },
rateLimit: { maxAttempts: 60, windowMs: 60_000 },
retryPolicy: { backoffMs: [5 * 60_000, 30 * 60_000] },
},
}),
headers: { "content-type": "application/json" },
method: "POST",
});
await fetch("/api/voice/campaigns/campaign-1/recipients/import", {
body: JSON.stringify({
csv: `id,name,phone,consent,segment
recipient-1,Ada,+15550001001,yes,trial
recipient-2,Grace,+15550001002,true,enterprise
recipient-3,Linus,not-a-phone,yes,partner
recipient-4,Barbara,+15550001004,no,trial`,
metadataColumns: ["segment"],
requireConsent: true,
variableColumns: ["segment"],
}),
headers: { "content-type": "application/json" },
method: "POST",
});
await fetch("/api/voice/campaigns/campaign-1/enqueue", {
method: "POST",
});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.
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.
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.
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.
Use-Case Recipe: Meeting Recorder
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.
The production shape is:
- Mount a browser voice route with persistent session, trace, review, task, and audit-capable storage.
- Use framework stream/controller helpers for the microphone, transcript, reconnect state, and recording status.
- Persist a review artifact on completion with transcript, summary, latency, outcome, and recommended follow-up.
- Mount trace timelines, operations records, production readiness, and data-control routes.
- Gate release with the
meeting-recorderreadiness profile so reconnect, barge-in/interruption, provider routing, latency, and session-health proof stay visible.
import { Elysia } from "elysia";
import {
createVoiceDataControlRoutes,
createVoiceFileRuntimeStorage,
createVoiceOperationsRecordRoutes,
createVoiceProductionReadinessRoutes,
createVoiceReadinessProfile,
createVoiceTraceTimelineRoutes,
voice,
voiceComplianceRedactionDefaults,
} from "@absolutejs/voice";
import { deepgram } from "@absolutejs/voice-deepgram";
const runtime = createVoiceFileRuntimeStorage({
directory: ".voice-runtime/meeting-recorder",
});
const app = new Elysia()
.use(
voice({
path: "/voice/meeting-recorder",
preset: "reliability",
session: runtime.session,
stt: deepgram({
apiKey: process.env.DEEPGRAM_API_KEY!,
model: "flux-general-en",
}),
trace: runtime.traces,
async onTurn({ turn }) {
return {
assistantText: "",
metadata: {
recorder: true,
transcript: turn.text,
},
};
},
onComplete: async () => {},
ops: {
events: runtime.events,
reviews: runtime.reviews,
tasks: runtime.tasks,
buildReview: ({ session }) => ({
errors: [],
latencyBreakdown: [],
notes: ["Generated by the self-hosted meeting recorder path."],
postCall: {
label: "Meeting summary",
recommendedAction: "Review the transcript and share action items.",
summary: "Review transcript, decisions, and follow-up owners.",
},
summary: {
outcome: "completed",
pass: true,
turnCount: session.turns.length,
},
title: `Meeting recorder review for ${session.id}`,
timeline: [],
transcript: {
actual: session.turns
.map((turn) => turn.text)
.filter(Boolean)
.join("\n"),
},
}),
},
}),
)
.use(
createVoiceTraceTimelineRoutes({
htmlPath: "/traces",
path: "/api/voice-traces",
store: runtime.traces,
}),
)
.use(
createVoiceOperationsRecordRoutes({
htmlPath: "/voice-operations/:sessionId",
integrationEvents: runtime.events,
path: "/api/voice-operations/:sessionId",
reviews: runtime.reviews,
store: runtime.traces,
tasks: runtime.tasks,
}),
)
.use(
createVoiceDataControlRoutes({
...runtime,
audit: runtime.audit,
auditDeliveries: runtime.auditDeliveries,
redact: voiceComplianceRedactionDefaults,
traceDeliveries: runtime.traceDeliveries,
}),
)
.use(
createVoiceProductionReadinessRoutes({
...createVoiceReadinessProfile("meeting-recorder", {
explain: true,
}),
links: {
dataControl: "/data-control",
operationsRecords: "/voice-operations/:sessionId",
traces: "/traces",
},
store: runtime.traces,
}),
);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.
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.
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.
Use-Case Recipe: Compliance-Sensitive Calls
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.
The production shape is:
- Use customer-owned runtime storage, preferably Postgres for production records and S3/webhook delivery for exported evidence.
- Keep provider keys in the app owner environment, not in a hosted voice dashboard.
- Pass
auditinto agents/tools/squads so provider calls, tool executions, handoffs, retention runs, and operator actions are recorded. - Mount data-control routes for redacted audit export, retention dry-runs, guarded deletion, zero-retention planning, and provider-key recommendations.
- Make audit evidence, recent retention-policy evidence, and audit/trace delivery health part of production readiness.
import { Elysia } from "elysia";
import {
applyVoiceDataRetentionPolicy,
buildVoiceDataRetentionPlan,
createVoiceAuditLogger,
createVoiceDataControlRoutes,
createVoiceOperationsRecordRoutes,
createVoicePostgresRuntimeStorage,
createVoiceProductionReadinessRoutes,
createVoiceZeroRetentionPolicy,
exportVoiceAuditTrail,
renderVoiceAuditMarkdown,
voiceComplianceRedactionDefaults,
} from "@absolutejs/voice";
const runtime = createVoicePostgresRuntimeStorage({
connectionString: process.env.DATABASE_URL!,
schemaName: "voice_ops",
tablePrefix: "sensitive",
});
const audit = createVoiceAuditLogger(runtime.audit);
const app = new Elysia()
.use(
createVoiceDataControlRoutes({
...runtime,
audit: runtime.audit,
auditDeliveries: runtime.auditDeliveries,
path: "/data-control",
redact: voiceComplianceRedactionDefaults,
title: "Sensitive Voice Data Control",
traceDeliveries: runtime.traceDeliveries,
}),
)
.use(
createVoiceOperationsRecordRoutes({
audit: runtime.audit,
htmlPath: "/voice-operations/:sessionId",
integrationEvents: runtime.events,
path: "/api/voice-operations/:sessionId",
reviews: runtime.reviews,
store: runtime.traces,
tasks: runtime.tasks,
}),
)
.use(
createVoiceProductionReadinessRoutes({
audit: {
require: [
{ type: "provider.call" },
{ type: "operator.action" },
{ type: "retention.policy", maxAgeMs: 7 * 24 * 60 * 60 * 1000 },
],
store: runtime.audit,
},
auditDeliveries: runtime.auditDeliveries,
links: {
audit: "/audit",
auditDeliveries: "/audit/deliveries",
dataControl: "/data-control",
operationsRecords: "/voice-operations/:sessionId",
traceDeliveries: "/traces/deliveries",
},
store: runtime.traces,
traceDeliveries: runtime.traceDeliveries,
}),
);
await audit.operatorAction({
action: "retention.policy.reviewed",
actor: { id: "ops-admin", type: "operator" },
outcome: "ok",
resource: { id: "zero-retention-policy", type: "voice.retention" },
});
const zeroRetentionPolicy = createVoiceZeroRetentionPolicy({
...runtime,
audit: runtime.audit,
auditDeliveries: runtime.auditDeliveries,
traceDeliveries: runtime.traceDeliveries,
});
const retentionPlan = await buildVoiceDataRetentionPlan(zeroRetentionPolicy);
if (retentionPlan.deletedCount > 0) {
await applyVoiceDataRetentionPolicy({
...zeroRetentionPolicy,
dryRun: false,
});
}
const auditExport = await exportVoiceAuditTrail({
redact: voiceComplianceRedactionDefaults,
store: runtime.audit,
});
const redactedAuditMarkdown = renderVoiceAuditMarkdown(auditExport.events, {
title: "Sensitive Voice Audit Export",
});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.
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".
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.
How This Differs From Hosted Voice Platforms
Hosted voice-agent platforms are strongest when you want a managed dashboard, phone-number provisioning, hosted orchestration, and campaign tooling out of the box.
AbsoluteJS Voice is strongest when voice is part of your own product and you need code-owned primitives:
- Your app stores the call data instead of a vendor dashboard being the source of truth.
- Your app controls provider routing, fallback, retries, handoffs, and retention.
- Your team can inspect and extend every primitive.
- Your framework UI can render first-class voice state without iframe/dashboard handoffs.
- Your production checks and evals can run in CI, smoke tests, or your own admin UI.
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.
Default Debug Path
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.
Mount `createVoiceOpera
