@absolutejs/voice 0.0.22-beta.1 → 0.0.22-beta.2

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
@@ -73,6 +73,88 @@ const app = new Elysia()
73
73
 
74
74
  `createVoiceMemoryStore()` is dev-only. Real deployments should provide a shared store backed by Redis, Postgres, or equivalent.
75
75
 
76
+ ## Voice Assistants
77
+
78
+ Use `createVoiceAssistant(...)` when you want one product-level surface for a voice agent instead of wiring tools, guardrails, experiments, traces, and ops recipes separately. It returns a standard `onTurn` handler, plus an `ops` object you can pass to `voice(...)`.
79
+
80
+ ```ts
81
+ import {
82
+ createVoiceAssistant,
83
+ createVoiceExperiment,
84
+ createVoiceFileRuntimeStorage,
85
+ createVoiceMemoryStore,
86
+ createVoiceAgentTool,
87
+ voice
88
+ } from '@absolutejs/voice';
89
+ import { deepgram } from '@absolutejs/voice-deepgram';
90
+
91
+ const runtimeStorage = createVoiceFileRuntimeStorage({
92
+ directory: '.voice-runtime/support'
93
+ });
94
+
95
+ const lookupOrder = createVoiceAgentTool({
96
+ name: 'lookup_order',
97
+ description: 'Look up an order by id.',
98
+ execute: async ({ args }) => ({ orderId: args.orderId, status: 'shipped' })
99
+ });
100
+
101
+ const assistant = createVoiceAssistant({
102
+ id: 'support',
103
+ artifactPlan: {
104
+ ops: {
105
+ events: runtimeStorage.events,
106
+ reviews: runtimeStorage.reviews,
107
+ tasks: runtimeStorage.tasks
108
+ },
109
+ preset: {
110
+ name: 'support-triage',
111
+ options: {
112
+ queue: 'support-triage'
113
+ }
114
+ }
115
+ },
116
+ experiment: createVoiceExperiment({
117
+ id: 'support-prompt',
118
+ variants: [
119
+ { id: 'baseline', weight: 1 },
120
+ {
121
+ id: 'direct',
122
+ weight: 1,
123
+ system: 'You are concise, practical, and resolve the caller quickly.'
124
+ }
125
+ ]
126
+ }),
127
+ guardrails: {
128
+ beforeTurn: ({ turn }) =>
129
+ turn.text.toLowerCase().includes('human')
130
+ ? { escalate: { reason: 'caller requested a human' } }
131
+ : undefined
132
+ },
133
+ model: {
134
+ async generate({ messages, tools }) {
135
+ return {
136
+ assistantText: `I can help. Available tools: ${tools.map((tool) => tool.name).join(', ')}`
137
+ };
138
+ }
139
+ },
140
+ system: 'You are a support voice assistant.',
141
+ tools: [lookupOrder],
142
+ trace: runtimeStorage.traces
143
+ });
144
+
145
+ voice({
146
+ path: '/voice',
147
+ session: createVoiceMemoryStore(),
148
+ stt: deepgram({ apiKey: process.env.DEEPGRAM_API_KEY! }),
149
+ trace: runtimeStorage.traces,
150
+ ops: assistant.ops,
151
+ onTurn: assistant.onTurn,
152
+ onComplete: async () => {}
153
+ });
154
+ ```
155
+
156
+ Assistant experiments are deterministic by session id, so a caller stays on the same variant for a call. Variants can change the model, system prompt, tools, and tool-round budget; guardrails can block a turn before model execution or rewrite the returned `VoiceRouteResult`.
157
+
76
158
  ## Agent Tools And Squads
77
159
 
78
160
  For assistant-style products, use `createVoiceAgent(...)` as the `onTurn` handler. The agent layer is provider-neutral: plug in any model adapter, register server-side tools, and return normal voice route results like `assistantText`, `transfer`, `escalate`, or `complete`.
