@absolutejs/voice 0.0.22-beta.124 → 0.0.22-beta.126

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1534,6 +1534,40 @@ const policy = resolveVoiceProviderRoutingPolicyPreset('cost-cap', {
1534
1534
  });
1535
1535
  ```
1536
1536
 
1537
+ 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.
1538
+
1539
+ ```ts
1540
+ import { runVoiceProviderRoutingContract } from '@absolutejs/voice';
1541
+
1542
+ const report = await runVoiceProviderRoutingContract({
1543
+ store: runtime.traces,
1544
+ contract: {
1545
+ id: 'openai-to-anthropic-fallback',
1546
+ expect: [
1547
+ {
1548
+ kind: 'llm',
1549
+ provider: 'openai',
1550
+ selectedProvider: 'openai',
1551
+ fallbackProvider: 'anthropic',
1552
+ status: 'error'
1553
+ },
1554
+ {
1555
+ kind: 'llm',
1556
+ provider: 'anthropic',
1557
+ selectedProvider: 'openai',
1558
+ status: 'fallback'
1559
+ }
1560
+ ]
1561
+ }
1562
+ });
1563
+
1564
+ if (!report.pass) {
1565
+ throw new Error(report.issues.map((issue) => issue.message).join('\n'));
1566
+ }
1567
+ ```
1568
+
1569
+ 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.
1570
+
1537
1571
  For full control, pass an object policy:
1538
1572
 
1539
1573
  ```ts
package/dist/index.d.ts CHANGED
@@ -26,6 +26,7 @@ export { createAnthropicVoiceAssistantModel, createGeminiVoiceAssistantModel, cr
26
26
  export { createOpenAIVoiceTTS } from './openaiTTS';
27
27
  export { createVoiceProviderHealthHTMLHandler, createVoiceProviderHealthJSONHandler, createVoiceProviderHealthRoutes, renderVoiceProviderHealthHTML, summarizeVoiceProviderHealth } from './providerHealth';
28
28
  export { createVoiceProviderCapabilityHTMLHandler, createVoiceProviderCapabilityJSONHandler, createVoiceProviderCapabilityRoutes, renderVoiceProviderCapabilityHTML, summarizeVoiceProviderCapabilities } from './providerCapabilities';
29
+ export { assertVoiceProviderRoutingContract, runVoiceProviderRoutingContract } from './providerRoutingContract';
29
30
  export { buildVoiceProductionReadinessReport, createVoiceProductionReadinessRoutes, renderVoiceProductionReadinessHTML } from './productionReadiness';
30
31
  export { buildVoiceOpsConsoleReport, createVoiceOpsConsoleRoutes, renderVoiceOpsConsoleHTML } from './opsConsoleRoutes';
31
32
  export { createVoiceQualityRoutes, evaluateVoiceQuality, renderVoiceQualityHTML } from './qualityRoutes';
@@ -71,6 +72,7 @@ export type { AnthropicVoiceAssistantModelOptions, GeminiVoiceAssistantModelOpti
71
72
  export type { OpenAIVoiceTTSOptions, OpenAIVoiceTTSVoice } from './openaiTTS';
72
73
  export type { VoiceProviderHealthStatus, VoiceProviderHealthSummary, VoiceProviderHealthSummaryOptions } from './providerHealth';
73
74
  export type { VoiceProviderCapabilityDefinition, VoiceProviderCapabilityHandlerOptions, VoiceProviderCapabilityHTMLHandlerOptions, VoiceProviderCapabilityKind, VoiceProviderCapabilityOptions, VoiceProviderCapabilityReport, VoiceProviderCapabilityRoutesOptions, VoiceProviderCapabilitySummary } from './providerCapabilities';
75
+ export type { VoiceProviderRoutingContractDefinition, VoiceProviderRoutingContractIssue, VoiceProviderRoutingContractReport, VoiceProviderRoutingContractRunOptions, VoiceProviderRoutingExpectation, VoiceProviderRoutingStatus } from './providerRoutingContract';
74
76
  export type { VoiceTurnLatencyHTMLHandlerOptions, VoiceTurnLatencyItem, VoiceTurnLatencyOptions, VoiceTurnLatencyReport, VoiceTurnLatencyRoutesOptions, VoiceTurnLatencyStage, VoiceTurnLatencyStatus } from './turnLatency';
75
77
  export type { VoiceLiveLatencyOptions, VoiceLiveLatencyReport, VoiceLiveLatencyRoutesOptions, VoiceLiveLatencySample, VoiceLiveLatencyStatus } from './liveLatency';
76
78
  export type { VoiceTurnQualityHTMLHandlerOptions, VoiceTurnQualityItem, VoiceTurnQualityOptions, VoiceTurnQualityReport, VoiceTurnQualityRoutesOptions, VoiceTurnQualityStatus } from './turnQuality';
package/dist/index.js CHANGED
@@ -9831,6 +9831,18 @@ var resolveCarriers = async (options, input) => {
9831
9831
  providers: [...providers]
9832
9832
  });
9833
9833
  };
