@absolutejs/voice 0.0.22-beta.67 → 0.0.22-beta.69

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.d.ts CHANGED
@@ -10,6 +10,8 @@ export { createVoiceAgent, createVoiceAgentSquad, createVoiceAgentTool } from '.
10
10
  export { createVoiceToolIdempotencyKey, createVoiceToolRuntime } from './toolRuntime';
11
11
  export { createVoiceToolContract, createVoiceToolContractHTMLHandler, createVoiceToolContractJSONHandler, createVoiceToolContractRoutes, createVoiceToolRuntimeContractDefaults, renderVoiceToolContractHTML, runVoiceToolContractSuite, runVoiceToolContract } from './toolContract';
12
12
  export { createVoiceTurnQualityHTMLHandler, createVoiceTurnQualityJSONHandler, createVoiceTurnQualityRoutes, renderVoiceTurnQualityHTML, summarizeVoiceTurnQuality } from './turnQuality';
13
+ export { createVoiceOutcomeContractHTMLHandler, createVoiceOutcomeContractJSONHandler, createVoiceOutcomeContractRoutes, renderVoiceOutcomeContractHTML, runVoiceOutcomeContractSuite } from './outcomeContract';
14
+ export { applyVoiceTelephonyOutcome, createVoiceTelephonyOutcomePolicy, resolveVoiceTelephonyOutcome, voiceTelephonyOutcomeToRouteResult } from './telephonyOutcome';
13
15
  export { createStoredVoiceCallReviewArtifact, createStoredVoiceExternalObjectMap, createStoredVoiceIntegrationEvent, createStoredVoiceOpsTask, createVoiceFileExternalObjectMapStore, createVoiceFileAssistantMemoryStore, createVoiceFileIntegrationEventStore, createVoiceFileReviewStore, createVoiceFileRuntimeStorage, createVoiceFileSessionStore, createVoiceFileTaskStore, createVoiceFileTraceSinkDeliveryStore, createVoiceFileTraceEventStore } from './fileStore';
14
16
  export { createVoiceAssistantMemoryHandle, createVoiceAssistantMemoryRecord, createVoiceMemoryAssistantMemoryStore, resolveVoiceAssistantMemoryNamespace } from './assistantMemory';
15
17
  export { createAnthropicVoiceAssistantModel, createGeminiVoiceAssistantModel, createJSONVoiceAssistantModel, createOpenAIVoiceAssistantModel, resolveVoiceProviderRoutingPolicyPreset, createVoiceProviderRouter } from './modelAdapters';
@@ -54,6 +56,8 @@ export type { AnthropicVoiceAssistantModelOptions, GeminiVoiceAssistantModelOpti
54
56
  export type { VoiceProviderHealthStatus, VoiceProviderHealthSummary, VoiceProviderHealthSummaryOptions } from './providerHealth';
55
57
  export type { VoiceProviderCapabilityDefinition, VoiceProviderCapabilityHandlerOptions, VoiceProviderCapabilityHTMLHandlerOptions, VoiceProviderCapabilityKind, VoiceProviderCapabilityOptions, VoiceProviderCapabilityReport, VoiceProviderCapabilityRoutesOptions, VoiceProviderCapabilitySummary } from './providerCapabilities';
56
58
  export type { VoiceTurnQualityHTMLHandlerOptions, VoiceTurnQualityItem, VoiceTurnQualityOptions, VoiceTurnQualityReport, VoiceTurnQualityRoutesOptions, VoiceTurnQualityStatus } from './turnQuality';
59
+ export type { VoiceOutcomeContractDefinition, VoiceOutcomeContractHTMLHandlerOptions, VoiceOutcomeContractIssue, VoiceOutcomeContractOptions, VoiceOutcomeContractReport, VoiceOutcomeContractRoutesOptions, VoiceOutcomeContractStatus, VoiceOutcomeContractSuiteReport } from './outcomeContract';
60
+ export type { VoiceTelephonyOutcomeAction, VoiceTelephonyOutcomeDecision, VoiceTelephonyOutcomePolicy, VoiceTelephonyOutcomeProviderEvent, VoiceTelephonyOutcomeRouteResult, VoiceTelephonyOutcomeStatusDecision } from './telephonyOutcome';
57
61
  export type { VoiceOpsConsoleLink, VoiceOpsConsoleReport, VoiceOpsConsoleRoutesOptions } from './opsConsoleRoutes';
58
62
  export type { VoiceQualityLink, VoiceQualityMetric, VoiceQualityReport, VoiceQualityRoutesOptions, VoiceQualityStatus, VoiceQualityThresholds } from './qualityRoutes';
59
63
  export type { VoiceResilienceIOSimulator, VoiceResilienceLink, VoiceResiliencePageData, VoiceResilienceRoutesOptions, VoiceResilienceSimulationProvider, VoiceRoutingDecisionSummary, VoiceRoutingDecisionSummaryOptions, VoiceRoutingEvent, VoiceRoutingEventKind } from './resilienceRoutes';
package/dist/index.js CHANGED
@@ -10410,6 +10410,429 @@ var createVoiceTurnQualityRoutes = (options) => {
10410
10410
  }
10411
10411
  return routes;
10412
10412
  };