@@ -0,0 +1,99 @@
1
+ import { type VoiceAgent, type VoiceAgentModel, type VoiceAgentOptions, type VoiceAgentSquadOptions, type VoiceAgentTool } from './agent';
2
+ import { type VoiceOutcomeRecipeName, type VoiceOutcomeRecipeOptions } from './outcomeRecipes';
3
+ import type { VoiceNormalizedRouteConfig, VoiceOnTurnObjectHandler, VoiceRouteConfig, VoiceRouteResult, VoiceRuntimeOpsConfig, VoiceSessionRecord } from './types';
4
+ export type VoiceAssistantPreset = VoiceOutcomeRecipeName;
5
+ export type VoiceAssistantArtifactPlan<TContext = unknown, TSession extends VoiceSessionRecord = VoiceSessionRecord, TResult = unknown> = {
6
+ ops?: VoiceRuntimeOpsConfig<TContext, TSession, TResult>;
7
+ preset?: VoiceAssistantPreset | {
8
+ name: VoiceAssistantPreset;
9
+ options?: VoiceOutcomeRecipeOptions;
10
+ };
11
+ };
12
+ export type VoiceAssistantGuardrailInput<TContext = unknown, TSession extends VoiceSessionRecord = VoiceSessionRecord, TResult = unknown> = Parameters<VoiceOnTurnObjectHandler<TContext, TSession, TResult>>[0] & {
13
+ assistantId: string;
14
+ };
15
+ export type VoiceAssistantOutputGuardrailInput<TContext = unknown, TSession extends VoiceSessionRecord = VoiceSessionRecord, TResult = unknown> = VoiceAssistantGuardrailInput<TContext, TSession, TResult> & {
16
+ result: VoiceRouteResult<TResult>;
17
+ };
18
+ export type VoiceAssistantGuardrails<TContext = unknown, TSession extends VoiceSessionRecord = VoiceSessionRecord, TResult = unknown> = {
19
+ beforeTurn?: (input: VoiceAssistantGuardrailInput<TContext, TSession, TResult>) => Promise<VoiceRouteResult<TResult> | void> | VoiceRouteResult<TResult> | void;
20
+ afterTurn?: (input: VoiceAssistantOutputGuardrailInput<TContext, TSession, TResult>) => Promise<VoiceRouteResult<TResult> | void> | VoiceRouteResult<TResult> | void;
21
+ };
22
+ export type VoiceAssistantVariant<TContext = unknown, TSession extends VoiceSessionRecord = VoiceSessionRecord, TResult = unknown> = {
23
+ id: string;
24
+ maxToolRounds?: number;
25
+ metadata?: Record<string, unknown>;
26
+ model?: VoiceAgentModel<TContext, TSession, TResult>;
27
+ system?: VoiceAgentOptions<TContext, TSession, TResult>['system'];
28
+ tools?: Array<VoiceAgentTool<TContext, TSession, Record<string, unknown>, unknown, TResult>>;
29
+ weight?: number;
30
+ };
31
+ export type VoiceAssistantExperimentResolverInput<TContext = unknown, TSession extends VoiceSessionRecord = VoiceSessionRecord> = {
32
+ assistantId: string;
33
+ context: TContext;
34
+ session: TSession;
35
+ turnId?: string;
36
+ };
37
+ export type VoiceAssistantExperiment<TContext = unknown, TSession extends VoiceSessionRecord = VoiceSessionRecord, TResult = unknown> = {
38
+ id: string;
39
+ resolve: (input: VoiceAssistantExperimentResolverInput<TContext, TSession>) => VoiceAssistantVariant<TContext, TSession, TResult>;
40
+ variants: Array<VoiceAssistantVariant<TContext, TSession, TResult>>;
41
+ };
42
+ export type VoiceAssistantExperimentOptions<TContext = unknown, TSession extends VoiceSessionRecord = VoiceSessionRecord, TResult = unknown> = {
43
+ id: string;
44
+ selectVariant?: (input: VoiceAssistantExperimentResolverInput<TContext, TSession> & {
45
+ variants: Array<VoiceAssistantVariant<TContext, TSession, TResult>>;
46
+ }) => VoiceAssistantVariant<TContext, TSession, TResult> | string | void;
47
+ variants: Array<VoiceAssistantVariant<TContext, TSession, TResult>>;
48
+ };
49
+ type VoiceAssistantAgentSource<TContext, TSession extends VoiceSessionRecord, TResult> = {
50
+ agent: VoiceAgent<TContext, TSession, TResult>;
51
+ agents?: never;
52
+ defaultAgentId?: never;
53
+ maxHandoffsPerTurn?: never;
54
+ maxToolRounds?: never;
55
+ model?: never;
56
+ selectAgent?: never;
57
+ system?: never;
58
+ tools?: never;
59
+ } | {
60
+ agent?: never;
61
+ agents: Array<VoiceAgent<TContext, TSession, TResult>>;
62
+ defaultAgentId: string;
63
+ maxHandoffsPerTurn?: number;
64
+ maxToolRounds?: never;
65
+ model?: never;
66
+ selectAgent?: VoiceAgentSquadOptions<TContext, TSession, TResult>['selectAgent'];
67
+ system?: never;
68
+ tools?: never;
69
+ } | {
70
+ agent?: never;
71
+ agents?: never;
72
+ defaultAgentId?: never;
73
+ maxHandoffsPerTurn?: never;
74
+ maxToolRounds?: number;
75
+ model: VoiceAgentModel<TContext, TSession, TResult>;
76
+ selectAgent?: never;
77
+ system?: VoiceAgentOptions<TContext, TSession, TResult>['system'];
78
+ tools?: Array<VoiceAgentTool<TContext, TSession, Record<string, unknown>, unknown, TResult>>;
79
+ };
80
+ export type VoiceAssistantOptions<TContext = unknown, TSession extends VoiceSessionRecord = VoiceSessionRecord, TResult = unknown> = VoiceAssistantAgentSource<TContext, TSession, TResult> & {
81
+ artifactPlan?: VoiceAssistantArtifactPlan<TContext, TSession, TResult>;
82
+ experiment?: VoiceAssistantExperiment<TContext, TSession, TResult>;
83
+ guardrails?: VoiceAssistantGuardrails<TContext, TSession, TResult>;
84
+ id: string;
85
+ ops?: VoiceRuntimeOpsConfig<TContext, TSession, TResult>;
86
+ trace?: VoiceAgentOptions<TContext, TSession, TResult>['trace'];
87
+ };
88
+ export type VoiceAssistant<TContext = unknown, TSession extends VoiceSessionRecord = VoiceSessionRecord, TResult = unknown> = {
89
+ agent: VoiceAgent<TContext, TSession, TResult>;
90
+ id: string;
91
+ onTurn: VoiceOnTurnObjectHandler<TContext, TSession, TResult>;
92
+ ops?: VoiceRuntimeOpsConfig<TContext, TSession, TResult>;
93
+ route: (overrides: Omit<VoiceRouteConfig<TContext, TSession, TResult>, 'onComplete' | 'onTurn'> & {
94
+ onComplete?: VoiceRouteConfig<TContext, TSession, TResult>['onComplete'];
95
+ }) => VoiceNormalizedRouteConfig<TContext, TSession, TResult>;
96
+ };
97
+ export declare const createVoiceExperiment: <TContext = unknown, TSession extends VoiceSessionRecord = VoiceSessionRecord, TResult = unknown>(options: VoiceAssistantExperimentOptions<TContext, TSession, TResult>) => VoiceAssistantExperiment<TContext, TSession, TResult>;
98
+ export declare const createVoiceAssistant: <TContext = unknown, TSession extends VoiceSessionRecord = VoiceSessionRecord, TResult = unknown>(options: VoiceAssistantOptions<TContext, TSession, TResult>) => VoiceAssistant<TContext, TSession, TResult>;
99
+ export {};
package/dist/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export { voice } from './plugin';
2
+ export { createVoiceAssistant, createVoiceExperiment } from './assistant';
2
3
  export { createVoiceAgent, createVoiceAgentSquad, createVoiceAgentTool } from './agent';