9834
+ var resolveAgentSquadContracts = async (options, input) => {
9835
+ if (options.agentSquadContracts === false || options.agentSquadContracts === undefined) {
9836
+ return;
9837
+ }
9838
+ return typeof options.agentSquadContracts === "function" ? await options.agentSquadContracts(input) : options.agentSquadContracts;
9839
+ };
9840
+ var resolveProviderRoutingContracts = async (options, input) => {
9841
+ if (options.providerRoutingContracts === false || options.providerRoutingContracts === undefined) {
9842
+ return;
9843
+ }
9844
+ return typeof options.providerRoutingContracts === "function" ? await options.providerRoutingContracts(input) : options.providerRoutingContracts;
9845
+ };
9834
9846
  var summarizeLiveLatency = (events, options) => {
9835
9847
  const warnAfterMs = options.liveLatencyWarnAfterMs ?? 1800;
9836
9848
  const failAfterMs = options.liveLatencyFailAfterMs ?? 3200;
@@ -9853,7 +9865,15 @@ var buildVoiceProductionReadinessReport = async (options, input = {}) => {
9853
9865
  const routingEvents = listVoiceRoutingEvents(events);
9854
9866
  const routingSessions = summarizeVoiceRoutingSessions(routingEvents);
9855
9867
  const liveLatency = summarizeLiveLatency(events, options);
9856
- const [quality, providers, sessions, handoffs, carriers] = await Promise.all([
9868
+ const [
9869
+ quality,
9870
+ providers,
9871
+ sessions,
9872
+ handoffs,
9873
+ carriers,
9874
+ agentSquadContracts,
9875
+ providerRoutingContracts
9876
+ ] = await Promise.all([
9857
9877
  evaluateVoiceQuality({ events }),
9858
9878
  Promise.all([
9859
9879
  summarizeVoiceProviderHealth({
@@ -9871,7 +9891,9 @@ var buildVoiceProductionReadinessReport = async (options, input = {}) => {
9871
9891
  ]).then((groups) => groups.flat()),
9872
9892
  summarizeVoiceSessions({ events, status: "all" }),
9873
9893
  summarizeVoiceHandoffHealth({ events }),
9874
- resolveCarriers(options, { query, request })
9894
+ resolveCarriers(options, { query, request }),
9895
+ resolveAgentSquadContracts(options, { query, request }),
9896
+ resolveProviderRoutingContracts(options, { query, request })
9875
9897
  ]);
9876
9898
  const degradedProviders = providers.filter((provider) => provider.status === "degraded" || provider.status === "rate-limited" || provider.status === "suppressed").length;
9877
9899
  const failedSessions = sessions.filter((session) => session.status === "failed").length;
@@ -9980,6 +10002,50 @@ var buildVoiceProductionReadinessReport = async (options, input = {}) => {
9980
10002
  status: carrierStatus(carriers),
9981
10003
  warnings: carriers.summary.warnings
9982
10004
  } : undefined;
10005
+ const agentSquadContractSummary = agentSquadContracts ? {
10006
+ failed: agentSquadContracts.filter((report) => !report.pass).length,
10007
+ passed: agentSquadContracts.filter((report) => report.pass).length,
10008
+ status: agentSquadContracts.some((report) => !report.pass) ? "fail" : agentSquadContracts.length === 0 ? "warn" : "pass",
10009
+ total: agentSquadContracts.length
10010
+ } : undefined;
10011
+ const providerRoutingContractSummary = providerRoutingContracts ? {
10012
+ failed: providerRoutingContracts.filter((report) => !report.pass).length,
10013
+ passed: providerRoutingContracts.filter((report) => report.pass).length,
10014
+ status: providerRoutingContracts.some((report) => !report.pass) ? "fail" : providerRoutingContracts.length === 0 ? "warn" : "pass",
10015
+ total: providerRoutingContracts.length
10016
+ } : undefined;
10017
+ if (agentSquadContractSummary) {
10018
+ checks.push({
10019
+ detail: agentSquadContractSummary.status === "pass" ? `${agentSquadContractSummary.passed} agent squad contract(s) are passing.` : agentSquadContractSummary.total === 0 ? "No agent squad contracts are configured." : `${agentSquadContractSummary.failed} agent squad contract(s) failed.`,
10020
+ href: options.links?.agentSquadContracts ?? "/agent-squad-contract",
10021
+ label: "Agent squad contracts",
10022
+ status: agentSquadContractSummary.status,
10023
+ value: `${agentSquadContractSummary.passed}/${agentSquadContractSummary.total}`,
10024
+ actions: agentSquadContractSummary.status === "pass" ? [] : [
10025
+ {
10026
+ description: "Open the specialist routing contract report and inspect failing handoff paths.",
10027
+ href: options.links?.agentSquadContracts ?? "/agent-squad-contract",
10028
+ label: "Open squad contracts"
10029
+ }
10030
+ ]
10031
+ });
10032
+ }
10033
+ if (providerRoutingContractSummary) {
10034
+ checks.push({
10035
+ detail: providerRoutingContractSummary.status === "pass" ? `${providerRoutingContractSummary.passed} provider routing contract(s) are passing.` : providerRoutingContractSummary.total === 0 ? "No provider routing contracts are configured." : `${providerRoutingContractSummary.failed} provider routing contract(s) failed.`,
10036
+ href: options.links?.providerRoutingContracts ?? options.links?.resilience ?? "/resilience",
10037
+ label: "Provider routing contracts",
10038
+ status: providerRoutingContractSummary.status,
10039
+ value: `${providerRoutingContractSummary.passed}/${providerRoutingContractSummary.total}`,
10040
+ actions: providerRoutingContractSummary.status === "pass" ? [] : [
10041
+ {
10042
+ description: "Open provider routing evidence and inspect failed fallback expectations.",
10043
+ href: options.links?.providerRoutingContracts ?? options.links?.resilience ?? "/resilience",
10044
+ label: "Open provider routing contracts"
10045
+ }
10046
+ ]
10047
+ });
10048
+ }
9983
10049
  if (carriers && carrierSummary) {
9984
10050
  checks.push({
9985
10051
  detail: carrierSummary.status === "pass" ? "Configured carrier setup and contract checks are passing." : `${carrierSummary.failing} carrier(s) failing, ${carrierSummary.warnings} warning(s).`,
@@ -10000,10 +10066,12 @@ var buildVoiceProductionReadinessReport = async (options, input = {}) => {
10000
10066
  checkedAt: Date.now(),
10001
10067
  checks,
10002
10068
  links: {
10069
+ agentSquadContracts: "/agent-squad-contract",
10003
10070
  carriers: "/carriers",
10004
10071
  handoffs: "/handoffs",
10005
10072
  handoffRetry: "/api/voice-handoffs/retry",
10006
10073
  liveLatency: "/traces",
10074
+ providerRoutingContracts: "/resilience",
10007
10075
  quality: "/quality",
10008
10076
  resilience: "/resilience",
10009
10077
  sessions: "/sessions",
@@ -10011,6 +10079,7 @@ var buildVoiceProductionReadinessReport = async (options, input = {}) => {
10011
10079
  },
10012
10080
  status: rollupStatus(checks),
10013
10081
  summary: {
10082
+ agentSquadContracts: agentSquadContractSummary,
10014
10083
  carriers: carrierSummary,
10015
10084
  handoffs: {
10016
10085
  failed: handoffs.failed,
@@ -10021,6 +10090,7 @@ var buildVoiceProductionReadinessReport = async (options, input = {}) => {
10021
10090
  degraded: degradedProviders,
10022
10091
  total: providers.length
10023
10092
  },
10093
+ providerRoutingContracts: providerRoutingContractSummary,
10024
10094
  quality: {
10025
10095
  status: quality.status
10026
10096
  },
@@ -17035,6 +17105,43 @@ var createOpenAIVoiceTTS = (options) => {
17035
17105
  }
17036
17106
  };
17037
17107
  };
17108
+ // src/providerRoutingContract.ts
17109
+ var isRoutingEvent = (event) => Boolean(event && typeof event === "object" && "status" in event && "kind" in event && "sessionId" in event);
17110
+ var normalizeEvents = (events) => (events.every(isRoutingEvent) ? [...events] : listVoiceRoutingEvents(events)).sort((left, right) => left.at - right.at);
17111
+ var matchesExpectation = (event, expectation) => (expectation.kind === undefined || event.kind === expectation.kind) && (expectation.operation === undefined || event.operation === expectation.operation) && (expectation.provider === undefined || event.provider === expectation.provider) && (expectation.selectedProvider === undefined || event.selectedProvider === expectation.selectedProvider) && (expectation.fallbackProvider === undefined || event.fallbackProvider === expectation.fallbackProvider) && (expectation.status === undefined || event.status === expectation.status);
17112
+ var describeExpectation = (expectation) => Object.entries(expectation).map(([key, value]) => `${key}=${String(value)}`).join(", ");
17113
+ var runVoiceProviderRoutingContract = async (options) => {
17114
+ const rawEvents = options.events ?? await options.store?.list() ?? [];
17115
+ const events = normalizeEvents(rawEvents).filter((event) => (!options.contract.sessionId || event.sessionId === options.contract.sessionId) && (!options.contract.scenarioId || event.sessionId === options.contract.scenarioId || rawEvents.some((rawEvent) => !isRoutingEvent(rawEvent) && rawEvent.sessionId === event.sessionId && rawEvent.scenarioId === options.contract.scenarioId)));
17116
+ const issues = [];
17117
+ let searchFrom = 0;
17118
+ for (const [index, expectation] of options.contract.expect.entries()) {
17119
+ const matchIndex = events.findIndex((event, eventIndex) => eventIndex >= searchFrom && matchesExpectation(event, expectation));
17120
+ if (matchIndex === -1) {
17121
+ issues.push({
17122
+ code: "provider_routing.expected_event_missing",
17123
+ message: `Expected provider routing event ${index + 1}: ${describeExpectation(expectation)}.`
17124
+ });
17125
+ continue;
17126
+ }
17127
+ searchFrom = matchIndex + 1;
17128
+ }
17129
+ return {
17130
+ contractId: options.contract.id,
17131
+ events,
17132
+ issues,
17133
+ pass: issues.length === 0,
17134
+ scenarioId: options.contract.scenarioId,
17135
+ sessionId: options.contract.sessionId
17136
+ };
17137
+ };
17138
+ var assertVoiceProviderRoutingContract = async (options) => {
17139
+ const report = await runVoiceProviderRoutingContract(options);
17140
+ if (!report.pass) {
17141
+ throw new Error(`Voice provider routing contract ${report.contractId} failed: ${report.issues.map((issue) => issue.message).join(" ")}`);
17142
+ }
17143
+ return report;
17144
+ };
17038
17145
  // src/providerAdapters.ts
17039
17146
  class VoiceIOProviderTimeoutError extends Error {
17040
17147
  provider;
@@ -19934,6 +20041,7 @@ export {
19934
20041
  runVoiceSessionEvals,
19935
20042
  runVoiceScenarioFixtureEvals,
19936
20043
  runVoiceScenarioEvals,
20044
+ runVoiceProviderRoutingContract,
19937
20045
  runVoiceOutcomeContractSuite,
19938
20046
  runVoiceCampaignProof,
19939
20047
  runVoiceCampaignDialerProof,
@@ -20215,6 +20323,7 @@ export {
20215
20323
  buildVoiceDiagnosticsMarkdown,
20216
20324
  buildVoiceCampaignObservabilityReport,
20217
20325
  assignVoiceOpsTask,
20326
+ assertVoiceProviderRoutingContract,
20218
20327
  assertVoiceAgentSquadContract,
20219
20328
  applyVoiceTelephonyOutcome,
20220
20329
  applyVoiceOpsTaskPolicy,
@@ -1,6 +1,8 @@
1
1
  import { Elysia } from 'elysia';
2
2
  import { type VoiceTelephonyCarrierMatrixInput } from './telephony/matrix';
3
3
  import type { VoiceTraceEventStore } from './trace';
4
+ import type { VoiceAgentSquadContractReport } from './agentSquadContract';
5
+ import type { VoiceProviderRoutingContractReport } from './providerRoutingContract';
4
6
  export type VoiceProductionReadinessStatus = 'fail' | 'pass' | 'warn';
5
7
  export type VoiceProductionReadinessAction = {
6
8
  description?: string;
@@ -20,16 +22,24 @@ export type VoiceProductionReadinessReport = {
20
22
  checkedAt: number;
21
23
  checks: VoiceProductionReadinessCheck[];
22
24
  links: {
25
+ agentSquadContracts?: string;
23
26
  carriers?: string;
24
27
  handoffs?: string;
25
28
  handoffRetry?: string;
26
29
  liveLatency?: string;
30
+ providerRoutingContracts?: string;
27
31
  quality?: string;
28
32
  resilience?: string;
29
33
  sessions?: string;
30
34
  };
31
35
  status: VoiceProductionReadinessStatus;
32
36
  summary: {
37
+ agentSquadContracts?: {
38
+ failed: number;
39
+ passed: number;
40
+ status: VoiceProductionReadinessStatus;
41
+ total: number;
42
+ };
33
43
  carriers?: {
34
44
  failing: number;
35
45
  providers: number;
@@ -52,6 +62,12 @@ export type VoiceProductionReadinessReport = {
52
62
  degraded: number;
53
63
  total: number;
54
64
  };
65
+ providerRoutingContracts?: {
66
+ failed: number;
67
+ passed: number;
68
+ status: VoiceProductionReadinessStatus;
69
+ total: number;
70
+ };
55
71
  quality: {
56
72
  status: 'fail' | 'pass';
57
73
  };
@@ -66,6 +82,10 @@ export type VoiceProductionReadinessReport = {
66
82
  };
67
83
  };
68
84
  export type VoiceProductionReadinessRoutesOptions = {
85
+ agentSquadContracts?: false | readonly VoiceAgentSquadContractReport[] | ((input: {
86
+ query: Record<string, unknown>;
87
+ request: Request;
88
+ }) => Promise<readonly VoiceAgentSquadContractReport[]> | readonly VoiceAgentSquadContractReport[]);
69
89
  carriers?: false | readonly VoiceTelephonyCarrierMatrixInput[] | ((input: {
70
90
  query: Record<string, unknown>;
71
91
  request: Request;
@@ -76,6 +96,10 @@ export type VoiceProductionReadinessRoutesOptions = {
76
96
  llmProviders?: readonly string[];
77
97
  name?: string;
78
98
  path?: string;
99
+ providerRoutingContracts?: false | readonly VoiceProviderRoutingContractReport[] | ((input: {
100
+ query: Record<string, unknown>;
101
+ request: Request;
102
+ }) => Promise<readonly VoiceProviderRoutingContractReport[]> | readonly VoiceProviderRoutingContractReport[]);
79
103
  render?: (report: VoiceProductionReadinessReport) => string | Promise<string>;
80
104
  store: VoiceTraceEventStore;
81
105
  sttProviders?: readonly string[];
@@ -0,0 +1,38 @@
1
+ import { type VoiceRoutingEvent, type VoiceRoutingEventKind } from './resilienceRoutes';
2
+ import type { StoredVoiceTraceEvent, VoiceTraceEventStore } from './trace';
3
+ export type VoiceProviderRoutingStatus = 'error' | 'fallback' | 'success';
4
+ export type VoiceProviderRoutingExpectation = {
5
+ fallbackProvider?: string;
6
+ kind?: VoiceRoutingEventKind;
7
+ operation?: string;
8
+ provider?: string;
9
+ selectedProvider?: string;
10
+ status?: VoiceProviderRoutingStatus;
11
+ };
12
+ export type VoiceProviderRoutingContractDefinition = {
13
+ description?: string;
14
+ expect: VoiceProviderRoutingExpectation[];
15
+ id: string;
16
+ label?: string;
17
+ scenarioId?: string;
18
+ sessionId?: string;
19
+ };
20
+ export type VoiceProviderRoutingContractIssue = {
21
+ code: string;
22
+ message: string;
23
+ };
24
+ export type VoiceProviderRoutingContractReport = {
25
+ contractId: string;
26
+ events: VoiceRoutingEvent[];
27
+ issues: VoiceProviderRoutingContractIssue[];
28
+ pass: boolean;
29
+ scenarioId?: string;
30
+ sessionId?: string;
31
+ };
32
+ export type VoiceProviderRoutingContractRunOptions = {
33
+ contract: VoiceProviderRoutingContractDefinition;
34
+ events?: StoredVoiceTraceEvent[] | VoiceRoutingEvent[];
35
+ store?: VoiceTraceEventStore;
36
+ };
37
+ export declare const runVoiceProviderRoutingContract: (options: VoiceProviderRoutingContractRunOptions) => Promise<VoiceProviderRoutingContractReport>;
38
+ export declare const assertVoiceProviderRoutingContract: (options: VoiceProviderRoutingContractRunOptions) => Promise<VoiceProviderRoutingContractReport>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@absolutejs/voice",
3
- "version": "0.0.22-beta.124",
3
+ "version": "0.0.22-beta.126",
4
4
  "description": "Voice primitives and Elysia plugin for AbsoluteJS",
5
5
  "repository": {
6
6
  "type": "git",