@absolutejs/voice 0.0.22-beta.125 → 0.0.22-beta.127

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.
Files changed (44) hide show
  1. package/README.md +236 -26
  2. package/dist/agent.d.ts +5 -0
  3. package/dist/angular/index.d.ts +1 -1
  4. package/dist/angular/index.js +16 -16
  5. package/dist/angular/voice-ops-status.service.d.ts +12 -0
  6. package/dist/audit.d.ts +128 -0
  7. package/dist/auditDeliveryRoutes.d.ts +85 -0
  8. package/dist/auditExport.d.ts +34 -0
  9. package/dist/auditRoutes.d.ts +66 -0
  10. package/dist/auditSinks.d.ts +133 -0
  11. package/dist/client/index.d.ts +2 -2
  12. package/dist/client/index.js +11 -11
  13. package/dist/client/opsStatus.d.ts +19 -0
  14. package/dist/client/opsStatusWidget.d.ts +7 -7
  15. package/dist/dataControl.d.ts +47 -0
  16. package/dist/demoReadyRoutes.d.ts +98 -0
  17. package/dist/fileStore.d.ts +8 -2
  18. package/dist/index.d.ts +27 -6
  19. package/dist/index.js +14600 -12518
  20. package/dist/opsStatus.d.ts +65 -0
  21. package/dist/opsStatusRoutes.d.ts +33 -0
  22. package/dist/phoneAgent.d.ts +4 -0
  23. package/dist/phoneAgentProductionSmoke.d.ts +115 -0
  24. package/dist/postgresStore.d.ts +8 -2
  25. package/dist/productionReadiness.d.ts +94 -0
  26. package/dist/providerRoutingContract.d.ts +38 -0
  27. package/dist/react/index.d.ts +1 -1
  28. package/dist/react/index.js +16 -16
  29. package/dist/react/useVoiceOpsStatus.d.ts +8 -0
  30. package/dist/sqliteStore.d.ts +8 -2
  31. package/dist/svelte/createVoiceOpsStatus.d.ts +2 -2
  32. package/dist/svelte/index.d.ts +0 -1
  33. package/dist/svelte/index.js +72 -75
  34. package/dist/traceDeliveryRoutes.d.ts +86 -0
  35. package/dist/vue/index.d.ts +1 -1
  36. package/dist/vue/index.js +15 -15
  37. package/dist/vue/useVoiceOpsStatus.d.ts +9 -0
  38. package/package.json +1 -1
  39. package/dist/angular/voice-app-kit-status.service.d.ts +0 -12
  40. package/dist/appKit.d.ts +0 -100
  41. package/dist/client/appKitStatus.d.ts +0 -19
  42. package/dist/react/useVoiceAppKitStatus.d.ts +0 -8
  43. package/dist/svelte/createVoiceAppKitStatus.d.ts +0 -8
  44. package/dist/vue/useVoiceAppKitStatus.d.ts +0 -9
package/README.md CHANGED
@@ -11,7 +11,7 @@ Use it when you want Vapi/Retell/Bland-style voice-agent capability, but you wan
11
11
  - Self-hosted by default: your app owns sessions, traces, reviews, tasks, handoffs, retention, and provider keys.
12
12
  - Provider-neutral: use Deepgram, AssemblyAI, OpenAI, Anthropic, Gemini, ElevenLabs-style TTS, or your own adapters without rewriting app workflow code.
13
13
  - Browser and phone surfaces: mount browser WebSocket voice routes plus Twilio, Telnyx, and Plivo telephony routes from the same package.
14
- - Production proof: App Kit, production readiness, turn quality, turn latency, live browser p50/p95 latency, trace timelines, evals, fixtures, and contracts are package primitives.
14
+ - Production proof: ops status, production readiness, turn quality, turn latency, live browser p50/p95 latency, trace timelines, evals, fixtures, and contracts are package primitives.
15
15
  - Framework parity: React, Vue, Svelte, Angular, HTML, HTMX, and plain client entrypoints share the same core behavior.
16
16
  - No hosted platform tax: AbsoluteJS Voice does not add a mandatory per-minute orchestration fee between your app and your providers.
17
17
 
@@ -21,7 +21,7 @@ Pick the path that matches what you are building:
21
21
 
22
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
23
  - Phone voice agent: mount Twilio, Telnyx, or Plivo routes, normalize carrier outcomes, inspect carrier readiness, and persist call lifecycle traces.