3
4
  export { createStoredVoiceCallReviewArtifact, createStoredVoiceExternalObjectMap, createStoredVoiceIntegrationEvent, createStoredVoiceOpsTask, createVoiceFileExternalObjectMapStore, createVoiceFileIntegrationEventStore, createVoiceFileReviewStore, createVoiceFileRuntimeStorage, createVoiceFileSessionStore, createVoiceFileTaskStore, createVoiceFileTraceSinkDeliveryStore, createVoiceFileTraceEventStore } from './fileStore';
4
5
  export { buildVoiceTraceReplay, createVoiceMemoryTraceSinkDeliveryStore, createVoiceTraceHTTPSink, createVoiceMemoryTraceEventStore, createVoiceTraceSinkDeliveryId, createVoiceTraceSinkDeliveryRecord, createVoiceTraceSinkStore, createVoiceTraceEvent, createVoiceTraceEventId, deliverVoiceTraceEventsToSinks, evaluateVoiceTrace, exportVoiceTrace, filterVoiceTraceEvents, pruneVoiceTraceEvents, redactVoiceTraceEvent, redactVoiceTraceEvents, redactVoiceTraceText, renderVoiceTraceHTML, renderVoiceTraceMarkdown, resolveVoiceTraceRedactionOptions, selectVoiceTraceEventsForPrune, summarizeVoiceTrace } from './trace';
@@ -21,6 +22,7 @@ export { conditionAudioChunk, resolveAudioConditioningConfig } from './audioCond
21
22
  export { resolveVoiceRuntimePreset } from './presets';
22
23
  export { resolveTurnDetectionConfig, TURN_PROFILE_DEFAULTS } from './turnProfiles';
23
24
  export { createVoiceCallReviewFromLiveTelephonyReport, createVoiceCallReviewRecorder, renderVoiceCallReviewHTML, renderVoiceCallReviewMarkdown } from './testing/review';
25
+ export type { VoiceAssistant, VoiceAssistantArtifactPlan, VoiceAssistantExperiment, VoiceAssistantExperimentOptions, VoiceAssistantGuardrailInput, VoiceAssistantGuardrails, VoiceAssistantOptions, VoiceAssistantOutputGuardrailInput, VoiceAssistantPreset, VoiceAssistantVariant } from './assistant';
24
26
  export type { VoiceAgent, VoiceAgentMessage, VoiceAgentMessageRole, VoiceAgentModel, VoiceAgentModelInput, VoiceAgentModelOutput, VoiceAgentOptions, VoiceAgentRunResult, VoiceAgentSquadOptions, VoiceAgentTool, VoiceAgentToolCall, VoiceAgentToolResult } from './agent';
25
27
  export type { VoiceOpsRuntime, VoiceOpsRuntimeConfig, VoiceOpsRuntimeSummary, VoiceOpsRuntimeSinkWorkerConfig, VoiceOpsRuntimeTaskWorkerConfig, VoiceOpsRuntimeTickResult, VoiceOpsRuntimeWebhookWorkerConfig } from './opsRuntime';
26
28
  export type { VoiceOpsPresetName, VoiceOpsPresetOverrides, VoiceResolvedOpsPreset } from './opsPresets';
package/dist/index.js CHANGED
@@ -5368,6 +5368,381 @@ var createVoiceAgentSquad = (options) => {
5368
5368
  run
5369
5369
  };
5370
5370
  };