10413
+ // src/outcomeContract.ts
10414
+ import { Elysia as Elysia15 } from "elysia";
10415
+ var escapeHtml16 = (value) => value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
10416
+ var getPayloadString = (event, key) => typeof event.payload[key] === "string" ? event.payload[key] : undefined;
10417
+ var toList = async (input) => Array.isArray(input) ? input : await input?.list() ?? [];
10418
+ var hydrateSessions = async (input) => {
10419
+ if (!input)
10420
+ return [];
10421
+ if (Array.isArray(input))
10422
+ return input;
10423
+ const summaries = await input.list();
10424
+ const sessions = await Promise.all(summaries.map((summary) => input.get(summary.id)));
10425
+ const hydrated = [];
10426
+ for (const session of sessions) {
10427
+ if (session) {
10428
+ hydrated.push(session);
10429
+ }
10430
+ }
10431
+ return hydrated;
10432
+ };
10433
+ var dispositionForSession = (session) => session.call?.disposition ?? (session.status === "completed" ? "completed" : undefined);
10434
+ var matchesDisposition = (disposition, expected) => expected === undefined || disposition === expected;
10435
+ var reportContract = (input) => {
10436
+ const { contract } = input;
10437
+ const sessions = input.sessions.filter((session) => (!contract.scenarioId || session.scenarioId === contract.scenarioId) && matchesDisposition(dispositionForSession(session), contract.expectedDisposition));
10438
+ const sessionIds = new Set(sessions.map((session) => session.id));
10439
+ const reviews = input.reviews.filter((review) => matchesDisposition(review.summary.outcome, contract.expectedDisposition));
10440
+ const tasks = input.tasks.filter((task) => matchesDisposition(task.outcome, contract.expectedDisposition));
10441
+ const handoffs = input.handoffs.filter((handoff) => (!contract.expectedDisposition || handoff.action === contract.expectedDisposition || contract.expectedDisposition === "transferred" && handoff.action === "transfer" || contract.expectedDisposition === "escalated" && handoff.action === "escalate") && (sessionIds.size === 0 || sessionIds.has(handoff.sessionId)));
10442
+ const events = input.events.filter((event) => {
10443
+ const eventSessionId = getPayloadString(event, "sessionId");
10444
+ const eventOutcome = getPayloadString(event, "outcome") ?? getPayloadString(event, "disposition");
10445
+ return (sessionIds.size === 0 || !eventSessionId || sessionIds.has(eventSessionId)) && (!contract.expectedDisposition || eventOutcome === contract.expectedDisposition);
10446
+ });
10447
+ const issues = [];
10448
+ const minSessions = contract.minSessions ?? 1;
10449
+ if (sessions.length < minSessions) {
10450
+ issues.push({
10451
+ code: "outcome.sessions_missing",
10452
+ message: `Expected at least ${minSessions} matching session(s), saw ${sessions.length}.`
10453
+ });
10454
+ }
10455
+ if (contract.requireReview !== false && reviews.length === 0) {
10456
+ issues.push({
10457
+ code: "outcome.review_missing",
10458
+ message: "Expected at least one matching review artifact."
10459
+ });
10460
+ }
10461
+ if (contract.requireTask && tasks.length < (contract.minTasks ?? 1)) {
10462
+ issues.push({
10463
+ code: "outcome.task_missing",
10464
+ message: `Expected at least ${contract.minTasks ?? 1} matching task(s), saw ${tasks.length}.`
10465
+ });
10466
+ }
10467
+ for (const action of contract.requireHandoffActions ?? []) {
10468
+ if (!handoffs.some((handoff) => handoff.action === action)) {
10469
+ issues.push({
10470
+ code: "outcome.handoff_missing",
10471
+ message: `Expected handoff action ${action}.`
10472
+ });
10473
+ }
10474
+ }
10475
+ for (const type of contract.requireIntegrationEvents ?? []) {
10476
+ if (!events.some((event) => event.type === type)) {
10477
+ issues.push({
10478
+ code: "outcome.integration_event_missing",
10479
+ message: `Expected integration event ${type}.`
10480
+ });
10481
+ }
10482
+ }
10483
+ return {
10484
+ contractId: contract.id,
10485
+ description: contract.description,
10486
+ issues,
10487
+ label: contract.label,
10488
+ matched: {
10489
+ handoffs: handoffs.length,
10490
+ integrationEvents: events.length,
10491
+ reviews: reviews.length,
10492
+ sessions: sessions.length,
10493
+ tasks: tasks.length
10494
+ },
10495
+ pass: issues.length === 0
10496
+ };
10497
+ };
10498
+ var runVoiceOutcomeContractSuite = async (options) => {
10499
+ const [sessions, reviews, tasks, events, handoffs] = await Promise.all([
10500
+ hydrateSessions(options.sessions),
10501
+ toList(options.reviews),
10502
+ toList(options.tasks),
10503
+ toList(options.events),
10504
+ toList(options.handoffs)
10505
+ ]);
10506
+ const contracts = options.contracts.map((contract) => reportContract({ contract, events, handoffs, reviews, sessions, tasks }));
10507
+ const passed = contracts.filter((contract) => contract.pass).length;
10508
+ const failed = contracts.length - passed;
10509
+ return {
10510
+ checkedAt: Date.now(),
10511
+ contracts,
10512
+ failed,
10513
+ passed,
10514
+ status: failed > 0 ? "fail" : "pass",
10515
+ total: contracts.length
10516
+ };
10517
+ };
10518
+ var renderVoiceOutcomeContractHTML = (report, options = {}) => {
10519
+ const title = options.title ?? "Voice Outcome Contracts";
10520
+ const contracts = report.contracts.map((contract) => `<section class="contract ${contract.pass ? "pass" : "fail"}">
10521
+ <div class="contract-header">
10522
+ <div>
10523
+ <p class="eyebrow">${escapeHtml16(contract.contractId)}</p>
10524
+ <h2>${escapeHtml16(contract.label ?? contract.contractId)}</h2>
10525
+ ${contract.description ? `<p>${escapeHtml16(contract.description)}</p>` : ""}
10526
+ </div>
10527
+ <strong>${contract.pass ? "pass" : "fail"}</strong>
10528
+ </div>
10529
+ <div class="grid">
10530
+ <span>sessions ${String(contract.matched.sessions)}</span>
10531
+ <span>reviews ${String(contract.matched.reviews)}</span>
10532
+ <span>tasks ${String(contract.matched.tasks)}</span>
10533
+ <span>handoffs ${String(contract.matched.handoffs)}</span>
10534
+ <span>events ${String(contract.matched.integrationEvents)}</span>
10535
+ </div>
10536
+ ${contract.issues.length ? `<ul>${contract.issues.map((issue) => `<li>${escapeHtml16(issue.message)}</li>`).join("")}</ul>` : ""}
10537
+ </section>`).join("");
10538
+ return `<!doctype html><html lang="en"><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /><title>${escapeHtml16(title)}</title><style>body{background:#101316;color:#f6f2e8;font-family:ui-sans-serif,system-ui,sans-serif;margin:0}main{margin:auto;max-width:1180px;padding:32px}.hero,.contract{background:#181d22;border:1px solid #2a323a;border-radius:20px;margin-bottom:16px;padding:20px}.hero{background:linear-gradient(135deg,rgba(34,197,94,.14),rgba(14,165,233,.12))}.eyebrow{color:#7dd3fc;font-size:.78rem;font-weight:900;letter-spacing:.08em;text-transform:uppercase}h1{font-size:clamp(2.3rem,6vw,5rem);letter-spacing:-.06em;line-height:.9;margin:.2rem 0 1rem}h2{margin:.2rem 0}.summary,.grid{display:flex;flex-wrap:wrap;gap:10px}.pill,.grid span{background:#0f1217;border:1px solid #3f3f46;border-radius:999px;padding:7px 10px}.contract-header{display:flex;gap:16px;justify-content:space-between}.pass{color:#86efac}.fail{color:#fca5a5}.contract.fail{border-color:rgba(248,113,113,.45)}li{margin:8px 0}@media(max-width:800px){main{padding:18px}.contract-header{display:block}}</style></head><body><main><section class="hero"><p class="eyebrow">Business Outcome Verification</p><h1>${escapeHtml16(title)}</h1><div class="summary"><span class="pill ${report.status}">${report.status}</span><span class="pill">${String(report.passed)} passing</span><span class="pill">${String(report.failed)} failing</span><span class="pill">${String(report.total)} contracts</span></div></section>${contracts || '<section class="contract"><p>No outcome contracts configured.</p></section>'}</main></body></html>`;
10539
+ };
10540
+ var createVoiceOutcomeContractJSONHandler = (options) => async () => runVoiceOutcomeContractSuite(options);
10541
+ var createVoiceOutcomeContractHTMLHandler = (options) => async () => {
10542
+ const report = await runVoiceOutcomeContractSuite(options);
10543
+ const render = options.render ?? ((input) => renderVoiceOutcomeContractHTML(input, options));
10544
+ return new Response(await render(report), {
10545
+ headers: {
10546
+ "Content-Type": "text/html; charset=utf-8",
10547
+ ...options.headers
10548
+ }
10549
+ });
10550
+ };
10551
+ var createVoiceOutcomeContractRoutes = (options) => {
10552
+ const path = options.path ?? "/api/outcome-contracts";
10553
+ const htmlPath = options.htmlPath === undefined ? `${path}/htmx` : options.htmlPath;
10554
+ const routes = new Elysia15({
10555
+ name: options.name ?? "absolutejs-voice-outcome-contracts"
10556
+ }).get(path, createVoiceOutcomeContractJSONHandler(options));
10557
+ if (htmlPath) {
10558
+ routes.get(htmlPath, createVoiceOutcomeContractHTMLHandler(options));
10559
+ }
10560
+ return routes;
10561
+ };
10562
+ // src/telephonyOutcome.ts
10563
+ var DEFAULT_COMPLETED_STATUSES = [
10564
+ "answered",
10565
+ "completed",
10566
+ "complete",
10567
+ "connected",
10568
+ "in-progress",
10569
+ "live"
10570
+ ];
10571
+ var DEFAULT_NO_ANSWER_STATUSES = [
10572
+ "busy",
10573
+ "canceled",
10574
+ "cancelled",
10575
+ "failed",
10576
+ "no-answer",
10577
+ "no_answer",
10578
+ "not-answered",
10579
+ "ring-no-answer",
10580
+ "timeout",
10581
+ "unanswered"
10582
+ ];
10583
+ var DEFAULT_VOICEMAIL_STATUSES = [
10584
+ "answering-machine",
10585
+ "machine",
10586
+ "voicemail",
10587
+ "voice-mail"
10588
+ ];
10589
+ var DEFAULT_TRANSFER_STATUSES = ["bridged", "forwarded", "transferred"];
10590
+ var DEFAULT_ESCALATION_STATUSES = ["escalated", "human-required", "operator"];
10591
+ var DEFAULT_FAILED_STATUSES = ["busy", "failed", "no-answer"];
10592
+ var DEFAULT_MACHINE_VOICEMAIL_VALUES = [
10593
+ "answering-machine",
10594
+ "fax",
10595
+ "machine",
10596
+ "machine-end-beep",
10597
+ "machine-end-other",
10598
+ "machine-start",
10599
+ "voicemail"
10600
+ ];
10601
+ var DEFAULT_NO_ANSWER_SIP_CODES = [408, 480, 486, 487, 603];
10602
+ var normalizeToken = (value) => typeof value === "string" ? value.trim().toLowerCase().replace(/\s+/g, "-").replace(/_+/g, "-") : undefined;
10603
+ var normalizeList = (values, fallback) => new Set((values ?? fallback).map(normalizeToken).filter(Boolean));
10604
+ var metadataValue = (metadata, keys) => {
10605
+ for (const key of keys) {
10606
+ const value = metadata?.[key];
10607
+ if (typeof value === "string" && value.trim()) {
10608
+ return value.trim();
10609
+ }
10610
+ }
10611
+ };
10612
+ var resolveTransferTarget = (event, policy) => {
10613
+ if (typeof event.target === "string" && event.target.trim()) {
10614
+ return event.target.trim();
10615
+ }
10616
+ const metadataTarget = metadataValue(event.metadata, [
10617
+ "transferTarget",
10618
+ "target",
10619
+ "queue",
10620
+ "department"
10621
+ ]);
10622
+ if (metadataTarget) {
10623
+ return metadataTarget;
10624
+ }
10625
+ if (typeof policy.transferTarget === "function") {
10626
+ const target = policy.transferTarget(event);
10627
+ return typeof target === "string" && target.trim() ? target.trim() : undefined;
10628
+ }
10629
+ return typeof policy.transferTarget === "string" && policy.transferTarget.trim() ? policy.transferTarget.trim() : undefined;
10630
+ };
10631
+ var mergeMetadata = (event, policy) => ({
10632
+ ...policy.includeProviderPayload ? {
10633
+ answeredBy: event.answeredBy,
10634
+ durationMs: event.durationMs,
10635
+ provider: event.provider,
10636
+ reason: event.reason,
10637
+ sipCode: event.sipCode,
10638
+ status: event.status
10639
+ } : undefined,
10640
+ ...policy.metadata,
10641
+ ...event.metadata
10642
+ });
10643
+ var withDecisionDefaults = (decision, input) => {
10644
+ if (typeof decision === "string") {
10645
+ return buildDecision(decision, input);
10646
+ }
10647
+ return {
10648
+ ...buildDecision(decision.action, input),
10649
+ ...decision,
10650
+ confidence: decision.confidence ?? "high",
10651
+ metadata: {
10652
+ ...mergeMetadata(input.event, input.policy),
10653
+ ...decision.metadata
10654
+ },
10655
+ source: decision.source ?? input.source,
10656
+ target: decision.target ?? (decision.action === "transfer" ? resolveTransferTarget(input.event, input.policy) : undefined)
10657
+ };
10658
+ };
10659
+ var dispositionForAction = (action) => {
10660
+ switch (action) {
10661
+ case "complete":
10662
+ return "completed";
10663
+ case "escalate":
10664
+ return "escalated";
10665
+ case "no-answer":
10666
+ return "no-answer";
10667
+ case "transfer":
10668
+ return "transferred";
10669
+ case "voicemail":
10670
+ return "voicemail";
10671
+ default:
10672
+ return;
10673
+ }
10674
+ };
10675
+ var buildDecision = (action, input) => ({
10676
+ action,
10677
+ confidence: action === "ignore" ? "low" : "high",
10678
+ disposition: dispositionForAction(action),
10679
+ metadata: mergeMetadata(input.event, input.policy),
10680
+ reason: input.event.reason,
10681
+ source: input.source,
10682
+ target: action === "transfer" ? resolveTransferTarget(input.event, input.policy) : undefined
10683
+ });
10684
+ var createVoiceTelephonyOutcomePolicy = (policy = {}) => ({
10685
+ completedStatuses: policy.completedStatuses ?? DEFAULT_COMPLETED_STATUSES,
10686
+ escalationStatuses: policy.escalationStatuses ?? DEFAULT_ESCALATION_STATUSES,
10687
+ failedAsNoAnswer: policy.failedAsNoAnswer ?? true,
10688
+ failedStatuses: policy.failedStatuses ?? DEFAULT_FAILED_STATUSES,
10689
+ includeProviderPayload: policy.includeProviderPayload ?? true,
10690
+ machineDetectionVoicemailValues: policy.machineDetectionVoicemailValues ?? DEFAULT_MACHINE_VOICEMAIL_VALUES,
10691
+ metadata: policy.metadata,
10692
+ minAnsweredDurationMs: policy.minAnsweredDurationMs,
10693
+ noAnswerOnZeroDuration: policy.noAnswerOnZeroDuration ?? true,
10694
+ noAnswerSipCodes: policy.noAnswerSipCodes ?? DEFAULT_NO_ANSWER_SIP_CODES,
10695
+ noAnswerStatuses: policy.noAnswerStatuses ?? DEFAULT_NO_ANSWER_STATUSES,
10696
+ statusMap: policy.statusMap,
10697
+ transferStatuses: policy.transferStatuses ?? DEFAULT_TRANSFER_STATUSES,
10698
+ transferTarget: policy.transferTarget,
10699
+ voicemailStatuses: policy.voicemailStatuses ?? DEFAULT_VOICEMAIL_STATUSES
10700
+ });
10701
+ var resolveVoiceTelephonyOutcome = (event, policyInput = {}) => {
10702
+ const policy = createVoiceTelephonyOutcomePolicy(policyInput);
10703
+ const status = normalizeToken(event.status);
10704
+ const provider = normalizeToken(event.provider);
10705
+ const answeredBy = normalizeToken(event.answeredBy);
10706
+ const target = resolveTransferTarget(event, policy);
10707
+ if (status) {
10708
+ const mapped = policy.statusMap?.[status] ?? (provider ? policy.statusMap?.[`${provider}:${status}`] : undefined);
10709
+ if (mapped) {
10710
+ return withDecisionDefaults(mapped, {
10711
+ event,
10712
+ policy,
10713
+ source: "policy"
10714
+ });
10715
+ }
10716
+ }
10717
+ if (answeredBy && normalizeList(policy.machineDetectionVoicemailValues, []).has(answeredBy)) {
10718
+ return buildDecision("voicemail", { event, policy, source: "answered-by" });
10719
+ }
10720
+ if (typeof event.sipCode === "number" && policy.noAnswerSipCodes.includes(event.sipCode)) {
10721
+ return buildDecision("no-answer", { event, policy, source: "sip" });
10722
+ }
10723
+ if (target && status && normalizeList(policy.transferStatuses, []).has(status)) {
10724
+ return buildDecision("transfer", { event, policy, source: "status" });
10725
+ }
10726
+ if (status && normalizeList(policy.voicemailStatuses, []).has(status)) {
10727
+ return buildDecision("voicemail", { event, policy, source: "status" });
10728
+ }
10729
+ if (status && normalizeList(policy.escalationStatuses, []).has(status)) {
10730
+ return buildDecision("escalate", { event, policy, source: "status" });
10731
+ }
10732
+ if (status && (policy.failedAsNoAnswer ? normalizeList(policy.noAnswerStatuses, []).has(status) || normalizeList(policy.failedStatuses, []).has(status) : normalizeList(policy.noAnswerStatuses, []).has(status))) {
10733
+ return buildDecision("no-answer", { event, policy, source: "status" });
10734
+ }
10735
+ if (policy.noAnswerOnZeroDuration && typeof event.durationMs === "number" && event.durationMs <= 0) {
10736
+ return buildDecision("no-answer", { event, policy, source: "duration" });
10737
+ }
10738
+ if (typeof policy.minAnsweredDurationMs === "number" && typeof event.durationMs === "number" && event.durationMs < policy.minAnsweredDurationMs) {
10739
+ return {
10740
+ ...buildDecision("no-answer", { event, policy, source: "duration" }),
10741
+ confidence: "medium"
10742
+ };
10743
+ }
10744
+ if (status && normalizeList(policy.completedStatuses, []).has(status)) {
10745
+ return buildDecision("complete", { event, policy, source: "status" });
10746
+ }
10747
+ if (target) {
10748
+ return {
10749
+ ...buildDecision("transfer", { event, policy, source: "explicit-target" }),
10750
+ confidence: "medium"
10751
+ };
10752
+ }
10753
+ return buildDecision("ignore", { event, policy, source: "status" });
10754
+ };
10755
+ var voiceTelephonyOutcomeToRouteResult = (decision, result) => {
10756
+ switch (decision.action) {
10757
+ case "complete":
10758
+ return { complete: true, result };
10759
+ case "escalate":
10760
+ return {
10761
+ escalate: {
10762
+ metadata: decision.metadata,
10763
+ reason: decision.reason ?? "telephony-escalation"
10764
+ },
10765
+ result
10766
+ };
10767
+ case "no-answer":
10768
+ return {
10769
+ noAnswer: {
10770
+ metadata: decision.metadata
10771
+ },
10772
+ result
10773
+ };
10774
+ case "transfer":
10775
+ if (!decision.target) {
10776
+ return { result };
10777
+ }
10778
+ return {
10779
+ result,
10780
+ transfer: {
10781
+ metadata: decision.metadata,
10782
+ reason: decision.reason,
10783
+ target: decision.target
10784
+ }
10785
+ };
10786
+ case "voicemail":
10787
+ return {
10788
+ result,
10789
+ voicemail: {
10790
+ metadata: decision.metadata
10791
+ }
10792
+ };
10793
+ default:
10794
+ return { result };
10795
+ }
10796
+ };
10797
+ var applyVoiceTelephonyOutcome = async (api, decision, result) => {
10798
+ switch (decision.action) {
10799
+ case "complete":
10800
+ await api.complete(result);
10801
+ break;
10802
+ case "escalate":
10803
+ await api.escalate({
10804
+ metadata: decision.metadata,
10805
+ reason: decision.reason ?? "telephony-escalation",
10806
+ result
10807
+ });
10808
+ break;
10809
+ case "no-answer":
10810
+ await api.markNoAnswer({
10811
+ metadata: decision.metadata,
10812
+ result
10813
+ });
10814
+ break;
10815
+ case "transfer":
10816
+ if (!decision.target) {
10817
+ return;
10818
+ }
10819
+ await api.transfer({
10820
+ metadata: decision.metadata,
10821
+ reason: decision.reason,
10822
+ result,
10823
+ target: decision.target
10824
+ });
10825
+ break;
10826
+ case "voicemail":
10827
+ await api.markVoicemail({
10828
+ metadata: decision.metadata,
10829
+ result
10830
+ });
10831
+ break;
10832
+ default:
10833
+ break;
10834
+ }
10835
+ };
10413
10836
  // src/fileStore.ts