24
- - Production readiness: mount `createVoiceAppKitRoutes(...)` to get ops console/status, quality, evals, provider health, sessions, handoffs, diagnostics, and readiness gates.
24
+ - Production readiness: mount the status and proof primitives you need, such as `createVoiceOpsStatusRoutes(...)`, `createVoiceProductionReadinessRoutes(...)`, quality routes, trace routes, eval routes, and smoke contracts.
25
25
  - Provider routing and fallback: use LLM/STT/TTS provider routers, provider health, provider simulation controls, and cost/latency-aware routing policies.
26
26
  - Evals and simulation: mount `createVoiceSimulationSuiteRoutes(...)` to run scenario fixtures, workflow contracts, tool contracts, outcome contracts, baseline comparisons, and saved benchmark artifacts before live traffic.
27
27
 
@@ -117,14 +117,17 @@ const app = new Elysia()
117
117
 
118
118
  ## Production Readiness Path
119
119
 
120
- Once the basic route works, mount the App Kit. This gives you the self-hosted operational surface that hosted platforms usually make mandatory:
120
+ 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:
121
121
 
122
122
  ```ts
123
123
  import {
124
- createVoiceAppKitRoutes,
124
+ createVoiceAuditDeliveryRoutes,
125
+ createVoiceDemoReadyRoutes,
125
126
  createVoiceFileRuntimeStorage,
126
127
  createVoiceLiveLatencyRoutes,
128
+ createVoiceOpsStatusRoutes,
127
129
  createVoiceProductionReadinessRoutes,
130
+ createVoiceTraceDeliveryRoutes,
128
131
  createVoiceTraceTimelineRoutes,
129
132
  createVoiceTurnLatencyRoutes,
130
133
  createVoiceTurnQualityRoutes
@@ -136,11 +139,11 @@ const runtime = createVoiceFileRuntimeStorage({
136
139
 
137
140
  app
138
141
  .use(
139
- createVoiceAppKitRoutes({
142
+ createVoiceOpsStatusRoutes({
140
143
  store: runtime.traces,
141
144
  llmProviders: ['openai', 'anthropic', 'gemini'],
142
145
  sttProviders: ['deepgram', 'assemblyai']
143
- }).routes
146
+ })
144
147
  )
145
148
  .use(
146
149
  createVoiceTurnLatencyRoutes({
@@ -173,18 +176,40 @@ app
173
176
  )
174
177
  .use(
175
178
  createVoiceProductionReadinessRoutes({
179
+ audit: runtime.audit,
180
+ auditDeliveries: runtime.auditDeliveries,
176
181
  htmlPath: '/production-readiness',
177
182
  path: '/api/production-readiness',
178
- store: runtime.traces
183
+ store: runtime.traces,
184
+ traceDeliveries: runtime.traceDeliveries
185
+ })
186
+ )
187
+ .use(
188
+ createVoiceAuditDeliveryRoutes({
189
+ htmlPath: '/audit/deliveries',
190
+ path: '/api/voice-audit-deliveries',
191
+ store: runtime.auditDeliveries
192
+ })
193
+ )
194
+ .use(
195
+ createVoiceTraceDeliveryRoutes({
196
+ htmlPath: '/traces/deliveries',
197
+ path: '/api/voice-trace-deliveries',
198
+ store: runtime.traceDeliveries
179
199
  })
180
200
  );
181
201
  ```
182
202
 
183
203
  Recommended proof routes:
184
204
 
185
- - `/app-kit/status`: compact customer-facing app status.
205
+ - `/api/voice/ops-status`: compact status for hooks, widgets, and customer-facing demos.
206
+ - `/api/voice/ops-status/html`: HTML status card for quick internal review.
207
+ - `/demo-ready`: customer-facing demo readiness checklist.
186
208
  - `/production-readiness`: production gate summary.
209
+ - `/audit/deliveries`: audit sink export queue and failed delivery details.
210
+ - `/voice/phone/smoke-contract`: trace-backed phone-agent production smoke proof.
187
211
  - `/traces`: per-session trace timelines.
212
+ - `/traces/deliveries`: trace sink export queue and failed delivery details.
188
213
  - `/turn-latency`: server-side turn-stage latency.
189
214
  - `/live-latency`: browser-measured speech-to-assistant p50/p95 latency.
190
215
  - `/turn-quality`: STT confidence, correction, fallback, and transcript diagnostics.
