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

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,406 @@ 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 runVoiceSimulationSuite = async (options) => {
11136
+ const include = options.include ?? {};
11137
+ const shouldRunSessions = include.sessions ?? Boolean(options.store || !hasWork(options));
11138
+ const shouldRunScenarios = include.scenarios ?? Boolean(options.scenarios?.length);
11139
+ const shouldRunFixtures = include.fixtures ?? Boolean((options.fixtures?.length ?? 0) > 0 || options.fixtureStore);
11140
+ const shouldRunTools = include.tools ?? Boolean(options.tools?.length);
11141
+ const shouldRunOutcomes = include.outcomes ?? Boolean(options.outcomes?.contracts.length);
11142
+ const [sessions, scenarios, fixtures, tools, outcomes] = await Promise.all([
11143
+ shouldRunSessions ? runVoiceSessionEvals({
11144
+ limit: options.limit,
11145
+ store: options.store,
11146
+ thresholds: options.thresholds
11147
+ }) : undefined,
11148
+ shouldRunScenarios ? runVoiceScenarioEvals({
11149
+ scenarios: options.scenarios,
11150
+ store: options.store
11151
+ }) : undefined,
11152
+ shouldRunFixtures ? runVoiceScenarioFixtureEvals({
11153
+ fixtures: options.fixtures,
11154
+ fixtureStore: options.fixtureStore,
11155
+ scenarios: options.scenarios
11156
+ }) : undefined,
11157
+ shouldRunTools ? runVoiceToolContractSuite({
11158
+ contracts: options.tools ?? []
11159
+ }) : undefined,
11160
+ shouldRunOutcomes && options.outcomes ? runVoiceOutcomeContractSuite(options.outcomes) : undefined
11161
+ ]);
11162
+ const sections = [sessions, scenarios, fixtures, tools, outcomes].filter((report) => Boolean(report));
11163
+ const failed = sections.filter((section) => section.status === "fail").length;
11164
+ const passed = sections.length - failed;
11165
+ return {
11166
+ checkedAt: Date.now(),
11167
+ failed,
11168
+ fixtures,
11169
+ outcomes,
11170
+ passed,
11171
+ scenarios,
11172
+ sessions,
11173
+ status: failed > 0 ? "fail" : "pass",
11174
+ summary: {
11175
+ fixtures: fixtures && summarizeSection(fixtures),
11176
+ outcomes: outcomes && summarizeSection(outcomes),
11177
+ scenarios: scenarios && summarizeSection(scenarios),
11178
+ sessions: sessions && summarizeSection(sessions),
11179
+ tools: tools && summarizeSection(tools)
11180
+ },
11181
+ tools,
11182
+ total: sections.length
11183
+ };
11184
+ };
11185
+ var renderSection = (label, summary) => {
11186
+ if (!summary) {
11187
+ return "";
11188
+ }
11189
+ 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>`;
11190
+ };
11191
+ var renderVoiceSimulationSuiteHTML = (report, options = {}) => {
11192
+ const title = options.title ?? "Voice Simulation Suite";
11193
+ 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{display:grid;gap:14px;grid-template-columns:repeat(auto-fit,minmax(190px,1fr));margin:18px 0}.grid article{background:#151d27;border:1px solid #283544;border-radius:18px;padding:16px}.grid span{color:#aab5c0}.grid strong{display:block;font-size:2rem;margin:.25rem 0;text-transform:uppercase}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><pre>${escapeHtml20(JSON.stringify(report.summary, null, 2))}</pre></main></body></html>`;
11194
+ };
11195
+ var createVoiceSimulationSuiteRoutes = (options) => {
11196
+ const path = options.path ?? "/api/voice/simulations";
11197
+ const htmlPath = options.htmlPath === undefined ? "/voice/simulations" : options.htmlPath;
11198
+ const app = new Elysia19({
11199
+ name: options.name ?? "absolutejs-voice-simulation-suite"
11200
+ }).get(path, () => runVoiceSimulationSuite(options));
11201
+ if (htmlPath) {
11202
+ app.get(htmlPath, async () => {
11203
+ const report = await runVoiceSimulationSuite(options);
11204
+ const html = options.render ? await options.render(report) : renderVoiceSimulationSuiteHTML(report, options);
11205
+ return new Response(html, {
11206
+ headers: {
11207
+ "content-type": "text/html; charset=utf-8",
11208
+ ...options.headers
11209
+ }
11210
+ });
11211
+ });
11212
+ }
11213
+ return app;
11214
+ };
11215
+ // src/workflowContract.ts
11216
+ var getObject2 = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : undefined;
11217
+ var getPathValue2 = (value, path) => {
11218
+ let current = value;
11219
+ for (const part of path.split(".").filter(Boolean)) {
11220
+ const record = getObject2(current);
11221
+ if (!record || !(part in record)) {
11222
+ return;
11223
+ }
11224
+ current = record[part];
11225
+ }
11226
+ return current;
11227
+ };
11228
+ var hasValue = (value, match) => {
11229
+ switch (match) {
11230
+ case "boolean":
11231
+ return typeof value === "boolean";
11232
+ case "number":
11233
+ return typeof value === "number" && Number.isFinite(value);
11234
+ case "string":
11235
+ return typeof value === "string";
11236
+ case "truthy":
11237
+ return Boolean(value);
11238
+ case "non-empty":
11239
+ default:
11240
+ return Array.isArray(value) ? value.length > 0 : typeof value === "string" ? value.trim().length > 0 : value !== undefined && value !== null;
11241
+ }
11242
+ };
11243
+ var resolveOutcome2 = (routeResult) => {
11244
+ if (routeResult.complete)
11245
+ return "complete";
11246
+ if (routeResult.transfer)
11247
+ return "transfer";
11248
+ if (routeResult.escalate)
11249
+ return "escalate";
11250
+ if (routeResult.voicemail)
11251
+ return "voicemail";
11252
+ if (routeResult.noAnswer)
11253
+ return "no-answer";
11254
+ return;
11255
+ };
11256
+ var validateVoiceWorkflowRouteResult = (definition, routeResult) => {
11257
+ const issues = [];
11258
+ const requiredFields = (definition.fields ?? []).filter((field) => field.required !== false).map((field) => field.path);
11259
+ const missingFields = [];
11260
+ const outcome = resolveOutcome2(routeResult);
11261
+ if (definition.outcome && outcome !== definition.outcome) {
11262
+ issues.push({
11263
+ code: "workflow.outcome_mismatch",
11264
+ message: `Expected workflow outcome ${definition.outcome}, saw ${outcome ?? "none"}.`
11265
+ });
11266
+ }
11267
+ for (const field of definition.fields ?? []) {
11268
+ if (field.required === false)
11269
+ continue;
11270
+ const paths = [field.path, ...field.aliases ?? []];
11271
+ const present = paths.some((path) => hasValue(getPathValue2(routeResult.result, path), field.match ?? "non-empty"));
11272
+ if (!present) {
11273
+ missingFields.push(field.path);
11274
+ issues.push({
11275
+ code: "workflow.missing_field",
11276
+ field: field.path,
11277
+ message: `Missing required workflow field: ${field.label ?? field.path}.`
11278
+ });
11279
+ }
11280
+ }
11281
+ issues.push(...definition.validate?.({
11282
+ result: routeResult.result,
11283
+ routeResult
11284
+ }) ?? []);
11285
+ return {
11286
+ contractId: definition.id,
11287
+ issues,
11288
+ missingFields,
11289
+ outcome,
11290
+ pass: issues.length === 0,
11291
+ requiredFields
11292
+ };
11293
+ };
11294
+ var createVoiceWorkflowScenario = (definition, overrides = {}) => ({
11295
+ description: definition.description,
11296
+ forbiddenHandoffActions: definition.forbiddenHandoffActions,
11297
+ id: definition.id,
11298
+ label: definition.label,
11299
+ maxProviderErrors: definition.maxProviderErrors,
11300
+ maxSessionErrors: definition.maxSessionErrors,
11301
+ minSessions: definition.minSessions,
11302
+ minTurns: definition.minTurns,
11303
+ requiredAssistantIncludes: definition.requiredAssistantIncludes,
11304
+ requiredDisposition: definition.requiredDisposition,
11305
+ requiredHandoffActions: definition.requiredHandoffActions,
11306
+ requiredLifecycleTypes: definition.requiredLifecycleTypes,
11307
+ requiredTranscriptIncludes: definition.requiredTranscriptIncludes,
11308
+ requiredWorkflowContracts: [definition.id],
11309
+ scenarioId: definition.scenarioId,
11310
+ ...overrides
11311
+ });
11312
+ var createVoiceWorkflowContract = (definition) => ({
11313
+ assertRouteResult: (routeResult) => {
11314
+ const validation = validateVoiceWorkflowRouteResult(definition, routeResult);
11315
+ if (!validation.pass) {
11316
+ throw new Error(`Voice workflow contract ${definition.id} failed: ${validation.issues.map((issue) => issue.message).join(" ")}`);
11317
+ }
11318
+ },
11319
+ definition,
11320
+ toScenarioEval: (overrides) => createVoiceWorkflowScenario(definition, overrides),
11321
+ validateRouteResult: (routeResult) => validateVoiceWorkflowRouteResult(definition, routeResult)
11322
+ });
11323
+ var presetDefinitions = {
11324
+ "appointment-booking": {
11325
+ description: "Appointment booking should complete with enough identity, appointment, and follow-up details to act on.",
11326
+ fields: [
11327
+ { aliases: ["name", "customer.name"], label: "Caller name", path: "caller.name" },
11328
+ {
11329
+ aliases: ["phone", "customer.phone"],
11330
+ label: "Caller phone",
11331
+ path: "caller.phone"
11332
+ },
11333
+ {
11334
+ aliases: ["appointment.start", "appointment.time", "scheduledAt"],
11335
+ label: "Appointment time",
11336
+ path: "appointment.startsAt"
11337
+ },
11338
+ {
11339
+ aliases: ["summary", "assistantSummary"],
11340
+ label: "Summary",
11341
+ path: "appointment.summary"
11342
+ }
11343
+ ],
11344
+ id: "appointment-booking",
11345
+ label: "Appointment booking",
11346
+ outcome: "complete",
11347
+ requiredDisposition: "completed"
11348
+ },
11349
+ "lead-qualification": {
11350
+ description: "Lead qualification should complete with contact, need, qualification, and next-step fields.",
11351
+ fields: [
11352
+ { aliases: ["name", "lead.name"], label: "Lead name", path: "contact.name" },
11353
+ {
11354
+ aliases: ["email", "lead.email"],
11355
+ label: "Lead email",
11356
+ path: "contact.email"
11357
+ },
11358
+ {
11359
+ aliases: ["need", "pain", "summary"],
11360
+ label: "Need",
11361
+ path: "qualification.need"
11362
+ },
11363
+ {
11364
+ aliases: ["qualified", "qualification.qualified"],
11365
+ label: "Qualified",
11366
+ match: "boolean",
11367
+ path: "qualification.isQualified"
11368
+ },
11369
+ {
11370
+ aliases: ["nextStep", "followUp"],
11371
+ label: "Next step",
11372
+ path: "qualification.nextStep"
11373
+ }
11374
+ ],
11375
+ id: "lead-qualification",
11376
+ label: "Lead qualification",
11377
+ outcome: "complete",
11378
+ requiredDisposition: "completed"
11379
+ },
11380
+ "support-triage": {
11381
+ description: "Support triage should capture identity, issue summary, severity, and the operational follow-up.",
11382
+ fields: [
11383
+ {
11384
+ aliases: ["name", "customer.name"],
11385
+ label: "Customer name",
11386
+ path: "customer.name"
11387
+ },
11388
+ {
11389
+ aliases: ["issue", "summary", "assistantSummary"],
11390
+ label: "Issue summary",
11391
+ path: "issue.summary"
11392
+ },
11393
+ {
11394
+ aliases: ["priority", "severity"],
11395
+ label: "Severity",
11396
+ path: "issue.severity"
11397
+ },
11398
+ {
11399
+ aliases: ["nextStep", "task.title"],
11400
+ label: "Next step",
11401
+ path: "resolution.nextStep"
11402
+ }
11403
+ ],
11404
+ id: "support-triage",
11405
+ label: "Support triage",
11406
+ outcome: "complete",
11407
+ requiredDisposition: "completed"
11408
+ },
11409
+ "transfer-handoff": {
11410
+ description: "Transfer handoff should produce a routed transfer plus handoff evidence.",
11411
+ fields: [
11412
+ {
11413
+ aliases: ["target", "callTarget"],
11414
+ label: "Transfer target",
11415
+ path: "transfer.target"
11416
+ },
11417
+ {
11418
+ aliases: ["reason", "callReason"],
11419
+ label: "Transfer reason",
11420
+ path: "transfer.reason"
11421
+ },
11422
+ {
11423
+ aliases: ["summary", "assistantSummary"],
11424
+ label: "Transfer summary",
11425
+ path: "transfer.summary"
11426
+ }
11427
+ ],
11428
+ id: "transfer-handoff",
11429
+ label: "Transfer handoff",
11430
+ outcome: "transfer",
11431
+ requiredDisposition: "transferred",
11432
+ requiredHandoffActions: ["transfer"]
11433
+ },
11434
+ "voicemail-callback": {
11435
+ description: "Voicemail callback should preserve enough caller and callback context for follow-up.",
11436
+ fields: [
11437
+ {
11438
+ aliases: ["name", "caller.name"],
11439
+ label: "Caller name",
11440
+ path: "voicemail.callerName"
11441
+ },
11442
+ {
11443
+ aliases: ["phone", "caller.phone"],
11444
+ label: "Callback phone",
11445
+ path: "voicemail.callbackPhone"
11446
+ },
11447
+ {
11448
+ aliases: ["message", "summary", "assistantSummary"],
11449
+ label: "Voicemail summary",
11450
+ path: "voicemail.summary"
11451
+ }
11452
+ ],
11453
+ id: "voicemail-callback",
11454
+ label: "Voicemail callback",
11455
+ outcome: "voicemail",
11456
+ requiredDisposition: "voicemail",
11457
+ requiredHandoffActions: ["voicemail"]
11458
+ }
11459
+ };
11460
+ var createVoiceWorkflowContractPreset = (name, options = {}) => {
11461
+ const preset = presetDefinitions[name];
11462
+ return createVoiceWorkflowContract({
11463
+ ...preset,
11464
+ ...options,
11465
+ fields: options.fields ?? preset.fields,
11466
+ id: options.id ?? preset.id
11467
+ });
11468
+ };
11469
+ var recordVoiceWorkflowContractTrace = async (input) => input.store.append({
11470
+ at: input.at ?? Date.now(),
11471
+ payload: {
11472
+ contractId: input.contractId ?? input.validation.contractId,
11473
+ issues: input.validation.issues,
11474
+ missingFields: input.validation.missingFields,
11475
+ outcome: input.validation.outcome,
11476
+ requiredFields: input.validation.requiredFields,
11477
+ status: input.validation.pass ? "pass" : "fail"
11478
+ },
11479
+ scenarioId: input.scenarioId,
11480
+ sessionId: input.sessionId,
11481
+ traceId: input.traceId,
11482
+ turnId: input.turnId,
11483
+ type: "workflow.contract"
11484
+ });
11485
+ var createVoiceWorkflowContractHandler = (input) => {
11486
+ return async (session, turn, api, context) => {
11487
+ const legacyHandler = input.handler;
11488
+ const objectHandler = input.handler;
11489
+ const result = input.handler.length >= 4 ? await legacyHandler(session, turn, api, context) : await objectHandler({ api, context, session, turn });
11490
+ if (!result)
11491
+ return result;
11492
+ const resolved = input.resolveContract?.({ context, result, session, turn }) ?? input.contract;
11493
+ if (!resolved)
11494
+ return result;
11495
+ const contract = "validateRouteResult" in resolved ? resolved : createVoiceWorkflowContract(resolved);
11496
+ const validation = contract.validateRouteResult(result);
11497
+ if (input.store) {
11498
+ await recordVoiceWorkflowContractTrace({
11499
+ scenarioId: session.scenarioId,
11500
+ sessionId: session.id,
11501
+ store: input.store,
11502
+ turnId: turn.id,
11503
+ validation
11504
+ });
11505
+ }
11506
+ return result;
11507
+ };
11262
11508
  };