10414
10837
  import { mkdir as mkdir2, readFile, readdir, rename, rm, writeFile } from "fs/promises";
10415
10838
  import { join } from "path";
@@ -12503,7 +12926,7 @@ var createVoiceMemoryStore = () => {
12503
12926
  return { get, getOrCreate, list, remove, set };
12504
12927
  };
12505
12928
  // src/opsWebhook.ts
12506
- import { Elysia as Elysia15 } from "elysia";
12929
+ import { Elysia as Elysia16 } from "elysia";
12507
12930
  var toHex5 = (bytes) => Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
12508
12931
  var signVoiceOpsWebhookBody = async (input) => {
12509
12932
  const encoder = new TextEncoder;
@@ -12633,7 +13056,7 @@ var verifyVoiceOpsWebhookSignature = async (input) => {
12633
13056
  };
12634
13057
  var createVoiceOpsWebhookReceiverRoutes = (options = {}) => {
12635
13058
  const path = options.path ?? "/api/voice-ops/webhook";
12636
- return new Elysia15().post(path, async ({ body, request, set }) => {
13059
+ return new Elysia16().post(path, async ({ body, request, set }) => {
12637
13060
  const bodyText = typeof body === "string" ? body : JSON.stringify(body);
12638
13061
  if (options.signingSecret) {
12639
13062
  const verification = await verifyVoiceOpsWebhookSignature({
@@ -14775,6 +15198,7 @@ var shapeTelephonyAssistantText = (text, options = {}) => {
14775
15198
  export {
14776
15199
  withVoiceOpsTaskId,
14777
15200
  withVoiceIntegrationEventId,
15201
+ voiceTelephonyOutcomeToRouteResult,
14778
15202
  voice,
14779
15203
  verifyVoiceOpsWebhookSignature,
14780
15204
  validateVoiceWorkflowRouteResult,
@@ -14805,7 +15229,9 @@ export {
14805
15229
  runVoiceSessionEvals,
14806
15230
  runVoiceScenarioFixtureEvals,
14807
15231
  runVoiceScenarioEvals,
15232
+ runVoiceOutcomeContractSuite,
14808
15233
  resolveVoiceTraceRedactionOptions,
15234
+ resolveVoiceTelephonyOutcome,
14809
15235
  resolveVoiceSTTRoutingStrategy,
14810
15236
  resolveVoiceRuntimePreset,
14811
15237
  resolveVoiceProviderRoutingPolicyPreset,
@@ -14831,6 +15257,7 @@ export {
14831
15257
  renderVoiceQualityHTML,
14832
15258
  renderVoiceProviderHealthHTML,
14833
15259
  renderVoiceProviderCapabilityHTML,
15260
+ renderVoiceOutcomeContractHTML,
14834
15261
  renderVoiceOpsConsoleHTML,
14835
15262
  renderVoiceHandoffHealthHTML,
14836
15263
  renderVoiceEvalHTML,
@@ -14893,6 +15320,7 @@ export {
14893
15320
  createVoiceToolContractJSONHandler,
14894
15321
  createVoiceToolContractHTMLHandler,
14895
15322
  createVoiceToolContract,
15323
+ createVoiceTelephonyOutcomePolicy,
14896
15324
  createVoiceTaskUpdatedEvent,
14897
15325
  createVoiceTaskSLABreachedEvent,
14898
15326
  createVoiceTaskCreatedEvent,
@@ -14937,6 +15365,9 @@ export {
14937
15365
  createVoicePostgresReviewStore,
14938
15366
  createVoicePostgresIntegrationEventStore,
14939
15367
  createVoicePostgresExternalObjectMapStore,
15368
+ createVoiceOutcomeContractRoutes,
15369
+ createVoiceOutcomeContractJSONHandler,
15370
+ createVoiceOutcomeContractHTMLHandler,
14940
15371
  createVoiceOpsWebhookSink,
14941
15372
  createVoiceOpsWebhookReceiverRoutes,
14942
15373
  createVoiceOpsWebhookEnvelope,
@@ -15024,6 +15455,7 @@ export {
15024
15455
  buildVoiceOpsConsoleReport,
15025
15456
  buildVoiceDiagnosticsMarkdown,
15026
15457
  assignVoiceOpsTask,
15458
+ applyVoiceTelephonyOutcome,
15027
15459
  applyVoiceOpsTaskPolicy,
15028
15460
  applyVoiceOpsTaskAssignmentRule,
15029
15461
  applyVoiceHandoffDeliveryResult,
@@ -0,0 +1,112 @@
1
+ import { Elysia } from 'elysia';
2
+ import type { StoredVoiceHandoffDelivery, VoiceCallDisposition, VoiceHandoffAction, VoiceHandoffDeliveryStore, VoiceSessionRecord, VoiceSessionStore } from './types';
3
+ import type { StoredVoiceIntegrationEvent, StoredVoiceOpsTask, VoiceIntegrationEventType } from './ops';
4
+ import type { StoredVoiceCallReviewArtifact } from './testing/review';
5
+ export type VoiceOutcomeContractStatus = 'pass' | 'fail';
6
+ export type VoiceOutcomeContractDefinition = {
7
+ description?: string;
8
+ expectedDisposition?: VoiceCallDisposition;
9
+ id: string;
10
+ label?: string;
11
+ minSessions?: number;
12
+ minTasks?: number;
13
+ requireHandoffActions?: VoiceHandoffAction[];
14
+ requireIntegrationEvents?: VoiceIntegrationEventType[];
15
+ requireReview?: boolean;
16
+ requireTask?: boolean;
17
+ scenarioId?: string;
18
+ };
19
+ export type VoiceOutcomeContractIssue = {
20
+ code: string;
21
+ message: string;
22
+ };
23
+ export type VoiceOutcomeContractReport = {
24
+ contractId: string;
25
+ description?: string;
26
+ issues: VoiceOutcomeContractIssue[];
27
+ label?: string;
28
+ matched: {
29
+ handoffs: number;
30
+ integrationEvents: number;
31
+ reviews: number;
32
+ sessions: number;
33
+ tasks: number;
34
+ };
35
+ pass: boolean;
36
+ };
37
+ export type VoiceOutcomeContractSuiteReport = {
38
+ checkedAt: number;
39
+ contracts: VoiceOutcomeContractReport[];
40
+ failed: number;
41
+ passed: number;
42
+ status: VoiceOutcomeContractStatus;
43
+ total: number;
44
+ };
45
+ type ListStore<T> = {
46
+ list: () => Promise<T[]> | T[];
47
+ };
48
+ export type VoiceOutcomeContractOptions<TSession extends VoiceSessionRecord = VoiceSessionRecord> = {
49
+ contracts: VoiceOutcomeContractDefinition[];
50
+ events?: StoredVoiceIntegrationEvent[] | ListStore<StoredVoiceIntegrationEvent>;
51
+ handoffs?: StoredVoiceHandoffDelivery[] | VoiceHandoffDeliveryStore;
52
+ reviews?: StoredVoiceCallReviewArtifact[] | ListStore<StoredVoiceCallReviewArtifact>;
53
+ sessions?: TSession[] | VoiceSessionStore<TSession>;
54
+ tasks?: StoredVoiceOpsTask[] | ListStore<StoredVoiceOpsTask>;
55
+ };
56
+ export type VoiceOutcomeContractHTMLHandlerOptions<TSession extends VoiceSessionRecord = VoiceSessionRecord> = VoiceOutcomeContractOptions<TSession> & {
57
+ headers?: HeadersInit;
58
+ render?: (report: VoiceOutcomeContractSuiteReport) => string | Promise<string>;
59
+ title?: string;
60
+ };
61
+ export type VoiceOutcomeContractRoutesOptions<TSession extends VoiceSessionRecord = VoiceSessionRecord> = VoiceOutcomeContractHTMLHandlerOptions<TSession> & {
62
+ htmlPath?: false | string;
63
+ name?: string;
64
+ path?: string;
65
+ };
66
+ export declare const runVoiceOutcomeContractSuite: <TSession extends VoiceSessionRecord = VoiceSessionRecord>(options: VoiceOutcomeContractOptions<TSession>) => Promise<VoiceOutcomeContractSuiteReport>;
67
+ export declare const renderVoiceOutcomeContractHTML: (report: VoiceOutcomeContractSuiteReport, options?: {
68
+ title?: string;
69
+ }) => string;
70
+ export declare const createVoiceOutcomeContractJSONHandler: <TSession extends VoiceSessionRecord = VoiceSessionRecord>(options: VoiceOutcomeContractOptions<TSession>) => () => Promise<VoiceOutcomeContractSuiteReport>;
71
+ export declare const createVoiceOutcomeContractHTMLHandler: <TSession extends VoiceSessionRecord = VoiceSessionRecord>(options: VoiceOutcomeContractHTMLHandlerOptions<TSession>) => () => Promise<Response>;
72
+ export declare const createVoiceOutcomeContractRoutes: <TSession extends VoiceSessionRecord = VoiceSessionRecord>(options: VoiceOutcomeContractRoutesOptions<TSession>) => Elysia<"", {
73
+ decorator: {};
74
+ store: {};
75
+ derive: {};
76
+ resolve: {};
77
+ }, {
78
+ typebox: {};
79
+ error: {};
80
+ }, {
81
+ schema: {};
82
+ standaloneSchema: {};
83
+ macro: {};
84
+ macroFn: {};
85
+ parser: {};
86
+ response: {};
87
+ }, {
88
+ [x: string]: {
89
+ get: {
90
+ body: unknown;
91
+ params: {};
92
+ query: unknown;
93
+ headers: unknown;
94
+ response: {
95
+ 200: VoiceOutcomeContractSuiteReport;
96
+ };
97
+ };
98
+ };
99
+ }, {
100
+ derive: {};
101
+ resolve: {};
102
+ schema: {};
103
+ standaloneSchema: {};
104
+ response: {};
105
+ }, {
106
+ derive: {};
107
+ resolve: {};
108
+ schema: {};
109
+ standaloneSchema: {};
110
+ response: {};
111
+ }>;
112
+ export {};
@@ -0,0 +1,49 @@
1
+ import type { VoiceCallDisposition, VoiceRouteResult, VoiceSessionHandle, VoiceSessionRecord } from './types';
2
+ export type VoiceTelephonyOutcomeAction = 'complete' | 'escalate' | 'ignore' | 'no-answer' | 'transfer' | 'voicemail';
3
+ export type VoiceTelephonyOutcomeProviderEvent = {
4
+ answeredBy?: string;
5
+ durationMs?: number;
6
+ from?: string;
7
+ metadata?: Record<string, unknown>;
8
+ provider?: string;
9
+ reason?: string;
10
+ sipCode?: number;
11
+ status?: string;
12
+ target?: string;
13
+ to?: string;
14
+ };
15
+ export type VoiceTelephonyOutcomeDecision = {
16
+ action: VoiceTelephonyOutcomeAction;
17
+ confidence: 'high' | 'low' | 'medium';
18
+ disposition?: VoiceCallDisposition;
19
+ metadata?: Record<string, unknown>;
20
+ reason?: string;
21
+ source: 'answered-by' | 'duration' | 'explicit-target' | 'policy' | 'sip' | 'status';
22
+ target?: string;
23
+ };
24
+ export type VoiceTelephonyOutcomeStatusDecision = VoiceTelephonyOutcomeAction | Omit<VoiceTelephonyOutcomeDecision, 'confidence' | 'source'> & {
25
+ confidence?: VoiceTelephonyOutcomeDecision['confidence'];
26
+ source?: VoiceTelephonyOutcomeDecision['source'];
27
+ };
28
+ export type VoiceTelephonyOutcomePolicy = {
29
+ completedStatuses?: string[];
30
+ escalationStatuses?: string[];
31
+ failedAsNoAnswer?: boolean;
32
+ failedStatuses?: string[];
33
+ includeProviderPayload?: boolean;
34
+ machineDetectionVoicemailValues?: string[];
35
+ metadata?: Record<string, unknown>;
36
+ minAnsweredDurationMs?: number;
37
+ noAnswerOnZeroDuration?: boolean;
38
+ noAnswerSipCodes?: number[];
39
+ noAnswerStatuses?: string[];
40
+ statusMap?: Record<string, VoiceTelephonyOutcomeStatusDecision>;
41
+ transferStatuses?: string[];
42
+ transferTarget?: string | ((event: VoiceTelephonyOutcomeProviderEvent) => string | undefined);
43
+ voicemailStatuses?: string[];
44
+ };
45
+ export type VoiceTelephonyOutcomeRouteResult<TResult = unknown> = VoiceRouteResult<TResult>;
46
+ export declare const createVoiceTelephonyOutcomePolicy: (policy?: VoiceTelephonyOutcomePolicy) => Required<Pick<VoiceTelephonyOutcomePolicy, "completedStatuses" | "escalationStatuses" | "failedAsNoAnswer" | "failedStatuses" | "includeProviderPayload" | "machineDetectionVoicemailValues" | "noAnswerOnZeroDuration" | "noAnswerSipCodes" | "noAnswerStatuses" | "transferStatuses" | "voicemailStatuses">> & VoiceTelephonyOutcomePolicy;
47
+ export declare const resolveVoiceTelephonyOutcome: (event: VoiceTelephonyOutcomeProviderEvent, policyInput?: VoiceTelephonyOutcomePolicy) => VoiceTelephonyOutcomeDecision;
48
+ export declare const voiceTelephonyOutcomeToRouteResult: <TResult = unknown>(decision: VoiceTelephonyOutcomeDecision, result?: TResult) => VoiceTelephonyOutcomeRouteResult<TResult>;
49
+ export declare const applyVoiceTelephonyOutcome: <TContext = unknown, TSession extends VoiceSessionRecord = VoiceSessionRecord, TResult = unknown>(api: VoiceSessionHandle<TContext, TSession, TResult>, decision: VoiceTelephonyOutcomeDecision, result?: TResult) => Promise<void>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@absolutejs/voice",
3
- "version": "0.0.22-beta.67",
3
+ "version": "0.0.22-beta.69",
4
4
  "description": "Voice primitives and Elysia plugin for AbsoluteJS",
5
5
  "repository": {
6
6
  "type": "git",