@@ -231,7 +256,8 @@ Use `createVoicePhoneAgent(...)` when the agent needs to answer or place calls t
231
256
  ```ts
232
257
  import {
233
258
  createVoicePhoneAgent,
234
- createVoiceTelephonyOutcomePolicy
259
+ createVoiceTelephonyOutcomePolicy,
260
+ runVoicePhoneAgentProductionSmokeContract
235
261
  } from '@absolutejs/voice';
236
262
  import { deepgram } from '@absolutejs/voice-deepgram';
237
263
 
@@ -250,6 +276,19 @@ app
250
276
  path: '/api/carriers',
251
277
  title: 'AbsoluteJS Voice Carrier Matrix'
252
278
  },
279
+ productionSmoke: {
280
+ maxAgeMs: 24 * 60 * 60 * 1000,
281
+ required: [
282
+ 'carrier-contract',
283
+ 'media-started',
284
+ 'transcript',
285
+ 'assistant-response',
286
+ 'lifecycle-outcome',
287
+ 'no-session-error',
288
+ 'fresh-trace'
289
+ ],
290
+ store: runtime.traces
291
+ },
253
292
  carriers: [
254
293
  {
255
294
  provider: 'twilio',
@@ -284,6 +323,8 @@ The wrapper mounts selected carrier routes plus two proof surfaces:
284
323
  - `/api/voice/phone/setup?format=html`: copy/paste setup page for carrier dashboards.
285
324
  - `/api/carriers`: carrier matrix JSON for Twilio, Telnyx, and Plivo.
286
325
  - `/api/carriers?format=html`: side-by-side carrier readiness matrix.
326
+ - `/api/voice/phone/smoke-contract?sessionId=...`: trace-backed production smoke contract.
327
+ - `/voice/phone/smoke-contract?sessionId=...`: HTML production smoke contract.
287
328
 
288
329
  The setup page tells you exactly what to copy into the carrier dashboard:
289
330
 
@@ -315,36 +356,89 @@ The phone-agent report normalizes the lifecycle schema across carriers:
315
356
 
316
357
  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.
317
358
 
318
- ## App Kit And Status Widgets
359
+ After running a real smoke call, certify the phone-agent path from traces:
319
360
 
320
- Use `createVoiceAppKitRoutes(...)` when you want a self-hosted operations surface without hand-wiring every dashboard route. It adds the ops console, quality gates, eval routes, provider health, session replay, handoff health, diagnostics, and `GET /app-kit/status`.
361
+ ```ts
362
+ const smoke = await runVoicePhoneAgentProductionSmokeContract({
363
+ maxAgeMs: 24 * 60 * 60 * 1000,
364
+ required: [
365
+ 'media-started',
366
+ 'transcript',
367
+ 'assistant-response',
368
+ 'lifecycle-outcome',
369
+ 'no-session-error',
370
+ 'fresh-trace'
371
+ ],
372
+ sessionId: 'phone-smoke-session',
373
+ store: runtime.traces
374
+ });
375
+
376
+ if (!smoke.pass) {
377
+ throw new Error(smoke.issues.map((issue) => issue.message).join('\n'));
378
+ }
379
+ ```
380
+
381
+ 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.
382
+
383
+ 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`.
384
+
385
+ ## Ops Status Hooks And Widgets
386
+
387
+ 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.
321
388
 
322
389
  ```ts
323
- import { createVoiceAppKitRoutes, createVoiceFileRuntimeStorage } from '@absolutejs/voice';
390
+ import {
391
+ createVoiceDemoReadyRoutes,
392
+ createVoiceFileRuntimeStorage,
393
+ createVoiceOpsStatusRoutes,
394
+ summarizeVoiceOpsStatus
395
+ } from '@absolutejs/voice';
324
396
 
325
397
  const runtime = createVoiceFileRuntimeStorage({ directory: '.voice-runtime/support' });
326
398
 
327
399
  app.use(
328
- createVoiceAppKitRoutes({
400
+ createVoiceOpsStatusRoutes({
329
401
  store: runtime.traces,
330
402
  llmProviders: ['openai', 'anthropic', 'gemini'],
331
403
  sttProviders: ['deepgram', 'assemblyai']
332
- }).routes
404
+ })
333
405
  );
334
406
  ```
335
407
 
336
- 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 ops pages.
408
+ 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.
409
+
410
+ 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:
337
411
 