5371
+
5372
+ // src/outcomeRecipes.ts
5373
+ var RECIPE_DEFAULTS = {
5374
+ "appointment-booking": {
5375
+ completedAction: "Verify appointment details, confirm calendar state, and send any required confirmation.",
5376
+ completedDescription: "The call completed an appointment-booking flow and should be checked against the scheduling system.",
5377
+ completedKind: "appointment-booking",
5378
+ completedTitle: "Confirm booked appointment",
5379
+ defaultCompletedCreatesTask: true,
5380
+ defaultDueInMs: 30 * 60000,
5381
+ defaultPriority: "normal",
5382
+ defaultQueue: "appointments",
5383
+ description: "Creates appointment confirmation work for completed calls and callback/retry work for missed booking attempts.",
5384
+ escalationQueue: "appointments-escalations"
5385
+ },
5386
+ "lead-qualification": {
5387
+ completedAction: "Review qualification signals, update CRM fields, and route the lead to the right owner.",
5388
+ completedDescription: "The call completed a lead-qualification flow and should be reviewed for sales follow-up.",
5389
+ completedKind: "lead-qualification",
5390
+ completedTitle: "Review qualified lead",
5391
+ defaultCompletedCreatesTask: true,
5392
+ defaultDueInMs: 15 * 60000,
5393
+ defaultPriority: "high",
5394
+ defaultQueue: "sales-leads",
5395
+ description: "Creates sales follow-up work for completed qualification calls and fast callbacks for missed leads.",
5396
+ escalationQueue: "sales-escalations"
5397
+ },
5398
+ "support-triage": {
5399
+ completedAction: "Review the triage result, confirm the support category, and route any unresolved issue.",
5400
+ completedDescription: "The call completed support triage and may need queue routing or human follow-up.",
5401
+ completedKind: "support-triage",
5402
+ completedTitle: "Review support triage",
5403
+ defaultCompletedCreatesTask: true,
5404
+ defaultDueInMs: 20 * 60000,
5405
+ defaultPriority: "normal",
5406
+ defaultQueue: "support-triage",
5407
+ description: "Creates support triage work for completed calls and urgent escalation/callback work for unresolved callers.",
5408
+ escalationQueue: "support-escalations"
5409
+ },
5410
+ "voicemail-callback": {
5411
+ completedAction: "No callback is required for completed calls.",
5412
+ completedDescription: "The call completed without requiring voicemail follow-up.",
5413
+ completedKind: "callback",
5414
+ completedTitle: "Completed call",
5415
+ defaultCompletedCreatesTask: false,
5416
+ defaultDueInMs: 15 * 60000,
5417
+ defaultPriority: "high",
5418
+ defaultQueue: "callbacks",
5419
+ description: "Creates callback work for voicemail, no-answer, failed, or escalated calls while ignoring completed calls.",
5420
+ escalationQueue: "callback-escalations"
5421
+ },
5422
+ "warm-transfer": {
5423
+ completedAction: "Confirm the handoff target received the caller context and close the transfer loop.",
5424
+ completedDescription: "The call is part of a warm-transfer flow and should be verified downstream.",
5425
+ completedKind: "transfer-check",
5426
+ completedTitle: "Verify warm transfer",
5427
+ defaultCompletedCreatesTask: false,
5428
+ defaultDueInMs: 10 * 60000,
5429
+ defaultPriority: "normal",
5430
+ defaultQueue: "transfer-verification",
5431
+ description: "Creates transfer verification work for transferred calls and escalation work when the handoff fails.",
5432
+ escalationQueue: "transfer-escalations"
5433
+ }
5434
+ };
5435
+ var buildRecipeTask = (input) => {
5436
+ const createdAt = input.review.generatedAt ?? Date.now();
5437
+ const queue = input.options.queue ?? input.defaults.defaultQueue;
5438
+ const target = input.options.target ?? input.review.postCall?.target;
5439
+ const common = {
5440
+ assignee: input.options.assignee,
5441
+ createdAt,
5442
+ history: [
5443
+ {
5444
+ actor: "system",
5445
+ at: createdAt,
5446
+ detail: input.review.postCall?.summary,
5447
+ type: "created"
5448
+ }
5449
+ ],
5450
+ id: `${input.review.id}:${input.defaults.completedKind}`,
5451
+ intakeId: input.review.id,
5452
+ outcome: input.review.summary.outcome,
5453
+ priority: input.options.priority ?? input.defaults.defaultPriority,
5454
+ queue,
5455
+ reviewId: input.review.id,
5456
+ status: "open",
5457
+ target,
5458
+ updatedAt: createdAt
5459
+ };
5460
+ switch (input.disposition) {
5461
+ case "completed":
5462
+ if (!(input.options.completedCreatesTask ?? input.defaults.defaultCompletedCreatesTask)) {
5463
+ return null;
5464
+ }
5465
+ return {
5466
+ ...common,
5467
+ description: input.defaults.completedDescription,
5468
+ kind: input.defaults.completedKind,
5469
+ recommendedAction: input.defaults.completedAction,
5470
+ title: target ? `${input.defaults.completedTitle}: ${target}` : input.defaults.completedTitle
5471
+ };
5472
+ case "voicemail":
5473
+ return {
5474
+ ...common,
5475
+ description: input.review.postCall?.summary ?? "The caller reached voicemail and needs a callback.",
5476
+ id: `${input.review.id}:callback`,
5477
+ kind: "callback",
5478
+ recommendedAction: input.review.postCall?.recommendedAction ?? "Call the customer back and continue the original flow.",
5479
+ title: target ? `Call back ${target}` : "Call back voicemail lead"
5480
+ };
5481
+ case "no-answer":
5482
+ return {
5483
+ ...common,
5484
+ description: input.review.postCall?.summary ?? "The call did not reach a live respondent and should be retried.",
5485
+ id: `${input.review.id}:retry`,
5486
+ kind: "callback",
5487
+ recommendedAction: input.review.postCall?.recommendedAction ?? "Retry the call or schedule a callback.",
5488
+ title: "Retry no-answer call"
5489
+ };
5490
+ case "transferred":
5491
+ return {
5492
+ ...common,
5493
+ description: input.review.postCall?.summary ?? "The call was transferred and should be verified downstream.",
5494
+ id: `${input.review.id}:transfer-check`,
5495
+ kind: "transfer-check",
5496
+ recommendedAction: input.review.postCall?.recommendedAction ?? "Confirm the receiving team got the caller context.",
5497
+ title: target ? `Verify transfer to ${target}` : "Verify call transfer"
5498
+ };
5499
+ case "escalated":
5500
+ return {
5501
+ ...common,
5502
+ description: input.review.postCall?.summary ?? "The call escalated and needs human review.",
5503
+ id: `${input.review.id}:escalation`,
5504
+ kind: "escalation",
5505
+ priority: "urgent",
5506
+ queue: input.options.escalationQueue ?? input.defaults.escalationQueue,
5507
+ assignee: input.options.escalationAssignee ?? input.options.assignee,
5508
+ recommendedAction: input.review.postCall?.recommendedAction ?? "Review the escalated call and respond immediately.",
5509
+ title: "Review escalated call"
5510
+ };
5511
+ case "failed":
5512
+ case "closed":
5513
+ return {
5514
+ ...common,
5515
+ description: input.review.postCall?.summary ?? "The call ended before successful completion and needs review.",
5516
+ id: `${input.review.id}:retry-review`,
5517
+ kind: "retry-review",
5518
+ priority: "high",
5519
+ recommendedAction: input.review.postCall?.recommendedAction ?? "Inspect the call and decide whether to retry, escalate, or close.",
5520
+ title: "Inspect incomplete call"
5521
+ };
5522
+ default:
5523
+ return null;
5524
+ }
5525
+ };
5526
+ var resolveVoiceOutcomeRecipe = (name, options = {}) => {
5527
+ const defaults = RECIPE_DEFAULTS[name];
5528
+ const taskPolicies = {
5529
+ completed: {
5530
+ assignee: options.assignee,
5531
+ dueInMs: options.dueInMs ?? defaults.defaultDueInMs,
5532
+ name: `${name}-completed`,
5533
+ priority: options.priority ?? defaults.defaultPriority,
5534
+ queue: options.queue ?? defaults.defaultQueue
5535
+ },
5536
+ escalated: {
5537
+ assignee: options.escalationAssignee ?? options.assignee,
5538
+ dueInMs: Math.min(options.dueInMs ?? defaults.defaultDueInMs, 10 * 60000),
5539
+ name: `${name}-escalation`,
5540
+ priority: "urgent",
5541
+ queue: options.escalationQueue ?? defaults.escalationQueue
5542
+ },
5543
+ failed: {
5544
+ assignee: options.assignee,
5545
+ dueInMs: options.dueInMs ?? defaults.defaultDueInMs,
5546
+ name: `${name}-failed-review`,
5547
+ priority: "high",
5548
+ queue: options.queue ?? defaults.defaultQueue
5549
+ },
5550
+ "no-answer": {
5551
+ assignee: options.assignee,
5552
+ dueInMs: options.dueInMs ?? defaults.defaultDueInMs,
5553
+ name: `${name}-no-answer`,
5554
+ priority: options.priority ?? defaults.defaultPriority,
5555
+ queue: options.queue ?? defaults.defaultQueue
5556
+ },
5557
+ transferred: {
5558
+ assignee: options.assignee,
5559
+ dueInMs: Math.min(options.dueInMs ?? defaults.defaultDueInMs, 20 * 60000),
5560
+ name: `${name}-transfer-check`,
5561
+ priority: options.priority ?? defaults.defaultPriority,
5562
+ queue: name === "warm-transfer" ? options.queue ?? defaults.defaultQueue : "transfer-verification"
5563
+ },
5564
+ voicemail: {
5565
+ assignee: options.assignee,
5566
+ dueInMs: options.dueInMs ?? defaults.defaultDueInMs,
5567
+ name: `${name}-voicemail`,
5568
+ priority: options.priority ?? defaults.defaultPriority,
5569
+ queue: options.queue ?? defaults.defaultQueue
5570
+ }
5571
+ };
5572
+ const taskAssignmentRules = [
5573
+ {
5574
+ assign: options.escalationAssignee ?? options.assignee,
5575
+ description: `Route urgent ${name} work to the escalation lane.`,
5576
+ name: `${name}-urgent-routing`,
5577
+ queue: options.escalationQueue ?? defaults.escalationQueue,
5578
+ when: {
5579
+ priority: "urgent"
5580
+ }
5581
+ }
5582
+ ].filter((rule) => rule.assign || rule.queue);
5583
+ return {
5584
+ createTaskFromReview: ({ disposition, review }) => buildRecipeTask({
5585
+ defaults,
5586
+ disposition,
5587
+ options,
5588
+ review
5589
+ }),
5590
+ description: defaults.description,
5591
+ name,
5592
+ taskAssignmentRules,
5593
+ taskPolicies
5594
+ };
5595
+ };
5596
+
5597
+ // src/assistant.ts
5598
+ var hashString = (value) => {
5599
+ let hash = 2166136261;
5600
+ for (let index = 0;index < value.length; index += 1) {
5601
+ hash ^= value.charCodeAt(index);
5602
+ hash = Math.imul(hash, 16777619);
5603
+ }
5604
+ return hash >>> 0;
5605
+ };
5606
+ var resolvePresetOps = (artifactPlan) => {
5607
+ const preset = artifactPlan?.preset;
5608
+ if (!preset) {
5609
+ return artifactPlan?.ops;
5610
+ }
5611
+ const recipe = typeof preset === "string" ? resolveVoiceOutcomeRecipe(preset) : resolveVoiceOutcomeRecipe(preset.name, preset.options);
5612
+ return {
5613
+ ...recipe,
5614
+ ...artifactPlan?.ops
5615
+ };
5616
+ };
5617
+ var mergeOps = (base, override) => {
5618
+ if (!base && !override) {
5619
+ return;
5620
+ }
5621
+ return {
5622
+ ...base,
5623
+ ...override,
5624
+ taskAssignmentRules: base?.taskAssignmentRules || override?.taskAssignmentRules ? [
5625
+ ...base?.taskAssignmentRules ?? [],
5626
+ ...override?.taskAssignmentRules ?? []
5627
+ ] : undefined,
5628
+ taskPolicies: base?.taskPolicies || override?.taskPolicies ? {
5629
+ ...base?.taskPolicies ?? {},
5630
+ ...override?.taskPolicies ?? {}
5631
+ } : undefined
5632
+ };
5633
+ };
5634
+ var createVoiceExperiment = (options) => {
5635
+ if (!options.variants.length) {
5636
+ throw new Error("createVoiceExperiment requires at least one variant.");
5637
+ }
5638
+ const firstVariant = options.variants[0];
5639
+ return {
5640
+ id: options.id,
5641
+ resolve: (input) => {
5642
+ const selected = options.selectVariant?.({
5643
+ ...input,
5644
+ variants: options.variants
5645
+ });
5646
+ if (selected && typeof selected !== "object") {
5647
+ const variant = options.variants.find((item) => item.id === selected);
5648
+ if (variant) {
5649
+ return variant;
5650
+ }
5651
+ }
5652
+ if (selected && typeof selected === "object" && "id" in selected) {
5653
+ return selected;
5654
+ }
5655
+ const totalWeight = options.variants.reduce((total, variant) => total + Math.max(0, variant.weight ?? 1), 0);
5656
+ if (totalWeight <= 0) {
5657
+ return firstVariant;
5658
+ }
5659
+ const bucket = hashString(`${options.id}:${input.assistantId}:${input.session.id}`) % totalWeight;
5660
+ let cursor = 0;
5661
+ for (const variant of options.variants) {
5662
+ cursor += Math.max(0, variant.weight ?? 1);
5663
+ if (bucket < cursor) {
5664
+ return variant;
5665
+ }
5666
+ }
5667
+ return firstVariant;
5668
+ },
5669
+ variants: options.variants
5670
+ };
5671
+ };
5672
+ var createVoiceAssistant = (options) => {
5673
+ const ops = mergeOps(resolvePresetOps(options.artifactPlan), options.ops);
5674
+ let agent;
5675
+ const baseModelOptions = "model" in options && options.model ? {
5676
+ maxToolRounds: options.maxToolRounds,
5677
+ model: options.model,
5678
+ system: options.system,
5679
+ tools: options.tools
5680
+ } : undefined;
5681
+ if ("agent" in options && options.agent) {
5682
+ agent = options.agent;
5683
+ } else if ("agents" in options && options.agents) {
5684
+ agent = createVoiceAgentSquad({
5685
+ agents: options.agents,
5686
+ defaultAgentId: options.defaultAgentId,
5687
+ id: options.id,
5688
+ maxHandoffsPerTurn: options.maxHandoffsPerTurn,
5689
+ selectAgent: options.selectAgent,
5690
+ trace: options.trace
5691
+ });
5692
+ } else {
5693
+ agent = createVoiceAgent({
5694
+ id: options.id,
5695
+ maxToolRounds: options.maxToolRounds,
5696
+ model: options.model,
5697
+ system: options.system,
5698
+ trace: options.trace,
5699
+ tools: options.tools
5700
+ });
5701
+ }
5702
+ const onTurn = async (input) => {
5703
+ const guardrailInput = {
5704
+ ...input,
5705
+ assistantId: options.id
5706
+ };
5707
+ const blocked = await options.guardrails?.beforeTurn?.(guardrailInput);
5708
+ if (blocked) {
5709
+ return blocked;
5710
+ }
5711
+ const variant = options.experiment?.resolve({
5712
+ assistantId: options.id,
5713
+ context: input.context,
5714
+ session: input.session,
5715
+ turnId: input.turn.id
5716
+ });
5717
+ const runner = variant && baseModelOptions ? createVoiceAgent({
5718
+ id: `${options.id}:${variant.id}`,
5719
+ maxToolRounds: variant.maxToolRounds ?? baseModelOptions.maxToolRounds,
5720
+ model: variant.model ?? baseModelOptions.model,
5721
+ system: variant.system ?? baseModelOptions.system,
5722
+ trace: options.trace,
5723
+ tools: variant.tools ?? baseModelOptions.tools
5724
+ }) : agent;
5725
+ const result = await runner.run(input) ?? {};
5726
+ const guarded = await options.guardrails?.afterTurn?.({
5727
+ ...guardrailInput,
5728
+ result
5729
+ });
5730
+ return guarded ?? result;
5731
+ };
5732
+ return {
5733
+ agent,
5734
+ id: options.id,
5735
+ onTurn,
5736
+ ops,
5737
+ route: (overrides) => ({
5738
+ ...overrides,
5739
+ onComplete: overrides.onComplete ?? (() => {
5740
+ return;
5741
+ }),
5742
+ onTurn
5743
+ })
5744
+ };
5745
+ };
5371
5746
  // src/fileStore.ts
