@absolutejs/voice 0.0.22-beta.123 → 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
@@ -593,6 +593,54 @@ voice({
593
593
 
594
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
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
+
596
644
  ## Traces And Replay
597
645
 
598
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.
@@ -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';
@@ -82,6 +83,7 @@ export type { VoiceQualityLink, VoiceQualityMetric, VoiceQualityReport, VoiceQua
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
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
@@ -12733,6 +12733,164 @@ var createVoiceWorkflowContractHandler = (input) => {
12733
12733
  return result;
12734
12734
  };
12735
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
+ };
12736
12894
  // src/turnLatency.ts
12737
12895
  import { Elysia as Elysia21 } from "elysia";
12738
12896
  var DEFAULT_WARN_AFTER_MS = 1800;
@@ -19779,6 +19937,7 @@ export {
19779
19937
  runVoiceOutcomeContractSuite,
19780
19938
  runVoiceCampaignProof,
19781
19939
  runVoiceCampaignDialerProof,
19940
+ runVoiceAgentSquadContract,
19782
19941
  resolveVoiceTraceRedactionOptions,
19783
19942
  resolveVoiceTelephonyOutcome,
19784
19943
  resolveVoiceSTTRoutingStrategy,
@@ -20056,6 +20215,7 @@ export {
20056
20215
  buildVoiceDiagnosticsMarkdown,
20057
20216
  buildVoiceCampaignObservabilityReport,
20058
20217
  assignVoiceOpsTask,
20218
+ assertVoiceAgentSquadContract,
20059
20219
  applyVoiceTelephonyOutcome,
20060
20220
  applyVoiceOpsTaskPolicy,
20061
20221
  applyVoiceOpsTaskAssignmentRule,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@absolutejs/voice",
3
- "version": "0.0.22-beta.123",
3
+ "version": "0.0.22-beta.124",
4
4
  "description": "Voice primitives and Elysia plugin for AbsoluteJS",
5
5
  "repository": {
6
6
  "type": "git",