11263
11509
  // src/turnLatency.ts
11264
- import { Elysia as Elysia18 } from "elysia";
11510
+ import { Elysia as Elysia20 } from "elysia";
11265
11511
  var DEFAULT_WARN_AFTER_MS = 1800;
11266
11512
  var DEFAULT_FAIL_AFTER_MS = 3200;
11267
- var escapeHtml19 = (value) => value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
11513
+ var escapeHtml21 = (value) => value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
11268
11514
  var firstNumber2 = (values) => values.filter((value) => typeof value === "number").sort((left, right) => left - right)[0];
11269
11515
  var getString10 = (value) => typeof value === "string" && value.trim() ? value : undefined;
11270
11516
  var createTraceStageIndex = (events) => {
@@ -11378,11 +11624,11 @@ var summarizeVoiceTurnLatency = async (options) => {
11378
11624
  var formatMs2 = (value) => typeof value === "number" ? `${Math.round(value)}ms` : "n/a";
11379
11625
  var renderVoiceTurnLatencyHTML = (report, options = {}) => {
11380
11626
  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>
11627
+ const turns = report.turns.map((turn) => `<article class="turn ${escapeHtml21(turn.status)}">
11628
+ <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>
11629
+ <dl>${turn.stages.map((stage) => `<div><dt>${escapeHtml21(stage.label)}</dt><dd>${escapeHtml21(formatMs2(stage.valueMs))}</dd></div>`).join("")}</dl>
11384
11630
  </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>`;
11631
+ 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
11632
  };
11387
11633
  var createVoiceTurnLatencyJSONHandler = (options) => async () => summarizeVoiceTurnLatency(options);
11388
11634
  var createVoiceTurnLatencyHTMLHandler = (options) => async () => {
@@ -11399,7 +11645,7 @@ var createVoiceTurnLatencyHTMLHandler = (options) => async () => {
11399
11645
  var createVoiceTurnLatencyRoutes = (options) => {
11400
11646
  const path = options.path ?? "/api/turn-latency";
11401
11647
  const htmlPath = options.htmlPath === undefined ? `${path}/htmx` : options.htmlPath;
11402
- const routes = new Elysia18({
11648
+ const routes = new Elysia20({
11403
11649
  name: options.name ?? "absolutejs-voice-turn-latency"
11404
11650
  }).get(path, createVoiceTurnLatencyJSONHandler(options));
11405
11651
  if (htmlPath) {
@@ -11408,8 +11654,8 @@ var createVoiceTurnLatencyRoutes = (options) => {
11408
11654
  return routes;
11409
11655
  };
11410
11656
  // 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;");
11657
+ import { Elysia as Elysia21 } from "elysia";
11658
+ var escapeHtml22 = (value) => value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
11413
11659
  var percentile = (values, percentileValue) => {
11414
11660
  if (values.length === 0) {
11415
11661
  return;
@@ -11457,13 +11703,13 @@ var summarizeVoiceLiveLatency = async (options) => {
11457
11703
  var formatMs3 = (value) => typeof value === "number" ? `${Math.round(value)}ms` : "n/a";
11458
11704
  var renderVoiceLiveLatencyHTML = (report, options = {}) => {
11459
11705
  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>`;
11706
+ 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("");
11707
+ 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
11708
  };
11463
11709
  var createVoiceLiveLatencyRoutes = (options) => {
11464
11710
  const path = options.path ?? "/api/live-latency";
11465
11711
  const htmlPath = options.htmlPath === undefined ? "/live-latency" : options.htmlPath;
11466
- const routes = new Elysia19({
11712
+ const routes = new Elysia21({
11467
11713
  name: options.name ?? "absolutejs-voice-live-latency"
11468
11714
  }).get(path, () => summarizeVoiceLiveLatency(options));
11469
11715
  if (htmlPath) {
@@ -11480,9 +11726,9 @@ var createVoiceLiveLatencyRoutes = (options) => {
11480
11726
  return routes;
11481
11727
  };
11482
11728
  // src/turnQuality.ts
11483
- import { Elysia as Elysia20 } from "elysia";
11729
+ import { Elysia as Elysia22 } from "elysia";
11484
11730
  var DEFAULT_CONFIDENCE_WARN_THRESHOLD = 0.72;
11485
- var escapeHtml21 = (value) => value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
11731
+ var escapeHtml23 = (value) => value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
11486
11732
  var getTurnLatencyMs = (turn) => {
11487
11733
  const firstTranscriptAt = turn.transcripts.map((transcript) => transcript.endedAtMs ?? transcript.startedAtMs).filter((value) => typeof value === "number").sort((left, right) => left - right)[0];
11488
11734
  if (firstTranscriptAt === undefined) {
@@ -11553,24 +11799,24 @@ var summarizeVoiceTurnQuality = async (options) => {
11553
11799
  };
11554
11800
  var renderVoiceTurnQualityHTML = (report, options = {}) => {
11555
11801
  const title = options.title ?? "Voice Turn Quality";
11556
- const turns = report.turns.map((turn) => `<article class="turn ${escapeHtml21(turn.status)}">
11802
+ const turns = report.turns.map((turn) => `<article class="turn ${escapeHtml23(turn.status)}">
11557
11803
  <div class="turn-header">
11558
11804
  <div>
11559
- <p class="eyebrow">${escapeHtml21(turn.sessionId)} \xB7 ${escapeHtml21(turn.turnId)}</p>
11560
- <h2>${escapeHtml21(turn.text || "Empty turn")}</h2>
11805
+ <p class="eyebrow">${escapeHtml23(turn.sessionId)} \xB7 ${escapeHtml23(turn.turnId)}</p>
11806
+ <h2>${escapeHtml23(turn.text || "Empty turn")}</h2>
11561
11807
  </div>
11562
- <strong>${escapeHtml21(turn.status)}</strong>
11808
+ <strong>${escapeHtml23(turn.status)}</strong>
11563
11809
  </div>
11564
11810
  <dl>
11565
- <div><dt>Source</dt><dd>${escapeHtml21(turn.source ?? "unknown")}</dd></div>
11811
+ <div><dt>Source</dt><dd>${escapeHtml23(turn.source ?? "unknown")}</dd></div>
11566
11812
  <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>
11813
+ <div><dt>Fallback</dt><dd>${turn.fallbackUsed ? `yes (${escapeHtml23(turn.fallbackSelectionReason ?? "selected")})` : "no"}</dd></div>
11814
+ <div><dt>Correction</dt><dd>${turn.correctionChanged ? `changed${turn.correctionProvider ? ` by ${escapeHtml23(turn.correctionProvider)}` : ""}` : "none"}</dd></div>
11569
11815
  <div><dt>Transcripts</dt><dd>${String(turn.selectedTranscriptCount)} selected \xB7 ${String(turn.finalTranscriptCount)} final \xB7 ${String(turn.partialTranscriptCount)} partial</dd></div>
11570
11816
  <div><dt>Cost</dt><dd>${turn.costUnits === undefined ? "n/a" : String(turn.costUnits)}</dd></div>
11571
11817
  </dl>
11572
11818
  </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>`;
11819
+ 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
11820
  };
11575
11821
  var createVoiceTurnQualityJSONHandler = (options) => async () => summarizeVoiceTurnQuality(options);
11576
11822
  var createVoiceTurnQualityHTMLHandler = (options) => async () => {
@@ -11587,7 +11833,7 @@ var createVoiceTurnQualityHTMLHandler = (options) => async () => {
11587
11833
  var createVoiceTurnQualityRoutes = (options) => {
11588
11834
  const path = options.path ?? "/api/turn-quality";
11589
11835
  const htmlPath = options.htmlPath === undefined ? `${path}/htmx` : options.htmlPath;
11590
- const routes = new Elysia20({
11836
+ const routes = new Elysia22({
11591
11837
  name: options.name ?? "absolutejs-voice-turn-quality"
11592
11838
  }).get(path, createVoiceTurnQualityJSONHandler(options));
11593
11839
  if (htmlPath) {
@@ -11595,157 +11841,8 @@ var createVoiceTurnQualityRoutes = (options) => {
11595
11841
  }
11596
11842
  return routes;
11597
11843
  };
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
11844
  // src/telephonyOutcome.ts
11748
- import { Elysia as Elysia22 } from "elysia";
11845
+ import { Elysia as Elysia23 } from "elysia";
11749
11846
  var DEFAULT_COMPLETED_STATUSES = [
11750
11847
  "answered",
11751
11848
  "completed",
@@ -12392,7 +12489,7 @@ var createVoiceTelephonyWebhookHandler = (options = {}) => async (input) => {
12392
12489
  var createVoiceTelephonyWebhookRoutes = (options = {}) => {
12393
12490
  const path = options.path ?? "/api/voice/telephony/webhook";
12394
12491
  const handler = createVoiceTelephonyWebhookHandler(options);
12395
- return new Elysia22({
12492
+ return new Elysia23({
12396
12493
  name: options.name ?? "absolutejs-voice-telephony-webhooks"
12397
12494
  }).post(path, async ({ query, request }) => {
12398
12495
  try {
@@ -12413,15 +12510,15 @@ var createVoiceTelephonyWebhookRoutes = (options = {}) => {
12413
12510
  });
12414
12511
  };
12415
12512
  // src/phoneAgent.ts
12416
- import { Elysia as Elysia26 } from "elysia";
12513
+ import { Elysia as Elysia27 } from "elysia";
12417
12514
 
12418
12515
  // src/telephony/plivo.ts
12419
12516
  import { Buffer as Buffer4 } from "buffer";
12420
- import { Elysia as Elysia24 } from "elysia";
12517
+ import { Elysia as Elysia25 } from "elysia";
12421
12518
 
12422
12519
  // src/telephony/twilio.ts
12423
12520
  import { Buffer as Buffer3 } from "buffer";
12424
- import { Elysia as Elysia23 } from "elysia";
12521
+ import { Elysia as Elysia24 } from "elysia";
12425
12522
  var TWILIO_MULAW_SAMPLE_RATE = 8000;
12426
12523
  var VOICE_PCM_SAMPLE_RATE = 16000;
12427
12524
  var escapeXml2 = (value) => value.replaceAll("&", "&amp;").replaceAll('"', "&quot;").replaceAll("'", "&apos;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
@@ -12451,7 +12548,7 @@ var resolveTwilioStreamParameters = async (parameters, input) => {
12451
12548
  return parameters;
12452
12549
  };
12453
12550
  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;");
12551
+ var escapeHtml24 = (value) => value.replaceAll("&", "&amp;").replaceAll('"', "&quot;").replaceAll("'", "&#39;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
12455
12552
  var getWebhookVerificationUrl = (webhook, input) => {
12456
12553
  if (!webhook?.verificationUrl) {
12457
12554
  return;
@@ -12494,23 +12591,23 @@ var buildTwilioVoiceSetupStatus = async (options, input) => {
12494
12591
  };
12495
12592
  var renderTwilioVoiceSetupHTML = (status, title) => `<main style="font-family: ui-sans-serif, system-ui; max-width: 860px; margin: 40px auto; padding: 0 20px;">
12496
12593
  <p style="letter-spacing: .12em; text-transform: uppercase; color: #52606d;">Twilio setup</p>
12497
- <h1>${escapeHtml23(title)}</h1>
12594
+ <h1>${escapeHtml24(title)}</h1>
12498
12595
  <p><strong>Status:</strong> ${status.ready ? "Ready" : "Needs attention"}</p>
12499
12596
  <section>
12500
12597
  <h2>URLs</h2>
12501
12598
  <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>
12599
+ <li><strong>TwiML:</strong> <code>${escapeHtml24(status.urls.twiml)}</code></li>
12600
+ <li><strong>Media stream:</strong> <code>${escapeHtml24(status.urls.stream)}</code></li>
12601
+ <li><strong>Status webhook:</strong> <code>${escapeHtml24(status.urls.webhook)}</code></li>
12505
12602
  </ul>
12506
12603
  </section>
12507
12604
  <section>
12508
12605
  <h2>Signing</h2>
12509
12606
  <p>Mode: <code>${status.signing.mode}</code></p>
12510
- ${status.signing.verificationUrl ? `<p>Verification URL: <code>${escapeHtml23(status.signing.verificationUrl)}</code></p>` : ""}
12607
+ ${status.signing.verificationUrl ? `<p>Verification URL: <code>${escapeHtml24(status.signing.verificationUrl)}</code></p>` : ""}
12511
12608
  </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>` : ""}
12609
+ ${status.missing.length ? `<section><h2>Missing env</h2><ul>${status.missing.map((name) => `<li><code>${escapeHtml24(name)}</code></li>`).join("")}</ul></section>` : ""}
12610
+ ${status.warnings.length ? `<section><h2>Warnings</h2><ul>${status.warnings.map((warning) => `<li>${escapeHtml24(warning)}</li>`).join("")}</ul></section>` : ""}
12514
12611
  </main>`;
12515
12612
  var extractTwilioStreamUrl = (twiml) => twiml.match(/<Stream\b[^>]*\surl="([^"]+)"/i)?.[1]?.replaceAll("&amp;", "&");
12516
12613
  var createSmokeCheck = (name, status, message, details) => ({
@@ -12521,20 +12618,20 @@ var createSmokeCheck = (name, status, message, details) => ({
12521
12618
  });
12522
12619
  var renderTwilioVoiceSmokeHTML = (report, title) => `<main style="font-family: ui-sans-serif, system-ui; max-width: 860px; margin: 40px auto; padding: 0 20px;">
12523
12620
  <p style="letter-spacing: .12em; text-transform: uppercase; color: #52606d;">Twilio smoke test</p>
12524
- <h1>${escapeHtml23(title)}</h1>
12621
+ <h1>${escapeHtml24(title)}</h1>
12525
12622
  <p><strong>Status:</strong> ${report.pass ? "Pass" : "Fail"}</p>
12526
12623
  <section>
12527
12624
  <h2>Checks</h2>
12528
12625
  <ul>
12529
- ${report.checks.map((check) => `<li><strong>${escapeHtml23(check.name)}</strong>: ${escapeHtml23(check.status)}${check.message ? ` - ${escapeHtml23(check.message)}` : ""}</li>`).join("")}
12626
+ ${report.checks.map((check) => `<li><strong>${escapeHtml24(check.name)}</strong>: ${escapeHtml24(check.status)}${check.message ? ` - ${escapeHtml24(check.message)}` : ""}</li>`).join("")}
12530
12627
  </ul>
12531
12628
  </section>
12532
12629
  <section>
12533
12630
  <h2>Observed URLs</h2>
12534
12631
  <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>
12632
+ <li><strong>TwiML:</strong> <code>${escapeHtml24(report.setup.urls.twiml)}</code></li>
12633
+ <li><strong>Stream:</strong> <code>${escapeHtml24(report.twiml?.streamUrl ?? report.setup.urls.stream)}</code></li>
12634
+ <li><strong>Webhook:</strong> <code>${escapeHtml24(report.setup.urls.webhook)}</code></li>
12538
12635
  </ul>
12539
12636
  </section>
12540
12637
  </main>`;
@@ -12994,7 +13091,7 @@ var createTwilioVoiceRoutes = (options) => {
12994
13091
  const smokePath = options.smoke?.path === false ? false : options.smoke?.path ?? "/api/voice/twilio/smoke";
12995
13092
  const bridges = new WeakMap;
12996
13093
  const webhookPolicy = options.webhook?.policy ?? options.outcomePolicy ?? createVoiceTelephonyOutcomePolicy();
12997
- const app = new Elysia23({
13094
+ const app = new Elysia24({
12998
13095
  name: options.name ?? "absolutejs-voice-twilio"
12999
13096
  }).get(twimlPath, async ({ query, request }) => {
13000
13097
  const streamUrl = await resolveTwilioStreamUrl(options, {
@@ -13131,7 +13228,7 @@ var createTwilioVoiceRoutes = (options) => {
13131
13228
 
13132
13229
  // src/telephony/plivo.ts
13133
13230
  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;");
13231
+ var escapeHtml25 = (value) => value.replaceAll("&", "&amp;").replaceAll('"', "&quot;").replaceAll("'", "&#39;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
13135
13232
  var joinUrlPath2 = (origin, path) => `${origin.replace(/\/$/, "")}${path.startsWith("/") ? path : `/${path}`}`;
13136
13233
  var resolveRequestOrigin2 = (request) => {
13137
13234
  const url = new URL(request.url);
@@ -13382,21 +13479,21 @@ var buildPlivoVoiceSetupStatus = async (options, input) => {
13382
13479
  };
13383
13480
  var renderPlivoSetupHTML = (status, title) => `<main style="font-family: ui-sans-serif, system-ui; max-width: 860px; margin: 40px auto; padding: 0 20px;">
13384
13481
  <p style="letter-spacing: .12em; text-transform: uppercase; color: #52606d;">Plivo setup</p>
13385
- <h1>${escapeHtml24(title)}</h1>
13482
+ <h1>${escapeHtml25(title)}</h1>
13386
13483
  <p><strong>Status:</strong> ${status.ready ? "Ready" : "Needs attention"}</p>
13387
13484
  <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>
13485
+ <li><strong>Answer XML:</strong> <code>${escapeHtml25(status.urls.answer)}</code></li>
13486
+ <li><strong>Audio stream:</strong> <code>${escapeHtml25(status.urls.stream)}</code></li>
13487
+ <li><strong>Status webhook:</strong> <code>${escapeHtml25(status.urls.webhook)}</code></li>
13391
13488
  </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>` : ""}
13489
+ ${status.missing.length ? `<h2>Missing env</h2><ul>${status.missing.map((name) => `<li><code>${escapeHtml25(name)}</code></li>`).join("")}</ul>` : ""}
13490
+ ${status.warnings.length ? `<h2>Warnings</h2><ul>${status.warnings.map((warning) => `<li>${escapeHtml25(warning)}</li>`).join("")}</ul>` : ""}
13394
13491
  </main>`;
13395
13492
  var renderPlivoSmokeHTML = (report, title) => `<main style="font-family: ui-sans-serif, system-ui; max-width: 860px; margin: 40px auto; padding: 0 20px;">
13396
13493
  <p style="letter-spacing: .12em; text-transform: uppercase; color: #52606d;">Plivo smoke test</p>
13397
- <h1>${escapeHtml24(title)}</h1>
13494
+ <h1>${escapeHtml25(title)}</h1>
13398
13495
  <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>
13496
+ <ul>${report.checks.map((check) => `<li><strong>${escapeHtml25(check.name)}</strong>: ${escapeHtml25(check.status)}${check.message ? ` - ${escapeHtml25(check.message)}` : ""}</li>`).join("")}</ul>
13400
13497
  </main>`;
13401
13498
  var runPlivoSmokeTest = async (input) => {
13402
13499
  const setup = await buildPlivoVoiceSetupStatus(input.options, input);
@@ -13491,7 +13588,7 @@ var createPlivoVoiceRoutes = (options = {}) => {
13491
13588
  request: input.request
13492
13589
  }) : verificationUrl ?? input.request.url
13493
13590
  }) : undefined);
13494
- const app = new Elysia24({
13591
+ const app = new Elysia25({
13495
13592
  name: options.name ?? "absolutejs-voice-plivo"
13496
13593
  }).get(answerPath, async ({ query, request }) => {
13497
13594
  const streamUrl = await resolvePlivoStreamUrl(options, {
@@ -13602,9 +13699,9 @@ var createPlivoVoiceRoutes = (options = {}) => {
13602
13699
 
13603
13700
  // src/telephony/telnyx.ts
13604
13701
  import { Buffer as Buffer5 } from "buffer";
13605
- import { Elysia as Elysia25 } from "elysia";
13702
+ import { Elysia as Elysia26 } from "elysia";
13606
13703
  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;");
13704
+ var escapeHtml26 = (value) => value.replaceAll("&", "&amp;").replaceAll('"', "&quot;").replaceAll("'", "&#39;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
13608
13705
  var joinUrlPath3 = (origin, path) => `${origin.replace(/\/$/, "")}${path.startsWith("/") ? path : `/${path}`}`;
13609
13706
  var resolveRequestOrigin3 = (request) => {
13610
13707
  const url = new URL(request.url);
@@ -13805,21 +13902,21 @@ var buildTelnyxVoiceSetupStatus = async (options, input) => {
13805
13902
  };
13806
13903
  var renderTelnyxSetupHTML = (status, title) => `<main style="font-family: ui-sans-serif, system-ui; max-width: 860px; margin: 40px auto; padding: 0 20px;">
13807
13904
  <p style="letter-spacing: .12em; text-transform: uppercase; color: #52606d;">Telnyx setup</p>
13808
- <h1>${escapeHtml25(title)}</h1>
13905
+ <h1>${escapeHtml26(title)}</h1>
13809
13906
  <p><strong>Status:</strong> ${status.ready ? "Ready" : "Needs attention"}</p>
13810
13907
  <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>
13908
+ <li><strong>TeXML:</strong> <code>${escapeHtml26(status.urls.texml)}</code></li>
13909
+ <li><strong>Media stream:</strong> <code>${escapeHtml26(status.urls.stream)}</code></li>
13910
+ <li><strong>Status webhook:</strong> <code>${escapeHtml26(status.urls.webhook)}</code></li>
13814
13911
  </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>` : ""}
13912
+ ${status.missing.length ? `<h2>Missing env</h2><ul>${status.missing.map((name) => `<li><code>${escapeHtml26(name)}</code></li>`).join("")}</ul>` : ""}
13913
+ ${status.warnings.length ? `<h2>Warnings</h2><ul>${status.warnings.map((warning) => `<li>${escapeHtml26(warning)}</li>`).join("")}</ul>` : ""}
13817
13914
  </main>`;
13818
13915
  var renderTelnyxSmokeHTML = (report, title) => `<main style="font-family: ui-sans-serif, system-ui; max-width: 860px; margin: 40px auto; padding: 0 20px;">
13819
13916
  <p style="letter-spacing: .12em; text-transform: uppercase; color: #52606d;">Telnyx smoke test</p>
13820
- <h1>${escapeHtml25(title)}</h1>
13917
+ <h1>${escapeHtml26(title)}</h1>
13821
13918
  <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>
13919
+ <ul>${report.checks.map((check) => `<li><strong>${escapeHtml26(check.name)}</strong>: ${escapeHtml26(check.status)}${check.message ? ` - ${escapeHtml26(check.message)}` : ""}</li>`).join("")}</ul>
13823
13920
  </main>`;
13824
13921
  var runTelnyxSmokeTest = async (input) => {
13825
13922
  const setup = await buildTelnyxVoiceSetupStatus(input.options, input);
@@ -13913,7 +14010,7 @@ var createTelnyxVoiceRoutes = (options = {}) => {
13913
14010
  publicKey: options.webhook?.publicKey,
13914
14011
  toleranceSeconds: options.webhook?.toleranceSeconds
13915
14012
  }) : undefined);
13916
- const app = new Elysia25({
14013
+ const app = new Elysia26({
13917
14014
  name: options.name ?? "absolutejs-voice-telnyx"
13918
14015
  }).get(texmlPath, async ({ query, request }) => {
13919
14016
  const streamUrl = await resolveTelnyxStreamUrl(options, {
@@ -14083,7 +14180,7 @@ var createVoicePhoneAgent = (options) => {
14083
14180
  setupPath: resolveSetupPath(carrier),
14084
14181
  smokePath: resolveSmokePath(carrier)
14085
14182
  }));
14086
- const app = new Elysia26({
14183
+ const app = new Elysia27({
14087
14184
  name: options.name ?? "absolutejs-voice-phone-agent"
14088
14185
  });
14089
14186
  for (const carrier of options.carriers) {
@@ -16364,7 +16461,7 @@ var createVoiceMemoryStore = () => {
16364
16461
  return { get, getOrCreate, list, remove, set };
16365
16462
  };
16366
16463
  // src/opsWebhook.ts
16367
- import { Elysia as Elysia27 } from "elysia";
16464
+ import { Elysia as Elysia28 } from "elysia";
16368
16465
  var toHex5 = (bytes) => Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
16369
16466
  var signVoiceOpsWebhookBody = async (input) => {
16370
16467
  const encoder = new TextEncoder;
@@ -16494,7 +16591,7 @@ var verifyVoiceOpsWebhookSignature = async (input) => {
16494
16591
  };
16495
16592
  var createVoiceOpsWebhookReceiverRoutes = (options = {}) => {
16496
16593
  const path = options.path ?? "/api/voice-ops/webhook";
16497
- return new Elysia27().post(path, async ({ body, request, set }) => {
16594
+ return new Elysia28().post(path, async ({ body, request, set }) => {
16498
16595
  const bodyText = typeof body === "string" ? body : JSON.stringify(body);
16499
16596
  if (options.signingSecret) {
16500
16597
  const verification = await verifyVoiceOpsWebhookSignature({
@@ -18314,6 +18411,7 @@ export {
18314
18411
  selectVoiceTraceEventsForPrune,
18315
18412
  runVoiceToolContractSuite,
18316
18413
  runVoiceToolContract,
18414
+ runVoiceSimulationSuite,
18317
18415
  runVoiceSessionEvals,
18318
18416
  runVoiceScenarioFixtureEvals,
18319
18417
  runVoiceScenarioEvals,
@@ -18342,6 +18440,7 @@ export {
18342
18440
  renderVoiceTraceHTML,
18343
18441
  renderVoiceToolContractHTML,
18344
18442
  renderVoiceTelephonyCarrierMatrixHTML,
18443
+ renderVoiceSimulationSuiteHTML,
18345
18444
  renderVoiceSessionsHTML,
18346
18445
  renderVoiceScenarioFixtureEvalHTML,
18347
18446
  renderVoiceScenarioEvalHTML,
@@ -18430,6 +18529,7 @@ export {
18430
18529
  createVoiceTaskSLABreachedEvent,
18431
18530
  createVoiceTaskCreatedEvent,
18432
18531
  createVoiceTTSProviderRouter,
18532
+ createVoiceSimulationSuiteRoutes,
18433
18533
  createVoiceSessionsJSONHandler,
18434
18534
  createVoiceSessionsHTMLHandler,
18435
18535
  createVoiceSessionReplayRoutes,