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

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
@@ -563,7 +563,21 @@ const billingAgent = createVoiceAgent({
563
563
  const frontDesk = createVoiceAgentSquad({
564
564
  id: 'front-desk',
565
565
  defaultAgentId: 'support',
566
- agents: [supportAgent, billingAgent]
566
+ agents: [supportAgent, billingAgent],
567
+ handoffPolicy: ({ handoff }) => {
568
+ if (handoff.targetAgentId === 'billing') {
569
+ return {
570
+ summary: 'Route verified billing requests to the billing specialist.',
571
+ metadata: { queue: 'billing' }
572
+ };
573
+ }
574
+
575
+ return {
576
+ allow: false,
577
+ reason: `No approved route for ${handoff.targetAgentId}.`,
578
+ escalate: { reason: 'unsupported-specialist' }
579
+ };
580
+ }
567
581
  });
568
582
 
569
583
  voice({
@@ -577,6 +591,56 @@ voice({
577
591
 
578
592
  `createVoiceAgentSquad(...)` gives you squad-style specialization without locking your app into a hosted voice platform. An agent can return `handoff: { targetAgentId: 'billing' }`; the squad records the handoff, runs the target agent on the same turn, and still returns a standard `VoiceRouteResult`.
579
593
 
594
+ For production call centers, pass `handoffPolicy` to keep routing code-owned instead of dashboard-owned. The policy can allow a handoff, reroute it to a different specialist, merge handoff metadata, summarize the reason for the target agent, or block the handoff and return an escalation. Squad traces mark each handoff as `allowed`, `blocked`, `unknown-target`, or `max-exceeded`, so support teams can audit why a caller moved between specialists.
595
+
596
+ Use `runVoiceAgentSquadContract(...)` in tests or readiness checks when you need proof that a specialist graph still routes correctly:
597
+
598
+ ```ts
599
+ import {
600
+ createVoiceMemoryTraceEventStore,
601
+ runVoiceAgentSquadContract
602
+ } from '@absolutejs/voice';
603
+
604
+ const trace = createVoiceMemoryTraceEventStore();
605
+ const frontDesk = createVoiceAgentSquad({
606
+ id: 'front-desk',
607
+ defaultAgentId: 'support',
608
+ agents: [supportAgent, billingAgent],
609
+ trace
610
+ });
611
+
612
+ const report = await runVoiceAgentSquadContract({
613
+ context: {},
614
+ squad: frontDesk,
615
+ trace,
616
+ contract: {
617
+ id: 'billing-route',
618
+ scenarioId: 'billing-route',
619
+ turns: [
620
+ {
621
+ text: 'I have a billing question.',
622
+ expect: {
623
+ finalAgentId: 'billing',
624
+ outcome: 'assistant',
625
+ assistantIncludes: ['billing'],
626
+ handoffs: [
627
+ {
628
+ fromAgentId: 'support',
629
+ targetAgentId: 'billing',
630
+ status: 'allowed'
631
+ }
632
+ ]
633
+ }
634
+ }
635
+ ]
636
+ }
637
+ });
638
+
639
+ if (!report.pass) {
640
+ throw new Error(report.issues.map((issue) => issue.message).join('\n'));
641
+ }
642
+ ```
643
+
580
644
  ## Traces And Replay
581
645
 
582
646
  Use trace stores when you want every call to be inspectable outside a hosted platform. Trace events are append-only records for model passes, tool calls, handoffs, agent results, call lifecycle, turn timing, errors, and cost telemetry.
package/dist/agent.d.ts CHANGED
@@ -66,6 +66,14 @@ export type VoiceAgentRunResult<TResult = unknown> = VoiceRouteResult<TResult> &
66
66
  messages: VoiceAgentMessage[];
67
67
  toolResults: VoiceAgentToolResult[];
68
68
  };
69
+ export type VoiceAgentSquadHandoffPolicyResult<TResult = unknown> = {
70
+ allow?: boolean;
71
+ escalate?: VoiceRouteResult<TResult>['escalate'];
72
+ metadata?: Record<string, unknown>;
73
+ reason?: string;
74
+ summary?: string;
75
+ targetAgentId?: string;
76
+ };
69
77
  export type VoiceAgent<TContext = unknown, TSession extends VoiceSessionRecord = VoiceSessionRecord, TResult = unknown> = {
70
78
  id: string;
71
79
  onTurn: VoiceOnTurnObjectHandler<TContext, TSession, TResult>;
@@ -93,6 +101,15 @@ export type VoiceAgentOptions<TContext = unknown, TSession extends VoiceSessionR
93
101
  export type VoiceAgentSquadOptions<TContext = unknown, TSession extends VoiceSessionRecord = VoiceSessionRecord, TResult = unknown> = {
94
102
  agents: Array<VoiceAgent<TContext, TSession, TResult>>;
95
103
  defaultAgentId: string;
104
+ handoffPolicy?: (input: {
105
+ context: TContext;
106
+ fromAgentId: string;
107
+ handoff: NonNullable<VoiceAgentModelOutput<TResult>['handoff']>;
108
+ messages: VoiceAgentMessage[];
109
+ session: TSession;
110
+ targetAgent?: VoiceAgent<TContext, TSession, TResult>;
111
+ turn: VoiceTurnRecord;
112
+ }) => Promise<VoiceAgentSquadHandoffPolicyResult<TResult> | void> | VoiceAgentSquadHandoffPolicyResult<TResult> | void;
96
113
  id: string;
97
114
  maxHandoffsPerTurn?: number;
98
115
  onHandoff?: (input: {
@@ -0,0 +1,64 @@
1
+ import type { VoiceAgent, VoiceAgentRunResult } from './agent';
2
+ import type { VoiceTraceEventStore } from './trace';
3
+ import type { VoiceRouteResult, VoiceSessionHandle, VoiceSessionRecord } from './types';
4
+ export type VoiceAgentSquadContractOutcome = 'assistant' | 'complete' | 'escalate' | 'no-answer' | 'transfer' | 'voicemail';
5
+ export type VoiceAgentSquadHandoffExpectation = {
6
+ fromAgentId?: string;
7
+ status?: 'allowed' | 'blocked' | 'max-exceeded' | 'unknown-target';
8
+ targetAgentId?: string;
9
+ };
10
+ export type VoiceAgentSquadTurnExpectation<TResult = unknown> = {
11
+ assistantIncludes?: string[];
12
+ finalAgentId?: string;
13
+ handoffs?: VoiceAgentSquadHandoffExpectation[];
14
+ outcome?: VoiceAgentSquadContractOutcome;
15
+ result?: (input: {
16
+ result: TResult | undefined;
17
+ routeResult: VoiceRouteResult<TResult>;
18
+ }) => VoiceAgentSquadContractIssue[];
19
+ transferTarget?: string;
20
+ };
21
+ export type VoiceAgentSquadContractTurn<TResult = unknown> = {
22
+ expect?: VoiceAgentSquadTurnExpectation<TResult>;
23
+ id?: string;
24
+ text: string;
25
+ };
26
+ export type VoiceAgentSquadContractDefinition<TResult = unknown> = {
27
+ description?: string;
28
+ id: string;
29
+ label?: string;
30
+ scenarioId?: string;
31
+ turns: Array<VoiceAgentSquadContractTurn<TResult>>;
32
+ };
33
+ export type VoiceAgentSquadContractIssue = {
34
+ code: string;
35
+ message: string;
36
+ turnId?: string;
37
+ };
38
+ export type VoiceAgentSquadContractTurnReport<TResult = unknown> = {
39
+ agentId: string;
40
+ handoffs: VoiceAgentSquadHandoffExpectation[];
41
+ issues: VoiceAgentSquadContractIssue[];
42
+ outcome?: VoiceAgentSquadContractOutcome;
43
+ pass: boolean;
44
+ result: VoiceAgentRunResult<TResult>;
45
+ turnId: string;
46
+ };
47
+ export type VoiceAgentSquadContractReport<TResult = unknown> = {
48
+ contractId: string;
49
+ issues: VoiceAgentSquadContractIssue[];
50
+ pass: boolean;
51
+ scenarioId?: string;
52
+ sessionId: string;
53
+ turns: Array<VoiceAgentSquadContractTurnReport<TResult>>;
54
+ };
55
+ export type VoiceAgentSquadContractRunOptions<TContext = unknown, TSession extends VoiceSessionRecord = VoiceSessionRecord, TResult = unknown> = {
56
+ api?: VoiceSessionHandle<TContext, TSession, TResult>;
57
+ context: TContext;
58
+ contract: VoiceAgentSquadContractDefinition<TResult>;
59
+ session?: TSession;
60
+ squad: VoiceAgent<TContext, TSession, TResult>;
61
+ trace?: VoiceTraceEventStore;
62
+ };
63
+ export declare const runVoiceAgentSquadContract: <TContext = unknown, TSession extends VoiceSessionRecord = VoiceSessionRecord, TResult = unknown>(options: VoiceAgentSquadContractRunOptions<TContext, TSession, TResult>) => Promise<VoiceAgentSquadContractReport<TResult>>;
64
+ export declare const assertVoiceAgentSquadContract: <TContext = unknown, TSession extends VoiceSessionRecord = VoiceSessionRecord, TResult = unknown>(options: VoiceAgentSquadContractRunOptions<TContext, TSession, TResult>) => Promise<VoiceAgentSquadContractReport<TResult>>;
package/dist/index.d.ts CHANGED
@@ -11,6 +11,7 @@ export { createVoiceSimulationSuiteRoutes, renderVoiceSimulationSuiteHTML, runVo
11
11
  export { createVoiceWorkflowContract, createVoiceWorkflowContractHandler, createVoiceWorkflowContractPreset, createVoiceWorkflowScenario, recordVoiceWorkflowContractTrace, validateVoiceWorkflowRouteResult } from './workflowContract';
12
12
  export { createVoiceSessionListRoutes, createVoiceSessionReplayHTMLHandler, createVoiceSessionReplayJSONHandler, createVoiceSessionReplayRoutes, createVoiceSessionsHTMLHandler, createVoiceSessionsJSONHandler, renderVoiceSessionsHTML, summarizeVoiceSessions, summarizeVoiceSessionReplay } from './sessionReplay';
13
13
  export { createVoiceAgent, createVoiceAgentSquad, createVoiceAgentTool } from './agent';
14
+ export { assertVoiceAgentSquadContract, runVoiceAgentSquadContract } from './agentSquadContract';
14
15
  export { createVoiceToolIdempotencyKey, createVoiceToolRuntime } from './toolRuntime';
15
16
  export { createVoiceToolContract, createVoiceToolContractHTMLHandler, createVoiceToolContractJSONHandler, createVoiceToolContractRoutes, createVoiceToolRuntimeContractDefaults, renderVoiceToolContractHTML, runVoiceToolContractSuite, runVoiceToolContract } from './toolContract';
16
17
  export { createVoiceTurnLatencyHTMLHandler, createVoiceTurnLatencyJSONHandler, createVoiceTurnLatencyRoutes, renderVoiceTurnLatencyHTML, summarizeVoiceTurnLatency } from './turnLatency';
@@ -81,7 +82,8 @@ export type { VoiceProductionReadinessAction, VoiceProductionReadinessCheck, Voi
81
82
  export type { VoiceQualityLink, VoiceQualityMetric, VoiceQualityReport, VoiceQualityRoutesOptions, VoiceQualityStatus, VoiceQualityThresholds } from './qualityRoutes';
82
83
  export type { VoiceResilienceIOSimulator, VoiceResilienceLink, VoiceResiliencePageData, VoiceResilienceRoutesOptions, VoiceResilienceSimulationProvider, VoiceRoutingKindSummary, VoiceRoutingDecisionSummary, VoiceRoutingDecisionSummaryOptions, VoiceRoutingEvent, VoiceRoutingEventKind, VoiceRoutingSessionSummary, VoiceRoutingSessionSummaryOptions } from './resilienceRoutes';
83
84
  export type { VoiceIOProviderRouterEvent, VoiceIOProviderRouterOptions, VoiceIOProviderRouterPolicy, VoiceIOProviderRouterPolicyConfig, VoiceSTTProviderRouterOptions, VoiceTTSProviderRouterOptions } from './providerAdapters';
84
- export type { VoiceAgent, VoiceAgentMessage, VoiceAgentMessageRole, VoiceAgentModel, VoiceAgentModelInput, VoiceAgentModelOutput, VoiceAgentOptions, VoiceAgentRunResult, VoiceAgentSquadOptions, VoiceAgentTool, VoiceAgentToolCall, VoiceAgentToolResult } from './agent';
85
+ export type { VoiceAgent, VoiceAgentMessage, VoiceAgentMessageRole, VoiceAgentModel, VoiceAgentModelInput, VoiceAgentModelOutput, VoiceAgentOptions, VoiceAgentRunResult, VoiceAgentSquadHandoffPolicyResult, VoiceAgentSquadOptions, VoiceAgentTool, VoiceAgentToolCall, VoiceAgentToolResult } from './agent';
86
+ export type { VoiceAgentSquadContractDefinition, VoiceAgentSquadContractIssue, VoiceAgentSquadContractOutcome, VoiceAgentSquadContractReport, VoiceAgentSquadContractRunOptions, VoiceAgentSquadContractTurn, VoiceAgentSquadContractTurnReport, VoiceAgentSquadHandoffExpectation, VoiceAgentSquadTurnExpectation } from './agentSquadContract';
85
87
  export type { VoiceToolRetryDelay, VoiceToolRuntime, VoiceToolRuntimeExecuteInput, VoiceToolRuntimeOptions, VoiceToolRuntimeResult } from './toolRuntime';
86
88
  export type { VoiceToolContractCase, VoiceToolContractCaseReport, VoiceToolContractDefinition, VoiceToolContractExpectation, VoiceToolContractHandlerOptions, VoiceToolContractHTMLHandlerOptions, VoiceToolContractIssue, VoiceToolContractReport, VoiceToolContractRoutesOptions, VoiceToolContractSuiteReport } from './toolContract';
87
89
  export type { VoiceOpsRuntime, VoiceOpsRuntimeConfig, VoiceOpsRuntimeSummary, VoiceOpsRuntimeSinkWorkerConfig, VoiceOpsRuntimeTaskWorkerConfig, VoiceOpsRuntimeTickResult, VoiceOpsRuntimeWebhookWorkerConfig } from './opsRuntime';
package/dist/index.js CHANGED
@@ -5827,21 +5827,80 @@ var createVoiceAgentSquad = (options) => {
5827
5827
  });
5828
5828
  toolResults.push(...result.toolResults);
5829
5829
  for (let handoffCount = 0;result.handoff && handoffCount < maxHandoffs; handoffCount += 1) {
5830
- const nextAgent = agents.get(result.handoff.targetAgentId);
5830
+ const originalTargetAgentId = result.handoff.targetAgentId;
5831
+ const policy = await options.handoffPolicy?.({
5832
+ context: input.context,
5833
+ fromAgentId: agent.id,
5834
+ handoff: result.handoff,
5835
+ messages,
5836
+ session: input.session,
5837
+ targetAgent: agents.get(originalTargetAgentId),
5838
+ turn: input.turn
5839
+ });
5840
+ const targetAgentId = normalizeText3(policy?.targetAgentId) || originalTargetAgentId;
5841
+ const nextAgent = agents.get(targetAgentId);
5842
+ const handoffReason = policy?.summary ?? policy?.reason ?? result.handoff.reason;
5843
+ const handoffMetadata = {
5844
+ ...result.handoff.metadata,
5845
+ ...policy?.metadata
5846
+ };
5847
+ const metadata = Object.keys(handoffMetadata).length > 0 ? handoffMetadata : undefined;
5848
+ if (policy?.allow === false) {
5849
+ await appendVoiceAgentTrace({
5850
+ agentId: options.id,
5851
+ event: {
5852
+ fromAgentId: agent.id,
5853
+ metadata,
5854
+ originalTargetAgentId,
5855
+ reason: handoffReason,
5856
+ status: "blocked",
5857
+ targetAgentId
5858
+ },
5859
+ session: input.session,
5860
+ trace: options.trace,
5861
+ turn: input.turn,
5862
+ type: "agent.handoff"
5863
+ });
5864
+ return {
5865
+ ...result,
5866
+ escalate: result.escalate ?? policy.escalate ?? {
5867
+ metadata,
5868
+ reason: handoffReason ?? `Blocked handoff to ${targetAgentId}`
5869
+ },
5870
+ handoff: undefined,
5871
+ toolResults
5872
+ };
5873
+ }
5831
5874
  if (!nextAgent) {
5875
+ await appendVoiceAgentTrace({
5876
+ agentId: options.id,
5877
+ event: {
5878
+ fromAgentId: agent.id,
5879
+ metadata,
5880
+ originalTargetAgentId,
5881
+ reason: handoffReason,
5882
+ status: "unknown-target",
5883
+ targetAgentId
5884
+ },
5885
+ session: input.session,
5886
+ trace: options.trace,
5887
+ turn: input.turn,
5888
+ type: "agent.handoff"
5889
+ });
5832
5890
  return {
5833
5891
  ...result,
5834
5892
  escalate: result.escalate ?? {
5835
- metadata: result.handoff.metadata,
5836
- reason: `Unknown handoff target: ${result.handoff.targetAgentId}`
5893
+ metadata,
5894
+ reason: `Unknown handoff target: ${targetAgentId}`
5837
5895
  },
5896
+ handoff: undefined,
5838
5897
  toolResults
5839
5898
  };
5840
5899
  }
5841
5900
  await options.onHandoff?.({
5842
5901
  context: input.context,
5843
5902
  fromAgentId: agent.id,
5844
- reason: result.handoff.reason,
5903
+ reason: handoffReason,
5845
5904
  session: input.session,
5846
5905
  targetAgentId: nextAgent.id,
5847
5906
  turn: input.turn
@@ -5850,7 +5909,10 @@ var createVoiceAgentSquad = (options) => {
5850
5909
  agentId: options.id,
5851
5910
  event: {
5852
5911
  fromAgentId: agent.id,
5853
- reason: result.handoff.reason,
5912
+ metadata,
5913
+ originalTargetAgentId: originalTargetAgentId === nextAgent.id ? undefined : originalTargetAgentId,
5914
+ reason: handoffReason,
5915
+ status: "allowed",
5854
5916
  targetAgentId: nextAgent.id
5855
5917
  },
5856
5918
  session: input.session,
@@ -5859,8 +5921,8 @@ var createVoiceAgentSquad = (options) => {
5859
5921
  type: "agent.handoff"
5860
5922
  });
5861
5923
  messages.push({
5862
- content: result.handoff.reason ?? `Handoff to ${nextAgent.id}`,
5863
- metadata: result.handoff.metadata,
5924
+ content: handoffReason ?? `Handoff to ${nextAgent.id}`,
5925
+ metadata,
5864
5926
  name: nextAgent.id,
5865
5927
  role: "system"
5866
5928
  });
@@ -5872,6 +5934,31 @@ var createVoiceAgentSquad = (options) => {
5872
5934
  });
5873
5935
  toolResults.push(...result.toolResults);
5874
5936
  }
5937
+ if (result.handoff) {
5938
+ await appendVoiceAgentTrace({
5939
+ agentId: options.id,
5940
+ event: {
5941
+ fromAgentId: agent.id,
5942
+ metadata: result.handoff.metadata,
5943
+ reason: result.handoff.reason,
5944
+ status: "max-exceeded",
5945
+ targetAgentId: result.handoff.targetAgentId
5946
+ },
5947
+ session: input.session,
5948
+ trace: options.trace,
5949
+ turn: input.turn,
5950
+ type: "agent.handoff"
5951
+ });
5952
+ return {
5953
+ ...result,
5954
+ escalate: result.escalate ?? {
5955
+ metadata: result.handoff.metadata,
5956
+ reason: `Max handoffs exceeded: ${maxHandoffs}`
5957
+ },
5958
+ handoff: undefined,
5959
+ toolResults
5960
+ };
5961
+ }
5875
5962
  return {
5876
5963
  ...result,
5877
5964
  agentId,
@@ -12646,6 +12733,164 @@ var createVoiceWorkflowContractHandler = (input) => {
12646
12733
  return result;
12647
12734
  };
12648
12735
  };
12736
+ // src/agentSquadContract.ts
12737
+ var normalizeIncludes = (value) => value.trim().toLowerCase();
12738
+ var resolveOutcome3 = (result) => {
12739
+ if (result.complete)
12740
+ return "complete";
12741
+ if (result.transfer)
12742
+ return "transfer";
12743
+ if (result.escalate)
12744
+ return "escalate";
12745
+ if (result.voicemail)
12746
+ return "voicemail";
12747
+ if (result.noAnswer)
12748
+ return "no-answer";
12749
+ if (result.assistantText?.trim())
12750
+ return "assistant";
12751
+ return;
12752
+ };
12753
+ var getPayloadString2 = (event, key) => {
12754
+ const value = event.payload[key];
12755
+ return typeof value === "string" ? value : undefined;
12756
+ };
12757
+ var toHandoffExpectation = (event) => ({
12758
+ fromAgentId: getPayloadString2(event, "fromAgentId"),
12759
+ status: getPayloadString2(event, "status"),
12760
+ targetAgentId: getPayloadString2(event, "targetAgentId")
12761
+ });
12762
+ var createContractApi = (session) => ({
12763
+ close: async () => {},
12764
+ commitTurn: async () => {},
12765
+ complete: async () => {},
12766
+ connect: async () => {},
12767
+ disconnect: async () => {},
12768
+ escalate: async () => {},
12769
+ fail: async () => {},
12770
+ id: session.id,
12771
+ markNoAnswer: async () => {},
12772
+ markVoicemail: async () => {},
12773
+ receiveAudio: async () => {},
12774
+ snapshot: async () => session,
12775
+ transfer: async () => {}
12776
+ });
12777
+ var createContractTurn = (turn, index) => ({
12778
+ committedAt: Date.now(),
12779
+ id: turn.id ?? `turn-${index + 1}`,
12780
+ text: turn.text,
12781
+ transcripts: []
12782
+ });
12783
+ var appendIssue = (issues, issue, turnId) => {
12784
+ issues.push({
12785
+ ...issue,
12786
+ turnId: issue.turnId ?? turnId
12787
+ });
12788
+ };
12789
+ var runVoiceAgentSquadContract = async (options) => {
12790
+ const session = options.session ?? createVoiceSessionRecord(`agent-squad-contract-${options.contract.id}`, options.contract.scenarioId ?? options.contract.id);
12791
+ const api = options.api ?? createContractApi(session);
12792
+ const turnReports = [];
12793
+ const issues = [];
12794
+ for (const [index, contractTurn] of options.contract.turns.entries()) {
12795
+ const turn = createContractTurn(contractTurn, index);
12796
+ const result = await options.squad.run({
12797
+ api,
12798
+ context: options.context,
12799
+ session,
12800
+ turn
12801
+ });
12802
+ const handoffEvents = await options.trace?.list({
12803
+ sessionId: session.id,
12804
+ turnId: turn.id,
12805
+ type: "agent.handoff"
12806
+ }) ?? [];
12807
+ const handoffs = handoffEvents.map(toHandoffExpectation);
12808
+ const turnIssues = [];
12809
+ const expected = contractTurn.expect;
12810
+ const outcome = resolveOutcome3(result);
12811
+ if (expected?.finalAgentId && result.agentId !== expected.finalAgentId) {
12812
+ appendIssue(turnIssues, {
12813
+ code: "agent_squad.final_agent_mismatch",
12814
+ message: `Expected final agent ${expected.finalAgentId}, saw ${result.agentId}.`
12815
+ }, turn.id);
12816
+ }
12817
+ if (expected?.outcome && outcome !== expected.outcome) {
12818
+ appendIssue(turnIssues, {
12819
+ code: "agent_squad.outcome_mismatch",
12820
+ message: `Expected outcome ${expected.outcome}, saw ${outcome ?? "none"}.`
12821
+ }, turn.id);
12822
+ }
12823
+ if (expected?.transferTarget && result.transfer?.target !== expected.transferTarget) {
12824
+ appendIssue(turnIssues, {
12825
+ code: "agent_squad.transfer_target_mismatch",
12826
+ message: `Expected transfer target ${expected.transferTarget}, saw ${result.transfer?.target ?? "none"}.`
12827
+ }, turn.id);
12828
+ }
12829
+ const assistantText = normalizeIncludes(result.assistantText ?? "");
12830
+ for (const expectedText of expected?.assistantIncludes ?? []) {
12831
+ if (!assistantText.includes(normalizeIncludes(expectedText))) {
12832
+ appendIssue(turnIssues, {
12833
+ code: "agent_squad.assistant_text_missing",
12834
+ message: `Expected assistant text to include: ${expectedText}`
12835
+ }, turn.id);
12836
+ }
12837
+ }
12838
+ for (const [handoffIndex, expectedHandoff] of (expected?.handoffs ?? []).entries()) {
12839
+ const actual = handoffs[handoffIndex];
12840
+ if (!actual) {
12841
+ appendIssue(turnIssues, {
12842
+ code: "agent_squad.handoff_missing",
12843
+ message: `Expected handoff ${handoffIndex + 1}, but no trace event was recorded.`
12844
+ }, turn.id);
12845
+ continue;
12846
+ }
12847
+ for (const key of ["fromAgentId", "status", "targetAgentId"]) {
12848
+ if (expectedHandoff[key] && actual[key] !== expectedHandoff[key]) {
12849
+ appendIssue(turnIssues, {
12850
+ code: "agent_squad.handoff_mismatch",
12851
+ message: `Expected handoff ${handoffIndex + 1} ${key} ${expectedHandoff[key]}, saw ${actual[key] ?? "none"}.`
12852
+ }, turn.id);
12853
+ }
12854
+ }
12855
+ }
12856
+ for (const issue of expected?.result?.({
12857
+ result: result.result,
12858
+ routeResult: result
12859
+ }) ?? []) {
12860
+ appendIssue(turnIssues, issue, turn.id);
12861
+ }
12862
+ issues.push(...turnIssues);
12863
+ turnReports.push({
12864
+ agentId: result.agentId,
12865
+ handoffs,
12866
+ issues: turnIssues,
12867
+ outcome,
12868
+ pass: turnIssues.length === 0,
12869
+ result,
12870
+ turnId: turn.id
12871
+ });
12872
+ session.turns.push({
12873
+ ...turn,
12874
+ assistantText: result.assistantText,
12875
+ result: result.result
12876
+ });
12877
+ }
12878
+ return {
12879
+ contractId: options.contract.id,
12880
+ issues,
12881
+ pass: issues.length === 0,
12882
+ scenarioId: options.contract.scenarioId,
12883
+ sessionId: session.id,
12884
+ turns: turnReports
12885
+ };
12886
+ };
12887
+ var assertVoiceAgentSquadContract = async (options) => {
12888
+ const report = await runVoiceAgentSquadContract(options);
12889
+ if (!report.pass) {
12890
+ throw new Error(`Voice agent squad contract ${report.contractId} failed: ${report.issues.map((issue) => issue.message).join(" ")}`);
12891
+ }
12892
+ return report;
12893
+ };
12649
12894
  // src/turnLatency.ts
12650
12895
  import { Elysia as Elysia21 } from "elysia";
12651
12896
  var DEFAULT_WARN_AFTER_MS = 1800;
@@ -19692,6 +19937,7 @@ export {
19692
19937
  runVoiceOutcomeContractSuite,
19693
19938
  runVoiceCampaignProof,
19694
19939
  runVoiceCampaignDialerProof,
19940
+ runVoiceAgentSquadContract,
19695
19941
  resolveVoiceTraceRedactionOptions,
19696
19942
  resolveVoiceTelephonyOutcome,
19697
19943
  resolveVoiceSTTRoutingStrategy,
@@ -19969,6 +20215,7 @@ export {
19969
20215
  buildVoiceDiagnosticsMarkdown,
19970
20216
  buildVoiceCampaignObservabilityReport,
19971
20217
  assignVoiceOpsTask,
20218
+ assertVoiceAgentSquadContract,
19972
20219
  applyVoiceTelephonyOutcome,
19973
20220
  applyVoiceOpsTaskPolicy,
19974
20221
  applyVoiceOpsTaskAssignmentRule,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@absolutejs/voice",
3
- "version": "0.0.22-beta.122",
3
+ "version": "0.0.22-beta.124",
4
4
  "description": "Voice primitives and Elysia plugin for AbsoluteJS",
5
5
  "repository": {
6
6
  "type": "git",