@absolutejs/voice 0.0.22-beta.105 → 0.0.22-beta.107

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
@@ -10546,304 +10546,166 @@ var createVoiceAppKitRoutes = (options) => {
10546
10546
  };
10547
10547
  };
10548
10548
  var createVoiceAppKit = createVoiceAppKitRoutes;
10549
- // src/workflowContract.ts
10550
- var getObject2 = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : undefined;
10551
- var getPathValue2 = (value, path) => {
10552
- let current = value;
10553
- for (const part of path.split(".").filter(Boolean)) {
10554
- const record = getObject2(current);
10555
- if (!record || !(part in record)) {
10556
- return;
10549
+ // src/simulationSuite.ts
10550
+ import { Elysia as Elysia19 } from "elysia";
10551
+
10552
+ // src/outcomeContract.ts
10553
+ import { Elysia as Elysia17 } from "elysia";
10554
+ var escapeHtml18 = (value) => value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
10555
+ var getPayloadString = (event, key) => typeof event.payload[key] === "string" ? event.payload[key] : undefined;
10556
+ var toList = async (input) => Array.isArray(input) ? input : await input?.list() ?? [];
10557
+ var hydrateSessions = async (input) => {
10558
+ if (!input)
10559
+ return [];
10560
+ if (Array.isArray(input))
10561
+ return input;
10562
+ const summaries = await input.list();
10563
+ const sessions = await Promise.all(summaries.map((summary) => input.get(summary.id)));
10564
+ const hydrated = [];
10565
+ for (const session of sessions) {
10566
+ if (session) {
10567
+ hydrated.push(session);
10557
10568
  }
10558
- current = record[part];
10559
- }
10560
- return current;
10561
- };
10562
- var hasValue = (value, match) => {
10563
- switch (match) {
10564
- case "boolean":
10565
- return typeof value === "boolean";
10566
- case "number":
10567
- return typeof value === "number" && Number.isFinite(value);
10568
- case "string":
10569
- return typeof value === "string";
10570
- case "truthy":
10571
- return Boolean(value);
10572
- case "non-empty":
10573
- default:
10574
- return Array.isArray(value) ? value.length > 0 : typeof value === "string" ? value.trim().length > 0 : value !== undefined && value !== null;
10575
10569
  }
10570
+ return hydrated;
10576
10571
  };
10577
- var resolveOutcome2 = (routeResult) => {
10578
- if (routeResult.complete)
10579
- return "complete";
10580
- if (routeResult.transfer)
10581
- return "transfer";
10582
- if (routeResult.escalate)
10583
- return "escalate";
10584
- if (routeResult.voicemail)
10585
- return "voicemail";
10586
- if (routeResult.noAnswer)
10587
- return "no-answer";
10588
- return;
10589
- };
10590
- var validateVoiceWorkflowRouteResult = (definition, routeResult) => {
10572
+ var dispositionForSession = (session) => session.call?.disposition ?? (session.status === "completed" ? "completed" : undefined);
10573
+ var matchesDisposition = (disposition, expected) => expected === undefined || disposition === expected;
10574
+ var reportContract = (input) => {
10575
+ const { contract } = input;
10576
+ const sessions = input.sessions.filter((session) => (!contract.scenarioId || session.scenarioId === contract.scenarioId) && matchesDisposition(dispositionForSession(session), contract.expectedDisposition));
10577
+ const sessionIds = new Set(sessions.map((session) => session.id));
10578
+ const reviews = input.reviews.filter((review) => matchesDisposition(review.summary.outcome, contract.expectedDisposition));
10579
+ const tasks = input.tasks.filter((task) => matchesDisposition(task.outcome, contract.expectedDisposition));
10580
+ 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)));
10581
+ const events = input.events.filter((event) => {
10582
+ const eventSessionId = getPayloadString(event, "sessionId");
10583
+ const eventOutcome = getPayloadString(event, "outcome") ?? getPayloadString(event, "disposition");
10584
+ return (sessionIds.size === 0 || !eventSessionId || sessionIds.has(eventSessionId)) && (!contract.expectedDisposition || eventOutcome === contract.expectedDisposition);
10585
+ });
10591
10586
  const issues = [];
10592
- const requiredFields = (definition.fields ?? []).filter((field) => field.required !== false).map((field) => field.path);
10593
- const missingFields = [];
10594
- const outcome = resolveOutcome2(routeResult);
10595
- if (definition.outcome && outcome !== definition.outcome) {
10587
+ const minSessions = contract.minSessions ?? 1;
10588
+ if (sessions.length < minSessions) {
10596
10589
  issues.push({
10597
- code: "workflow.outcome_mismatch",
10598
- message: `Expected workflow outcome ${definition.outcome}, saw ${outcome ?? "none"}.`
10590
+ code: "outcome.sessions_missing",
10591
+ message: `Expected at least ${minSessions} matching session(s), saw ${sessions.length}.`
10599
10592
  });
10600
10593
  }
10601
- for (const field of definition.fields ?? []) {
10602
- if (field.required === false)
10603
- continue;
10604
- const paths = [field.path, ...field.aliases ?? []];
10605
- const present = paths.some((path) => hasValue(getPathValue2(routeResult.result, path), field.match ?? "non-empty"));
10606
- if (!present) {
10607
- missingFields.push(field.path);
10594
+ if (contract.requireReview !== false && reviews.length === 0) {
10595
+ issues.push({
10596
+ code: "outcome.review_missing",
10597
+ message: "Expected at least one matching review artifact."
10598
+ });
10599
+ }
10600
+ if (contract.requireTask && tasks.length < (contract.minTasks ?? 1)) {
10601
+ issues.push({
10602
+ code: "outcome.task_missing",
10603
+ message: `Expected at least ${contract.minTasks ?? 1} matching task(s), saw ${tasks.length}.`
10604
+ });
10605
+ }
10606
+ for (const action of contract.requireHandoffActions ?? []) {
10607
+ if (!handoffs.some((handoff) => handoff.action === action)) {
10608
10608
  issues.push({
10609
- code: "workflow.missing_field",
10610
- field: field.path,
10611
- message: `Missing required workflow field: ${field.label ?? field.path}.`
10609
+ code: "outcome.handoff_missing",
10610
+ message: `Expected handoff action ${action}.`
10611
+ });
10612
+ }
10613
+ }
10614
+ for (const type of contract.requireIntegrationEvents ?? []) {
10615
+ if (!events.some((event) => event.type === type)) {
10616
+ issues.push({
10617
+ code: "outcome.integration_event_missing",
10618
+ message: `Expected integration event ${type}.`
10612
10619
  });
10613
10620
  }
10614
10621
  }
10615
- issues.push(...definition.validate?.({
10616
- result: routeResult.result,
10617
- routeResult
10618
- }) ?? []);
10619
10622
  return {
10620
- contractId: definition.id,
10623
+ contractId: contract.id,
10624
+ description: contract.description,
10621
10625
  issues,
10622
- missingFields,
10623
- outcome,
10624
- pass: issues.length === 0,
10625
- requiredFields
10626
+ label: contract.label,
10627
+ matched: {
10628
+ handoffs: handoffs.length,
10629
+ integrationEvents: events.length,
10630
+ reviews: reviews.length,
10631
+ sessions: sessions.length,
10632
+ tasks: tasks.length
10633
+ },
10634
+ pass: issues.length === 0
10626
10635
  };
10627
10636
  };
10628
- var createVoiceWorkflowScenario = (definition, overrides = {}) => ({
10629
- description: definition.description,
10630
- forbiddenHandoffActions: definition.forbiddenHandoffActions,
10631
- id: definition.id,
10632
- label: definition.label,
10633
- maxProviderErrors: definition.maxProviderErrors,
10634
- maxSessionErrors: definition.maxSessionErrors,
10635
- minSessions: definition.minSessions,
10636
- minTurns: definition.minTurns,
10637
- requiredAssistantIncludes: definition.requiredAssistantIncludes,
10638
- requiredDisposition: definition.requiredDisposition,
10639
- requiredHandoffActions: definition.requiredHandoffActions,
10640
- requiredLifecycleTypes: definition.requiredLifecycleTypes,
10641
- requiredTranscriptIncludes: definition.requiredTranscriptIncludes,
10642
- requiredWorkflowContracts: [definition.id],
10643
- scenarioId: definition.scenarioId,
10644
- ...overrides
10645
- });
10646
- var createVoiceWorkflowContract = (definition) => ({
10647
- assertRouteResult: (routeResult) => {
10648
- const validation = validateVoiceWorkflowRouteResult(definition, routeResult);
10649
- if (!validation.pass) {
10650
- throw new Error(`Voice workflow contract ${definition.id} failed: ${validation.issues.map((issue) => issue.message).join(" ")}`);
10637
+ var runVoiceOutcomeContractSuite = async (options) => {
10638
+ const [sessions, reviews, tasks, events, handoffs] = await Promise.all([
10639
+ hydrateSessions(options.sessions),
10640
+ toList(options.reviews),
10641
+ toList(options.tasks),
10642
+ toList(options.events),
10643
+ toList(options.handoffs)
10644
+ ]);
10645
+ const contracts = options.contracts.map((contract) => reportContract({ contract, events, handoffs, reviews, sessions, tasks }));
10646
+ const passed = contracts.filter((contract) => contract.pass).length;
10647
+ const failed = contracts.length - passed;
10648
+ return {
10649
+ checkedAt: Date.now(),
10650
+ contracts,
10651
+ failed,
10652
+ passed,
10653
+ status: failed > 0 ? "fail" : "pass",
10654
+ total: contracts.length
10655
+ };
10656
+ };
10657
+ var renderVoiceOutcomeContractHTML = (report, options = {}) => {
10658
+ const title = options.title ?? "Voice Outcome Contracts";
10659
+ const contracts = report.contracts.map((contract) => `<section class="contract ${contract.pass ? "pass" : "fail"}">
10660
+ <div class="contract-header">
10661
+ <div>
10662
+ <p class="eyebrow">${escapeHtml18(contract.contractId)}</p>
10663
+ <h2>${escapeHtml18(contract.label ?? contract.contractId)}</h2>
10664
+ ${contract.description ? `<p>${escapeHtml18(contract.description)}</p>` : ""}
10665
+ </div>
10666
+ <strong>${contract.pass ? "pass" : "fail"}</strong>
10667
+ </div>
10668
+ <div class="grid">
10669
+ <span>sessions ${String(contract.matched.sessions)}</span>
10670
+ <span>reviews ${String(contract.matched.reviews)}</span>
10671
+ <span>tasks ${String(contract.matched.tasks)}</span>
10672
+ <span>handoffs ${String(contract.matched.handoffs)}</span>
10673
+ <span>events ${String(contract.matched.integrationEvents)}</span>
10674
+ </div>
10675
+ ${contract.issues.length ? `<ul>${contract.issues.map((issue) => `<li>${escapeHtml18(issue.message)}</li>`).join("")}</ul>` : ""}
10676
+ </section>`).join("");
10677
+ return `<!doctype html><html lang="en"><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /><title>${escapeHtml18(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>${escapeHtml18(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>`;
10678
+ };
10679
+ var createVoiceOutcomeContractJSONHandler = (options) => async () => runVoiceOutcomeContractSuite(options);
10680
+ var createVoiceOutcomeContractHTMLHandler = (options) => async () => {
10681
+ const report = await runVoiceOutcomeContractSuite(options);
10682
+ const render = options.render ?? ((input) => renderVoiceOutcomeContractHTML(input, options));
10683
+ return new Response(await render(report), {
10684
+ headers: {
10685
+ "Content-Type": "text/html; charset=utf-8",
10686
+ ...options.headers
10651
10687
  }
10652
- },
10653
- definition,
10654
- toScenarioEval: (overrides) => createVoiceWorkflowScenario(definition, overrides),
10655
- validateRouteResult: (routeResult) => validateVoiceWorkflowRouteResult(definition, routeResult)
10656
- });
10657
- var presetDefinitions = {
10658
- "appointment-booking": {
10659
- description: "Appointment booking should complete with enough identity, appointment, and follow-up details to act on.",
10660
- fields: [
10661
- { aliases: ["name", "customer.name"], label: "Caller name", path: "caller.name" },
10662
- {
10663
- aliases: ["phone", "customer.phone"],
10664
- label: "Caller phone",
10665
- path: "caller.phone"
10666
- },
10667
- {
10668
- aliases: ["appointment.start", "appointment.time", "scheduledAt"],
10669
- label: "Appointment time",
10670
- path: "appointment.startsAt"
10671
- },
10672
- {
10673
- aliases: ["summary", "assistantSummary"],
10674
- label: "Summary",
10675
- path: "appointment.summary"
10676
- }
10677
- ],
10678
- id: "appointment-booking",
10679
- label: "Appointment booking",
10680
- outcome: "complete",
10681
- requiredDisposition: "completed"
10682
- },
10683
- "lead-qualification": {
10684
- description: "Lead qualification should complete with contact, need, qualification, and next-step fields.",
10685
- fields: [
10686
- { aliases: ["name", "lead.name"], label: "Lead name", path: "contact.name" },
10687
- {
10688
- aliases: ["email", "lead.email"],
10689
- label: "Lead email",
10690
- path: "contact.email"
10691
- },
10692
- {
10693
- aliases: ["need", "pain", "summary"],
10694
- label: "Need",
10695
- path: "qualification.need"
10696
- },
10697
- {
10698
- aliases: ["qualified", "qualification.qualified"],
10699
- label: "Qualified",
10700
- match: "boolean",
10701
- path: "qualification.isQualified"
10702
- },
10703
- {
10704
- aliases: ["nextStep", "followUp"],
10705
- label: "Next step",
10706
- path: "qualification.nextStep"
10707
- }
10708
- ],
10709
- id: "lead-qualification",
10710
- label: "Lead qualification",
10711
- outcome: "complete",
10712
- requiredDisposition: "completed"
10713
- },
10714
- "support-triage": {
10715
- description: "Support triage should capture identity, issue summary, severity, and the operational follow-up.",
10716
- fields: [
10717
- {
10718
- aliases: ["name", "customer.name"],
10719
- label: "Customer name",
10720
- path: "customer.name"
10721
- },
10722
- {
10723
- aliases: ["issue", "summary", "assistantSummary"],
10724
- label: "Issue summary",
10725
- path: "issue.summary"
10726
- },
10727
- {
10728
- aliases: ["priority", "severity"],
10729
- label: "Severity",
10730
- path: "issue.severity"
10731
- },
10732
- {
10733
- aliases: ["nextStep", "task.title"],
10734
- label: "Next step",
10735
- path: "resolution.nextStep"
10736
- }
10737
- ],
10738
- id: "support-triage",
10739
- label: "Support triage",
10740
- outcome: "complete",
10741
- requiredDisposition: "completed"
10742
- },
10743
- "transfer-handoff": {
10744
- description: "Transfer handoff should produce a routed transfer plus handoff evidence.",
10745
- fields: [
10746
- {
10747
- aliases: ["target", "callTarget"],
10748
- label: "Transfer target",
10749
- path: "transfer.target"
10750
- },
10751
- {
10752
- aliases: ["reason", "callReason"],
10753
- label: "Transfer reason",
10754
- path: "transfer.reason"
10755
- },
10756
- {
10757
- aliases: ["summary", "assistantSummary"],
10758
- label: "Transfer summary",
10759
- path: "transfer.summary"
10760
- }
10761
- ],
10762
- id: "transfer-handoff",
10763
- label: "Transfer handoff",
10764
- outcome: "transfer",
10765
- requiredDisposition: "transferred",
10766
- requiredHandoffActions: ["transfer"]
10767
- },
10768
- "voicemail-callback": {
10769
- description: "Voicemail callback should preserve enough caller and callback context for follow-up.",
10770
- fields: [
10771
- {
10772
- aliases: ["name", "caller.name"],
10773
- label: "Caller name",
10774
- path: "voicemail.callerName"
10775
- },
10776
- {
10777
- aliases: ["phone", "caller.phone"],
10778
- label: "Callback phone",
10779
- path: "voicemail.callbackPhone"
10780
- },
10781
- {
10782
- aliases: ["message", "summary", "assistantSummary"],
10783
- label: "Voicemail summary",
10784
- path: "voicemail.summary"
10785
- }
10786
- ],
10787
- id: "voicemail-callback",
10788
- label: "Voicemail callback",
10789
- outcome: "voicemail",
10790
- requiredDisposition: "voicemail",
10791
- requiredHandoffActions: ["voicemail"]
10792
- }
10793
- };
10794
- var createVoiceWorkflowContractPreset = (name, options = {}) => {
10795
- const preset = presetDefinitions[name];
10796
- return createVoiceWorkflowContract({
10797
- ...preset,
10798
- ...options,
10799
- fields: options.fields ?? preset.fields,
10800
- id: options.id ?? preset.id
10801
- });
10802
- };
10803
- var recordVoiceWorkflowContractTrace = async (input) => input.store.append({
10804
- at: input.at ?? Date.now(),
10805
- payload: {
10806
- contractId: input.contractId ?? input.validation.contractId,
10807
- issues: input.validation.issues,
10808
- missingFields: input.validation.missingFields,
10809
- outcome: input.validation.outcome,
10810
- requiredFields: input.validation.requiredFields,
10811
- status: input.validation.pass ? "pass" : "fail"
10812
- },
10813
- scenarioId: input.scenarioId,
10814
- sessionId: input.sessionId,
10815
- traceId: input.traceId,
10816
- turnId: input.turnId,
10817
- type: "workflow.contract"
10818
- });
10819
- var createVoiceWorkflowContractHandler = (input) => {
10820
- return async (session, turn, api, context) => {
10821
- const legacyHandler = input.handler;
10822
- const objectHandler = input.handler;
10823
- const result = input.handler.length >= 4 ? await legacyHandler(session, turn, api, context) : await objectHandler({ api, context, session, turn });
10824
- if (!result)
10825
- return result;
10826
- const resolved = input.resolveContract?.({ context, result, session, turn }) ?? input.contract;
10827
- if (!resolved)
10828
- return result;
10829
- const contract = "validateRouteResult" in resolved ? resolved : createVoiceWorkflowContract(resolved);
10830
- const validation = contract.validateRouteResult(result);
10831
- if (input.store) {
10832
- await recordVoiceWorkflowContractTrace({
10833
- scenarioId: session.scenarioId,
10834
- sessionId: session.id,
10835
- store: input.store,
10836
- turnId: turn.id,
10837
- validation
10838
- });
10839
- }
10840
- return result;
10841
- };
10842
- };
10843
- // src/toolRuntime.ts
10844
- var toErrorMessage4 = (error) => error instanceof Error ? error.message : String(error);
10845
- var sleep4 = (ms) => new Promise((resolve2) => {
10846
- setTimeout(resolve2, ms);
10688
+ });
10689
+ };
10690
+ var createVoiceOutcomeContractRoutes = (options) => {
10691
+ const path = options.path ?? "/api/outcome-contracts";
10692
+ const htmlPath = options.htmlPath === undefined ? `${path}/htmx` : options.htmlPath;
10693
+ const routes = new Elysia17({
10694
+ name: options.name ?? "absolutejs-voice-outcome-contracts"
10695
+ }).get(path, createVoiceOutcomeContractJSONHandler(options));
10696
+ if (htmlPath) {
10697
+ routes.get(htmlPath, createVoiceOutcomeContractHTMLHandler(options));
10698
+ }
10699
+ return routes;
10700
+ };
10701
+
10702
+ // src/toolContract.ts
10703
+ import { Elysia as Elysia18 } from "elysia";
10704
+
10705
+ // src/toolRuntime.ts
10706
+ var toErrorMessage4 = (error) => error instanceof Error ? error.message : String(error);
10707
+ var sleep4 = (ms) => new Promise((resolve2) => {
10708
+ setTimeout(resolve2, ms);
10847
10709
  });
10848
10710
  var formatToolResult2 = (result) => {
10849
10711
  if (typeof result === "string") {
@@ -11036,8 +10898,8 @@ var createVoiceToolIdempotencyKey = (input) => {
11036
10898
  args
11037
10899
  ].join(":");
11038
10900
  };
10901
+
11039
10902
  // src/toolContract.ts
11040
- import { Elysia as Elysia17 } from "elysia";
11041
10903
  var createDefaultSession = (contractId, caseId) => createVoiceSessionRecord(`tool-contract-${contractId}-${caseId}`);
11042
10904
  var createDefaultTurn = (caseId) => ({
11043
10905
  committedAt: Date.now(),
@@ -11047,7 +10909,7 @@ var createDefaultTurn = (caseId) => ({
11047
10909
  });
11048
10910
  var defaultApi = {};
11049
10911
  var sameJSON = (left, right) => JSON.stringify(left) === JSON.stringify(right);
11050
- var escapeHtml18 = (value) => value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
10912
+ var escapeHtml19 = (value) => value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
11051
10913
  var evaluateExpectation = (input) => {
11052
10914
  const issues = [];
11053
10915
  const expect = input.expect;
@@ -11213,19 +11075,19 @@ var renderVoiceToolContractHTML = (report, options = {}) => {
11213
11075
  const title = options.title ?? "Voice Tool Contracts";
11214
11076
  const contracts = report.contracts.map((contract) => {
11215
11077
  const cases = contract.cases.map((testCase) => `<tr>
11216
- <td>${escapeHtml18(testCase.label ?? testCase.caseId)}</td>
11078
+ <td>${escapeHtml19(testCase.label ?? testCase.caseId)}</td>
11217
11079
  <td class="${testCase.pass ? "pass" : "fail"}">${testCase.pass ? "pass" : "fail"}</td>
11218
- <td>${escapeHtml18(testCase.status)}</td>
11080
+ <td>${escapeHtml19(testCase.status)}</td>
11219
11081
  <td>${String(testCase.attempts)}</td>
11220
11082
  <td>${String(testCase.elapsedMs)}ms</td>
11221
11083
  <td>${testCase.timedOut ? "yes" : "no"}</td>
11222
- <td>${escapeHtml18(testCase.issues.map((issue) => issue.message).join(" ") || testCase.error || "")}</td>
11084
+ <td>${escapeHtml19(testCase.issues.map((issue) => issue.message).join(" ") || testCase.error || "")}</td>
11223
11085
  </tr>`).join("");
11224
11086
  return `<section class="contract ${contract.pass ? "pass" : "fail"}">
11225
11087
  <div class="contract-header">
11226
11088
  <div>
11227
- <p class="eyebrow">${escapeHtml18(contract.toolName)}</p>
11228
- <h2>${escapeHtml18(contract.label ?? contract.contractId)}</h2>
11089
+ <p class="eyebrow">${escapeHtml19(contract.toolName)}</p>
11090
+ <h2>${escapeHtml19(contract.label ?? contract.contractId)}</h2>
11229
11091
  </div>
11230
11092
  <strong class="${contract.pass ? "pass" : "fail"}">${contract.pass ? "Passing" : "Failing"}</strong>
11231
11093
  </div>
@@ -11235,7 +11097,7 @@ var renderVoiceToolContractHTML = (report, options = {}) => {
11235
11097
  </table>
11236
11098
  </section>`;
11237
11099
  }).join("");
11238
- return `<!doctype html><html lang="en"><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /><title>${escapeHtml18(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(245,158,11,.12))}.eyebrow{color:#fbbf24;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}.summary{display:flex;flex-wrap:wrap;gap:10px}.pill{background:#0f1217;border:1px solid #3f3f46;border-radius:999px;padding:7px 10px}.contract-header{align-items:flex-start;display:flex;gap:16px;justify-content:space-between}h2{margin:.2rem 0 1rem}.pass{color:#86efac}.fail{color:#fca5a5}.contract.fail{border-color:rgba(248,113,113,.45)}table{border-collapse:collapse;width:100%}td,th{border-bottom:1px solid #2a323a;padding:12px;text-align:left;vertical-align:top}th{color:#a8b0b8;font-size:.82rem}@media(max-width:800px){main{padding:18px}table{display:block;overflow:auto}.contract-header{display:block}}</style></head><body><main><section class="hero"><p class="eyebrow">Tool Reliability</p><h1>${escapeHtml18(title)}</h1><div class="summary"><span class="pill ${report.status === "pass" ? "pass" : "fail"}">${escapeHtml18(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 tool contracts configured.</p></section>'}</main></body></html>`;
11100
+ return `<!doctype html><html lang="en"><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /><title>${escapeHtml19(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(245,158,11,.12))}.eyebrow{color:#fbbf24;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}.summary{display:flex;flex-wrap:wrap;gap:10px}.pill{background:#0f1217;border:1px solid #3f3f46;border-radius:999px;padding:7px 10px}.contract-header{align-items:flex-start;display:flex;gap:16px;justify-content:space-between}h2{margin:.2rem 0 1rem}.pass{color:#86efac}.fail{color:#fca5a5}.contract.fail{border-color:rgba(248,113,113,.45)}table{border-collapse:collapse;width:100%}td,th{border-bottom:1px solid #2a323a;padding:12px;text-align:left;vertical-align:top}th{color:#a8b0b8;font-size:.82rem}@media(max-width:800px){main{padding:18px}table{display:block;overflow:auto}.contract-header{display:block}}</style></head><body><main><section class="hero"><p class="eyebrow">Tool Reliability</p><h1>${escapeHtml19(title)}</h1><div class="summary"><span class="pill ${report.status === "pass" ? "pass" : "fail"}">${escapeHtml19(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 tool contracts configured.</p></section>'}</main></body></html>`;
11239
11101
  };
11240
11102
  var createVoiceToolContractJSONHandler = (options) => () => runVoiceToolContractSuite(options);
11241
11103
  var createVoiceToolContractHTMLHandler = (options) => async () => {
@@ -11249,22 +11111,484 @@ var createVoiceToolContractHTMLHandler = (options) => async () => {
11249
11111
  }
11250
11112
  });
11251
11113
  };
11252
- var createVoiceToolContractRoutes = (options) => {
11253
- const path = options.path ?? "/api/tool-contracts";
11254
- const htmlPath = options.htmlPath === undefined ? `${path}/htmx` : options.htmlPath;
11255
- const routes = new Elysia17({
11256
- name: options.name ?? "absolutejs-voice-tool-contracts"
11257
- }).get(path, createVoiceToolContractJSONHandler(options));
11258
- if (htmlPath) {
11259
- routes.get(htmlPath, createVoiceToolContractHTMLHandler(options));
11260
- }
11261
- return routes;
11114
+ var createVoiceToolContractRoutes = (options) => {
11115
+ const path = options.path ?? "/api/tool-contracts";
11116
+ const htmlPath = options.htmlPath === undefined ? `${path}/htmx` : options.htmlPath;
11117
+ const routes = new Elysia18({
11118
+ name: options.name ?? "absolutejs-voice-tool-contracts"
11119
+ }).get(path, createVoiceToolContractJSONHandler(options));
11120
+ if (htmlPath) {
11121
+ routes.get(htmlPath, createVoiceToolContractHTMLHandler(options));
11122
+ }
11123
+ return routes;
11124
+ };
11125
+
11126
+ // src/simulationSuite.ts
11127
+ var escapeHtml20 = (value) => value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
11128
+ var summarizeSection = (report) => ({
11129
+ failed: report.failed,
11130
+ passed: report.passed,
11131
+ status: report.status,
11132
+ total: report.total
11133
+ });
11134
+ var hasWork = (options) => Boolean(options.store || options.scenarios?.length || options.fixtures?.length || options.fixtureStore || options.tools?.length || options.outcomes?.contracts.length);
11135
+ var collectSimulationActions = (input) => {
11136
+ const actions = [];
11137
+ if (input.sessions?.failed) {
11138
+ const firstFailed = input.sessions.sessions.find((session) => session.status === "fail");
11139
+ actions.push({
11140
+ description: firstFailed ? `Inspect session ${firstFailed.sessionId}; at least one quality metric is outside threshold.` : "Inspect failing session quality reports.",
11141
+ href: input.links?.sessions,
11142
+ label: "Review failing session quality",
11143
+ section: "sessions",
11144
+ severity: "error"
11145
+ });
11146
+ }
11147
+ for (const scenario of input.scenarios?.scenarios ?? []) {
11148
+ if (scenario.status !== "fail") {
11149
+ continue;
11150
+ }
11151
+ const issue = scenario.issues[0] ?? scenario.sessions.find((session) => session.issues.length > 0)?.issues[0] ?? "Scenario did not meet its expected trace conditions.";
11152
+ actions.push({
11153
+ description: `${scenario.label}: ${issue}`,
11154
+ href: input.links?.scenarios,
11155
+ label: `Fix scenario ${scenario.label}`,
11156
+ section: "scenarios",
11157
+ severity: "error"
11158
+ });
11159
+ }
11160
+ for (const fixture of input.fixtures?.fixtures ?? []) {
11161
+ if (fixture.status !== "fail") {
11162
+ continue;
11163
+ }
11164
+ const failedScenario = fixture.report.scenarios.find((scenario) => scenario.status === "fail");
11165
+ actions.push({
11166
+ description: failedScenario ? `${fixture.label}: ${failedScenario.label} failed.` : `${fixture.label}: fixture simulation failed.`,
11167
+ href: input.links?.fixtures,
11168
+ label: `Update fixture ${fixture.label}`,
11169
+ section: "fixtures",
11170
+ severity: "error"
11171
+ });
11172
+ }
11173
+ for (const tool of input.tools?.contracts ?? []) {
11174
+ if (tool.pass) {
11175
+ continue;
11176
+ }
11177
+ const issue = tool.issues[0] ?? tool.cases.find((testCase) => !testCase.pass)?.issues[0];
11178
+ actions.push({
11179
+ description: issue?.message ?? `${tool.toolName} contract failed.`,
11180
+ href: input.links?.tools,
11181
+ label: `Fix tool contract ${tool.label ?? tool.contractId}`,
11182
+ section: "tools",
11183
+ severity: "error"
11184
+ });
11185
+ }
11186
+ for (const outcome of input.outcomes?.contracts ?? []) {
11187
+ if (outcome.pass) {
11188
+ continue;
11189
+ }
11190
+ actions.push({
11191
+ description: outcome.issues[0]?.message ?? `${outcome.label ?? outcome.contractId} is missing required outcome evidence.`,
11192
+ href: input.links?.outcomes,
11193
+ label: `Fix outcome ${outcome.label ?? outcome.contractId}`,
11194
+ section: "outcomes",
11195
+ severity: "error"
11196
+ });
11197
+ }
11198
+ return actions;
11199
+ };
11200
+ var runVoiceSimulationSuite = async (options) => {
11201
+ const include = options.include ?? {};
11202
+ const shouldRunSessions = include.sessions ?? Boolean(options.store || !hasWork(options));
11203
+ const shouldRunScenarios = include.scenarios ?? Boolean(options.scenarios?.length);
11204
+ const shouldRunFixtures = include.fixtures ?? Boolean((options.fixtures?.length ?? 0) > 0 || options.fixtureStore);
11205
+ const shouldRunTools = include.tools ?? Boolean(options.tools?.length);
11206
+ const shouldRunOutcomes = include.outcomes ?? Boolean(options.outcomes?.contracts.length);
11207
+ const [sessions, scenarios, fixtures, tools, outcomes] = await Promise.all([
11208
+ shouldRunSessions ? runVoiceSessionEvals({
11209
+ limit: options.limit,
11210
+ store: options.store,
11211
+ thresholds: options.thresholds
11212
+ }) : undefined,
11213
+ shouldRunScenarios ? runVoiceScenarioEvals({
11214
+ scenarios: options.scenarios,
11215
+ store: options.store
11216
+ }) : undefined,
11217
+ shouldRunFixtures ? runVoiceScenarioFixtureEvals({
11218
+ fixtures: options.fixtures,
11219
+ fixtureStore: options.fixtureStore,
11220
+ scenarios: options.scenarios
11221
+ }) : undefined,
11222
+ shouldRunTools ? runVoiceToolContractSuite({
11223
+ contracts: options.tools ?? []
11224
+ }) : undefined,
11225
+ shouldRunOutcomes && options.outcomes ? runVoiceOutcomeContractSuite(options.outcomes) : undefined
11226
+ ]);
11227
+ const sections = [sessions, scenarios, fixtures, tools, outcomes].filter((report) => Boolean(report));
11228
+ const failed = sections.filter((section) => section.status === "fail").length;
11229
+ const passed = sections.length - failed;
11230
+ const actions = collectSimulationActions({
11231
+ fixtures,
11232
+ links: options.actionLinks,
11233
+ outcomes,
11234
+ scenarios,
11235
+ sessions,
11236
+ tools
11237
+ });
11238
+ return {
11239
+ actions,
11240
+ checkedAt: Date.now(),
11241
+ failed,
11242
+ fixtures,
11243
+ outcomes,
11244
+ passed,
11245
+ scenarios,
11246
+ sessions,
11247
+ status: failed > 0 ? "fail" : "pass",
11248
+ summary: {
11249
+ fixtures: fixtures && summarizeSection(fixtures),
11250
+ outcomes: outcomes && summarizeSection(outcomes),
11251
+ scenarios: scenarios && summarizeSection(scenarios),
11252
+ sessions: sessions && summarizeSection(sessions),
11253
+ tools: tools && summarizeSection(tools)
11254
+ },
11255
+ tools,
11256
+ total: sections.length
11257
+ };
11258
+ };
11259
+ var renderSection = (label, summary) => {
11260
+ if (!summary) {
11261
+ return "";
11262
+ }
11263
+ return `<article class="${escapeHtml20(summary.status)}"><span>${escapeHtml20(label)}</span><strong>${escapeHtml20(summary.status)}</strong><p>${summary.passed}/${summary.total} passed, ${summary.failed} failed.</p></article>`;
11264
+ };
11265
+ var renderAction = (action) => {
11266
+ const content = `<strong>${escapeHtml20(action.label)}</strong><p>${escapeHtml20(action.description)}</p><span>${escapeHtml20(action.section)} / ${escapeHtml20(action.severity)}</span>`;
11267
+ return action.href ? `<a class="action" href="${escapeHtml20(action.href)}">${content}</a>` : `<article class="action">${content}</article>`;
11268
+ };
11269
+ var renderVoiceSimulationSuiteHTML = (report, options = {}) => {
11270
+ const title = options.title ?? "Voice Simulation Suite";
11271
+ return `<!doctype html><html lang="en"><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /><title>${escapeHtml20(title)}</title><style>body{background:#10151c;color:#f8f3e7;font-family:ui-sans-serif,system-ui,sans-serif;margin:0}main{margin:auto;max-width:1080px;padding:32px}.hero{background:linear-gradient(135deg,rgba(34,197,94,.18),rgba(59,130,246,.12));border:1px solid #283544;border-radius:28px;margin-bottom:18px;padding:28px}.eyebrow{color:#93c5fd;font-weight:900;letter-spacing:.12em;text-transform:uppercase}h1{font-size:clamp(2.4rem,6vw,5rem);line-height:.9;margin:.2rem 0 1rem}.badge{border:1px solid #3f3f46;border-radius:999px;display:inline-flex;padding:8px 12px}.pass{color:#86efac}.fail{color:#fca5a5}.grid,.actions{display:grid;gap:14px;grid-template-columns:repeat(auto-fit,minmax(190px,1fr));margin:18px 0}.grid article,.action{background:#151d27;border:1px solid #283544;border-radius:18px;color:inherit;padding:16px;text-decoration:none}.grid span,.action span{color:#aab5c0}.grid strong{display:block;font-size:2rem;margin:.25rem 0;text-transform:uppercase}.action strong{display:block;color:#f8f3e7;margin-bottom:.35rem}.action p{color:#d8dee6;margin:.3rem 0 .6rem}pre{background:#151d27;border:1px solid #283544;border-radius:18px;overflow:auto;padding:16px}</style></head><body><main><section class="hero"><p class="eyebrow">Pre-production proof</p><h1>${escapeHtml20(title)}</h1><p>One report for session quality, scenario evals, fixture simulations, tool contracts, and outcome contracts.</p><p class="badge ${escapeHtml20(report.status)}">Status: ${escapeHtml20(report.status)}</p><section class="grid">${renderSection("Sessions", report.summary.sessions)}${renderSection("Scenarios", report.summary.scenarios)}${renderSection("Fixtures", report.summary.fixtures)}${renderSection("Tools", report.summary.tools)}${renderSection("Outcomes", report.summary.outcomes)}</section></section><h2>Actions</h2><section class="actions">${report.actions.length > 0 ? report.actions.map(renderAction).join("") : '<article class="action"><strong>No action required</strong><p>All enabled simulation sections are passing.</p></article>'}</section><pre>${escapeHtml20(JSON.stringify({ summary: report.summary, actions: report.actions }, null, 2))}</pre></main></body></html>`;
11272
+ };
11273
+ var createVoiceSimulationSuiteRoutes = (options) => {
11274
+ const path = options.path ?? "/api/voice/simulations";
11275
+ const htmlPath = options.htmlPath === undefined ? "/voice/simulations" : options.htmlPath;
11276
+ const app = new Elysia19({
11277
+ name: options.name ?? "absolutejs-voice-simulation-suite"
11278
+ }).get(path, () => runVoiceSimulationSuite(options));
11279
+ if (htmlPath) {
11280
+ app.get(htmlPath, async () => {
11281
+ const report = await runVoiceSimulationSuite(options);
11282
+ const html = options.render ? await options.render(report) : renderVoiceSimulationSuiteHTML(report, options);
11283
+ return new Response(html, {
11284
+ headers: {
11285
+ "content-type": "text/html; charset=utf-8",
11286
+ ...options.headers
11287
+ }
11288
+ });
11289
+ });
11290
+ }
11291
+ return app;
11292
+ };
11293
+ // src/workflowContract.ts
11294
+ var getObject2 = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : undefined;
11295
+ var getPathValue2 = (value, path) => {
11296
+ let current = value;
11297
+ for (const part of path.split(".").filter(Boolean)) {
11298
+ const record = getObject2(current);
11299
+ if (!record || !(part in record)) {
11300
+ return;
11301
+ }
11302
+ current = record[part];
11303
+ }
11304
+ return current;
11305
+ };
11306
+ var hasValue = (value, match) => {
11307
+ switch (match) {
11308
+ case "boolean":
11309
+ return typeof value === "boolean";
11310
+ case "number":
11311
+ return typeof value === "number" && Number.isFinite(value);
11312
+ case "string":
11313
+ return typeof value === "string";
11314
+ case "truthy":
11315
+ return Boolean(value);
11316
+ case "non-empty":
11317
+ default:
11318
+ return Array.isArray(value) ? value.length > 0 : typeof value === "string" ? value.trim().length > 0 : value !== undefined && value !== null;
11319
+ }
11320
+ };
11321
+ var resolveOutcome2 = (routeResult) => {
11322
+ if (routeResult.complete)
11323
+ return "complete";
11324
+ if (routeResult.transfer)
11325
+ return "transfer";
11326
+ if (routeResult.escalate)
11327
+ return "escalate";
11328
+ if (routeResult.voicemail)
11329
+ return "voicemail";
11330
+ if (routeResult.noAnswer)
11331
+ return "no-answer";
11332
+ return;
11333
+ };
11334
+ var validateVoiceWorkflowRouteResult = (definition, routeResult) => {
11335
+ const issues = [];
11336
+ const requiredFields = (definition.fields ?? []).filter((field) => field.required !== false).map((field) => field.path);
11337
+ const missingFields = [];
11338
+ const outcome = resolveOutcome2(routeResult);
11339
+ if (definition.outcome && outcome !== definition.outcome) {
11340
+ issues.push({
11341
+ code: "workflow.outcome_mismatch",
11342
+ message: `Expected workflow outcome ${definition.outcome}, saw ${outcome ?? "none"}.`
11343
+ });
11344
+ }
11345
+ for (const field of definition.fields ?? []) {
11346
+ if (field.required === false)
11347
+ continue;
11348
+ const paths = [field.path, ...field.aliases ?? []];
11349
+ const present = paths.some((path) => hasValue(getPathValue2(routeResult.result, path), field.match ?? "non-empty"));
11350
+ if (!present) {
11351
+ missingFields.push(field.path);
11352
+ issues.push({
11353
+ code: "workflow.missing_field",
11354
+ field: field.path,
11355
+ message: `Missing required workflow field: ${field.label ?? field.path}.`
11356
+ });
11357
+ }
11358
+ }
11359
+ issues.push(...definition.validate?.({
11360
+ result: routeResult.result,
11361
+ routeResult
11362
+ }) ?? []);
11363
+ return {
11364
+ contractId: definition.id,
11365
+ issues,
11366
+ missingFields,
11367
+ outcome,
11368
+ pass: issues.length === 0,
11369
+ requiredFields
11370
+ };
11371
+ };
11372
+ var createVoiceWorkflowScenario = (definition, overrides = {}) => ({
11373
+ description: definition.description,
11374
+ forbiddenHandoffActions: definition.forbiddenHandoffActions,
11375
+ id: definition.id,
11376
+ label: definition.label,
11377
+ maxProviderErrors: definition.maxProviderErrors,
11378
+ maxSessionErrors: definition.maxSessionErrors,
11379
+ minSessions: definition.minSessions,
11380
+ minTurns: definition.minTurns,
11381
+ requiredAssistantIncludes: definition.requiredAssistantIncludes,
11382
+ requiredDisposition: definition.requiredDisposition,
11383
+ requiredHandoffActions: definition.requiredHandoffActions,
11384
+ requiredLifecycleTypes: definition.requiredLifecycleTypes,
11385
+ requiredTranscriptIncludes: definition.requiredTranscriptIncludes,
11386
+ requiredWorkflowContracts: [definition.id],
11387
+ scenarioId: definition.scenarioId,
11388
+ ...overrides
11389
+ });
11390
+ var createVoiceWorkflowContract = (definition) => ({
11391
+ assertRouteResult: (routeResult) => {
11392
+ const validation = validateVoiceWorkflowRouteResult(definition, routeResult);
11393
+ if (!validation.pass) {
11394
+ throw new Error(`Voice workflow contract ${definition.id} failed: ${validation.issues.map((issue) => issue.message).join(" ")}`);
11395
+ }
11396
+ },
11397
+ definition,
11398
+ toScenarioEval: (overrides) => createVoiceWorkflowScenario(definition, overrides),
11399
+ validateRouteResult: (routeResult) => validateVoiceWorkflowRouteResult(definition, routeResult)
11400
+ });
11401
+ var presetDefinitions = {
11402
+ "appointment-booking": {
11403
+ description: "Appointment booking should complete with enough identity, appointment, and follow-up details to act on.",
11404
+ fields: [
11405
+ { aliases: ["name", "customer.name"], label: "Caller name", path: "caller.name" },
11406
+ {
11407
+ aliases: ["phone", "customer.phone"],
11408
+ label: "Caller phone",
11409
+ path: "caller.phone"
11410
+ },
11411
+ {
11412
+ aliases: ["appointment.start", "appointment.time", "scheduledAt"],
11413
+ label: "Appointment time",
11414
+ path: "appointment.startsAt"
11415
+ },
11416
+ {
11417
+ aliases: ["summary", "assistantSummary"],
11418
+ label: "Summary",
11419
+ path: "appointment.summary"
11420
+ }
11421
+ ],
11422
+ id: "appointment-booking",
11423
+ label: "Appointment booking",
11424
+ outcome: "complete",
11425
+ requiredDisposition: "completed"
11426
+ },
11427
+ "lead-qualification": {
11428
+ description: "Lead qualification should complete with contact, need, qualification, and next-step fields.",
11429
+ fields: [
11430
+ { aliases: ["name", "lead.name"], label: "Lead name", path: "contact.name" },
11431
+ {
11432
+ aliases: ["email", "lead.email"],
11433
+ label: "Lead email",
11434
+ path: "contact.email"
11435
+ },
11436
+ {
11437
+ aliases: ["need", "pain", "summary"],
11438
+ label: "Need",
11439
+ path: "qualification.need"
11440
+ },
11441
+ {
11442
+ aliases: ["qualified", "qualification.qualified"],
11443
+ label: "Qualified",
11444
+ match: "boolean",
11445
+ path: "qualification.isQualified"
11446
+ },
11447
+ {
11448
+ aliases: ["nextStep", "followUp"],
11449
+ label: "Next step",
11450
+ path: "qualification.nextStep"
11451
+ }
11452
+ ],
11453
+ id: "lead-qualification",
11454
+ label: "Lead qualification",
11455
+ outcome: "complete",
11456
+ requiredDisposition: "completed"
11457
+ },
11458
+ "support-triage": {
11459
+ description: "Support triage should capture identity, issue summary, severity, and the operational follow-up.",
11460
+ fields: [
11461
+ {
11462
+ aliases: ["name", "customer.name"],
11463
+ label: "Customer name",
11464
+ path: "customer.name"
11465
+ },
11466
+ {
11467
+ aliases: ["issue", "summary", "assistantSummary"],
11468
+ label: "Issue summary",
11469
+ path: "issue.summary"
11470
+ },
11471
+ {
11472
+ aliases: ["priority", "severity"],
11473
+ label: "Severity",
11474
+ path: "issue.severity"
11475
+ },
11476
+ {
11477
+ aliases: ["nextStep", "task.title"],
11478
+ label: "Next step",
11479
+ path: "resolution.nextStep"
11480
+ }
11481
+ ],
11482
+ id: "support-triage",
11483
+ label: "Support triage",
11484
+ outcome: "complete",
11485
+ requiredDisposition: "completed"
11486
+ },
11487
+ "transfer-handoff": {
11488
+ description: "Transfer handoff should produce a routed transfer plus handoff evidence.",
11489
+ fields: [
11490
+ {
11491
+ aliases: ["target", "callTarget"],
11492
+ label: "Transfer target",
11493
+ path: "transfer.target"
11494
+ },
11495
+ {
11496
+ aliases: ["reason", "callReason"],
11497
+ label: "Transfer reason",
11498
+ path: "transfer.reason"
11499
+ },
11500
+ {
11501
+ aliases: ["summary", "assistantSummary"],
11502
+ label: "Transfer summary",
11503
+ path: "transfer.summary"
11504
+ }
11505
+ ],
11506
+ id: "transfer-handoff",
11507
+ label: "Transfer handoff",
11508
+ outcome: "transfer",
11509
+ requiredDisposition: "transferred",
11510
+ requiredHandoffActions: ["transfer"]
11511
+ },
11512
+ "voicemail-callback": {
11513
+ description: "Voicemail callback should preserve enough caller and callback context for follow-up.",
11514
+ fields: [
11515
+ {
11516
+ aliases: ["name", "caller.name"],
11517
+ label: "Caller name",
11518
+ path: "voicemail.callerName"
11519
+ },
11520
+ {
11521
+ aliases: ["phone", "caller.phone"],
11522
+ label: "Callback phone",
11523
+ path: "voicemail.callbackPhone"
11524
+ },
11525
+ {
11526
+ aliases: ["message", "summary", "assistantSummary"],
11527
+ label: "Voicemail summary",
11528
+ path: "voicemail.summary"
11529
+ }
11530
+ ],
11531
+ id: "voicemail-callback",
11532
+ label: "Voicemail callback",
11533
+ outcome: "voicemail",
11534
+ requiredDisposition: "voicemail",
11535
+ requiredHandoffActions: ["voicemail"]
11536
+ }
11537
+ };
11538
+ var createVoiceWorkflowContractPreset = (name, options = {}) => {
11539
+ const preset = presetDefinitions[name];
11540
+ return createVoiceWorkflowContract({
11541
+ ...preset,
11542
+ ...options,
11543
+ fields: options.fields ?? preset.fields,
11544
+ id: options.id ?? preset.id
11545
+ });
11546
+ };
11547
+ var recordVoiceWorkflowContractTrace = async (input) => input.store.append({
11548
+ at: input.at ?? Date.now(),
11549
+ payload: {
11550
+ contractId: input.contractId ?? input.validation.contractId,
11551
+ issues: input.validation.issues,
11552
+ missingFields: input.validation.missingFields,
11553
+ outcome: input.validation.outcome,
11554
+ requiredFields: input.validation.requiredFields,
11555
+ status: input.validation.pass ? "pass" : "fail"
11556
+ },
11557
+ scenarioId: input.scenarioId,
11558
+ sessionId: input.sessionId,
11559
+ traceId: input.traceId,
11560
+ turnId: input.turnId,
11561
+ type: "workflow.contract"
11562
+ });
11563
+ var createVoiceWorkflowContractHandler = (input) => {
11564
+ return async (session, turn, api, context) => {
11565
+ const legacyHandler = input.handler;
11566
+ const objectHandler = input.handler;
11567
+ const result = input.handler.length >= 4 ? await legacyHandler(session, turn, api, context) : await objectHandler({ api, context, session, turn });
11568
+ if (!result)
11569
+ return result;
11570
+ const resolved = input.resolveContract?.({ context, result, session, turn }) ?? input.contract;
11571
+ if (!resolved)
11572
+ return result;
11573
+ const contract = "validateRouteResult" in resolved ? resolved : createVoiceWorkflowContract(resolved);
11574
+ const validation = contract.validateRouteResult(result);
11575
+ if (input.store) {
11576
+ await recordVoiceWorkflowContractTrace({
11577
+ scenarioId: session.scenarioId,
11578
+ sessionId: session.id,
11579
+ store: input.store,
11580
+ turnId: turn.id,
11581
+ validation
11582
+ });
11583
+ }
11584
+ return result;
11585
+ };
11262
11586
  };
11263
11587
  // src/turnLatency.ts
11264
- import { Elysia as Elysia18 } from "elysia";
11588
+ import { Elysia as Elysia20 } from "elysia";
11265
11589
  var DEFAULT_WARN_AFTER_MS = 1800;
11266
11590
  var DEFAULT_FAIL_AFTER_MS = 3200;
11267
- var escapeHtml19 = (value) => value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
11591
+ var escapeHtml21 = (value) => value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
11268
11592
  var firstNumber2 = (values) => values.filter((value) => typeof value === "number").sort((left, right) => left - right)[0];
11269
11593
  var getString10 = (value) => typeof value === "string" && value.trim() ? value : undefined;
11270
11594
  var createTraceStageIndex = (events) => {
@@ -11378,11 +11702,11 @@ var summarizeVoiceTurnLatency = async (options) => {
11378
11702
  var formatMs2 = (value) => typeof value === "number" ? `${Math.round(value)}ms` : "n/a";
11379
11703
  var renderVoiceTurnLatencyHTML = (report, options = {}) => {
11380
11704
  const title = options.title ?? "Voice Turn Latency";
11381
- const turns = report.turns.map((turn) => `<article class="turn ${escapeHtml19(turn.status)}">
11382
- <header><div><p class="eyebrow">${escapeHtml19(turn.sessionId)} \xB7 ${escapeHtml19(turn.turnId)}</p><h2>${escapeHtml19(turn.text || "Empty turn")}</h2></div><strong>${escapeHtml19(turn.status)}</strong></header>
11383
- <dl>${turn.stages.map((stage) => `<div><dt>${escapeHtml19(stage.label)}</dt><dd>${escapeHtml19(formatMs2(stage.valueMs))}</dd></div>`).join("")}</dl>
11705
+ const turns = report.turns.map((turn) => `<article class="turn ${escapeHtml21(turn.status)}">
11706
+ <header><div><p class="eyebrow">${escapeHtml21(turn.sessionId)} \xB7 ${escapeHtml21(turn.turnId)}</p><h2>${escapeHtml21(turn.text || "Empty turn")}</h2></div><strong>${escapeHtml21(turn.status)}</strong></header>
11707
+ <dl>${turn.stages.map((stage) => `<div><dt>${escapeHtml21(stage.label)}</dt><dd>${escapeHtml21(formatMs2(stage.valueMs))}</dd></div>`).join("")}</dl>
11384
11708
  </article>`).join("");
11385
- return `<!doctype html><html lang="en"><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /><title>${escapeHtml19(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,.turn{background:#181d22;border:1px solid #2a323a;border-radius:20px;margin-bottom:16px;padding:20px}.hero{background:linear-gradient(135deg,rgba(94,234,212,.16),rgba(251,191,36,.1))}.eyebrow{color:#5eead4;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 1rem}.summary{display:flex;flex-wrap:wrap;gap:10px}.pill{background:#0f1217;border:1px solid #3f3f46;border-radius:999px;padding:7px 10px}.turn header{align-items:flex-start;display:flex;gap:16px;justify-content:space-between}.pass{color:#86efac}.warn,.empty{color:#fde68a}.fail{color:#fca5a5}.turn.fail{border-color:rgba(248,113,113,.45)}dl{display:grid;gap:8px;grid-template-columns:repeat(auto-fit,minmax(160px,1fr))}dt{color:#a8b0b8;font-size:.8rem}dd{font-weight:900;margin:0}@media(max-width:800px){main{padding:18px}.turn header{display:block}}</style></head><body><main><section class="hero"><p class="eyebrow">End-to-end responsiveness</p><h1>${escapeHtml19(title)}</h1><div class="summary"><span class="pill ${escapeHtml19(report.status)}">${escapeHtml19(report.status)}</span><span class="pill">${String(report.total)} turns</span><span class="pill">avg ${escapeHtml19(formatMs2(report.averageTotalMs))}</span><span class="pill">${String(report.warnings)} warnings</span><span class="pill">${String(report.failed)} failed</span></div></section>${turns || '<section class="turn"><p>No committed turns found.</p></section>'}</main></body></html>`;
11709
+ return `<!doctype html><html lang="en"><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /><title>${escapeHtml21(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,.turn{background:#181d22;border:1px solid #2a323a;border-radius:20px;margin-bottom:16px;padding:20px}.hero{background:linear-gradient(135deg,rgba(94,234,212,.16),rgba(251,191,36,.1))}.eyebrow{color:#5eead4;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 1rem}.summary{display:flex;flex-wrap:wrap;gap:10px}.pill{background:#0f1217;border:1px solid #3f3f46;border-radius:999px;padding:7px 10px}.turn header{align-items:flex-start;display:flex;gap:16px;justify-content:space-between}.pass{color:#86efac}.warn,.empty{color:#fde68a}.fail{color:#fca5a5}.turn.fail{border-color:rgba(248,113,113,.45)}dl{display:grid;gap:8px;grid-template-columns:repeat(auto-fit,minmax(160px,1fr))}dt{color:#a8b0b8;font-size:.8rem}dd{font-weight:900;margin:0}@media(max-width:800px){main{padding:18px}.turn header{display:block}}</style></head><body><main><section class="hero"><p class="eyebrow">End-to-end responsiveness</p><h1>${escapeHtml21(title)}</h1><div class="summary"><span class="pill ${escapeHtml21(report.status)}">${escapeHtml21(report.status)}</span><span class="pill">${String(report.total)} turns</span><span class="pill">avg ${escapeHtml21(formatMs2(report.averageTotalMs))}</span><span class="pill">${String(report.warnings)} warnings</span><span class="pill">${String(report.failed)} failed</span></div></section>${turns || '<section class="turn"><p>No committed turns found.</p></section>'}</main></body></html>`;
11386
11710
  };
11387
11711
  var createVoiceTurnLatencyJSONHandler = (options) => async () => summarizeVoiceTurnLatency(options);
11388
11712
  var createVoiceTurnLatencyHTMLHandler = (options) => async () => {
@@ -11399,7 +11723,7 @@ var createVoiceTurnLatencyHTMLHandler = (options) => async () => {
11399
11723
  var createVoiceTurnLatencyRoutes = (options) => {
11400
11724
  const path = options.path ?? "/api/turn-latency";
11401
11725
  const htmlPath = options.htmlPath === undefined ? `${path}/htmx` : options.htmlPath;
11402
- const routes = new Elysia18({
11726
+ const routes = new Elysia20({
11403
11727
  name: options.name ?? "absolutejs-voice-turn-latency"
11404
11728
  }).get(path, createVoiceTurnLatencyJSONHandler(options));
11405
11729
  if (htmlPath) {
@@ -11408,8 +11732,8 @@ var createVoiceTurnLatencyRoutes = (options) => {
11408
11732
  return routes;
11409
11733
  };
11410
11734
  // src/liveLatency.ts
11411
- import { Elysia as Elysia19 } from "elysia";
11412
- var escapeHtml20 = (value) => value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
11735
+ import { Elysia as Elysia21 } from "elysia";
11736
+ var escapeHtml22 = (value) => value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
11413
11737
  var percentile = (values, percentileValue) => {
11414
11738
  if (values.length === 0) {
11415
11739
  return;
@@ -11457,13 +11781,13 @@ var summarizeVoiceLiveLatency = async (options) => {
11457
11781
  var formatMs3 = (value) => typeof value === "number" ? `${Math.round(value)}ms` : "n/a";
11458
11782
  var renderVoiceLiveLatencyHTML = (report, options = {}) => {
11459
11783
  const title = options.title ?? "Voice Live Latency";
11460
- const rows = report.recent.map((sample) => `<tr><td>${escapeHtml20(sample.sessionId)}</td><td>${escapeHtml20(formatMs3(sample.latencyMs))}</td><td>${escapeHtml20(sample.status ?? "unknown")}</td><td>${escapeHtml20(new Date(sample.at).toLocaleString())}</td></tr>`).join("");
11461
- return `<!doctype html><html lang="en"><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /><title>${escapeHtml20(title)}</title><style>body{background:#0c0f14;color:#f6f2e8;font-family:ui-sans-serif,system-ui,sans-serif;margin:0}main{margin:auto;max-width:1060px;padding:32px}.hero{background:linear-gradient(135deg,rgba(94,234,212,.16),rgba(245,158,11,.1));border:1px solid #26313d;border-radius:28px;margin-bottom:18px;padding:28px}.eyebrow{color:#5eead4;font-weight:900;letter-spacing:.12em;text-transform:uppercase}h1{font-size:clamp(2.4rem,6vw,5rem);line-height:.9;margin:.2rem 0 1rem}.status{border:1px solid #3f3f46;border-radius:999px;display:inline-flex;padding:8px 12px}.pass{color:#86efac}.warn,.empty{color:#fbbf24}.fail{color:#fca5a5}.metrics{display:grid;gap:14px;grid-template-columns:repeat(auto-fit,minmax(160px,1fr));margin:18px 0}.metrics article,table{background:#141922;border:1px solid #26313d;border-radius:18px}.metrics article{padding:16px}.metrics span{color:#a8b0b8}.metrics strong{display:block;font-size:2rem;margin-top:.25rem}table{border-collapse:collapse;overflow:hidden;width:100%}td,th{border-bottom:1px solid #26313d;padding:12px;text-align:left}@media(max-width:760px){main{padding:20px}}</style></head><body><main><section class="hero"><p class="eyebrow">Browser proof</p><h1>${escapeHtml20(title)}</h1><p>Recent real browser speech-to-assistant response measurements from persisted <code>client.live_latency</code> traces.</p><p class="status ${escapeHtml20(report.status)}">Status: ${escapeHtml20(report.status)}</p><section class="metrics"><article><span>p50</span><strong>${escapeHtml20(formatMs3(report.p50LatencyMs))}</strong></article><article><span>p95</span><strong>${escapeHtml20(formatMs3(report.p95LatencyMs))}</strong></article><article><span>Average</span><strong>${escapeHtml20(formatMs3(report.averageLatencyMs))}</strong></article><article><span>Samples</span><strong>${String(report.total)}</strong></article></section></section><table><thead><tr><th>Session</th><th>Latency</th><th>Status</th><th>Measured</th></tr></thead><tbody>${rows || '<tr><td colspan="4">No live latency samples yet.</td></tr>'}</tbody></table></main></body></html>`;
11784
+ const rows = report.recent.map((sample) => `<tr><td>${escapeHtml22(sample.sessionId)}</td><td>${escapeHtml22(formatMs3(sample.latencyMs))}</td><td>${escapeHtml22(sample.status ?? "unknown")}</td><td>${escapeHtml22(new Date(sample.at).toLocaleString())}</td></tr>`).join("");
11785
+ return `<!doctype html><html lang="en"><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /><title>${escapeHtml22(title)}</title><style>body{background:#0c0f14;color:#f6f2e8;font-family:ui-sans-serif,system-ui,sans-serif;margin:0}main{margin:auto;max-width:1060px;padding:32px}.hero{background:linear-gradient(135deg,rgba(94,234,212,.16),rgba(245,158,11,.1));border:1px solid #26313d;border-radius:28px;margin-bottom:18px;padding:28px}.eyebrow{color:#5eead4;font-weight:900;letter-spacing:.12em;text-transform:uppercase}h1{font-size:clamp(2.4rem,6vw,5rem);line-height:.9;margin:.2rem 0 1rem}.status{border:1px solid #3f3f46;border-radius:999px;display:inline-flex;padding:8px 12px}.pass{color:#86efac}.warn,.empty{color:#fbbf24}.fail{color:#fca5a5}.metrics{display:grid;gap:14px;grid-template-columns:repeat(auto-fit,minmax(160px,1fr));margin:18px 0}.metrics article,table{background:#141922;border:1px solid #26313d;border-radius:18px}.metrics article{padding:16px}.metrics span{color:#a8b0b8}.metrics strong{display:block;font-size:2rem;margin-top:.25rem}table{border-collapse:collapse;overflow:hidden;width:100%}td,th{border-bottom:1px solid #26313d;padding:12px;text-align:left}@media(max-width:760px){main{padding:20px}}</style></head><body><main><section class="hero"><p class="eyebrow">Browser proof</p><h1>${escapeHtml22(title)}</h1><p>Recent real browser speech-to-assistant response measurements from persisted <code>client.live_latency</code> traces.</p><p class="status ${escapeHtml22(report.status)}">Status: ${escapeHtml22(report.status)}</p><section class="metrics"><article><span>p50</span><strong>${escapeHtml22(formatMs3(report.p50LatencyMs))}</strong></article><article><span>p95</span><strong>${escapeHtml22(formatMs3(report.p95LatencyMs))}</strong></article><article><span>Average</span><strong>${escapeHtml22(formatMs3(report.averageLatencyMs))}</strong></article><article><span>Samples</span><strong>${String(report.total)}</strong></article></section></section><table><thead><tr><th>Session</th><th>Latency</th><th>Status</th><th>Measured</th></tr></thead><tbody>${rows || '<tr><td colspan="4">No live latency samples yet.</td></tr>'}</tbody></table></main></body></html>`;
11462
11786
  };
11463
11787
  var createVoiceLiveLatencyRoutes = (options) => {
11464
11788
  const path = options.path ?? "/api/live-latency";
11465
11789
  const htmlPath = options.htmlPath === undefined ? "/live-latency" : options.htmlPath;
11466
- const routes = new Elysia19({
11790
+ const routes = new Elysia21({
11467
11791
  name: options.name ?? "absolutejs-voice-live-latency"
11468
11792
  }).get(path, () => summarizeVoiceLiveLatency(options));
11469
11793
  if (htmlPath) {
@@ -11480,9 +11804,9 @@ var createVoiceLiveLatencyRoutes = (options) => {
11480
11804
  return routes;
11481
11805
  };
11482
11806
  // src/turnQuality.ts
11483
- import { Elysia as Elysia20 } from "elysia";
11807
+ import { Elysia as Elysia22 } from "elysia";
11484
11808
  var DEFAULT_CONFIDENCE_WARN_THRESHOLD = 0.72;
11485
- var escapeHtml21 = (value) => value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
11809
+ var escapeHtml23 = (value) => value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
11486
11810
  var getTurnLatencyMs = (turn) => {
11487
11811
  const firstTranscriptAt = turn.transcripts.map((transcript) => transcript.endedAtMs ?? transcript.startedAtMs).filter((value) => typeof value === "number").sort((left, right) => left - right)[0];
11488
11812
  if (firstTranscriptAt === undefined) {
@@ -11553,24 +11877,24 @@ var summarizeVoiceTurnQuality = async (options) => {
11553
11877
  };
11554
11878
  var renderVoiceTurnQualityHTML = (report, options = {}) => {
11555
11879
  const title = options.title ?? "Voice Turn Quality";
11556
- const turns = report.turns.map((turn) => `<article class="turn ${escapeHtml21(turn.status)}">
11880
+ const turns = report.turns.map((turn) => `<article class="turn ${escapeHtml23(turn.status)}">
11557
11881
  <div class="turn-header">
11558
11882
  <div>
11559
- <p class="eyebrow">${escapeHtml21(turn.sessionId)} \xB7 ${escapeHtml21(turn.turnId)}</p>
11560
- <h2>${escapeHtml21(turn.text || "Empty turn")}</h2>
11883
+ <p class="eyebrow">${escapeHtml23(turn.sessionId)} \xB7 ${escapeHtml23(turn.turnId)}</p>
11884
+ <h2>${escapeHtml23(turn.text || "Empty turn")}</h2>
11561
11885
  </div>
11562
- <strong>${escapeHtml21(turn.status)}</strong>
11886
+ <strong>${escapeHtml23(turn.status)}</strong>
11563
11887
  </div>
11564
11888
  <dl>
11565
- <div><dt>Source</dt><dd>${escapeHtml21(turn.source ?? "unknown")}</dd></div>
11889
+ <div><dt>Source</dt><dd>${escapeHtml23(turn.source ?? "unknown")}</dd></div>
11566
11890
  <div><dt>Confidence</dt><dd>${turn.averageConfidence === undefined ? "n/a" : `${Math.round(turn.averageConfidence * 100)}%`}</dd></div>
11567
- <div><dt>Fallback</dt><dd>${turn.fallbackUsed ? `yes (${escapeHtml21(turn.fallbackSelectionReason ?? "selected")})` : "no"}</dd></div>
11568
- <div><dt>Correction</dt><dd>${turn.correctionChanged ? `changed${turn.correctionProvider ? ` by ${escapeHtml21(turn.correctionProvider)}` : ""}` : "none"}</dd></div>
11891
+ <div><dt>Fallback</dt><dd>${turn.fallbackUsed ? `yes (${escapeHtml23(turn.fallbackSelectionReason ?? "selected")})` : "no"}</dd></div>
11892
+ <div><dt>Correction</dt><dd>${turn.correctionChanged ? `changed${turn.correctionProvider ? ` by ${escapeHtml23(turn.correctionProvider)}` : ""}` : "none"}</dd></div>
11569
11893
  <div><dt>Transcripts</dt><dd>${String(turn.selectedTranscriptCount)} selected \xB7 ${String(turn.finalTranscriptCount)} final \xB7 ${String(turn.partialTranscriptCount)} partial</dd></div>
11570
11894
  <div><dt>Cost</dt><dd>${turn.costUnits === undefined ? "n/a" : String(turn.costUnits)}</dd></div>
11571
11895
  </dl>
11572
11896
  </article>`).join("");
11573
- return `<!doctype html><html lang="en"><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /><title>${escapeHtml21(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,.turn{background:#181d22;border:1px solid #2a323a;border-radius:20px;margin-bottom:16px;padding:20px}.hero{background:linear-gradient(135deg,rgba(251,191,36,.16),rgba(34,197,94,.1))}.eyebrow{color:#fbbf24;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 1rem}.summary{display:flex;flex-wrap:wrap;gap:10px}.pill{background:#0f1217;border:1px solid #3f3f46;border-radius:999px;padding:7px 10px}.turn-header{align-items:flex-start;display:flex;gap:16px;justify-content:space-between}.pass{color:#86efac}.warn,.unknown{color:#fde68a}.fail{color:#fca5a5}.turn.fail{border-color:rgba(248,113,113,.45)}dl{display:grid;gap:8px;grid-template-columns:repeat(auto-fit,minmax(160px,1fr))}dt{color:#a8b0b8;font-size:.8rem}dd{margin:0}@media(max-width:800px){main{padding:18px}.turn-header{display:block}}</style></head><body><main><section class="hero"><p class="eyebrow">Realtime STT Debugging</p><h1>${escapeHtml21(title)}</h1><div class="summary"><span class="pill ${escapeHtml21(report.status)}">${escapeHtml21(report.status)}</span><span class="pill">${String(report.total)} turns</span><span class="pill">${String(report.warnings)} warnings</span><span class="pill">${String(report.failed)} failed</span><span class="pill">${String(report.sessions)} sessions</span></div></section>${turns || '<section class="turn"><p>No committed turns found.</p></section>'}</main></body></html>`;
11897
+ return `<!doctype html><html lang="en"><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /><title>${escapeHtml23(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,.turn{background:#181d22;border:1px solid #2a323a;border-radius:20px;margin-bottom:16px;padding:20px}.hero{background:linear-gradient(135deg,rgba(251,191,36,.16),rgba(34,197,94,.1))}.eyebrow{color:#fbbf24;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 1rem}.summary{display:flex;flex-wrap:wrap;gap:10px}.pill{background:#0f1217;border:1px solid #3f3f46;border-radius:999px;padding:7px 10px}.turn-header{align-items:flex-start;display:flex;gap:16px;justify-content:space-between}.pass{color:#86efac}.warn,.unknown{color:#fde68a}.fail{color:#fca5a5}.turn.fail{border-color:rgba(248,113,113,.45)}dl{display:grid;gap:8px;grid-template-columns:repeat(auto-fit,minmax(160px,1fr))}dt{color:#a8b0b8;font-size:.8rem}dd{margin:0}@media(max-width:800px){main{padding:18px}.turn-header{display:block}}</style></head><body><main><section class="hero"><p class="eyebrow">Realtime STT Debugging</p><h1>${escapeHtml23(title)}</h1><div class="summary"><span class="pill ${escapeHtml23(report.status)}">${escapeHtml23(report.status)}</span><span class="pill">${String(report.total)} turns</span><span class="pill">${String(report.warnings)} warnings</span><span class="pill">${String(report.failed)} failed</span><span class="pill">${String(report.sessions)} sessions</span></div></section>${turns || '<section class="turn"><p>No committed turns found.</p></section>'}</main></body></html>`;
11574
11898
  };
11575
11899
  var createVoiceTurnQualityJSONHandler = (options) => async () => summarizeVoiceTurnQuality(options);
11576
11900
  var createVoiceTurnQualityHTMLHandler = (options) => async () => {
@@ -11587,7 +11911,7 @@ var createVoiceTurnQualityHTMLHandler = (options) => async () => {
11587
11911
  var createVoiceTurnQualityRoutes = (options) => {
11588
11912
  const path = options.path ?? "/api/turn-quality";
11589
11913
  const htmlPath = options.htmlPath === undefined ? `${path}/htmx` : options.htmlPath;
11590
- const routes = new Elysia20({
11914
+ const routes = new Elysia22({
11591
11915
  name: options.name ?? "absolutejs-voice-turn-quality"
11592
11916
  }).get(path, createVoiceTurnQualityJSONHandler(options));
11593
11917
  if (htmlPath) {
@@ -11595,157 +11919,8 @@ var createVoiceTurnQualityRoutes = (options) => {
11595
11919
  }
11596
11920
  return routes;
11597
11921
  };
11598
- // src/outcomeContract.ts
11599
- import { Elysia as Elysia21 } from "elysia";
11600
- var escapeHtml22 = (value) => value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
11601
- var getPayloadString = (event, key) => typeof event.payload[key] === "string" ? event.payload[key] : undefined;
11602
- var toList = async (input) => Array.isArray(input) ? input : await input?.list() ?? [];
11603
- var hydrateSessions = async (input) => {
11604
- if (!input)
11605
- return [];
11606
- if (Array.isArray(input))
11607
- return input;
11608
- const summaries = await input.list();
11609
- const sessions = await Promise.all(summaries.map((summary) => input.get(summary.id)));
11610
- const hydrated = [];
11611
- for (const session of sessions) {
11612
- if (session) {
11613
- hydrated.push(session);
11614
- }
11615
- }
11616
- return hydrated;
11617
- };
11618
- var dispositionForSession = (session) => session.call?.disposition ?? (session.status === "completed" ? "completed" : undefined);
11619
- var matchesDisposition = (disposition, expected) => expected === undefined || disposition === expected;
11620
- var reportContract = (input) => {
11621
- const { contract } = input;
11622
- const sessions = input.sessions.filter((session) => (!contract.scenarioId || session.scenarioId === contract.scenarioId) && matchesDisposition(dispositionForSession(session), contract.expectedDisposition));
11623
- const sessionIds = new Set(sessions.map((session) => session.id));
11624
- const reviews = input.reviews.filter((review) => matchesDisposition(review.summary.outcome, contract.expectedDisposition));
11625
- const tasks = input.tasks.filter((task) => matchesDisposition(task.outcome, contract.expectedDisposition));
11626
- 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)));
11627
- const events = input.events.filter((event) => {
11628
- const eventSessionId = getPayloadString(event, "sessionId");
11629
- const eventOutcome = getPayloadString(event, "outcome") ?? getPayloadString(event, "disposition");
11630
- return (sessionIds.size === 0 || !eventSessionId || sessionIds.has(eventSessionId)) && (!contract.expectedDisposition || eventOutcome === contract.expectedDisposition);
11631
- });
11632
- const issues = [];
11633
- const minSessions = contract.minSessions ?? 1;
11634
- if (sessions.length < minSessions) {
11635
- issues.push({
11636
- code: "outcome.sessions_missing",
11637
- message: `Expected at least ${minSessions} matching session(s), saw ${sessions.length}.`
11638
- });
11639
- }
11640
- if (contract.requireReview !== false && reviews.length === 0) {
11641
- issues.push({
11642
- code: "outcome.review_missing",
11643
- message: "Expected at least one matching review artifact."
11644
- });
11645
- }
11646
- if (contract.requireTask && tasks.length < (contract.minTasks ?? 1)) {
11647
- issues.push({
11648
- code: "outcome.task_missing",
11649
- message: `Expected at least ${contract.minTasks ?? 1} matching task(s), saw ${tasks.length}.`
11650
- });
11651
- }
11652
- for (const action of contract.requireHandoffActions ?? []) {
11653
- if (!handoffs.some((handoff) => handoff.action === action)) {
11654
- issues.push({
11655
- code: "outcome.handoff_missing",
11656
- message: `Expected handoff action ${action}.`
11657
- });
11658
- }
11659
- }
11660
- for (const type of contract.requireIntegrationEvents ?? []) {
11661
- if (!events.some((event) => event.type === type)) {
11662
- issues.push({
11663
- code: "outcome.integration_event_missing",
11664
- message: `Expected integration event ${type}.`
11665
- });
11666
- }
11667
- }
11668
- return {
11669
- contractId: contract.id,
11670
- description: contract.description,
11671
- issues,
11672
- label: contract.label,
11673
- matched: {
11674
- handoffs: handoffs.length,
11675
- integrationEvents: events.length,
11676
- reviews: reviews.length,
11677
- sessions: sessions.length,
11678
- tasks: tasks.length
11679
- },
11680
- pass: issues.length === 0
11681
- };
11682
- };
11683
- var runVoiceOutcomeContractSuite = async (options) => {
11684
- const [sessions, reviews, tasks, events, handoffs] = await Promise.all([
11685
- hydrateSessions(options.sessions),
11686
- toList(options.reviews),
11687
- toList(options.tasks),
11688
- toList(options.events),
11689
- toList(options.handoffs)
11690
- ]);
11691
- const contracts = options.contracts.map((contract) => reportContract({ contract, events, handoffs, reviews, sessions, tasks }));
11692
- const passed = contracts.filter((contract) => contract.pass).length;
11693
- const failed = contracts.length - passed;
11694
- return {
11695
- checkedAt: Date.now(),
11696
- contracts,
11697
- failed,
11698
- passed,
11699
- status: failed > 0 ? "fail" : "pass",
11700
- total: contracts.length
11701
- };
11702
- };
11703
- var renderVoiceOutcomeContractHTML = (report, options = {}) => {
11704
- const title = options.title ?? "Voice Outcome Contracts";
11705
- const contracts = report.contracts.map((contract) => `<section class="contract ${contract.pass ? "pass" : "fail"}">
11706
- <div class="contract-header">
11707
- <div>
11708
- <p class="eyebrow">${escapeHtml22(contract.contractId)}</p>
11709
- <h2>${escapeHtml22(contract.label ?? contract.contractId)}</h2>
11710
- ${contract.description ? `<p>${escapeHtml22(contract.description)}</p>` : ""}
11711
- </div>
11712
- <strong>${contract.pass ? "pass" : "fail"}</strong>
11713
- </div>
11714
- <div class="grid">
11715
- <span>sessions ${String(contract.matched.sessions)}</span>
11716
- <span>reviews ${String(contract.matched.reviews)}</span>
11717
- <span>tasks ${String(contract.matched.tasks)}</span>
11718
- <span>handoffs ${String(contract.matched.handoffs)}</span>
11719
- <span>events ${String(contract.matched.integrationEvents)}</span>
11720
- </div>
11721
- ${contract.issues.length ? `<ul>${contract.issues.map((issue) => `<li>${escapeHtml22(issue.message)}</li>`).join("")}</ul>` : ""}
11722
- </section>`).join("");
11723
- return `<!doctype html><html lang="en"><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /><title>${escapeHtml22(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>${escapeHtml22(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>`;
11724
- };
11725
- var createVoiceOutcomeContractJSONHandler = (options) => async () => runVoiceOutcomeContractSuite(options);
11726
- var createVoiceOutcomeContractHTMLHandler = (options) => async () => {
11727
- const report = await runVoiceOutcomeContractSuite(options);
11728
- const render = options.render ?? ((input) => renderVoiceOutcomeContractHTML(input, options));
11729
- return new Response(await render(report), {
11730
- headers: {
11731
- "Content-Type": "text/html; charset=utf-8",
11732
- ...options.headers
11733
- }
11734
- });
11735
- };
11736
- var createVoiceOutcomeContractRoutes = (options) => {
11737
- const path = options.path ?? "/api/outcome-contracts";
11738
- const htmlPath = options.htmlPath === undefined ? `${path}/htmx` : options.htmlPath;
11739
- const routes = new Elysia21({
11740
- name: options.name ?? "absolutejs-voice-outcome-contracts"
11741
- }).get(path, createVoiceOutcomeContractJSONHandler(options));
11742
- if (htmlPath) {
11743
- routes.get(htmlPath, createVoiceOutcomeContractHTMLHandler(options));
11744
- }
11745
- return routes;
11746
- };
11747
11922
  // src/telephonyOutcome.ts
11748
- import { Elysia as Elysia22 } from "elysia";
11923
+ import { Elysia as Elysia23 } from "elysia";
11749
11924
  var DEFAULT_COMPLETED_STATUSES = [
11750
11925
  "answered",
11751
11926
  "completed",
@@ -12392,7 +12567,7 @@ var createVoiceTelephonyWebhookHandler = (options = {}) => async (input) => {
12392
12567
  var createVoiceTelephonyWebhookRoutes = (options = {}) => {
12393
12568
  const path = options.path ?? "/api/voice/telephony/webhook";
12394
12569
  const handler = createVoiceTelephonyWebhookHandler(options);
12395
- return new Elysia22({
12570
+ return new Elysia23({
12396
12571
  name: options.name ?? "absolutejs-voice-telephony-webhooks"
12397
12572
  }).post(path, async ({ query, request }) => {
12398
12573
  try {
@@ -12413,15 +12588,15 @@ var createVoiceTelephonyWebhookRoutes = (options = {}) => {
12413
12588
  });
12414
12589
  };
12415
12590
  // src/phoneAgent.ts
12416
- import { Elysia as Elysia26 } from "elysia";
12591
+ import { Elysia as Elysia27 } from "elysia";
12417
12592
 
12418
12593
  // src/telephony/plivo.ts
12419
12594
  import { Buffer as Buffer4 } from "buffer";
12420
- import { Elysia as Elysia24 } from "elysia";
12595
+ import { Elysia as Elysia25 } from "elysia";
12421
12596
 
12422
12597
  // src/telephony/twilio.ts
12423
12598
  import { Buffer as Buffer3 } from "buffer";
12424
- import { Elysia as Elysia23 } from "elysia";
12599
+ import { Elysia as Elysia24 } from "elysia";
12425
12600
  var TWILIO_MULAW_SAMPLE_RATE = 8000;
12426
12601
  var VOICE_PCM_SAMPLE_RATE = 16000;
12427
12602
  var escapeXml2 = (value) => value.replaceAll("&", "&amp;").replaceAll('"', "&quot;").replaceAll("'", "&apos;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
@@ -12451,7 +12626,7 @@ var resolveTwilioStreamParameters = async (parameters, input) => {
12451
12626
  return parameters;
12452
12627
  };
12453
12628
  var joinUrlPath = (origin, path) => `${origin.replace(/\/$/, "")}${path.startsWith("/") ? path : `/${path}`}`;
12454
- var escapeHtml23 = (value) => value.replaceAll("&", "&amp;").replaceAll('"', "&quot;").replaceAll("'", "&#39;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
12629
+ var escapeHtml24 = (value) => value.replaceAll("&", "&amp;").replaceAll('"', "&quot;").replaceAll("'", "&#39;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
12455
12630
  var getWebhookVerificationUrl = (webhook, input) => {
12456
12631
  if (!webhook?.verificationUrl) {
12457
12632
  return;
@@ -12494,23 +12669,23 @@ var buildTwilioVoiceSetupStatus = async (options, input) => {
12494
12669
  };
12495
12670
  var renderTwilioVoiceSetupHTML = (status, title) => `<main style="font-family: ui-sans-serif, system-ui; max-width: 860px; margin: 40px auto; padding: 0 20px;">
12496
12671
  <p style="letter-spacing: .12em; text-transform: uppercase; color: #52606d;">Twilio setup</p>
12497
- <h1>${escapeHtml23(title)}</h1>
12672
+ <h1>${escapeHtml24(title)}</h1>
12498
12673
  <p><strong>Status:</strong> ${status.ready ? "Ready" : "Needs attention"}</p>
12499
12674
  <section>
12500
12675
  <h2>URLs</h2>
12501
12676
  <ul>
12502
- <li><strong>TwiML:</strong> <code>${escapeHtml23(status.urls.twiml)}</code></li>
12503
- <li><strong>Media stream:</strong> <code>${escapeHtml23(status.urls.stream)}</code></li>
12504
- <li><strong>Status webhook:</strong> <code>${escapeHtml23(status.urls.webhook)}</code></li>
12677
+ <li><strong>TwiML:</strong> <code>${escapeHtml24(status.urls.twiml)}</code></li>
12678
+ <li><strong>Media stream:</strong> <code>${escapeHtml24(status.urls.stream)}</code></li>
12679
+ <li><strong>Status webhook:</strong> <code>${escapeHtml24(status.urls.webhook)}</code></li>
12505
12680
  </ul>
12506
12681
  </section>
12507
12682
  <section>
12508
12683
  <h2>Signing</h2>
12509
12684
  <p>Mode: <code>${status.signing.mode}</code></p>
12510
- ${status.signing.verificationUrl ? `<p>Verification URL: <code>${escapeHtml23(status.signing.verificationUrl)}</code></p>` : ""}
12685
+ ${status.signing.verificationUrl ? `<p>Verification URL: <code>${escapeHtml24(status.signing.verificationUrl)}</code></p>` : ""}
12511
12686
  </section>
12512
- ${status.missing.length ? `<section><h2>Missing env</h2><ul>${status.missing.map((name) => `<li><code>${escapeHtml23(name)}</code></li>`).join("")}</ul></section>` : ""}
12513
- ${status.warnings.length ? `<section><h2>Warnings</h2><ul>${status.warnings.map((warning) => `<li>${escapeHtml23(warning)}</li>`).join("")}</ul></section>` : ""}
12687
+ ${status.missing.length ? `<section><h2>Missing env</h2><ul>${status.missing.map((name) => `<li><code>${escapeHtml24(name)}</code></li>`).join("")}</ul></section>` : ""}
12688
+ ${status.warnings.length ? `<section><h2>Warnings</h2><ul>${status.warnings.map((warning) => `<li>${escapeHtml24(warning)}</li>`).join("")}</ul></section>` : ""}
12514
12689
  </main>`;
12515
12690
  var extractTwilioStreamUrl = (twiml) => twiml.match(/<Stream\b[^>]*\surl="([^"]+)"/i)?.[1]?.replaceAll("&amp;", "&");
12516
12691
  var createSmokeCheck = (name, status, message, details) => ({
@@ -12521,20 +12696,20 @@ var createSmokeCheck = (name, status, message, details) => ({
12521
12696
  });
12522
12697
  var renderTwilioVoiceSmokeHTML = (report, title) => `<main style="font-family: ui-sans-serif, system-ui; max-width: 860px; margin: 40px auto; padding: 0 20px;">
12523
12698
  <p style="letter-spacing: .12em; text-transform: uppercase; color: #52606d;">Twilio smoke test</p>
12524
- <h1>${escapeHtml23(title)}</h1>
12699
+ <h1>${escapeHtml24(title)}</h1>
12525
12700
  <p><strong>Status:</strong> ${report.pass ? "Pass" : "Fail"}</p>
12526
12701
  <section>
12527
12702
  <h2>Checks</h2>
12528
12703
  <ul>
12529
- ${report.checks.map((check) => `<li><strong>${escapeHtml23(check.name)}</strong>: ${escapeHtml23(check.status)}${check.message ? ` - ${escapeHtml23(check.message)}` : ""}</li>`).join("")}
12704
+ ${report.checks.map((check) => `<li><strong>${escapeHtml24(check.name)}</strong>: ${escapeHtml24(check.status)}${check.message ? ` - ${escapeHtml24(check.message)}` : ""}</li>`).join("")}
12530
12705
  </ul>
12531
12706
  </section>
12532
12707
  <section>
12533
12708
  <h2>Observed URLs</h2>
12534
12709
  <ul>
12535
- <li><strong>TwiML:</strong> <code>${escapeHtml23(report.setup.urls.twiml)}</code></li>
12536
- <li><strong>Stream:</strong> <code>${escapeHtml23(report.twiml?.streamUrl ?? report.setup.urls.stream)}</code></li>
12537
- <li><strong>Webhook:</strong> <code>${escapeHtml23(report.setup.urls.webhook)}</code></li>
12710
+ <li><strong>TwiML:</strong> <code>${escapeHtml24(report.setup.urls.twiml)}</code></li>
12711
+ <li><strong>Stream:</strong> <code>${escapeHtml24(report.twiml?.streamUrl ?? report.setup.urls.stream)}</code></li>
12712
+ <li><strong>Webhook:</strong> <code>${escapeHtml24(report.setup.urls.webhook)}</code></li>
12538
12713
  </ul>
12539
12714
  </section>
12540
12715
  </main>`;
@@ -12994,7 +13169,7 @@ var createTwilioVoiceRoutes = (options) => {
12994
13169
  const smokePath = options.smoke?.path === false ? false : options.smoke?.path ?? "/api/voice/twilio/smoke";
12995
13170
  const bridges = new WeakMap;
12996
13171
  const webhookPolicy = options.webhook?.policy ?? options.outcomePolicy ?? createVoiceTelephonyOutcomePolicy();
12997
- const app = new Elysia23({
13172
+ const app = new Elysia24({
12998
13173
  name: options.name ?? "absolutejs-voice-twilio"
12999
13174
  }).get(twimlPath, async ({ query, request }) => {
13000
13175
  const streamUrl = await resolveTwilioStreamUrl(options, {
@@ -13131,7 +13306,7 @@ var createTwilioVoiceRoutes = (options) => {
13131
13306
 
13132
13307
  // src/telephony/plivo.ts
13133
13308
  var escapeXml3 = (value) => value.replaceAll("&", "&amp;").replaceAll('"', "&quot;").replaceAll("'", "&apos;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
13134
- var escapeHtml24 = (value) => value.replaceAll("&", "&amp;").replaceAll('"', "&quot;").replaceAll("'", "&#39;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
13309
+ var escapeHtml25 = (value) => value.replaceAll("&", "&amp;").replaceAll('"', "&quot;").replaceAll("'", "&#39;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
13135
13310
  var joinUrlPath2 = (origin, path) => `${origin.replace(/\/$/, "")}${path.startsWith("/") ? path : `/${path}`}`;
13136
13311
  var resolveRequestOrigin2 = (request) => {
13137
13312
  const url = new URL(request.url);
@@ -13382,21 +13557,21 @@ var buildPlivoVoiceSetupStatus = async (options, input) => {
13382
13557
  };
13383
13558
  var renderPlivoSetupHTML = (status, title) => `<main style="font-family: ui-sans-serif, system-ui; max-width: 860px; margin: 40px auto; padding: 0 20px;">
13384
13559
  <p style="letter-spacing: .12em; text-transform: uppercase; color: #52606d;">Plivo setup</p>
13385
- <h1>${escapeHtml24(title)}</h1>
13560
+ <h1>${escapeHtml25(title)}</h1>
13386
13561
  <p><strong>Status:</strong> ${status.ready ? "Ready" : "Needs attention"}</p>
13387
13562
  <ul>
13388
- <li><strong>Answer XML:</strong> <code>${escapeHtml24(status.urls.answer)}</code></li>
13389
- <li><strong>Audio stream:</strong> <code>${escapeHtml24(status.urls.stream)}</code></li>
13390
- <li><strong>Status webhook:</strong> <code>${escapeHtml24(status.urls.webhook)}</code></li>
13563
+ <li><strong>Answer XML:</strong> <code>${escapeHtml25(status.urls.answer)}</code></li>
13564
+ <li><strong>Audio stream:</strong> <code>${escapeHtml25(status.urls.stream)}</code></li>
13565
+ <li><strong>Status webhook:</strong> <code>${escapeHtml25(status.urls.webhook)}</code></li>
13391
13566
  </ul>
13392
- ${status.missing.length ? `<h2>Missing env</h2><ul>${status.missing.map((name) => `<li><code>${escapeHtml24(name)}</code></li>`).join("")}</ul>` : ""}
13393
- ${status.warnings.length ? `<h2>Warnings</h2><ul>${status.warnings.map((warning) => `<li>${escapeHtml24(warning)}</li>`).join("")}</ul>` : ""}
13567
+ ${status.missing.length ? `<h2>Missing env</h2><ul>${status.missing.map((name) => `<li><code>${escapeHtml25(name)}</code></li>`).join("")}</ul>` : ""}
13568
+ ${status.warnings.length ? `<h2>Warnings</h2><ul>${status.warnings.map((warning) => `<li>${escapeHtml25(warning)}</li>`).join("")}</ul>` : ""}
13394
13569
  </main>`;
13395
13570
  var renderPlivoSmokeHTML = (report, title) => `<main style="font-family: ui-sans-serif, system-ui; max-width: 860px; margin: 40px auto; padding: 0 20px;">
13396
13571
  <p style="letter-spacing: .12em; text-transform: uppercase; color: #52606d;">Plivo smoke test</p>
13397
- <h1>${escapeHtml24(title)}</h1>
13572
+ <h1>${escapeHtml25(title)}</h1>
13398
13573
  <p><strong>Status:</strong> ${report.pass ? "Pass" : "Fail"}</p>
13399
- <ul>${report.checks.map((check) => `<li><strong>${escapeHtml24(check.name)}</strong>: ${escapeHtml24(check.status)}${check.message ? ` - ${escapeHtml24(check.message)}` : ""}</li>`).join("")}</ul>
13574
+ <ul>${report.checks.map((check) => `<li><strong>${escapeHtml25(check.name)}</strong>: ${escapeHtml25(check.status)}${check.message ? ` - ${escapeHtml25(check.message)}` : ""}</li>`).join("")}</ul>
13400
13575
  </main>`;
13401
13576
  var runPlivoSmokeTest = async (input) => {
13402
13577
  const setup = await buildPlivoVoiceSetupStatus(input.options, input);
@@ -13491,7 +13666,7 @@ var createPlivoVoiceRoutes = (options = {}) => {
13491
13666
  request: input.request
13492
13667
  }) : verificationUrl ?? input.request.url
13493
13668
  }) : undefined);
13494
- const app = new Elysia24({
13669
+ const app = new Elysia25({
13495
13670
  name: options.name ?? "absolutejs-voice-plivo"
13496
13671
  }).get(answerPath, async ({ query, request }) => {
13497
13672
  const streamUrl = await resolvePlivoStreamUrl(options, {
@@ -13602,9 +13777,9 @@ var createPlivoVoiceRoutes = (options = {}) => {
13602
13777
 
13603
13778
  // src/telephony/telnyx.ts
13604
13779
  import { Buffer as Buffer5 } from "buffer";
13605
- import { Elysia as Elysia25 } from "elysia";
13780
+ import { Elysia as Elysia26 } from "elysia";
13606
13781
  var escapeXml4 = (value) => value.replaceAll("&", "&amp;").replaceAll('"', "&quot;").replaceAll("'", "&apos;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
13607
- var escapeHtml25 = (value) => value.replaceAll("&", "&amp;").replaceAll('"', "&quot;").replaceAll("'", "&#39;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
13782
+ var escapeHtml26 = (value) => value.replaceAll("&", "&amp;").replaceAll('"', "&quot;").replaceAll("'", "&#39;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
13608
13783
  var joinUrlPath3 = (origin, path) => `${origin.replace(/\/$/, "")}${path.startsWith("/") ? path : `/${path}`}`;
13609
13784
  var resolveRequestOrigin3 = (request) => {
13610
13785
  const url = new URL(request.url);
@@ -13805,21 +13980,21 @@ var buildTelnyxVoiceSetupStatus = async (options, input) => {
13805
13980
  };
13806
13981
  var renderTelnyxSetupHTML = (status, title) => `<main style="font-family: ui-sans-serif, system-ui; max-width: 860px; margin: 40px auto; padding: 0 20px;">
13807
13982
  <p style="letter-spacing: .12em; text-transform: uppercase; color: #52606d;">Telnyx setup</p>
13808
- <h1>${escapeHtml25(title)}</h1>
13983
+ <h1>${escapeHtml26(title)}</h1>
13809
13984
  <p><strong>Status:</strong> ${status.ready ? "Ready" : "Needs attention"}</p>
13810
13985
  <ul>
13811
- <li><strong>TeXML:</strong> <code>${escapeHtml25(status.urls.texml)}</code></li>
13812
- <li><strong>Media stream:</strong> <code>${escapeHtml25(status.urls.stream)}</code></li>
13813
- <li><strong>Status webhook:</strong> <code>${escapeHtml25(status.urls.webhook)}</code></li>
13986
+ <li><strong>TeXML:</strong> <code>${escapeHtml26(status.urls.texml)}</code></li>
13987
+ <li><strong>Media stream:</strong> <code>${escapeHtml26(status.urls.stream)}</code></li>
13988
+ <li><strong>Status webhook:</strong> <code>${escapeHtml26(status.urls.webhook)}</code></li>
13814
13989
  </ul>
13815
- ${status.missing.length ? `<h2>Missing env</h2><ul>${status.missing.map((name) => `<li><code>${escapeHtml25(name)}</code></li>`).join("")}</ul>` : ""}
13816
- ${status.warnings.length ? `<h2>Warnings</h2><ul>${status.warnings.map((warning) => `<li>${escapeHtml25(warning)}</li>`).join("")}</ul>` : ""}
13990
+ ${status.missing.length ? `<h2>Missing env</h2><ul>${status.missing.map((name) => `<li><code>${escapeHtml26(name)}</code></li>`).join("")}</ul>` : ""}
13991
+ ${status.warnings.length ? `<h2>Warnings</h2><ul>${status.warnings.map((warning) => `<li>${escapeHtml26(warning)}</li>`).join("")}</ul>` : ""}
13817
13992
  </main>`;
13818
13993
  var renderTelnyxSmokeHTML = (report, title) => `<main style="font-family: ui-sans-serif, system-ui; max-width: 860px; margin: 40px auto; padding: 0 20px;">
13819
13994
  <p style="letter-spacing: .12em; text-transform: uppercase; color: #52606d;">Telnyx smoke test</p>
13820
- <h1>${escapeHtml25(title)}</h1>
13995
+ <h1>${escapeHtml26(title)}</h1>
13821
13996
  <p><strong>Status:</strong> ${report.pass ? "Pass" : "Fail"}</p>
13822
- <ul>${report.checks.map((check) => `<li><strong>${escapeHtml25(check.name)}</strong>: ${escapeHtml25(check.status)}${check.message ? ` - ${escapeHtml25(check.message)}` : ""}</li>`).join("")}</ul>
13997
+ <ul>${report.checks.map((check) => `<li><strong>${escapeHtml26(check.name)}</strong>: ${escapeHtml26(check.status)}${check.message ? ` - ${escapeHtml26(check.message)}` : ""}</li>`).join("")}</ul>
13823
13998
  </main>`;
13824
13999
  var runTelnyxSmokeTest = async (input) => {
13825
14000
  const setup = await buildTelnyxVoiceSetupStatus(input.options, input);
@@ -13913,7 +14088,7 @@ var createTelnyxVoiceRoutes = (options = {}) => {
13913
14088
  publicKey: options.webhook?.publicKey,
13914
14089
  toleranceSeconds: options.webhook?.toleranceSeconds
13915
14090
  }) : undefined);
13916
- const app = new Elysia25({
14091
+ const app = new Elysia26({
13917
14092
  name: options.name ?? "absolutejs-voice-telnyx"
13918
14093
  }).get(texmlPath, async ({ query, request }) => {
13919
14094
  const streamUrl = await resolveTelnyxStreamUrl(options, {
@@ -14083,7 +14258,7 @@ var createVoicePhoneAgent = (options) => {
14083
14258
  setupPath: resolveSetupPath(carrier),
14084
14259
  smokePath: resolveSmokePath(carrier)
14085
14260
  }));
14086
- const app = new Elysia26({
14261
+ const app = new Elysia27({
14087
14262
  name: options.name ?? "absolutejs-voice-phone-agent"
14088
14263
  });
14089
14264
  for (const carrier of options.carriers) {
@@ -16364,7 +16539,7 @@ var createVoiceMemoryStore = () => {
16364
16539
  return { get, getOrCreate, list, remove, set };
16365
16540
  };
16366
16541
  // src/opsWebhook.ts
16367
- import { Elysia as Elysia27 } from "elysia";
16542
+ import { Elysia as Elysia28 } from "elysia";
16368
16543
  var toHex5 = (bytes) => Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
16369
16544
  var signVoiceOpsWebhookBody = async (input) => {
16370
16545
  const encoder = new TextEncoder;
@@ -16494,7 +16669,7 @@ var verifyVoiceOpsWebhookSignature = async (input) => {
16494
16669
  };
16495
16670
  var createVoiceOpsWebhookReceiverRoutes = (options = {}) => {
16496
16671
  const path = options.path ?? "/api/voice-ops/webhook";
16497
- return new Elysia27().post(path, async ({ body, request, set }) => {
16672
+ return new Elysia28().post(path, async ({ body, request, set }) => {
16498
16673
  const bodyText = typeof body === "string" ? body : JSON.stringify(body);
16499
16674
  if (options.signingSecret) {
16500
16675
  const verification = await verifyVoiceOpsWebhookSignature({
@@ -18314,6 +18489,7 @@ export {
18314
18489
  selectVoiceTraceEventsForPrune,
18315
18490
  runVoiceToolContractSuite,
18316
18491
  runVoiceToolContract,
18492
+ runVoiceSimulationSuite,
18317
18493
  runVoiceSessionEvals,
18318
18494
  runVoiceScenarioFixtureEvals,
18319
18495
  runVoiceScenarioEvals,
@@ -18342,6 +18518,7 @@ export {
18342
18518
  renderVoiceTraceHTML,
18343
18519
  renderVoiceToolContractHTML,
18344
18520
  renderVoiceTelephonyCarrierMatrixHTML,
18521
+ renderVoiceSimulationSuiteHTML,
18345
18522
  renderVoiceSessionsHTML,
18346
18523
  renderVoiceScenarioFixtureEvalHTML,
18347
18524
  renderVoiceScenarioEvalHTML,
@@ -18430,6 +18607,7 @@ export {
18430
18607
  createVoiceTaskSLABreachedEvent,
18431
18608
  createVoiceTaskCreatedEvent,
18432
18609
  createVoiceTTSProviderRouter,
18610
+ createVoiceSimulationSuiteRoutes,
18433
18611
  createVoiceSessionsJSONHandler,
18434
18612
  createVoiceSessionsHTMLHandler,
18435
18613
  createVoiceSessionReplayRoutes,