@absolutejs/voice 0.0.22-beta.121 → 0.0.22-beta.123

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
@@ -224,9 +224,9 @@ app.use(
224
224
 
225
225
  The suite rolls up session quality, scenario evals, fixture simulations, tool contracts, and outcome contracts into one pass/fail report. It is the code-owned equivalent of "test this voice flow before production" without requiring a hosted voice-agent dashboard.
226
226
 
227
- ## Phone Voice Agent Path
227
+ ## Phone Voice Agent In 20 Minutes
228
228
 
229
- Use the telephony primitives when the agent needs to answer or place calls through your own carrier account:
229
+ Use `createVoicePhoneAgent(...)` when the agent needs to answer or place calls through your own Twilio, Telnyx, or Plivo account. This is the self-hosted alternative to a hosted phone-agent dashboard: your app owns the carrier routes, stream URLs, webhooks, traces, readiness checks, and lifecycle outcomes.
230
230
 
231
231
  ```ts
232
232
  import {
@@ -242,6 +242,10 @@ const outcomePolicy = createVoiceTelephonyOutcomePolicy({
242
242
  app
243
243
  .use(
244
244
  createVoicePhoneAgent({
245
+ setup: {
246
+ path: '/api/voice/phone/setup',
247
+ title: 'Support Phone Agent'
248
+ },
245
249
  matrix: {
246
250
  path: '/api/carriers',
247
251
  title: 'AbsoluteJS Voice Carrier Matrix'
@@ -274,7 +278,42 @@ app
274
278
  );
275
279
  ```
276
280
 
277
- The wrapper mounts selected carrier routes and a readiness matrix. 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.
281
+ The wrapper mounts selected carrier routes plus two proof surfaces:
282
+
283
+ - `/api/voice/phone/setup`: one setup report with carrier URLs, smoke links, lifecycle stages, and readiness.
284
+ - `/api/voice/phone/setup?format=html`: copy/paste setup page for carrier dashboards.
285
+ - `/api/carriers`: carrier matrix JSON for Twilio, Telnyx, and Plivo.
286
+ - `/api/carriers?format=html`: side-by-side carrier readiness matrix.
287
+
288
+ The setup page tells you exactly what to copy into the carrier dashboard:
289
+
290
+ - Twilio: set the phone number voice webhook/TwiML URL to the reported TwiML URL, set the status callback to the reported webhook URL, and allow the reported `wss://` media stream.
291
+ - Telnyx: set the connection TeXML URL to the reported TeXML URL, set the status webhook to the reported webhook URL, and allow the reported `wss://` media stream.
292
+ - Plivo: set the answer URL to the reported answer URL, set the status callback to the reported webhook URL, and allow the reported `wss://` media stream.
293
+
294
+ Each configured carrier can also expose its own setup and smoke pages, for example:
295
+
296
+ - `/api/voice/twilio/setup?format=html`
297
+ - `/api/voice/twilio/smoke?format=html`
298
+ - `/api/voice/telnyx/setup?format=html`
299
+ - `/api/voice/telnyx/smoke?format=html`
300
+ - `/api/voice/plivo/setup?format=html`
301
+ - `/api/voice/plivo/smoke?format=html`
302
+
303
+ The phone-agent report normalizes the lifecycle schema across carriers:
304
+
305
+ - `ringing`
306
+ - `answered`
307
+ - `media-started`
308
+ - `transcript`
309
+ - `assistant-response`
310
+ - `transfer`
311
+ - `voicemail`
312
+ - `no-answer`
313
+ - `completed`
314
+ - `failed`
315
+
316
+ 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.
278
317
 
279
318
  ## App Kit And Status Widgets
280
319
 
@@ -524,7 +563,21 @@ const billingAgent = createVoiceAgent({
524
563
  const frontDesk = createVoiceAgentSquad({
525
564
  id: 'front-desk',
526
565
  defaultAgentId: 'support',
527
- 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
+ }
528
581
  });
529
582
 
530
583
  voice({
@@ -538,6 +591,8 @@ voice({
538
591
 
539
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`.
540
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
+
541
596
  ## Traces And Replay
542
597
 
543
598
  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: {
package/dist/index.d.ts CHANGED
@@ -81,7 +81,7 @@ export type { VoiceProductionReadinessAction, VoiceProductionReadinessCheck, Voi
81
81
  export type { VoiceQualityLink, VoiceQualityMetric, VoiceQualityReport, VoiceQualityRoutesOptions, VoiceQualityStatus, VoiceQualityThresholds } from './qualityRoutes';
82
82
  export type { VoiceResilienceIOSimulator, VoiceResilienceLink, VoiceResiliencePageData, VoiceResilienceRoutesOptions, VoiceResilienceSimulationProvider, VoiceRoutingKindSummary, VoiceRoutingDecisionSummary, VoiceRoutingDecisionSummaryOptions, VoiceRoutingEvent, VoiceRoutingEventKind, VoiceRoutingSessionSummary, VoiceRoutingSessionSummaryOptions } from './resilienceRoutes';
83
83
  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';
84
+ export type { VoiceAgent, VoiceAgentMessage, VoiceAgentMessageRole, VoiceAgentModel, VoiceAgentModelInput, VoiceAgentModelOutput, VoiceAgentOptions, VoiceAgentRunResult, VoiceAgentSquadHandoffPolicyResult, VoiceAgentSquadOptions, VoiceAgentTool, VoiceAgentToolCall, VoiceAgentToolResult } from './agent';
85
85
  export type { VoiceToolRetryDelay, VoiceToolRuntime, VoiceToolRuntimeExecuteInput, VoiceToolRuntimeOptions, VoiceToolRuntimeResult } from './toolRuntime';
86
86
  export type { VoiceToolContractCase, VoiceToolContractCaseReport, VoiceToolContractDefinition, VoiceToolContractExpectation, VoiceToolContractHandlerOptions, VoiceToolContractHTMLHandlerOptions, VoiceToolContractIssue, VoiceToolContractReport, VoiceToolContractRoutesOptions, VoiceToolContractSuiteReport } from './toolContract';
87
87
  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,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@absolutejs/voice",
3
- "version": "0.0.22-beta.121",
3
+ "version": "0.0.22-beta.123",
4
4
  "description": "Voice primitives and Elysia plugin for AbsoluteJS",
5
5
  "repository": {
6
6
  "type": "git",