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

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/dist/index.js CHANGED
@@ -5150,6 +5150,17 @@ var createVoiceAgent = (options) => {
5150
5150
  if (output.assistantText?.trim()) {
5151
5151
  messages.push({
5152
5152
  content: output.assistantText,
5153
+ metadata: output.toolCalls?.length ? {
5154
+ toolCalls: output.toolCalls
5155
+ } : undefined,
5156
+ role: "assistant"
5157
+ });
5158
+ } else if (output.toolCalls?.length) {
5159
+ messages.push({
5160
+ content: "",
5161
+ metadata: {
5162
+ toolCalls: output.toolCalls
5163
+ },
5153
5164
  role: "assistant"
5154
5165
  });
5155
5166
  }
@@ -5352,20 +5363,742 @@ var createVoiceAgentSquad = (options) => {
5352
5363
  agentId = nextAgent.id;
5353
5364
  result = await agent.run({
5354
5365
  ...input,
5355
- messages
5366
+ messages
5367
+ });
5368
+ toolResults.push(...result.toolResults);
5369
+ }
5370
+ return {
5371
+ ...result,
5372
+ agentId,
5373
+ toolResults
5374
+ };
5375
+ };
5376
+ return {
5377
+ id: options.id,
5378
+ onTurn: async (input) => run(input),
5379
+ run
5380
+ };
5381
+ };
5382
+
5383
+ // src/outcomeRecipes.ts
5384
+ var RECIPE_DEFAULTS = {
5385
+ "appointment-booking": {
5386
+ completedAction: "Verify appointment details, confirm calendar state, and send any required confirmation.",
5387
+ completedDescription: "The call completed an appointment-booking flow and should be checked against the scheduling system.",
5388
+ completedKind: "appointment-booking",
5389
+ completedTitle: "Confirm booked appointment",
5390
+ defaultCompletedCreatesTask: true,
5391
+ defaultDueInMs: 30 * 60000,
5392
+ defaultPriority: "normal",
5393
+ defaultQueue: "appointments",
5394
+ description: "Creates appointment confirmation work for completed calls and callback/retry work for missed booking attempts.",
5395
+ escalationQueue: "appointments-escalations"
5396
+ },
5397
+ "lead-qualification": {
5398
+ completedAction: "Review qualification signals, update CRM fields, and route the lead to the right owner.",
5399
+ completedDescription: "The call completed a lead-qualification flow and should be reviewed for sales follow-up.",
5400
+ completedKind: "lead-qualification",
5401
+ completedTitle: "Review qualified lead",
5402
+ defaultCompletedCreatesTask: true,
5403
+ defaultDueInMs: 15 * 60000,
5404
+ defaultPriority: "high",
5405
+ defaultQueue: "sales-leads",
5406
+ description: "Creates sales follow-up work for completed qualification calls and fast callbacks for missed leads.",
5407
+ escalationQueue: "sales-escalations"
5408
+ },
5409
+ "support-triage": {
5410
+ completedAction: "Review the triage result, confirm the support category, and route any unresolved issue.",
5411
+ completedDescription: "The call completed support triage and may need queue routing or human follow-up.",
5412
+ completedKind: "support-triage",
5413
+ completedTitle: "Review support triage",
5414
+ defaultCompletedCreatesTask: true,
5415
+ defaultDueInMs: 20 * 60000,
5416
+ defaultPriority: "normal",
5417
+ defaultQueue: "support-triage",
5418
+ description: "Creates support triage work for completed calls and urgent escalation/callback work for unresolved callers.",
5419
+ escalationQueue: "support-escalations"
5420
+ },
5421
+ "voicemail-callback": {
5422
+ completedAction: "No callback is required for completed calls.",
5423
+ completedDescription: "The call completed without requiring voicemail follow-up.",
5424
+ completedKind: "callback",
5425
+ completedTitle: "Completed call",
5426
+ defaultCompletedCreatesTask: false,
5427
+ defaultDueInMs: 15 * 60000,
5428
+ defaultPriority: "high",
5429
+ defaultQueue: "callbacks",
5430
+ description: "Creates callback work for voicemail, no-answer, failed, or escalated calls while ignoring completed calls.",
5431
+ escalationQueue: "callback-escalations"
5432
+ },
5433
+ "warm-transfer": {
5434
+ completedAction: "Confirm the handoff target received the caller context and close the transfer loop.",
5435
+ completedDescription: "The call is part of a warm-transfer flow and should be verified downstream.",
5436
+ completedKind: "transfer-check",
5437
+ completedTitle: "Verify warm transfer",
5438
+ defaultCompletedCreatesTask: false,
5439
+ defaultDueInMs: 10 * 60000,
5440
+ defaultPriority: "normal",
5441
+ defaultQueue: "transfer-verification",
5442
+ description: "Creates transfer verification work for transferred calls and escalation work when the handoff fails.",
5443
+ escalationQueue: "transfer-escalations"
5444
+ }
5445
+ };
5446
+ var buildRecipeTask = (input) => {
5447
+ const createdAt = input.review.generatedAt ?? Date.now();
5448
+ const queue = input.options.queue ?? input.defaults.defaultQueue;
5449
+ const target = input.options.target ?? input.review.postCall?.target;
5450
+ const common = {
5451
+ assignee: input.options.assignee,
5452
+ createdAt,
5453
+ history: [
5454
+ {
5455
+ actor: "system",
5456
+ at: createdAt,
5457
+ detail: input.review.postCall?.summary,
5458
+ type: "created"
5459
+ }
5460
+ ],
5461
+ id: `${input.review.id}:${input.defaults.completedKind}`,
5462
+ intakeId: input.review.id,
5463
+ outcome: input.review.summary.outcome,
5464
+ priority: input.options.priority ?? input.defaults.defaultPriority,
5465
+ queue,
5466
+ reviewId: input.review.id,
5467
+ status: "open",
5468
+ target,
5469
+ updatedAt: createdAt
5470
+ };
5471
+ switch (input.disposition) {
5472
+ case "completed":
5473
+ if (!(input.options.completedCreatesTask ?? input.defaults.defaultCompletedCreatesTask)) {
5474
+ return null;
5475
+ }
5476
+ return {
5477
+ ...common,
5478
+ description: input.defaults.completedDescription,
5479
+ kind: input.defaults.completedKind,
5480
+ recommendedAction: input.defaults.completedAction,
5481
+ title: target ? `${input.defaults.completedTitle}: ${target}` : input.defaults.completedTitle
5482
+ };
5483
+ case "voicemail":
5484
+ return {
5485
+ ...common,
5486
+ description: input.review.postCall?.summary ?? "The caller reached voicemail and needs a callback.",
5487
+ id: `${input.review.id}:callback`,
5488
+ kind: "callback",
5489
+ recommendedAction: input.review.postCall?.recommendedAction ?? "Call the customer back and continue the original flow.",
5490
+ title: target ? `Call back ${target}` : "Call back voicemail lead"
5491
+ };
5492
+ case "no-answer":
5493
+ return {
5494
+ ...common,
5495
+ description: input.review.postCall?.summary ?? "The call did not reach a live respondent and should be retried.",
5496
+ id: `${input.review.id}:retry`,
5497
+ kind: "callback",
5498
+ recommendedAction: input.review.postCall?.recommendedAction ?? "Retry the call or schedule a callback.",
5499
+ title: "Retry no-answer call"
5500
+ };
5501
+ case "transferred":
5502
+ return {
5503
+ ...common,
5504
+ description: input.review.postCall?.summary ?? "The call was transferred and should be verified downstream.",
5505
+ id: `${input.review.id}:transfer-check`,
5506
+ kind: "transfer-check",
5507
+ recommendedAction: input.review.postCall?.recommendedAction ?? "Confirm the receiving team got the caller context.",
5508
+ title: target ? `Verify transfer to ${target}` : "Verify call transfer"
5509
+ };
5510
+ case "escalated":
5511
+ return {
5512
+ ...common,
5513
+ description: input.review.postCall?.summary ?? "The call escalated and needs human review.",
5514
+ id: `${input.review.id}:escalation`,
5515
+ kind: "escalation",
5516
+ priority: "urgent",
5517
+ queue: input.options.escalationQueue ?? input.defaults.escalationQueue,
5518
+ assignee: input.options.escalationAssignee ?? input.options.assignee,
5519
+ recommendedAction: input.review.postCall?.recommendedAction ?? "Review the escalated call and respond immediately.",
5520
+ title: "Review escalated call"
5521
+ };
5522
+ case "failed":
5523
+ case "closed":
5524
+ return {
5525
+ ...common,
5526
+ description: input.review.postCall?.summary ?? "The call ended before successful completion and needs review.",
5527
+ id: `${input.review.id}:retry-review`,
5528
+ kind: "retry-review",
5529
+ priority: "high",
5530
+ recommendedAction: input.review.postCall?.recommendedAction ?? "Inspect the call and decide whether to retry, escalate, or close.",
5531
+ title: "Inspect incomplete call"
5532
+ };
5533
+ default:
5534
+ return null;
5535
+ }
5536
+ };
5537
+ var resolveVoiceOutcomeRecipe = (name, options = {}) => {
5538
+ const defaults = RECIPE_DEFAULTS[name];
5539
+ const taskPolicies = {
5540
+ completed: {
5541
+ assignee: options.assignee,
5542
+ dueInMs: options.dueInMs ?? defaults.defaultDueInMs,
5543
+ name: `${name}-completed`,
5544
+ priority: options.priority ?? defaults.defaultPriority,
5545
+ queue: options.queue ?? defaults.defaultQueue
5546
+ },
5547
+ escalated: {
5548
+ assignee: options.escalationAssignee ?? options.assignee,
5549
+ dueInMs: Math.min(options.dueInMs ?? defaults.defaultDueInMs, 10 * 60000),
5550
+ name: `${name}-escalation`,
5551
+ priority: "urgent",
5552
+ queue: options.escalationQueue ?? defaults.escalationQueue
5553
+ },
5554
+ failed: {
5555
+ assignee: options.assignee,
5556
+ dueInMs: options.dueInMs ?? defaults.defaultDueInMs,
5557
+ name: `${name}-failed-review`,
5558
+ priority: "high",
5559
+ queue: options.queue ?? defaults.defaultQueue
5560
+ },
5561
+ "no-answer": {
5562
+ assignee: options.assignee,
5563
+ dueInMs: options.dueInMs ?? defaults.defaultDueInMs,
5564
+ name: `${name}-no-answer`,
5565
+ priority: options.priority ?? defaults.defaultPriority,
5566
+ queue: options.queue ?? defaults.defaultQueue
5567
+ },
5568
+ transferred: {
5569
+ assignee: options.assignee,
5570
+ dueInMs: Math.min(options.dueInMs ?? defaults.defaultDueInMs, 20 * 60000),
5571
+ name: `${name}-transfer-check`,
5572
+ priority: options.priority ?? defaults.defaultPriority,
5573
+ queue: name === "warm-transfer" ? options.queue ?? defaults.defaultQueue : "transfer-verification"
5574
+ },
5575
+ voicemail: {
5576
+ assignee: options.assignee,
5577
+ dueInMs: options.dueInMs ?? defaults.defaultDueInMs,
5578
+ name: `${name}-voicemail`,
5579
+ priority: options.priority ?? defaults.defaultPriority,
5580
+ queue: options.queue ?? defaults.defaultQueue
5581
+ }
5582
+ };
5583
+ const taskAssignmentRules = [
5584
+ {
5585
+ assign: options.escalationAssignee ?? options.assignee,
5586
+ description: `Route urgent ${name} work to the escalation lane.`,
5587
+ name: `${name}-urgent-routing`,
5588
+ queue: options.escalationQueue ?? defaults.escalationQueue,
5589
+ when: {
5590
+ priority: "urgent"
5591
+ }
5592
+ }
5593
+ ].filter((rule) => rule.assign || rule.queue);
5594
+ return {
5595
+ createTaskFromReview: ({ disposition, review }) => buildRecipeTask({
5596
+ defaults,
5597
+ disposition,
5598
+ options,
5599
+ review
5600
+ }),
5601
+ description: defaults.description,
5602
+ name,
5603
+ taskAssignmentRules,
5604
+ taskPolicies
5605
+ };
5606
+ };
5607
+
5608
+ // src/assistantMemory.ts
5609
+ var createMemoryId = (input) => `${input.assistantId}:${input.namespace}:${input.key}`;
5610
+ var createVoiceAssistantMemoryRecord = (input) => {
5611
+ const now = Date.now();
5612
+ return {
5613
+ ...input,
5614
+ createdAt: input.createdAt ?? input.updatedAt ?? now,
5615
+ updatedAt: input.updatedAt ?? now
5616
+ };
5617
+ };
5618
+ var createVoiceMemoryAssistantMemoryStore = () => {
5619
+ const records = new Map;
5620
+ return {
5621
+ delete: async (input) => {
5622
+ records.delete(createMemoryId(input));
5623
+ },
5624
+ get: async (input) => records.get(createMemoryId(input)),
5625
+ list: async (input) => [...records.values()].filter((record) => record.assistantId === input.assistantId && (input.namespace === undefined || record.namespace === input.namespace)).sort((left, right) => right.updatedAt - left.updatedAt),
5626
+ set: async (input) => {
5627
+ const id = createMemoryId(input);
5628
+ const existing = records.get(id);
5629
+ const record = createVoiceAssistantMemoryRecord({
5630
+ ...input,
5631
+ createdAt: input.createdAt ?? existing?.createdAt,
5632
+ updatedAt: input.updatedAt
5633
+ });
5634
+ records.set(id, record);
5635
+ return record;
5636
+ }
5637
+ };
5638
+ };
5639
+ var resolveVoiceAssistantMemoryNamespace = async (input) => typeof input.memory.namespace === "function" ? await input.memory.namespace(input) : input.memory.namespace;
5640
+ var createVoiceAssistantMemoryHandle = async (input) => {
5641
+ const namespace = await resolveVoiceAssistantMemoryNamespace({
5642
+ assistantId: input.assistantId,
5643
+ context: input.context,
5644
+ memory: input.memory,
5645
+ session: input.session
5646
+ });
5647
+ const trace = async (event) => {
5648
+ await input.trace?.append({
5649
+ at: Date.now(),
5650
+ payload: {
5651
+ assistantId: input.assistantId,
5652
+ namespace,
5653
+ ...event
5654
+ },
5655
+ scenarioId: input.session.scenarioId,
5656
+ sessionId: input.session.id,
5657
+ type: "assistant.memory"
5658
+ });
5659
+ };
5660
+ return {
5661
+ delete: async (key) => {
5662
+ await input.memory.store.delete({
5663
+ assistantId: input.assistantId,
5664
+ key,
5665
+ namespace
5666
+ });
5667
+ await trace({
5668
+ action: "delete",
5669
+ key
5670
+ });
5671
+ },
5672
+ get: async (key) => {
5673
+ const record = await input.memory.store.get({
5674
+ assistantId: input.assistantId,
5675
+ key,
5676
+ namespace
5677
+ });
5678
+ await trace({
5679
+ action: "get",
5680
+ found: Boolean(record),
5681
+ key
5682
+ });
5683
+ return record?.value;
5684
+ },
5685
+ list: async () => {
5686
+ const records = await input.memory.store.list({
5687
+ assistantId: input.assistantId,
5688
+ namespace
5689
+ });
5690
+ await trace({
5691
+ action: "list",
5692
+ count: records.length
5693
+ });
5694
+ return records;
5695
+ },
5696
+ namespace,
5697
+ set: async (key, value, metadata) => {
5698
+ const record = await input.memory.store.set({
5699
+ assistantId: input.assistantId,
5700
+ key,
5701
+ metadata,
5702
+ namespace,
5703
+ value
5704
+ });
5705
+ await trace({
5706
+ action: "set",
5707
+ key
5708
+ });
5709
+ return record;
5710
+ }
5711
+ };
5712
+ };
5713
+
5714
+ // src/assistant.ts
5715
+ var hashString = (value) => {
5716
+ let hash = 2166136261;
5717
+ for (let index = 0;index < value.length; index += 1) {
5718
+ hash ^= value.charCodeAt(index);
5719
+ hash = Math.imul(hash, 16777619);
5720
+ }
5721
+ return hash >>> 0;
5722
+ };
5723
+ var increment = (record, key) => {
5724
+ record[key] = (record[key] ?? 0) + 1;
5725
+ };
5726
+ var resolveOutcome = (result) => {
5727
+ if (result.transfer) {
5728
+ return "transferred";
5729
+ }
5730
+ if (result.escalate) {
5731
+ return "escalated";
5732
+ }
5733
+ if (result.voicemail) {
5734
+ return "voicemail";
5735
+ }
5736
+ if (result.noAnswer) {
5737
+ return "no-answer";
5738
+ }
5739
+ if (result.complete) {
5740
+ return "completed";
5741
+ }
5742
+ return "continued";
5743
+ };
5744
+ var resolveArtifactPlanName = (artifactPlan) => {
5745
+ const preset = artifactPlan?.preset;
5746
+ if (!preset) {
5747
+ return artifactPlan?.ops ? "custom" : undefined;
5748
+ }
5749
+ return typeof preset === "string" ? preset : preset.name;
5750
+ };
5751
+ var appendAssistantTrace = async (input) => {
5752
+ await input.trace?.append({
5753
+ at: Date.now(),
5754
+ payload: {
5755
+ assistantId: input.assistantId,
5756
+ ...input.event
5757
+ },
5758
+ scenarioId: input.session.scenarioId,
5759
+ sessionId: input.session.id,
5760
+ turnId: input.turnId,
5761
+ type: input.type
5762
+ });
5763
+ };
5764
+ var resolvePresetOps = (artifactPlan) => {
5765
+ const preset = artifactPlan?.preset;
5766
+ if (!preset) {
5767
+ return artifactPlan?.ops;
5768
+ }
5769
+ const recipe = typeof preset === "string" ? resolveVoiceOutcomeRecipe(preset) : resolveVoiceOutcomeRecipe(preset.name, preset.options);
5770
+ return {
5771
+ ...recipe,
5772
+ ...artifactPlan?.ops
5773
+ };
5774
+ };
5775
+ var mergeOps = (base, override) => {
5776
+ if (!base && !override) {
5777
+ return;
5778
+ }
5779
+ return {
5780
+ ...base,
5781
+ ...override,
5782
+ taskAssignmentRules: base?.taskAssignmentRules || override?.taskAssignmentRules ? [
5783
+ ...base?.taskAssignmentRules ?? [],
5784
+ ...override?.taskAssignmentRules ?? []
5785
+ ] : undefined,
5786
+ taskPolicies: base?.taskPolicies || override?.taskPolicies ? {
5787
+ ...base?.taskPolicies ?? {},
5788
+ ...override?.taskPolicies ?? {}
5789
+ } : undefined
5790
+ };
5791
+ };
5792
+ var createVoiceExperiment = (options) => {
5793
+ if (!options.variants.length) {
5794
+ throw new Error("createVoiceExperiment requires at least one variant.");
5795
+ }
5796
+ const firstVariant = options.variants[0];
5797
+ return {
5798
+ id: options.id,
5799
+ resolve: (input) => {
5800
+ const selected = options.selectVariant?.({
5801
+ ...input,
5802
+ variants: options.variants
5803
+ });
5804
+ if (selected && typeof selected !== "object") {
5805
+ const variant = options.variants.find((item) => item.id === selected);
5806
+ if (variant) {
5807
+ return variant;
5808
+ }
5809
+ }
5810
+ if (selected && typeof selected === "object" && "id" in selected) {
5811
+ return selected;
5812
+ }
5813
+ const totalWeight = options.variants.reduce((total, variant) => total + Math.max(0, variant.weight ?? 1), 0);
5814
+ if (totalWeight <= 0) {
5815
+ return firstVariant;
5816
+ }
5817
+ const bucket = hashString(`${options.id}:${input.assistantId}:${input.session.id}`) % totalWeight;
5818
+ let cursor = 0;
5819
+ for (const variant of options.variants) {
5820
+ cursor += Math.max(0, variant.weight ?? 1);
5821
+ if (bucket < cursor) {
5822
+ return variant;
5823
+ }
5824
+ }
5825
+ return firstVariant;
5826
+ },
5827
+ variants: options.variants
5828
+ };
5829
+ };
5830
+ var createVoiceAssistant = (options) => {
5831
+ const ops = mergeOps(resolvePresetOps(options.artifactPlan), options.ops);
5832
+ const artifactPlanName = resolveArtifactPlanName(options.artifactPlan);
5833
+ let agent;
5834
+ const baseModelOptions = "model" in options && options.model ? {
5835
+ maxToolRounds: options.maxToolRounds,
5836
+ model: options.model,
5837
+ system: options.system,
5838
+ tools: options.tools
5839
+ } : undefined;
5840
+ if ("agent" in options && options.agent) {
5841
+ agent = options.agent;
5842
+ } else if ("agents" in options && options.agents) {
5843
+ agent = createVoiceAgentSquad({
5844
+ agents: options.agents,
5845
+ defaultAgentId: options.defaultAgentId,
5846
+ id: options.id,
5847
+ maxHandoffsPerTurn: options.maxHandoffsPerTurn,
5848
+ selectAgent: options.selectAgent,
5849
+ trace: options.trace
5850
+ });
5851
+ } else {
5852
+ agent = createVoiceAgent({
5853
+ id: options.id,
5854
+ maxToolRounds: options.maxToolRounds,
5855
+ model: options.model,
5856
+ system: options.system,
5857
+ trace: options.trace,
5858
+ tools: options.tools
5859
+ });
5860
+ }
5861
+ const onTurn = async (input) => {
5862
+ const memory = options.memory ? await createVoiceAssistantMemoryHandle({
5863
+ assistantId: options.id,
5864
+ context: input.context,
5865
+ memory: options.memory,
5866
+ session: input.session,
5867
+ trace: options.trace
5868
+ }) : undefined;
5869
+ const guardrailInput = {
5870
+ ...input,
5871
+ assistantId: options.id,
5872
+ memory
5873
+ };
5874
+ if (memory) {
5875
+ await options.memoryLifecycle?.beforeTurn?.({
5876
+ ...input,
5877
+ assistantId: options.id,
5878
+ memory
5879
+ });
5880
+ }
5881
+ const blocked = await options.guardrails?.beforeTurn?.(guardrailInput);
5882
+ if (blocked) {
5883
+ if (memory) {
5884
+ await options.memoryLifecycle?.afterTurn?.({
5885
+ ...input,
5886
+ assistantId: options.id,
5887
+ memory,
5888
+ result: blocked
5889
+ });
5890
+ }
5891
+ await appendAssistantTrace({
5892
+ assistantId: options.id,
5893
+ event: {
5894
+ action: "blocked",
5895
+ artifactPlan: artifactPlanName,
5896
+ outcome: resolveOutcome(blocked)
5897
+ },
5898
+ session: input.session,
5899
+ trace: options.trace,
5900
+ turnId: input.turn.id,
5901
+ type: "assistant.guardrail"
5902
+ });
5903
+ await appendAssistantTrace({
5904
+ assistantId: options.id,
5905
+ event: {
5906
+ artifactPlan: artifactPlanName,
5907
+ blocked: true,
5908
+ experimentId: options.experiment?.id,
5909
+ outcome: resolveOutcome(blocked)
5910
+ },
5911
+ session: input.session,
5912
+ trace: options.trace,
5913
+ turnId: input.turn.id,
5914
+ type: "assistant.run"
5915
+ });
5916
+ return blocked;
5917
+ }
5918
+ const startedAt = Date.now();
5919
+ const variant = options.experiment?.resolve({
5920
+ assistantId: options.id,
5921
+ context: input.context,
5922
+ session: input.session,
5923
+ turnId: input.turn.id
5924
+ });
5925
+ const runner = variant && baseModelOptions ? createVoiceAgent({
5926
+ id: `${options.id}:${variant.id}`,
5927
+ maxToolRounds: variant.maxToolRounds ?? baseModelOptions.maxToolRounds,
5928
+ model: variant.model ?? baseModelOptions.model,
5929
+ system: variant.system ?? baseModelOptions.system,
5930
+ trace: options.trace,
5931
+ tools: variant.tools ?? baseModelOptions.tools
5932
+ }) : agent;
5933
+ const runResult = await runner.run(input) ?? {};
5934
+ const result = runResult;
5935
+ const guarded = await options.guardrails?.afterTurn?.({
5936
+ ...guardrailInput,
5937
+ result
5938
+ });
5939
+ const finalResult = guarded ?? result;
5940
+ if (memory) {
5941
+ await options.memoryLifecycle?.afterTurn?.({
5942
+ ...input,
5943
+ assistantId: options.id,
5944
+ memory,
5945
+ result: finalResult
5356
5946
  });
5357
- toolResults.push(...result.toolResults);
5358
5947
  }
5359
- return {
5360
- ...result,
5361
- agentId,
5362
- toolResults
5363
- };
5948
+ if (guarded) {
5949
+ await appendAssistantTrace({
5950
+ assistantId: options.id,
5951
+ event: {
5952
+ action: "rewritten",
5953
+ artifactPlan: artifactPlanName,
5954
+ experimentId: options.experiment?.id,
5955
+ outcome: resolveOutcome(finalResult),
5956
+ variantId: variant?.id
5957
+ },
5958
+ session: input.session,
5959
+ trace: options.trace,
5960
+ turnId: input.turn.id,
5961
+ type: "assistant.guardrail"
5962
+ });
5963
+ }
5964
+ await appendAssistantTrace({
5965
+ assistantId: options.id,
5966
+ event: {
5967
+ artifactPlan: artifactPlanName,
5968
+ blocked: false,
5969
+ elapsedMs: Date.now() - startedAt,
5970
+ escalated: Boolean(finalResult.escalate),
5971
+ experimentId: options.experiment?.id,
5972
+ outcome: resolveOutcome(finalResult),
5973
+ toolNames: result.toolResults?.map((tool) => tool.toolName) ?? [],
5974
+ transferred: Boolean(finalResult.transfer),
5975
+ variantId: variant?.id
5976
+ },
5977
+ session: input.session,
5978
+ trace: options.trace,
5979
+ turnId: input.turn.id,
5980
+ type: "assistant.run"
5981
+ });
5982
+ return finalResult;
5364
5983
  };
5365
5984
  return {
5985
+ agent,
5366
5986
  id: options.id,
5367
- onTurn: async (input) => run(input),
5368
- run
5987
+ onTurn,
5988
+ ops,
5989
+ route: (overrides) => ({
5990
+ ...overrides,
5991
+ onComplete: overrides.onComplete ?? (() => {
5992
+ return;
5993
+ }),
5994
+ onTurn
5995
+ })
5996
+ };
5997
+ };
5998
+ var summarizeVoiceAssistantRuns = async (input) => {
5999
+ const events = Array.isArray(input) ? input : input.events ?? await input.store?.list() ?? [];
6000
+ const assistantRuns = events.filter((event) => event.type === "assistant.run");
6001
+ const guardrails = events.filter((event) => event.type === "assistant.guardrail");
6002
+ const byAssistant = new Map;
6003
+ const getSummary = (assistantId) => {
6004
+ let summary = byAssistant.get(assistantId);
6005
+ if (!summary) {
6006
+ summary = {
6007
+ assistantId,
6008
+ artifactPlans: {},
6009
+ blockedGuardrailCount: 0,
6010
+ elapsedCount: 0,
6011
+ elapsedTotal: 0,
6012
+ escalationCount: 0,
6013
+ experiments: {},
6014
+ guardrailCount: 0,
6015
+ memory: {
6016
+ deletes: 0,
6017
+ gets: 0,
6018
+ lists: 0,
6019
+ sets: 0
6020
+ },
6021
+ outcomes: {},
6022
+ runCount: 0,
6023
+ sessionIds: new Set,
6024
+ sessions: 0,
6025
+ toolCalls: {},
6026
+ transferCount: 0,
6027
+ variants: {}
6028
+ };
6029
+ byAssistant.set(assistantId, summary);
6030
+ }
6031
+ return summary;
6032
+ };
6033
+ for (const event of assistantRuns) {
6034
+ const assistantId = typeof event.payload.assistantId === "string" ? event.payload.assistantId : "unknown";
6035
+ const summary = getSummary(assistantId);
6036
+ summary.runCount += 1;
6037
+ summary.sessionIds.add(event.sessionId);
6038
+ if (typeof event.payload.artifactPlan === "string") {
6039
+ increment(summary.artifactPlans, event.payload.artifactPlan);
6040
+ }
6041
+ if (typeof event.payload.experimentId === "string") {
6042
+ increment(summary.experiments, event.payload.experimentId);
6043
+ }
6044
+ if (typeof event.payload.variantId === "string") {
6045
+ increment(summary.variants, event.payload.variantId);
6046
+ }
6047
+ if (typeof event.payload.outcome === "string") {
6048
+ increment(summary.outcomes, event.payload.outcome);
6049
+ }
6050
+ if (event.payload.escalated === true) {
6051
+ summary.escalationCount += 1;
6052
+ }
6053
+ if (event.payload.transferred === true) {
6054
+ summary.transferCount += 1;
6055
+ }
6056
+ if (event.payload.blocked === true) {
6057
+ summary.blockedGuardrailCount += 1;
6058
+ }
6059
+ if (typeof event.payload.elapsedMs === "number") {
6060
+ summary.elapsedCount += 1;
6061
+ summary.elapsedTotal += event.payload.elapsedMs;
6062
+ }
6063
+ if (Array.isArray(event.payload.toolNames)) {
6064
+ for (const toolName of event.payload.toolNames) {
6065
+ if (typeof toolName === "string") {
6066
+ increment(summary.toolCalls, toolName);
6067
+ }
6068
+ }
6069
+ }
6070
+ }
6071
+ for (const event of guardrails) {
6072
+ const assistantId = typeof event.payload.assistantId === "string" ? event.payload.assistantId : "unknown";
6073
+ const summary = getSummary(assistantId);
6074
+ summary.guardrailCount += 1;
6075
+ }
6076
+ for (const event of events.filter((event2) => event2.type === "assistant.memory")) {
6077
+ const assistantId = typeof event.payload.assistantId === "string" ? event.payload.assistantId : "unknown";
6078
+ const summary = getSummary(assistantId);
6079
+ switch (event.payload.action) {
6080
+ case "delete":
6081
+ summary.memory.deletes += 1;
6082
+ break;
6083
+ case "get":
6084
+ summary.memory.gets += 1;
6085
+ break;
6086
+ case "list":
6087
+ summary.memory.lists += 1;
6088
+ break;
6089
+ case "set":
6090
+ summary.memory.sets += 1;
6091
+ break;
6092
+ }
6093
+ }
6094
+ const assistants = [...byAssistant.values()].map(({ elapsedCount, elapsedTotal, sessionIds, ...summary }) => ({
6095
+ ...summary,
6096
+ averageElapsedMs: elapsedCount > 0 ? Math.round(elapsedTotal / elapsedCount) : undefined,
6097
+ sessions: sessionIds.size
6098
+ }));
6099
+ return {
6100
+ assistants: assistants.sort((left, right) => left.assistantId.localeCompare(right.assistantId)),
6101
+ totalRuns: assistantRuns.length
5369
6102
  };
5370
6103
  };