338
412
  ```ts
339
413
  app.use(
340
- createVoiceAppKitRoutes({
341
- appStatus: {
342
- include: { quality: false, sessions: false },
343
- preferFixtureWorkflows: true
414
+ createVoiceDemoReadyRoutes({
415
+ opsStatus: {
416
+ href: '/api/voice/ops-status',
417
+ load: () => summarizeVoiceOpsStatus(opsStatusOptions)
418
+ },
419
+ phoneSetup: {
420
+ href: '/api/voice/phone/setup?format=html',
421
+ load: () => phoneAgentSetupReport
422
+ },
423
+ phoneSmoke: {
424
+ href: '/voice/phone/smoke-contract',
425
+ load: () => phoneSmokeReport
344
426
  },
345
- evals: { fixtures: certificationFixtures, scenarios: workflowScenarios },
427
+ productionReadiness: {
428
+ href: '/production-readiness',
429
+ load: () => productionReadinessReport
430
+ }
431
+ })
432
+ );
433
+ ```
434
+
435
+ ```ts
436
+ app.use(
437
+ createVoiceOpsStatusRoutes({
438
+ include: { quality: false, sessions: false },
439
+ preferFixtureWorkflows: true,
346
440
  store: runtime.traces
347
- }).routes
441
+ })
348
442
  );
349
443
  ```
350
444
 
@@ -377,7 +471,7 @@ import { VoiceOpsStatus } from '@absolutejs/voice/vue';
377
471
  import { onDestroy, onMount } from 'svelte';
378
472
  import { createVoiceOpsStatus } from '@absolutejs/voice/svelte';
379
473
 
380
- const status = createVoiceOpsStatus('/app-kit/status', { intervalMs: 5000 });
474
+ const status = createVoiceOpsStatus('/api/voice/ops-status', { intervalMs: 5000 });
381
475
  let html = '';
382
476
  onMount(() => status.subscribe(() => (html = status.getHTML())));
383
477
  onDestroy(() => status.close());
@@ -389,9 +483,9 @@ import { VoiceOpsStatus } from '@absolutejs/voice/vue';
389
483
  ### Angular Status Widget
390
484
 
391
485
  ```ts
392
- import { VoiceAppKitStatusService } from '@absolutejs/voice/angular';
486
+ import { VoiceOpsStatusService } from '@absolutejs/voice/angular';
393
487
 
394
- status = inject(VoiceAppKitStatusService).connect('/app-kit/status', {
488
+ status = inject(VoiceOpsStatusService).connect('/api/voice/ops-status', {
395
489
  intervalMs: 5000
396
490
  });
397
491
  ```