5372
5747
  import { mkdir, readFile, readdir, rename, rm, writeFile } from "fs/promises";
5373
5748
  import { join } from "path";
@@ -7987,230 +8362,6 @@ var resolveVoiceOpsPreset = (name, overrides = {}) => {
7987
8362
  taskPolicies: mergePolicies(preset.taskPolicies, overrides.taskPolicies)
7988
8363
  };
7989
8364
  };
7990
- // src/outcomeRecipes.ts
7991
- var RECIPE_DEFAULTS = {
7992
- "appointment-booking": {
7993
- completedAction: "Verify appointment details, confirm calendar state, and send any required confirmation.",
7994
- completedDescription: "The call completed an appointment-booking flow and should be checked against the scheduling system.",
7995
- completedKind: "appointment-booking",
7996
- completedTitle: "Confirm booked appointment",
7997
- defaultCompletedCreatesTask: true,
7998
- defaultDueInMs: 30 * 60000,
7999
- defaultPriority: "normal",
8000
- defaultQueue: "appointments",
8001
- description: "Creates appointment confirmation work for completed calls and callback/retry work for missed booking attempts.",
8002
- escalationQueue: "appointments-escalations"
8003
- },
8004
- "lead-qualification": {
8005
- completedAction: "Review qualification signals, update CRM fields, and route the lead to the right owner.",
8006
- completedDescription: "The call completed a lead-qualification flow and should be reviewed for sales follow-up.",
8007
- completedKind: "lead-qualification",
8008
- completedTitle: "Review qualified lead",
8009
- defaultCompletedCreatesTask: true,
8010
- defaultDueInMs: 15 * 60000,
8011
- defaultPriority: "high",
8012
- defaultQueue: "sales-leads",
8013
- description: "Creates sales follow-up work for completed qualification calls and fast callbacks for missed leads.",
8014
- escalationQueue: "sales-escalations"
8015
- },
8016
- "support-triage": {
8017
- completedAction: "Review the triage result, confirm the support category, and route any unresolved issue.",
8018
- completedDescription: "The call completed support triage and may need queue routing or human follow-up.",
8019
- completedKind: "support-triage",
8020
- completedTitle: "Review support triage",
8021
- defaultCompletedCreatesTask: true,
8022
- defaultDueInMs: 20 * 60000,
8023
- defaultPriority: "normal",
8024
- defaultQueue: "support-triage",
8025
- description: "Creates support triage work for completed calls and urgent escalation/callback work for unresolved callers.",
8026
- escalationQueue: "support-escalations"
8027
- },
8028
- "voicemail-callback": {
8029
- completedAction: "No callback is required for completed calls.",
8030
- completedDescription: "The call completed without requiring voicemail follow-up.",
8031
- completedKind: "callback",
8032
- completedTitle: "Completed call",
8033
- defaultCompletedCreatesTask: false,
8034
- defaultDueInMs: 15 * 60000,
8035
- defaultPriority: "high",
8036
- defaultQueue: "callbacks",
8037
- description: "Creates callback work for voicemail, no-answer, failed, or escalated calls while ignoring completed calls.",
8038
- escalationQueue: "callback-escalations"
8039
- },
8040
- "warm-transfer": {
8041
- completedAction: "Confirm the handoff target received the caller context and close the transfer loop.",
8042
- completedDescription: "The call is part of a warm-transfer flow and should be verified downstream.",
8043
- completedKind: "transfer-check",
8044
- completedTitle: "Verify warm transfer",
8045
- defaultCompletedCreatesTask: false,
8046
- defaultDueInMs: 10 * 60000,
8047
- defaultPriority: "normal",
8048
- defaultQueue: "transfer-verification",
8049
- description: "Creates transfer verification work for transferred calls and escalation work when the handoff fails.",
8050
- escalationQueue: "transfer-escalations"
8051
- }
8052
- };
8053
- var buildRecipeTask = (input) => {
8054
- const createdAt = input.review.generatedAt ?? Date.now();
8055
- const queue = input.options.queue ?? input.defaults.defaultQueue;
8056
- const target = input.options.target ?? input.review.postCall?.target;
8057
- const common = {
8058
- assignee: input.options.assignee,
8059
- createdAt,
8060
- history: [
8061
- {
8062
- actor: "system",
8063
- at: createdAt,
8064
- detail: input.review.postCall?.summary,
8065
- type: "created"
8066
- }
8067
- ],
8068
- id: `${input.review.id}:${input.defaults.completedKind}`,
8069
- intakeId: input.review.id,
8070
- outcome: input.review.summary.outcome,
8071
- priority: input.options.priority ?? input.defaults.defaultPriority,
8072
- queue,
8073
- reviewId: input.review.id,
8074
- status: "open",
8075
- target,
8076
- updatedAt: createdAt
8077
- };
8078
- switch (input.disposition) {
8079
- case "completed":
8080
- if (!(input.options.completedCreatesTask ?? input.defaults.defaultCompletedCreatesTask)) {
8081
- return null;
8082
- }
8083
- return {
8084
- ...common,
8085
- description: input.defaults.completedDescription,
8086
- kind: input.defaults.completedKind,
8087
- recommendedAction: input.defaults.completedAction,
8088
- title: target ? `${input.defaults.completedTitle}: ${target}` : input.defaults.completedTitle
8089
- };
8090
- case "voicemail":
8091
- return {
8092
- ...common,
8093
- description: input.review.postCall?.summary ?? "The caller reached voicemail and needs a callback.",
8094
- id: `${input.review.id}:callback`,
8095
- kind: "callback",
8096
- recommendedAction: input.review.postCall?.recommendedAction ?? "Call the customer back and continue the original flow.",
8097
- title: target ? `Call back ${target}` : "Call back voicemail lead"
8098
- };
8099
- case "no-answer":
8100
- return {
8101
- ...common,
8102
- description: input.review.postCall?.summary ?? "The call did not reach a live respondent and should be retried.",
8103
- id: `${input.review.id}:retry`,
8104
- kind: "callback",
8105
- recommendedAction: input.review.postCall?.recommendedAction ?? "Retry the call or schedule a callback.",
8106
- title: "Retry no-answer call"
8107
- };
8108
- case "transferred":
8109
- return {
8110
- ...common,
8111
- description: input.review.postCall?.summary ?? "The call was transferred and should be verified downstream.",
8112
- id: `${input.review.id}:transfer-check`,
8113
- kind: "transfer-check",
8114
- recommendedAction: input.review.postCall?.recommendedAction ?? "Confirm the receiving team got the caller context.",
8115
- title: target ? `Verify transfer to ${target}` : "Verify call transfer"
8116
- };
8117
- case "escalated":
8118
- return {
8119
- ...common,
8120
- description: input.review.postCall?.summary ?? "The call escalated and needs human review.",
8121
- id: `${input.review.id}:escalation`,
8122
- kind: "escalation",
8123
- priority: "urgent",
8124
- queue: input.options.escalationQueue ?? input.defaults.escalationQueue,
8125
- assignee: input.options.escalationAssignee ?? input.options.assignee,
8126
- recommendedAction: input.review.postCall?.recommendedAction ?? "Review the escalated call and respond immediately.",
8127
- title: "Review escalated call"
8128
- };
8129
- case "failed":
8130
- case "closed":
8131
- return {
8132
- ...common,
8133
- description: input.review.postCall?.summary ?? "The call ended before successful completion and needs review.",
8134
- id: `${input.review.id}:retry-review`,
8135
- kind: "retry-review",
8136
- priority: "high",
8137
- recommendedAction: input.review.postCall?.recommendedAction ?? "Inspect the call and decide whether to retry, escalate, or close.",
8138
- title: "Inspect incomplete call"
8139
- };
8140
- default:
8141
- return null;
8142
- }
8143
- };
8144
- var resolveVoiceOutcomeRecipe = (name, options = {}) => {
8145
- const defaults = RECIPE_DEFAULTS[name];
8146
- const taskPolicies = {
8147
- completed: {
8148
- assignee: options.assignee,
8149
- dueInMs: options.dueInMs ?? defaults.defaultDueInMs,
8150
- name: `${name}-completed`,
8151
- priority: options.priority ?? defaults.defaultPriority,
8152
- queue: options.queue ?? defaults.defaultQueue
8153
- },
8154
- escalated: {
8155
- assignee: options.escalationAssignee ?? options.assignee,
8156
- dueInMs: Math.min(options.dueInMs ?? defaults.defaultDueInMs, 10 * 60000),
8157
- name: `${name}-escalation`,
8158
- priority: "urgent",
8159
- queue: options.escalationQueue ?? defaults.escalationQueue
8160
- },
8161
- failed: {
8162
- assignee: options.assignee,
8163
- dueInMs: options.dueInMs ?? defaults.defaultDueInMs,
8164
- name: `${name}-failed-review`,
8165
- priority: "high",
8166
- queue: options.queue ?? defaults.defaultQueue
8167
- },
8168
- "no-answer": {
8169
- assignee: options.assignee,
8170
- dueInMs: options.dueInMs ?? defaults.defaultDueInMs,
8171
- name: `${name}-no-answer`,
8172
- priority: options.priority ?? defaults.defaultPriority,
8173
- queue: options.queue ?? defaults.defaultQueue
8174
- },
8175
- transferred: {
8176
- assignee: options.assignee,
8177
- dueInMs: Math.min(options.dueInMs ?? defaults.defaultDueInMs, 20 * 60000),
8178
- name: `${name}-transfer-check`,
8179
- priority: options.priority ?? defaults.defaultPriority,
8180
- queue: name === "warm-transfer" ? options.queue ?? defaults.defaultQueue : "transfer-verification"
8181
- },
8182
- voicemail: {
8183
- assignee: options.assignee,
8184
- dueInMs: options.dueInMs ?? defaults.defaultDueInMs,
8185
- name: `${name}-voicemail`,
8186
- priority: options.priority ?? defaults.defaultPriority,
8187
- queue: options.queue ?? defaults.defaultQueue
8188
- }
8189
- };
8190
- const taskAssignmentRules = [
8191
- {
8192
- assign: options.escalationAssignee ?? options.assignee,
8193
- description: `Route urgent ${name} work to the escalation lane.`,
8194
- name: `${name}-urgent-routing`,
8195
- queue: options.escalationQueue ?? defaults.escalationQueue,
8196
- when: {
8197
- priority: "urgent"
8198
- }
8199
- }
8200
- ].filter((rule) => rule.assign || rule.queue);
8201
- return {
8202
- createTaskFromReview: ({ disposition, review }) => buildRecipeTask({
8203
- defaults,
8204
- disposition,
8205
- options,
8206
- review
8207
- }),
8208
- description: defaults.description,
8209
- name,
8210
- taskAssignmentRules,
8211
- taskPolicies
8212
- };
8213
- };
8214
8365
  // src/correction.ts
8215
8366
  var escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
8216
8367
  var buildAliasMatcher = (alias) => new RegExp(`(?<![\\p{L}\\p{N}'])${escapeRegExp(alias)}(?![\\p{L}\\p{N}'])`, "giu");
@@ -9097,11 +9248,13 @@ export {
9097
9248
  createVoiceFileExternalObjectMapStore,
9098
9249
  createVoiceExternalObjectMapId,
9099
9250
  createVoiceExternalObjectMap,
9251
+ createVoiceExperiment,
9100
9252
  createVoiceCallReviewRecorder,
9101
9253
  createVoiceCallReviewFromSession,
9102
9254
  createVoiceCallReviewFromLiveTelephonyReport,
9103
9255
  createVoiceCallCompletedEvent,
9104
9256
  createVoiceCRMActivitySink,
9257
+ createVoiceAssistant,
9105
9258
  createVoiceAgentTool,
9106
9259
  createVoiceAgentSquad,
9107
9260
  createVoiceAgent,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@absolutejs/voice",
3
- "version": "0.0.22-beta.1",
3
+ "version": "0.0.22-beta.2",
4
4
  "description": "Voice primitives and Elysia plugin for AbsoluteJS",
5
5
  "repository": {
6
6
  "type": "git",