5371
6104
  // src/fileStore.ts
@@ -6052,6 +6785,7 @@ var listJsonFiles = async (directory) => {
6052
6785
  };
6053
6786
  var encodeStoreId = (id) => `${encodeURIComponent(id)}.json`;
6054
6787
  var resolveFilePath = (directory, id) => join(directory, encodeStoreId(id));
6788
+ var createMemoryStoreId = (input) => `${input.assistantId}:${input.namespace}:${input.key}`;
6055
6789
  var readJsonFile = async (path) => JSON.parse(await readFile(path, "utf8"));
6056
6790
  var writeJsonFile = async (path, value, options) => {
6057
6791
  await mkdir(options.directory, {
@@ -6271,6 +7005,40 @@ var createVoiceFileTraceSinkDeliveryStore = (options) => {
6271
7005
  };
6272
7006
  return { get, list, remove, set };
6273
7007
  };
7008
+ var createVoiceFileAssistantMemoryStore = (options) => {
7009
+ const get = async (input) => {
7010
+ const path = resolveFilePath(options.directory, createMemoryStoreId(input));
7011
+ try {
7012
+ return await readJsonFile(path);
7013
+ } catch (error) {
7014
+ if (error.code === "ENOENT") {
7015
+ return;
7016
+ }
7017
+ throw error;
7018
+ }
7019
+ };
7020
+ const list = async (input) => {
7021
+ const files = await listJsonFiles(options.directory);
7022
+ const records = await Promise.all(files.map((file) => readJsonFile(file)));
7023
+ return records.filter((record) => record.assistantId === input.assistantId && (input.namespace === undefined || record.namespace === input.namespace)).sort((left, right) => right.updatedAt - left.updatedAt);
7024
+ };
7025
+ const set = async (input) => {
7026
+ const existing = await get(input);
7027
+ const record = createVoiceAssistantMemoryRecord({
7028
+ ...input,
7029
+ createdAt: input.createdAt ?? existing?.createdAt,
7030
+ updatedAt: input.updatedAt
7031
+ });
7032
+ await writeJsonFile(resolveFilePath(options.directory, createMemoryStoreId(record)), record, options);
7033
+ return record;
7034
+ };
7035
+ const remove = async (input) => {
7036
+ await rm(resolveFilePath(options.directory, createMemoryStoreId(input)), {
7037
+ force: true
7038
+ });
7039
+ };
7040
+ return { delete: remove, get, list, set };
7041
+ };
6274
7042
  var createVoiceFileRuntimeStorage = (options) => ({
6275
7043
  events: createVoiceFileIntegrationEventStore({
6276
7044
  ...options,
@@ -6280,6 +7048,10 @@ var createVoiceFileRuntimeStorage = (options) => ({
6280
7048
  ...options,
6281
7049
  directory: join(options.directory, "external-objects")
6282
7050
  }),
7051
+ memories: createVoiceFileAssistantMemoryStore({
7052
+ ...options,
7053
+ directory: join(options.directory, "memories")
7054
+ }),
6283
7055
  reviews: createVoiceFileReviewStore({
6284
7056
  ...options,
6285
7057
  directory: join(options.directory, "reviews")
@@ -6312,6 +7084,593 @@ var createStoredVoiceExternalObjectMap = (mapping) => createVoiceExternalObjectM
6312
7084
  sourceId: mapping.sourceId,
6313
7085
  sourceType: mapping.sourceType
6314
7086
  });
7087
+ // src/modelAdapters.ts
7088
+ var OUTPUT_SCHEMA = {
7089
+ additionalProperties: false,
7090
+ properties: {
7091
+ assistantText: {
7092
+ type: "string"
7093
+ },
7094
+ complete: {
7095
+ type: "boolean"
7096
+ },
7097
+ escalate: {
7098
+ additionalProperties: false,
7099
+ properties: {
7100
+ metadata: {
7101
+ additionalProperties: true,
7102
+ type: "object"
7103
+ },
7104
+ reason: {
7105
+ type: "string"
7106
+ }
7107
+ },
7108
+ required: ["reason"],
7109
+ type: "object"
7110
+ },
7111
+ noAnswer: {
7112
+ additionalProperties: false,
7113
+ properties: {
7114
+ metadata: {
7115
+ additionalProperties: true,
7116
+ type: "object"
7117
+ }
7118
+ },
7119
+ type: "object"
7120
+ },
7121
+ result: {
7122
+ additionalProperties: true,
7123
+ type: "object"
7124
+ },
7125
+ transfer: {
7126
+ additionalProperties: false,
7127
+ properties: {
7128
+ metadata: {
7129
+ additionalProperties: true,
7130
+ type: "object"
7131
+ },
7132
+ reason: {
7133
+ type: "string"
7134
+ },
7135
+ target: {
7136
+ type: "string"
7137
+ }
7138
+ },
7139
+ required: ["target"],
7140
+ type: "object"
7141
+ },
7142
+ voicemail: {
7143
+ additionalProperties: false,
7144
+ properties: {
7145
+ metadata: {
7146
+ additionalProperties: true,
7147
+ type: "object"
7148
+ }
7149
+ },
7150
+ type: "object"
7151
+ }
7152
+ },
7153
+ type: "object"
7154
+ };
7155
+ var ROUTE_RESULT_INSTRUCTION = "Return only a JSON object with assistantText, complete, transfer, escalate, voicemail, noAnswer, and result when you are not calling tools. Only set transfer, escalate, voicemail, or noAnswer when the user explicitly asks for that lifecycle outcome or a tool result says that exact outcome. Do not infer voicemail from generic words like voice, voice app, or voice integration.";
7156
+ var stripJSONCodeFence = (value) => {
7157
+ const trimmed = value.trim();
7158
+ const match = trimmed.match(/^```(?:json)?\s*([\s\S]*?)\s*```$/i);
7159
+ return match?.[1]?.trim() ?? value;
7160
+ };
7161
+ var parseJSON = (value) => {
7162
+ try {
7163
+ const parsed = JSON.parse(stripJSONCodeFence(value));
7164
+ return parsed && typeof parsed === "object" ? parsed : {};
7165
+ } catch {
7166
+ return {
7167
+ assistantText: value
7168
+ };
7169
+ }
7170
+ };
7171
+ var parseJSONValue = (value) => {
7172
+ try {
7173
+ return JSON.parse(value);
7174
+ } catch {
7175
+ return value;
7176
+ }
7177
+ };
7178
+ var getMessageToolCalls = (message) => {
7179
+ const toolCalls = message.metadata?.toolCalls;
7180
+ return Array.isArray(toolCalls) ? toolCalls.filter((toolCall) => toolCall && typeof toolCall === "object" && typeof toolCall.name === "string") : [];
7181
+ };
7182
+ var createHTTPError = (provider, response) => new Error(`${provider} voice assistant model failed: HTTP ${response.status}`);
7183
+ var sleep4 = (ms) => new Promise((resolve2) => {
7184
+ setTimeout(resolve2, ms);
7185
+ });
7186
+ var normalizeRouteOutput = (output) => {
7187
+ const result = {};
7188
+ if (typeof output.assistantText === "string") {
7189
+ result.assistantText = output.assistantText;
7190
+ }
7191
+ if (typeof output.complete === "boolean") {
7192
+ result.complete = output.complete;
7193
+ }
7194
+ if (output.result !== undefined) {
7195
+ result.result = output.result;
7196
+ }
7197
+ if (output.transfer && typeof output.transfer === "object") {
7198
+ const transfer = output.transfer;
7199
+ if (typeof transfer.target === "string") {
7200
+ result.transfer = {
7201
+ metadata: transfer.metadata && typeof transfer.metadata === "object" ? transfer.metadata : undefined,
7202
+ reason: typeof transfer.reason === "string" ? transfer.reason : undefined,
7203
+ target: transfer.target
7204
+ };
7205
+ }
7206
+ }
7207
+ if (output.escalate && typeof output.escalate === "object") {
7208
+ const escalate = output.escalate;
7209
+ if (typeof escalate.reason === "string") {
7210
+ result.escalate = {
7211
+ metadata: escalate.metadata && typeof escalate.metadata === "object" ? escalate.metadata : undefined,
7212
+ reason: escalate.reason
7213
+ };
7214
+ }
7215
+ }
7216
+ if (output.voicemail && typeof output.voicemail === "object") {
7217
+ const voicemail = output.voicemail;
7218
+ result.voicemail = {
7219
+ metadata: voicemail.metadata && typeof voicemail.metadata === "object" ? voicemail.metadata : undefined
7220
+ };
7221
+ }
7222
+ if (output.noAnswer && typeof output.noAnswer === "object") {
7223
+ const noAnswer = output.noAnswer;
7224
+ result.noAnswer = {
7225
+ metadata: noAnswer.metadata && typeof noAnswer.metadata === "object" ? noAnswer.metadata : undefined
7226
+ };
7227
+ }
7228
+ return result;
7229
+ };
7230
+ var createJSONVoiceAssistantModel = (options) => ({
7231
+ generate: async (input) => {
7232
+ const output = await options.generate(input);
7233
+ if ("assistantText" in output || "toolCalls" in output || "complete" in output || "transfer" in output || "escalate" in output) {
7234
+ return output;
7235
+ }
7236
+ return options.mapOutput?.(output) ?? normalizeRouteOutput(output);
7237
+ }
7238
+ });
7239
+ var messageToOpenAIInput = (message) => {
7240
+ if (message.role === "tool") {
7241
+ return [
7242
+ {
7243
+ call_id: message.toolCallId ?? message.name ?? crypto.randomUUID(),
7244
+ output: message.content,
7245
+ type: "function_call_output"
7246
+ }
7247
+ ];
7248
+ }
7249
+ const toolCalls = getMessageToolCalls(message);
7250
+ if (message.role === "assistant" && toolCalls.length) {
7251
+ return toolCalls.map((toolCall) => ({
7252
+ arguments: JSON.stringify(toolCall.args),
7253
+ call_id: toolCall.id ?? crypto.randomUUID(),
7254
+ name: toolCall.name,
7255
+ type: "function_call"
7256
+ }));
7257
+ }
7258
+ return [
7259
+ {
7260
+ content: message.content,
7261
+ role: message.role === "system" ? "developer" : message.role
7262
+ }
7263
+ ];
7264
+ };
7265
+ var messagesToOpenAIInput = (messages) => messages.flatMap(messageToOpenAIInput);
7266
+ var messageToAnthropicMessage = (message) => {
7267
+ if (message.role === "system") {
7268
+ return;
7269
+ }
7270
+ if (message.role === "tool") {
7271
+ if (!message.toolCallId) {
7272
+ return {
7273
+ content: `Tool result from ${message.name ?? "tool"}: ${message.content}`,
7274
+ role: "user"
7275
+ };
7276
+ }
7277
+ return {
7278
+ content: [
7279
+ {
7280
+ content: message.content,
7281
+ tool_use_id: message.toolCallId,
7282
+ type: "tool_result"
7283
+ }
7284
+ ],
7285
+ role: "user"
7286
+ };
7287
+ }
7288
+ const toolCalls = getMessageToolCalls(message);
7289
+ if (message.role === "assistant" && toolCalls.length) {
7290
+ return {
7291
+ content: [
7292
+ ...message.content ? [
7293
+ {
7294
+ text: message.content,
7295
+ type: "text"
7296
+ }
7297
+ ] : [],
7298
+ ...toolCalls.map((toolCall) => ({
7299
+ id: toolCall.id ?? crypto.randomUUID(),
7300
+ input: toolCall.args,
7301
+ name: toolCall.name,
7302
+ type: "tool_use"
7303
+ }))
7304
+ ],
7305
+ role: "assistant"
7306
+ };
7307
+ }
7308
+ return {
7309
+ content: message.content,
7310
+ role: message.role
7311
+ };
7312
+ };
7313
+ var toGeminiSchema = (schema) => {
7314
+ const next = {};
7315
+ for (const [key, value] of Object.entries(schema)) {
7316
+ if (key === "additionalProperties") {
7317
+ continue;
7318
+ }
7319
+ if (key === "type" && typeof value === "string") {
7320
+ next[key] = value.toUpperCase();
7321
+ continue;
7322
+ }
7323
+ if (Array.isArray(value)) {
7324
+ next[key] = value.map((item) => item && typeof item === "object" ? toGeminiSchema(item) : item);
7325
+ continue;
7326
+ }
7327
+ if (value && typeof value === "object") {
7328
+ next[key] = toGeminiSchema(value);
7329
+ continue;
7330
+ }
7331
+ next[key] = value;
7332
+ }
7333
+ return next;
7334
+ };
7335
+ var messageToGeminiContent = (message) => {
7336
+ if (message.role === "system") {
7337
+ return;
7338
+ }
7339
+ if (message.role === "tool") {
7340
+ return {
7341
+ parts: [
7342
+ {
7343
+ functionResponse: {
7344
+ id: message.toolCallId,
7345
+ name: message.name ?? "tool",
7346
+ response: {
7347
+ result: parseJSONValue(message.content)
7348
+ }
7349
+ }
7350
+ }
7351
+ ],
7352
+ role: "user"
7353
+ };
7354
+ }
7355
+ const toolCalls = getMessageToolCalls(message);
7356
+ if (message.role === "assistant" && toolCalls.length) {
7357
+ return {
7358
+ parts: [
7359
+ ...message.content ? [
7360
+ {
7361
+ text: message.content
7362
+ }
7363
+ ] : [],
7364
+ ...toolCalls.map((toolCall) => ({
7365
+ functionCall: {
7366
+ args: toolCall.args,
7367
+ id: toolCall.id,
7368
+ name: toolCall.name
7369
+ }
7370
+ }))
7371
+ ],
7372
+ role: "model"
7373
+ };
7374
+ }
7375
+ return {
7376
+ parts: [
7377
+ {
7378
+ text: message.content
7379
+ }
7380
+ ],
7381
+ role: message.role === "assistant" ? "model" : "user"
7382
+ };
7383
+ };
7384
+ var extractText = (response) => {
7385
+ if (typeof response.output_text === "string") {
7386
+ return response.output_text;
7387
+ }
7388
+ const output = Array.isArray(response.output) ? response.output : [];
7389
+ for (const item of output) {
7390
+ if (!item || typeof item !== "object") {
7391
+ continue;
7392
+ }
7393
+ const record = item;
7394
+ const content = Array.isArray(record.content) ? record.content : [];
7395
+ for (const contentItem of content) {
7396
+ if (!contentItem || typeof contentItem !== "object") {
7397
+ continue;
7398
+ }
7399
+ const contentRecord = contentItem;
7400
+ if (typeof contentRecord.text === "string") {
7401
+ return contentRecord.text;
7402
+ }
7403
+ }
7404
+ }
7405
+ return "";
7406
+ };
7407
+ var extractToolCalls = (response) => {
7408
+ const output = Array.isArray(response.output) ? response.output : [];
7409
+ const toolCalls = [];
7410
+ for (const item of output) {
7411
+ if (!item || typeof item !== "object") {
7412
+ continue;
7413
+ }
7414
+ const record = item;
7415
+ if (record.type !== "function_call" || typeof record.name !== "string") {
7416
+ continue;
7417
+ }
7418
+ const args = typeof record.arguments === "string" ? parseJSON(record.arguments) : {};
7419
+ toolCalls.push({
7420
+ args,
7421
+ id: typeof record.call_id === "string" ? record.call_id : typeof record.id === "string" ? record.id : undefined,
7422
+ name: record.name
7423
+ });
7424
+ }
7425
+ return toolCalls;
7426
+ };
7427
+ var createOpenAIVoiceAssistantModel = (options) => {
7428
+ const fetchImpl = options.fetch ?? globalThis.fetch;
7429
+ const baseUrl = options.baseUrl ?? "https://api.openai.com/v1";
7430
+ const model = options.model ?? "gpt-4.1-mini";
7431
+ return {
7432
+ generate: async (input) => {
7433
+ const response = await fetchImpl(`${baseUrl.replace(/\/$/, "")}/responses`, {
7434
+ body: JSON.stringify({
7435
+ input: messagesToOpenAIInput(input.messages),
7436
+ instructions: [
7437
+ input.system,
7438
+ "Return a JSON object with assistantText, complete, transfer, escalate, voicemail, noAnswer, and result when you are not calling tools."
7439
+ ].filter(Boolean).join(`
7440
+
7441
+ `),
7442
+ max_output_tokens: options.maxOutputTokens,
7443
+ model,
7444
+ temperature: options.temperature,
7445
+ text: {
7446
+ format: {
7447
+ name: "voice_route_result",
7448
+ schema: OUTPUT_SCHEMA,
7449
+ strict: false,
7450
+ type: "json_schema"
7451
+ }
7452
+ },
7453
+ tool_choice: input.tools.length ? "auto" : "none",
7454
+ tools: input.tools.map((tool) => ({
7455
+ description: tool.description,
7456
+ name: tool.name,
7457
+ parameters: tool.parameters ?? {
7458
+ additionalProperties: true,
7459
+ type: "object"
7460
+ },
7461
+ strict: false,
7462
+ type: "function"
7463
+ }))
7464
+ }),
7465
+ headers: {
7466
+ authorization: `Bearer ${options.apiKey}`,
7467
+ "content-type": "application/json"
7468
+ },
7469
+ method: "POST"
7470
+ });
7471
+ if (!response.ok) {
7472
+ throw createHTTPError("OpenAI", response);
7473
+ }
7474
+ const body = await response.json();
7475
+ if (body.usage && typeof body.usage === "object") {
7476
+ await options.onUsage?.(body.usage);
7477
+ }
7478
+ const toolCalls = extractToolCalls(body);
7479
+ if (toolCalls.length) {
7480
+ return {
7481
+ toolCalls
7482
+ };
7483
+ }
7484
+ return normalizeRouteOutput(parseJSON(extractText(body)));
7485
+ }
7486
+ };
7487
+ };
7488
+ var extractAnthropicText = (response) => {
7489
+ const content = Array.isArray(response.content) ? response.content : [];
7490
+ return content.map((item) => item && typeof item === "object" && item.type === "text" && typeof item.text === "string" ? item.text : "").filter(Boolean).join(`
7491
+ `);
7492
+ };
7493
+ var extractAnthropicToolCalls = (response) => {
7494
+ const content = Array.isArray(response.content) ? response.content : [];
7495
+ const toolCalls = [];
7496
+ for (const item of content) {
7497
+ if (!item || typeof item !== "object") {
7498
+ continue;
7499
+ }
7500
+ const record = item;
7501
+ if (record.type !== "tool_use" || typeof record.name !== "string") {
7502
+ continue;
7503
+ }
7504
+ toolCalls.push({
7505
+ args: record.input && typeof record.input === "object" ? record.input : {},
7506
+ id: typeof record.id === "string" ? record.id : undefined,
7507
+ name: record.name
7508
+ });
7509
+ }
7510
+ return toolCalls;
7511
+ };
7512
+ var createAnthropicVoiceAssistantModel = (options) => {
7513
+ const fetchImpl = options.fetch ?? globalThis.fetch;
7514
+ const baseUrl = options.baseUrl ?? "https://api.anthropic.com/v1";
7515
+ const model = options.model ?? "claude-sonnet-4-5";
7516
+ return {
7517
+ generate: async (input) => {
7518
+ const response = await fetchImpl(`${baseUrl.replace(/\/$/, "")}/messages`, {
7519
+ body: JSON.stringify({
7520
+ max_tokens: options.maxOutputTokens ?? 1024,
7521
+ messages: input.messages.map(messageToAnthropicMessage).filter(Boolean),
7522
+ model,
7523
+ system: [input.system, ROUTE_RESULT_INSTRUCTION].filter(Boolean).join(`
7524
+
7525
+ `),
7526
+ temperature: options.temperature,
7527
+ tool_choice: input.tools.length ? { type: "auto" } : { type: "none" },
7528
+ tools: input.tools.map((tool) => ({
7529
+ description: tool.description,
7530
+ input_schema: tool.parameters ?? {
7531
+ additionalProperties: true,
7532
+ type: "object"
7533
+ },
7534
+ name: tool.name
7535
+ }))
7536
+ }),
7537
+ headers: {
7538
+ "anthropic-version": options.version ?? "2023-06-01",
7539
+ "content-type": "application/json",
7540
+ "x-api-key": options.apiKey
7541
+ },
7542
+ method: "POST"
7543
+ });
7544
+ if (!response.ok) {
7545
+ throw createHTTPError("Anthropic", response);
7546
+ }
7547
+ const body = await response.json();
7548
+ if (body.usage && typeof body.usage === "object") {
7549
+ await options.onUsage?.(body.usage);
7550
+ }
7551
+ const toolCalls = extractAnthropicToolCalls(body);
7552
+ if (toolCalls.length) {
7553
+ return {
7554
+ assistantText: extractAnthropicText(body) || undefined,
7555
+ toolCalls
7556
+ };
7557
+ }
7558
+ return normalizeRouteOutput(parseJSON(extractAnthropicText(body)));
7559
+ }
7560
+ };
7561
+ };
7562
+ var extractGeminiCandidateParts = (response) => {
7563
+ const candidates = Array.isArray(response.candidates) ? response.candidates : [];
7564
+ const first = candidates[0];
7565
+ if (!first || typeof first !== "object") {
7566
+ return [];
7567
+ }
7568
+ const content = first.content;
7569
+ if (!content || typeof content !== "object") {
7570
+ return [];
7571
+ }
7572
+ const parts = content.parts;
7573
+ return Array.isArray(parts) ? parts : [];
7574
+ };
7575
+ var extractGeminiText = (response) => extractGeminiCandidateParts(response).map((part) => part && typeof part === "object" && typeof part.text === "string" ? part.text : "").filter(Boolean).join(`
7576
+ `);
7577
+ var extractGeminiToolCalls = (response) => {
7578
+ const toolCalls = [];
7579
+ for (const part of extractGeminiCandidateParts(response)) {
7580
+ if (!part || typeof part !== "object") {
7581
+ continue;
7582
+ }
7583
+ const functionCall = part.functionCall;
7584
+ if (!functionCall || typeof functionCall !== "object") {
7585
+ continue;
7586
+ }
7587
+ const record = functionCall;
7588
+ if (typeof record.name !== "string") {
7589
+ continue;
7590
+ }
7591
+ toolCalls.push({
7592
+ args: record.args && typeof record.args === "object" ? record.args : {},
7593
+ id: typeof record.id === "string" ? record.id : undefined,
7594
+ name: record.name
7595
+ });
7596
+ }
7597
+ return toolCalls;
7598
+ };
7599
+ var createGeminiVoiceAssistantModel = (options) => {
7600
+ const fetchImpl = options.fetch ?? globalThis.fetch;
7601
+ const baseUrl = options.baseUrl ?? "https://generativelanguage.googleapis.com/v1beta";
7602
+ const model = options.model ?? "gemini-2.5-flash";
7603
+ const maxRetries = Math.max(0, options.maxRetries ?? 2);
7604
+ return {
7605
+ generate: async (input) => {
7606
+ const endpoint = `${baseUrl.replace(/\/$/, "")}/models/${encodeURIComponent(model)}:generateContent?key=${encodeURIComponent(options.apiKey)}`;
7607
+ let response;
7608
+ for (let attempt = 0;attempt <= maxRetries; attempt += 1) {
7609
+ response = await fetchImpl(endpoint, {
7610
+ body: JSON.stringify({
7611
+ contents: input.messages.map(messageToGeminiContent).filter(Boolean),
7612
+ generationConfig: {
7613
+ maxOutputTokens: options.maxOutputTokens,
7614
+ ...input.tools.length ? {} : {
7615
+ responseMimeType: "application/json",
7616
+ responseSchema: toGeminiSchema(OUTPUT_SCHEMA)
7617
+ },
7618
+ temperature: options.temperature
7619
+ },
7620
+ systemInstruction: {
7621
+ parts: [
7622
+ {
7623
+ text: [input.system, ROUTE_RESULT_INSTRUCTION].filter(Boolean).join(`
7624
+
7625
+ `)
7626
+ }
7627
+ ]
7628
+ },
7629
+ tools: input.tools.length ? [
7630
+ {
7631
+ functionDeclarations: input.tools.map((tool) => ({
7632
+ description: tool.description,
7633
+ name: tool.name,
7634
+ parameters: toGeminiSchema(tool.parameters ?? {
7635
+ additionalProperties: true,
7636
+ type: "object"
7637
+ })
7638
+ }))
7639
+ }
7640
+ ] : undefined
7641
+ }),
7642
+ headers: {
7643
+ "content-type": "application/json"
7644
+ },
7645
+ method: "POST"
7646
+ });
7647
+ if (response.ok || response.status !== 429 && response.status < 500 || attempt === maxRetries) {
7648
+ break;
7649
+ }
7650
+ const retryAfter = Number(response.headers.get("retry-after"));
7651
+ await sleep4(Number.isFinite(retryAfter) && retryAfter > 0 ? retryAfter * 1000 : 500 * 2 ** attempt);
7652
+ }
7653
+ if (!response) {
7654
+ throw new Error("Gemini voice assistant model failed: no response");
7655
+ }
7656
+ if (!response.ok) {
7657
+ throw createHTTPError("Gemini", response);
7658
+ }
7659
+ const body = await response.json();
7660
+ if (body.usageMetadata && typeof body.usageMetadata === "object") {
7661
+ await options.onUsage?.(body.usageMetadata);
7662
+ }
7663
+ const toolCalls = extractGeminiToolCalls(body);
7664
+ if (toolCalls.length) {
7665
+ return {
7666
+ assistantText: extractGeminiText(body) || undefined,
7667
+ toolCalls
7668
+ };
7669
+ }
7670
+ return normalizeRouteOutput(parseJSON(extractGeminiText(body)));
7671
+ }
7672
+ };
7673
+ };
6315
7674
  // src/sqliteStore.ts
6316
7675
  import { Database } from "bun:sqlite";
6317
7676
  var normalizeTableNameSegment = (value) => value.trim().replace(/[^a-zA-Z0-9_]+/g, "_").replace(/^_+|_+$/g, "") || "voice";
@@ -7987,230 +9346,6 @@ var resolveVoiceOpsPreset = (name, overrides = {}) => {
7987
9346
  taskPolicies: mergePolicies(preset.taskPolicies, overrides.taskPolicies)
7988
9347
  };
7989
9348
  };
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
9349
  // src/correction.ts
8215
9350
  var escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
8216
9351
  var buildAliasMatcher = (alias) => new RegExp(`(?<![\\p{L}\\p{N}'])${escapeRegExp(alias)}(?![\\p{L}\\p{N}'])`, "giu");
@@ -8990,6 +10125,7 @@ export {
8990
10125
  summarizeVoiceOpsTaskQueue,
8991
10126
  summarizeVoiceOpsTaskAnalytics,
8992
10127
  summarizeVoiceIntegrationEvents,
10128
+ summarizeVoiceAssistantRuns,
8993
10129
  startVoiceOpsTask,
8994
10130
  shapeTelephonyAssistantText,
8995
10131
  selectVoiceTraceEventsForPrune,
@@ -9001,6 +10137,7 @@ export {
9001
10137
  resolveVoiceOpsTaskAssignment,
9002
10138
  resolveVoiceOpsTaskAgeBucket,
9003
10139
  resolveVoiceOpsPreset,
10140
+ resolveVoiceAssistantMemoryNamespace,
9004
10141
  resolveTurnDetectionConfig,
9005
10142
  resolveAudioConditioningConfig,
9006
10143
  requeueVoiceOpsTask,
@@ -9076,6 +10213,7 @@ export {
9076
10213
  createVoiceMemoryTraceSinkDeliveryStore,
9077
10214
  createVoiceMemoryTraceEventStore,
9078
10215
  createVoiceMemoryStore,
10216
+ createVoiceMemoryAssistantMemoryStore,
9079
10217
  createVoiceLinearIssueUpdateSink,
9080
10218
  createVoiceLinearIssueSyncSinks,
9081
10219
  createVoiceLinearIssueSink,
@@ -9095,13 +10233,18 @@ export {
9095
10233
  createVoiceFileReviewStore,
9096
10234
  createVoiceFileIntegrationEventStore,
9097
10235
  createVoiceFileExternalObjectMapStore,
10236
+ createVoiceFileAssistantMemoryStore,
9098
10237
  createVoiceExternalObjectMapId,
9099
10238
  createVoiceExternalObjectMap,
10239
+ createVoiceExperiment,
9100
10240
  createVoiceCallReviewRecorder,
9101
10241
  createVoiceCallReviewFromSession,
9102
10242
  createVoiceCallReviewFromLiveTelephonyReport,
9103
10243
  createVoiceCallCompletedEvent,
9104
10244
  createVoiceCRMActivitySink,
10245
+ createVoiceAssistantMemoryRecord,
10246
+ createVoiceAssistantMemoryHandle,
10247
+ createVoiceAssistant,
9105
10248
  createVoiceAgentTool,
9106
10249
  createVoiceAgentSquad,
9107
10250
  createVoiceAgent,
@@ -9113,9 +10256,13 @@ export {
9113
10256
  createStoredVoiceCallReviewArtifact,
9114
10257
  createRiskyTurnCorrectionHandler,
9115
10258
  createPhraseHintCorrectionHandler,
10259
+ createOpenAIVoiceAssistantModel,
10260
+ createJSONVoiceAssistantModel,
9116
10261
  createId,
10262
+ createGeminiVoiceAssistantModel,
9117
10263
  createDomainPhraseHints,
9118
10264
  createDomainLexicon,
10265
+ createAnthropicVoiceAssistantModel,
9119
10266
  conditionAudioChunk,
9120
10267
  completeVoiceOpsTask,
9121
10268
  claimVoiceOpsTask,