@@ -408,7 +502,7 @@ status = inject(VoiceAppKitStatusService).connect('/app-kit/status', {
408
502
  <script type="module">
409
503
  import { mountVoiceOpsStatus } from '@absolutejs/voice/client';
410
504
 
411
- mountVoiceOpsStatus(document.querySelector('#voice-ops-status'), '/app-kit/status', {
505
+ mountVoiceOpsStatus(document.querySelector('#voice-ops-status'), '/api/voice/ops-status', {
412
506
  intervalMs: 5000
413
507
  });
414
508
  </script>
@@ -648,13 +742,22 @@ Use trace stores when you want every call to be inspectable outside a hosted pla
648
742
  ```ts
649
743
  import {
650
744
  buildVoiceTraceReplay,
745
+ buildVoiceAuditExport,
746
+ createVoiceAuditHTTPSink,
747
+ createVoiceAuditLogger,
748
+ createVoiceAuditSinkDeliveryWorker,
749
+ createVoiceAuditSinkStore,
750
+ createVoiceAuditTrailRoutes,
651
751
  createVoiceAgent,
652
752
  createVoiceFileRuntimeStorage,
653
753
  createVoiceRedisTaskLeaseCoordinator,
754
+ createVoiceTraceDeliveryRoutes,
654
755
  createVoiceTraceHTTPSink,
655
756
  createVoiceTraceSinkStore,
656
757
  createVoiceTraceSinkDeliveryWorker,
758
+ buildVoiceDataRetentionPlan,
657
759
  exportVoiceTrace,
760
+ applyVoiceDataRetentionPolicy,
658
761
  pruneVoiceTraceEvents,
659
762
  voice
660
763
  } from '@absolutejs/voice';
@@ -666,6 +769,30 @@ const runtimeStorage = createVoiceFileRuntimeStorage({
666
769
  const redisLeases = createVoiceRedisTaskLeaseCoordinator({
667
770
  url: process.env.REDIS_URL
668
771
  });
772
+ const auditStore = createVoiceAuditSinkStore({
773
+ store: runtimeStorage.audit,
774
+ deliveryQueue: runtimeStorage.auditDeliveries,
775
+ sinks: [
776
+ createVoiceAuditHTTPSink({
777
+ id: 'security-warehouse',
778
+ signingSecret: process.env.VOICE_AUDIT_SINK_SECRET,
779
+ url: process.env.VOICE_AUDIT_SINK_URL!
780
+ })
781
+ ]
782
+ });
783
+ const audit = createVoiceAuditLogger(auditStore);
784
+ const auditSinkWorker = createVoiceAuditSinkDeliveryWorker({
785
+ deliveries: runtimeStorage.auditDeliveries,
786
+ leases: redisLeases,
787
+ sinks: [
788
+ createVoiceAuditHTTPSink({
789
+ id: 'security-warehouse',
790
+ signingSecret: process.env.VOICE_AUDIT_SINK_SECRET,
791
+ url: process.env.VOICE_AUDIT_SINK_URL!
792
+ })
793
+ ],
794
+ workerId: 'audit-sink-worker'
795
+ });
669
796
  const trace = createVoiceTraceSinkStore({
670
797
  store: runtimeStorage.traces,
671
798
  deliveryQueue: runtimeStorage.traceDeliveries,
@@ -692,6 +819,9 @@ const traceSinkWorker = createVoiceTraceSinkDeliveryWorker({
692
819
 
693
820
  const supportAgent = createVoiceAgent({
694
821
  id: 'support',
822
+ audit,
823
+ auditProvider: 'openai',
824
+ auditModel: 'gpt-4.1',
695
825
  trace,
696
826
  model: {
697
827
  async generate() {
@@ -708,6 +838,17 @@ voice({
708
838
  onTurn: supportAgent.onTurn,
709
839
  onComplete: async () => {}
710
840
  });
841
+ app.use(
842
+ createVoiceAuditTrailRoutes({
843
+ store: runtimeStorage.audit
844
+ })
845
+ );
846
+ app.use(
847
+ createVoiceTraceDeliveryRoutes({
848
+ store: runtimeStorage.traceDeliveries,
849
+ worker: traceSinkWorker
850
+ })
851
+ );
711
852
 
712
853
  const replay = await exportVoiceTrace({
713
854
  store: runtimeStorage.traces,
@@ -729,18 +870,53 @@ await pruneVoiceTraceEvents({
729
870
  store: runtimeStorage.traces,
730
871
  before: Date.now() - 30 * 24 * 60 * 60 * 1000
731
872
  });
873
+
874
+ await audit.operatorAction({
875
+ action: 'review.approve',
876
+ actor: { id: 'operator-123', kind: 'operator' },
877
+ resource: { id: 'review-123', type: 'review' }
878
+ });
732
879
  ```
733
880
 
734
881
  `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.
735
882
 
736
883
  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.
737
884
 
738
- 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.
885
+ 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.
739
886
 
740
887
  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.
741
888
 
742
889
  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`.
743
890
 
891
+ 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.
892
+
893
+ ```ts
894
+ const plan = await buildVoiceDataRetentionPlan({
895
+ before: Date.now() - 30 * 24 * 60 * 60 * 1000,
896
+ ...runtimeStorage
897
+ });
898
+
899
+ console.log(plan.scopes);
900
+
901
+ await applyVoiceDataRetentionPolicy({
902
+ audit: runtimeStorage.audit,
903
+ before: Date.now() - 30 * 24 * 60 * 60 * 1000,
904
+ ...runtimeStorage
905
+ });
906
+ ```
907
+
908
+ 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.
909
+
910
+ 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.
911
+
912
+ 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.
913
+
914
+ 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.
915
+
916
+ 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.
917
+
918
+ 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.
919
+
744
920
  ## Production Voice Ops
745
921
 
746
922
  The recommended production pattern is:
@@ -1534,6 +1710,40 @@ const policy = resolveVoiceProviderRoutingPolicyPreset('cost-cap', {
1534
1710
  });
1535
1711
  ```
1536
1712
 
1713
+ 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.
1714
+
1715
+ ```ts
1716
+ import { runVoiceProviderRoutingContract } from '@absolutejs/voice';
1717
+
1718
+ const report = await runVoiceProviderRoutingContract({
1719
+ store: runtime.traces,
1720
+ contract: {
1721
+ id: 'openai-to-anthropic-fallback',
1722
+ expect: [
1723
+ {
1724
+ kind: 'llm',
1725
+ provider: 'openai',
1726
+ selectedProvider: 'openai',
1727
+ fallbackProvider: 'anthropic',
1728
+ status: 'error'
1729
+ },
1730
+ {
1731
+ kind: 'llm',
1732
+ provider: 'anthropic',
1733
+ selectedProvider: 'openai',
1734
+ status: 'fallback'
1735
+ }
1736
+ ]
1737
+ }
1738
+ });
1739
+
1740
+ if (!report.pass) {
1741
+ throw new Error(report.issues.map((issue) => issue.message).join('\n'));
1742
+ }
1743
+ ```
1744
+
1745
+ 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.
1746
+
1537
1747
  For full control, pass an object policy:
1538
1748
 
1539
1749
  ```ts
package/dist/agent.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import type { VoiceOnTurnObjectHandler, VoiceRouteResult, VoiceSessionHandle, VoiceSessionRecord, VoiceTurnRecord } from './types';
2
2
  import type { VoiceTraceEventStore } from './trace';
3
3
  import type { VoiceToolRuntime } from './toolRuntime';
4
+ import { type VoiceAuditEventStore, type VoiceAuditLogger } from './audit';
4
5
  export type VoiceAgentMessageRole = 'assistant' | 'system' | 'tool' | 'user';
5
6
  export type VoiceAgentMessage = {
6
7
  content: string;
@@ -86,6 +87,9 @@ export type VoiceAgent<TContext = unknown, TSession extends VoiceSessionRecord =
86
87
  }) => Promise<VoiceAgentRunResult<TResult>>;
87
88
  };
88
89
  export type VoiceAgentOptions<TContext = unknown, TSession extends VoiceSessionRecord = VoiceSessionRecord, TResult = unknown> = {
90
+ audit?: VoiceAuditEventStore | VoiceAuditLogger;
91
+ auditModel?: string;
92
+ auditProvider?: string;
89
93
  id: string;
90
94
  maxToolRounds?: number;
91
95
  model: VoiceAgentModel<TContext, TSession, TResult>;
@@ -99,6 +103,7 @@ export type VoiceAgentOptions<TContext = unknown, TSession extends VoiceSessionR
99
103
  tools?: Array<VoiceAgentTool<TContext, TSession, Record<string, unknown>, unknown, TResult>>;
100
104
  };
101
105
  export type VoiceAgentSquadOptions<TContext = unknown, TSession extends VoiceSessionRecord = VoiceSessionRecord, TResult = unknown> = {
106
+ audit?: VoiceAuditEventStore | VoiceAuditLogger;
102
107
  agents: Array<VoiceAgent<TContext, TSession, TResult>>;
103
108
  defaultAgentId: string;
104
109
  handoffPolicy?: (input: {
@@ -1,4 +1,4 @@
1
- export { VoiceAppKitStatusService } from './voice-app-kit-status.service';
1
+ export { VoiceOpsStatusService } from './voice-ops-status.service';
2
2
  export { VoiceCampaignDialerProofService } from './voice-campaign-dialer-proof.service';
3
3
  export { VoiceStreamService } from './voice-stream.service';
4
4
  export { VoiceControllerService } from './voice-controller.service';
@@ -69,19 +69,19 @@ var __decorateElement = (array, flags, name, decorators, target, extra) => {
69
69
  return k || __decoratorMetadata(array, target), desc && __defProp(target, name, desc), p ? k ^ 4 ? extra : desc : target;
70
70
  };
71
71
 
72
- // src/angular/voice-app-kit-status.service.ts
72
+ // src/angular/voice-ops-status.service.ts
73
73
  import { computed, Injectable, signal } from "@angular/core";
74
74
 
75
- // src/client/appKitStatus.ts
76
- var fetchVoiceAppKitStatus = async (path = "/app-kit/status", options = {}) => {
75
+ // src/client/opsStatus.ts
76
+ var fetchVoiceOpsStatus = async (path = "/api/voice/ops-status", options = {}) => {
77
77
  const fetchImpl = options.fetch ?? globalThis.fetch;
78
78
  const response = await fetchImpl(path);
79
79
  if (!response.ok) {
80
- throw new Error(`Voice app kit status failed: HTTP ${response.status}`);
80
+ throw new Error(`Voice ops status failed: HTTP ${response.status}`);
81
81
  }
82
82
  return await response.json();
83
83
  };
84
- var createVoiceAppKitStatusStore = (path = "/app-kit/status", options = {}) => {
84
+ var createVoiceOpsStatusStore = (path = "/api/voice/ops-status", options = {}) => {
85
85
  const listeners = new Set;
86
86
  let closed = false;
87
87
  let timer;
@@ -105,7 +105,7 @@ var createVoiceAppKitStatusStore = (path = "/app-kit/status", options = {}) => {
105
105
  };
106
106
  emit();
107
107
  try {
108
- const report = await fetchVoiceAppKitStatus(path, options);
108
+ const report = await fetchVoiceOpsStatus(path, options);
109
109
  snapshot = {
110
110
  error: null,
111
111
  isLoading: false,
@@ -151,15 +151,15 @@ var createVoiceAppKitStatusStore = (path = "/app-kit/status", options = {}) => {
151
151
  };
152
152
  };
153
153
 
154
- // src/angular/voice-app-kit-status.service.ts
154
+ // src/angular/voice-ops-status.service.ts
155
155
  var _dec = [
156
156
  Injectable({ providedIn: "root" })
157
157
  ];
158
158
  var _init = __decoratorStart(undefined);
159
159
 
160
- class VoiceAppKitStatusService {
161
- connect(path = "/app-kit/status", options = {}) {
162
- const store = createVoiceAppKitStatusStore(path, options);
160
+ class VoiceOpsStatusService {
161
+ connect(path = "/api/voice/ops-status", options = {}) {
162
+ const store = createVoiceOpsStatusStore(path, options);
163
163
  const errorSignal = signal(null);
164
164
  const isLoadingSignal = signal(false);
165
165
  const reportSignal = signal(undefined);
@@ -189,10 +189,10 @@ class VoiceAppKitStatusService {
189
189
  };
190
190
  }
191
191
  }
192
- VoiceAppKitStatusService = __decorateElement(_init, 0, "VoiceAppKitStatusService", _dec, VoiceAppKitStatusService);
193
- __runInitializers(_init, 1, VoiceAppKitStatusService);
194
- __decoratorMetadata(_init, VoiceAppKitStatusService);
195
- let _VoiceAppKitStatusService = VoiceAppKitStatusService;
192
+ VoiceOpsStatusService = __decorateElement(_init, 0, "VoiceOpsStatusService", _dec, VoiceOpsStatusService);
193
+ __runInitializers(_init, 1, VoiceOpsStatusService);
194
+ __decoratorMetadata(_init, VoiceOpsStatusService);
195
+ let _VoiceOpsStatusService = VoiceOpsStatusService;
196
196
  // src/angular/voice-campaign-dialer-proof.service.ts
197
197
  import { computed as computed2, Injectable as Injectable2, signal as signal2 } from "@angular/core";
198
198
 
@@ -2522,7 +2522,7 @@ export {
2522
2522
  VoiceRoutingStatusService,
2523
2523
  VoiceProviderStatusService,
2524
2524
  VoiceProviderCapabilitiesService,
2525
+ VoiceOpsStatusService,
2525
2526
  VoiceControllerService,
2526
- VoiceCampaignDialerProofService,
2527
- VoiceAppKitStatusService
2527
+ VoiceCampaignDialerProofService
2528
2528
  };
@@ -0,0 +1,12 @@
1
+ import { type VoiceOpsStatusClientOptions } from '../client/opsStatus';
2
+ import type { VoiceOpsStatusReport } from '../opsStatus';
3
+ export declare class VoiceOpsStatusService {
4
+ connect(path?: string, options?: VoiceOpsStatusClientOptions): {
5
+ close: () => void;
6
+ error: import("@angular/core").Signal<string | null>;
7
+ isLoading: import("@angular/core").Signal<boolean>;
8
+ refresh: () => Promise<VoiceOpsStatusReport | undefined>;
9
+ report: import("@angular/core").Signal<VoiceOpsStatusReport | undefined>;
10
+ updatedAt: import("@angular/core").Signal<number | undefined>;
11
+ };
12
+ }