@absolutejs/voice 0.0.22-beta.47 → 0.0.22-beta.49

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
@@ -5473,6 +5473,12 @@ var voice = (config) => {
5473
5473
  }
5474
5474
  }).use(htmxRoutes());
5475
5475
  };
5476
+ // src/appKit.ts
5477
+ import { Elysia as Elysia11 } from "elysia";
5478
+
5479
+ // src/assistantHealth.ts
5480
+ import { Elysia as Elysia3 } from "elysia";
5481
+
5476
5482
  // src/agent.ts
5477
5483
  var normalizeText3 = (value) => typeof value === "string" ? value.trim() : "";
5478
5484
  var toErrorMessage3 = (error) => error instanceof Error ? error.message : String(error);
@@ -6522,8 +6528,6 @@ var summarizeVoiceAssistantRuns = async (input) => {
6522
6528
  totalRuns: assistantRuns.length
6523
6529
  };
6524
6530
  };
6525
- // src/assistantHealth.ts
6526
- import { Elysia as Elysia3 } from "elysia";
6527
6531
 
6528
6532
  // src/providerHealth.ts
6529
6533
  import { Elysia as Elysia2 } from "elysia";
@@ -6822,6 +6826,7 @@ var createVoiceAssistantHealthRoutes = (options) => {
6822
6826
  }
6823
6827
  return routes;
6824
6828
  };
6829
+
6825
6830
  // src/diagnosticsRoutes.ts
6826
6831
  import { Elysia as Elysia4 } from "elysia";
6827
6832
 
@@ -7623,6 +7628,7 @@ var createVoiceDiagnosticsRoutes = (options) => {
7623
7628
  });
7624
7629
  return routes;
7625
7630
  };
7631
+
7626
7632
  // src/evalRoutes.ts
7627
7633
  import { Elysia as Elysia7 } from "elysia";
7628
7634
  import { mkdir } from "fs/promises";
@@ -8454,159 +8460,1002 @@ var createVoiceEvalRoutes = (options) => {
8454
8460
  });
8455
8461
  return routes;
8456
8462
  };
8457
- // src/workflowContract.ts
8458
- var getObject2 = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : undefined;
8459
- var getPathValue2 = (value, path) => {
8460
- let current = value;
8461
- for (const part of path.split(".").filter(Boolean)) {
8462
- const record = getObject2(current);
8463
- if (!record || !(part in record)) {
8464
- return;
8463
+
8464
+ // src/opsConsoleRoutes.ts
8465
+ import { Elysia as Elysia10 } from "elysia";
8466
+
8467
+ // src/resilienceRoutes.ts
8468
+ import { Elysia as Elysia8 } from "elysia";
8469
+ var escapeHtml10 = (value) => value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
8470
+ var getString7 = (value) => typeof value === "string" ? value : undefined;
8471
+ var getNumber4 = (value) => typeof value === "number" && Number.isFinite(value) ? value : undefined;
8472
+ var getBoolean2 = (value) => value === true;
8473
+ var isProviderStatus2 = (value) => value === "error" || value === "fallback" || value === "success";
8474
+ var listVoiceRoutingEvents = (events) => {
8475
+ const routingEvents = [];
8476
+ for (const event of events) {
8477
+ if (event.type !== "session.error") {
8478
+ continue;
8465
8479
  }
8466
- current = record[part];
8480
+ const provider = getString7(event.payload.provider);
8481
+ const providerStatus = isProviderStatus2(event.payload.providerStatus) ? event.payload.providerStatus : undefined;
8482
+ if (!provider || !providerStatus) {
8483
+ continue;
8484
+ }
8485
+ const kind = getString7(event.payload.kind);
8486
+ routingEvents.push({
8487
+ at: event.at,
8488
+ attempt: getNumber4(event.payload.attempt),
8489
+ elapsedMs: getNumber4(event.payload.elapsedMs),
8490
+ error: getString7(event.payload.error),
8491
+ fallbackProvider: getString7(event.payload.fallbackProvider),
8492
+ kind: kind === "stt" || kind === "tts" ? kind : "llm",
8493
+ latencyBudgetMs: getNumber4(event.payload.latencyBudgetMs),
8494
+ operation: getString7(event.payload.operation),
8495
+ provider,
8496
+ selectedProvider: getString7(event.payload.selectedProvider),
8497
+ sessionId: event.sessionId,
8498
+ status: providerStatus,
8499
+ timedOut: getBoolean2(event.payload.timedOut),
8500
+ turnId: event.turnId
8501
+ });
8467
8502
  }
8468
- return current;
8503
+ return routingEvents.sort((left, right) => right.at - left.at);
8469
8504
  };
8470
- var hasValue = (value, match) => {
8471
- switch (match) {
8472
- case "boolean":
8473
- return typeof value === "boolean";
8474
- case "number":
8475
- return typeof value === "number" && Number.isFinite(value);
8476
- case "string":
8477
- return typeof value === "string";
8478
- case "truthy":
8479
- return Boolean(value);
8480
- case "non-empty":
8481
- default:
8482
- return Array.isArray(value) ? value.length > 0 : typeof value === "string" ? value.trim().length > 0 : value !== undefined && value !== null;
8505
+ var summarizeRoutingEvents = (events) => {
8506
+ const byKind = new Map;
8507
+ let errors = 0;
8508
+ let fallbacks = 0;
8509
+ let timeouts = 0;
8510
+ for (const event of events) {
8511
+ byKind.set(event.kind, (byKind.get(event.kind) ?? 0) + 1);
8512
+ if (event.status === "error") {
8513
+ errors += 1;
8514
+ }
8515
+ if (event.status === "fallback") {
8516
+ fallbacks += 1;
8517
+ }
8518
+ if (event.timedOut) {
8519
+ timeouts += 1;
8520
+ }
8483
8521
  }
8522
+ return {
8523
+ byKind,
8524
+ errors,
8525
+ fallbacks,
8526
+ timeouts,
8527
+ total: events.length
8528
+ };
8484
8529
  };
8485
- var resolveOutcome2 = (routeResult) => {
8486
- if (routeResult.complete)
8487
- return "complete";
8488
- if (routeResult.transfer)
8489
- return "transfer";
8490
- if (routeResult.escalate)
8491
- return "escalate";
8492
- if (routeResult.voicemail)
8493
- return "voicemail";
8494
- if (routeResult.noAnswer)
8495
- return "no-answer";
8496
- return;
8530
+ var renderProviderCards = (title, providers) => {
8531
+ if (providers.length === 0) {
8532
+ return `<p class="muted">No ${escapeHtml10(title)} provider health yet.</p>`;
8533
+ }
8534
+ return `<div class="provider-grid">${providers.map((provider) => `
8535
+ <article class="card provider ${escapeHtml10(provider.status)}">
8536
+ <div class="card-header">
8537
+ <strong>${escapeHtml10(provider.provider)}</strong>
8538
+ <span>${escapeHtml10(provider.status)}${provider.recommended ? " \xB7 recommended" : ""}</span>
8539
+ </div>
8540
+ <dl>
8541
+ <div><dt>Runs</dt><dd>${provider.runCount}</dd></div>
8542
+ <div><dt>Avg latency</dt><dd>${provider.averageElapsedMs ?? 0}ms</dd></div>
8543
+ <div><dt>Errors</dt><dd>${provider.errorCount}</dd></div>
8544
+ <div><dt>Timeouts</dt><dd>${provider.timeoutCount}</dd></div>
8545
+ <div><dt>Fallbacks</dt><dd>${provider.fallbackCount}</dd></div>
8546
+ </dl>
8547
+ ${provider.lastError ? `<p class="muted">${escapeHtml10(provider.lastError)}</p>` : ""}
8548
+ </article>
8549
+ `).join("")}</div>`;
8497
8550
  };
8498
- var validateVoiceWorkflowRouteResult = (definition, routeResult) => {
8499
- const issues = [];
8500
- const requiredFields = (definition.fields ?? []).filter((field) => field.required !== false).map((field) => field.path);
8501
- const missingFields = [];
8502
- const outcome = resolveOutcome2(routeResult);
8503
- if (definition.outcome && outcome !== definition.outcome) {
8504
- issues.push({
8505
- code: "workflow.outcome_mismatch",
8506
- message: `Expected workflow outcome ${definition.outcome}, saw ${outcome ?? "none"}.`
8507
- });
8551
+ var renderTimeline2 = (events) => {
8552
+ if (events.length === 0) {
8553
+ return '<p class="muted">No provider routing events yet. Run the app or simulate provider failover.</p>';
8508
8554
  }
8509
- for (const field of definition.fields ?? []) {
8510
- if (field.required === false)
8511
- continue;
8512
- const paths = [field.path, ...field.aliases ?? []];
8513
- const present = paths.some((path) => hasValue(getPathValue2(routeResult.result, path), field.match ?? "non-empty"));
8514
- if (!present) {
8515
- missingFields.push(field.path);
8516
- issues.push({
8517
- code: "workflow.missing_field",
8518
- field: field.path,
8519
- message: `Missing required workflow field: ${field.label ?? field.path}.`
8520
- });
8521
- }
8555
+ return `<div class="timeline">${events.slice(0, 40).map((event) => `
8556
+ <article class="card event ${escapeHtml10(event.status ?? "unknown")}">
8557
+ <div class="card-header">
8558
+ <strong>${escapeHtml10(event.kind.toUpperCase())} ${escapeHtml10(event.operation ?? "generate")}</strong>
8559
+ <span>${new Date(event.at).toLocaleString()}</span>
8560
+ </div>
8561
+ <p>
8562
+ <span class="pill">${escapeHtml10(event.status ?? "unknown")}</span>
8563
+ <span class="pill">provider: ${escapeHtml10(event.provider ?? "unknown")}</span>
8564
+ ${event.fallbackProvider ? `<span class="pill">fallback: ${escapeHtml10(event.fallbackProvider)}</span>` : ""}
8565
+ ${event.timedOut ? '<span class="pill danger">timed out</span>' : ""}
8566
+ </p>
8567
+ <dl>
8568
+ <div><dt>Attempt</dt><dd>${event.attempt ?? 0}</dd></div>
8569
+ <div><dt>Elapsed</dt><dd>${event.elapsedMs ?? 0}ms</dd></div>
8570
+ <div><dt>Budget</dt><dd>${event.latencyBudgetMs ?? 0}ms</dd></div>
8571
+ <div><dt>Session</dt><dd>${escapeHtml10(event.sessionId)}</dd></div>
8572
+ </dl>
8573
+ ${event.error ? `<p class="muted">${escapeHtml10(event.error)}</p>` : ""}
8574
+ </article>
8575
+ `).join("")}</div>`;
8576
+ };
8577
+ var renderSimulationControls = (kind, simulation) => {
8578
+ if (!simulation) {
8579
+ return "";
8522
8580
  }
8523
- issues.push(...definition.validate?.({
8524
- result: routeResult.result,
8525
- routeResult
8526
- }) ?? []);
8527
- return {
8528
- contractId: definition.id,
8529
- issues,
8530
- missingFields,
8531
- outcome,
8532
- pass: issues.length === 0,
8533
- requiredFields
8534
- };
8581
+ const configuredProviders = simulation.providers.filter((provider) => provider.configured !== false);
8582
+ if (configuredProviders.length === 0) {
8583
+ return `<p class="muted">No ${kind.toUpperCase()} providers are configured for simulation.</p>`;
8584
+ }
8585
+ const pathPrefix = simulation.pathPrefix ?? `/api/${kind}-simulate`;
8586
+ const failureProviders = simulation.failureProviders ?? configuredProviders.map(({ provider }) => provider);
8587
+ const canFail = (provider) => configuredProviders.some((entry) => entry.provider === provider) && (!simulation.fallbackRequiredProvider || configuredProviders.some((entry) => entry.provider === simulation.fallbackRequiredProvider));
8588
+ return `<div class="simulate-panel" data-sim-kind="${kind}" data-sim-prefix="${escapeHtml10(pathPrefix)}">
8589
+ <p class="muted">${escapeHtml10(simulation.failureMessage ?? `Simulate ${kind.toUpperCase()} provider failure without changing provider credentials.`)}</p>
8590
+ <div class="simulate-actions">
8591
+ ${failureProviders.map((provider) => `<button type="button" data-provider-fail="${escapeHtml10(provider)}"${canFail(provider) ? "" : " disabled"}>Simulate ${escapeHtml10(provider)} ${kind.toUpperCase()} failure</button>`).join("")}
8592
+ ${configuredProviders.map((provider) => `<button type="button" data-provider-recover="${escapeHtml10(provider.provider)}">Mark ${escapeHtml10(provider.provider)} recovered</button>`).join("")}
8593
+ </div>
8594
+ ${simulation.fallbackRequiredProvider && !configuredProviders.some((entry) => entry.provider === simulation.fallbackRequiredProvider) ? `<p class="muted">${escapeHtml10(simulation.fallbackRequiredMessage ?? `Configure ${simulation.fallbackRequiredProvider} to enable fallback simulation.`)}</p>` : ""}
8595
+ <pre class="simulate-output" hidden></pre>
8596
+ </div>`;
8535
8597
  };
8536
- var createVoiceWorkflowScenario = (definition, overrides = {}) => ({
8537
- description: definition.description,
8538
- forbiddenHandoffActions: definition.forbiddenHandoffActions,
8539
- id: definition.id,
8540
- label: definition.label,
8541
- maxProviderErrors: definition.maxProviderErrors,
8542
- maxSessionErrors: definition.maxSessionErrors,
8543
- minSessions: definition.minSessions,
8544
- minTurns: definition.minTurns,
8545
- requiredAssistantIncludes: definition.requiredAssistantIncludes,
8546
- requiredDisposition: definition.requiredDisposition,
8547
- requiredHandoffActions: definition.requiredHandoffActions,
8548
- requiredLifecycleTypes: definition.requiredLifecycleTypes,
8549
- requiredTranscriptIncludes: definition.requiredTranscriptIncludes,
8550
- requiredWorkflowContracts: [definition.id],
8551
- scenarioId: definition.scenarioId,
8552
- ...overrides
8553
- });
8554
- var createVoiceWorkflowContract = (definition) => ({
8555
- assertRouteResult: (routeResult) => {
8556
- const validation = validateVoiceWorkflowRouteResult(definition, routeResult);
8557
- if (!validation.pass) {
8558
- throw new Error(`Voice workflow contract ${definition.id} failed: ${validation.issues.map((issue) => issue.message).join(" ")}`);
8559
- }
8560
- },
8561
- definition,
8562
- toScenarioEval: (overrides) => createVoiceWorkflowScenario(definition, overrides),
8563
- validateRouteResult: (routeResult) => validateVoiceWorkflowRouteResult(definition, routeResult)
8564
- });
8565
- var presetDefinitions = {
8566
- "appointment-booking": {
8567
- description: "Appointment booking should complete with enough identity, appointment, and follow-up details to act on.",
8568
- fields: [
8569
- { aliases: ["name", "customer.name"], label: "Caller name", path: "caller.name" },
8570
- {
8571
- aliases: ["phone", "customer.phone"],
8572
- label: "Caller phone",
8573
- path: "caller.phone"
8574
- },
8575
- {
8576
- aliases: ["appointment.start", "appointment.time", "scheduledAt"],
8577
- label: "Appointment time",
8578
- path: "appointment.startsAt"
8579
- },
8580
- {
8581
- aliases: ["summary", "assistantSummary"],
8582
- label: "Summary",
8583
- path: "appointment.summary"
8584
- }
8585
- ],
8586
- id: "appointment-booking",
8587
- label: "Appointment booking",
8588
- outcome: "complete",
8589
- requiredDisposition: "completed"
8590
- },
8591
- "lead-qualification": {
8592
- description: "Lead qualification should complete with contact, need, qualification, and next-step fields.",
8593
- fields: [
8594
- { aliases: ["name", "lead.name"], label: "Lead name", path: "contact.name" },
8595
- {
8596
- aliases: ["email", "lead.email"],
8597
- label: "Lead email",
8598
- path: "contact.email"
8599
- },
8600
- {
8601
- aliases: ["need", "pain", "summary"],
8602
- label: "Need",
8603
- path: "qualification.need"
8604
- },
8605
- {
8606
- aliases: ["qualified", "qualification.qualified"],
8607
- label: "Qualified",
8608
- match: "boolean",
8609
- path: "qualification.isQualified"
8598
+ var renderVoiceResilienceHTML = (input) => {
8599
+ const summary = summarizeRoutingEvents(input.routingEvents);
8600
+ const kindCounts = [...summary.byKind.entries()].map(([kind, count]) => `<span class="pill">${escapeHtml10(kind)}: ${String(count)}</span>`).join("");
8601
+ const links = input.links?.length ? input.links.map((link) => `<a href="${escapeHtml10(link.href)}">${escapeHtml10(link.label)}</a>`).join(" \xB7 ") : "";
8602
+ return `<!doctype html>
8603
+ <html lang="en">
8604
+ <head>
8605
+ <meta charset="utf-8" />
8606
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
8607
+ <title>${escapeHtml10(input.title ?? "AbsoluteJS Voice Resilience")}</title>
8608
+ <style>
8609
+ :root { color-scheme: dark; }
8610
+ body { background: radial-gradient(circle at top left, #172554, #09090b 36%, #050505); color: #f4f4f5; font-family: ui-sans-serif, system-ui, sans-serif; margin: 0; padding: 24px; }
8611
+ main { display: grid; gap: 16px; margin: 0 auto; max-width: 1180px; }
8612
+ section, .card { background: rgba(19, 22, 27, 0.92); border: 1px solid #27272a; border-radius: 20px; padding: 20px; }
8613
+ .hero { background: linear-gradient(135deg, rgba(14, 165, 233, 0.18), rgba(245, 158, 11, 0.12)); }
8614
+ .grid, .provider-grid { display: grid; gap: 14px; grid-template-columns: repeat(4, minmax(0, 1fr)); }
8615
+ .timeline { display: grid; gap: 12px; }
8616
+ .card-header { align-items: center; display: flex; gap: 12px; justify-content: space-between; }
8617
+ .card-header strong { font-size: 1.05rem; }
8618
+ .metric strong { display: block; font-size: 2rem; margin-top: 6px; }
8619
+ .muted, dt, span { color: #a1a1aa; }
8620
+ dl { display: grid; gap: 8px; grid-template-columns: repeat(4, minmax(0, 1fr)); }
8621
+ dl div { background: #0f1217; border: 1px solid #27272a; border-radius: 12px; padding: 10px; }
8622
+ dd { font-weight: 800; margin: 4px 0 0; }
8623
+ .pill { background: #0f1217; border: 1px solid #3f3f46; border-radius: 999px; color: #d4d4d8; display: inline-flex; margin: 3px 4px 3px 0; padding: 5px 9px; }
8624
+ .danger { border-color: rgba(239, 68, 68, 0.75); color: #fecaca; }
8625
+ .event.error { border-color: rgba(239, 68, 68, 0.7); }
8626
+ .event.fallback { border-color: rgba(245, 158, 11, 0.7); }
8627
+ .event.success, .provider.healthy { border-color: rgba(34, 197, 94, 0.5); }
8628
+ .provider.suppressed, .provider.degraded, .provider.rate-limited { border-color: rgba(239, 68, 68, 0.7); }
8629
+ .provider.recoverable { border-color: rgba(59, 130, 246, 0.7); }
8630
+ button { background: #f59e0b; border: 0; border-radius: 999px; color: #111827; cursor: pointer; font-weight: 800; padding: 10px 14px; }
8631
+ button:disabled { cursor: not-allowed; opacity: 0.45; }
8632
+ .simulate-actions { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 12px; }
8633
+ .simulate-output { background: #050505; border: 1px solid #27272a; border-radius: 14px; color: #d4d4d8; overflow: auto; padding: 12px; white-space: pre-wrap; }
8634
+ a { color: #f59e0b; }
8635
+ @media (max-width: 850px) { .grid, .provider-grid, dl { grid-template-columns: 1fr; } }
8636
+ </style>
8637
+ </head>
8638
+ <body>
8639
+ <main>
8640
+ <section class="hero">
8641
+ <h1>Provider routing and resilience</h1>
8642
+ <p>One view for the production reliability story: LLM failover, STT/TTS routing, latency budgets, timeouts, and fallback decisions.</p>
8643
+ ${links ? `<p>${links}</p>` : ""}
8644
+ <p>${kindCounts || '<span class="pill">No routing events yet</span>'}</p>
8645
+ </section>
8646
+ <section class="grid">
8647
+ <article class="card metric"><span>Total routing events</span><strong>${summary.total}</strong></article>
8648
+ <article class="card metric"><span>Fallbacks</span><strong>${summary.fallbacks}</strong></article>
8649
+ <article class="card metric"><span>Errors</span><strong>${summary.errors}</strong></article>
8650
+ <article class="card metric"><span>Timeouts</span><strong>${summary.timeouts}</strong></article>
8651
+ </section>
8652
+ <section>
8653
+ <h2>LLM provider health</h2>
8654
+ ${renderProviderCards("LLM", input.llmProviderHealth)}
8655
+ </section>
8656
+ <section>
8657
+ <h2>STT provider health</h2>
8658
+ ${renderSimulationControls("stt", input.sttSimulation)}
8659
+ ${renderProviderCards("STT", input.sttProviderHealth)}
8660
+ </section>
8661
+ <section>
8662
+ <h2>TTS provider health</h2>
8663
+ ${renderSimulationControls("tts", input.ttsSimulation)}
8664
+ ${renderProviderCards("TTS", input.ttsProviderHealth)}
8665
+ </section>
8666
+ <section>
8667
+ <h2>Routing timeline</h2>
8668
+ ${renderTimeline2(input.routingEvents)}
8669
+ </section>
8670
+ </main>
8671
+ <script>
8672
+ const showResult = (panel, result) => {
8673
+ const output = panel.querySelector(".simulate-output");
8674
+ if (!output) return;
8675
+ output.hidden = false;
8676
+ output.textContent = JSON.stringify(result, null, 2);
8677
+ };
8678
+ document.querySelectorAll("[data-sim-prefix]").forEach((panel) => {
8679
+ const prefix = panel.getAttribute("data-sim-prefix");
8680
+ panel.querySelectorAll("[data-provider-fail]").forEach((button) => {
8681
+ button.addEventListener("click", async () => {
8682
+ const provider = button.getAttribute("data-provider-fail");
8683
+ const response = await fetch(prefix + "/failure?provider=" + encodeURIComponent(provider || ""), { method: "POST" });
8684
+ showResult(panel, await response.json());
8685
+ if (response.ok) window.setTimeout(() => window.location.reload(), 450);
8686
+ });
8687
+ });
8688
+ panel.querySelectorAll("[data-provider-recover]").forEach((button) => {
8689
+ button.addEventListener("click", async () => {
8690
+ const provider = button.getAttribute("data-provider-recover");
8691
+ const response = await fetch(prefix + "/recovery?provider=" + encodeURIComponent(provider || ""), { method: "POST" });
8692
+ showResult(panel, await response.json());
8693
+ if (response.ok) window.setTimeout(() => window.location.reload(), 450);
8694
+ });
8695
+ });
8696
+ });
8697
+ </script>
8698
+ </body>
8699
+ </html>`;
8700
+ };
8701
+ var providerFromQuery = (value, providers) => typeof value === "string" && providers.some((provider) => provider.provider === value && provider.configured !== false) ? value : undefined;
8702
+ var registerSimulationRoutes = (routes, simulation, defaultPathPrefix) => {
8703
+ if (!simulation) {
8704
+ return routes;
8705
+ }
8706
+ const pathPrefix = simulation.pathPrefix ?? defaultPathPrefix;
8707
+ routes.post(`${pathPrefix}/failure`, async ({ query, set }) => {
8708
+ const provider = providerFromQuery(query.provider, simulation.providers);
8709
+ if (!provider) {
8710
+ set.status = 400;
8711
+ return {
8712
+ error: "Provider is not configured for simulation."
8713
+ };
8714
+ }
8715
+ if (simulation.failureProviders && !simulation.failureProviders.includes(provider)) {
8716
+ set.status = 400;
8717
+ return {
8718
+ error: `${provider} is not configured for failure simulation.`
8719
+ };
8720
+ }
8721
+ if (simulation.fallbackRequiredProvider && !simulation.providers.some((entry) => entry.provider === simulation.fallbackRequiredProvider && entry.configured !== false)) {
8722
+ set.status = 400;
8723
+ return {
8724
+ error: simulation.fallbackRequiredMessage ?? `Configure ${simulation.fallbackRequiredProvider} before simulating fallback.`
8725
+ };
8726
+ }
8727
+ return simulation.run(provider, "failure");
8728
+ });
8729
+ routes.post(`${pathPrefix}/recovery`, async ({ query, set }) => {
8730
+ const provider = providerFromQuery(query.provider, simulation.providers);
8731
+ if (!provider) {
8732
+ set.status = 400;
8733
+ return {
8734
+ error: "Provider is not configured for simulation."
8735
+ };
8736
+ }
8737
+ return simulation.run(provider, "recovery");
8738
+ });
8739
+ return routes;
8740
+ };
8741
+ var createVoiceResilienceRoutes = (options) => {
8742
+ const path = options.path ?? "/resilience";
8743
+ const routes = new Elysia8({
8744
+ name: options.name ?? "absolutejs-voice-resilience"
8745
+ }).get(path, async () => {
8746
+ const events = await options.store.list();
8747
+ const sttEvents = events.filter((event) => event.payload.kind === "stt");
8748
+ const ttsEvents = events.filter((event) => event.payload.kind === "tts");
8749
+ const data = {
8750
+ links: options.links,
8751
+ llmProviderHealth: await summarizeVoiceProviderHealth({
8752
+ events,
8753
+ providers: options.llmProviders ?? []
8754
+ }),
8755
+ routingEvents: listVoiceRoutingEvents(events),
8756
+ sttProviderHealth: await summarizeVoiceProviderHealth({
8757
+ events: sttEvents,
8758
+ providers: options.sttProviders ?? []
8759
+ }),
8760
+ sttSimulation: options.sttSimulation,
8761
+ title: options.title,
8762
+ ttsProviderHealth: await summarizeVoiceProviderHealth({
8763
+ events: ttsEvents,
8764
+ providers: options.ttsProviders ?? []
8765
+ }),
8766
+ ttsSimulation: options.ttsSimulation
8767
+ };
8768
+ const body = await (options.render ?? renderVoiceResilienceHTML)(data);
8769
+ return new Response(body, {
8770
+ headers: {
8771
+ "Content-Type": "text/html; charset=utf-8",
8772
+ ...options.headers
8773
+ }
8774
+ });
8775
+ });
8776
+ registerSimulationRoutes(routes, options.sttSimulation, "/api/stt-simulate");
8777
+ registerSimulationRoutes(routes, options.ttsSimulation, "/api/tts-simulate");
8778
+ return routes;
8779
+ };
8780
+
8781
+ // src/sessionReplay.ts
8782
+ import { Elysia as Elysia9 } from "elysia";
8783
+ var getString8 = (value) => typeof value === "string" ? value : undefined;
8784
+ var escapeHtml11 = (value) => value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
8785
+ var increment3 = (record, key) => {
8786
+ record[key] = (record[key] ?? 0) + 1;
8787
+ };
8788
+ var buildReplayTurns = (events) => {
8789
+ const turns = new Map;
8790
+ const getTurn = (turnId) => {
8791
+ const existing = turns.get(turnId);
8792
+ if (existing) {
8793
+ return existing;
8794
+ }
8795
+ const turn = {
8796
+ assistantReplies: [],
8797
+ errors: [],
8798
+ id: turnId,
8799
+ modelCalls: [],
8800
+ tools: [],
8801
+ transcripts: []
8802
+ };
8803
+ turns.set(turnId, turn);
8804
+ return turn;
8805
+ };
8806
+ for (const event of events) {
8807
+ const turnId = event.turnId ?? "session";
8808
+ const turn = getTurn(turnId);
8809
+ switch (event.type) {
8810
+ case "turn.transcript":
8811
+ turn.transcripts.push({
8812
+ isFinal: event.payload.isFinal === true,
8813
+ text: getString8(event.payload.text)
8814
+ });
8815
+ break;
8816
+ case "turn.committed":
8817
+ turn.committedText = getString8(event.payload.text);
8818
+ break;
8819
+ case "turn.assistant": {
8820
+ const text = getString8(event.payload.text);
8821
+ if (text) {
8822
+ turn.assistantReplies.push(text);
8823
+ }
8824
+ break;
8825
+ }
8826
+ case "agent.model":
8827
+ case "assistant.run":
8828
+ turn.modelCalls.push(event.payload);
8829
+ break;
8830
+ case "agent.tool":
8831
+ turn.tools.push(event.payload);
8832
+ break;
8833
+ case "session.error":
8834
+ turn.errors.push(event.payload);
8835
+ break;
8836
+ }
8837
+ }
8838
+ return [...turns.values()];
8839
+ };
8840
+ var summarizeVoiceSessionReplay = async (options) => {
8841
+ const sourceEvents = options.events ?? await options.store?.list({ sessionId: options.sessionId }) ?? [];
8842
+ const events = filterVoiceTraceEvents(sourceEvents, {
8843
+ sessionId: options.sessionId
8844
+ });
8845
+ const replay = buildVoiceTraceReplay(events, {
8846
+ evaluation: options.evaluation,
8847
+ redact: options.redact,
8848
+ title: options.title ?? `Voice Session ${options.sessionId}`
8849
+ });
8850
+ const startedAt = replay.summary.startedAt;
8851
+ return {
8852
+ evaluation: replay.evaluation,
8853
+ events,
8854
+ html: replay.html,
8855
+ markdown: replay.markdown,
8856
+ sessionId: options.sessionId,
8857
+ summary: replay.summary,
8858
+ timeline: events.map((event) => ({
8859
+ at: event.at,
8860
+ offsetMs: startedAt === undefined ? undefined : Math.max(0, event.at - startedAt),
8861
+ payload: event.payload,
8862
+ turnId: event.turnId,
8863
+ type: event.type
8864
+ })),
8865
+ turns: buildReplayTurns(events)
8866
+ };
8867
+ };
8868
+ var summarizeVoiceSessions = async (options = {}) => {
8869
+ const events = options.events ?? await options.store?.list() ?? [];
8870
+ const grouped = new Map;
8871
+ for (const event of events) {
8872
+ grouped.set(event.sessionId, [...grouped.get(event.sessionId) ?? [], event]);
8873
+ }
8874
+ const sessions = [...grouped.entries()].map(([sessionId, sessionEvents]) => {
8875
+ const sorted = filterVoiceTraceEvents(sessionEvents);
8876
+ const summary = buildVoiceTraceReplay(sorted, {
8877
+ evaluation: {
8878
+ requireAssistantReply: false,
8879
+ requireCompletedCall: false,
8880
+ requireTranscript: false,
8881
+ requireTurn: false
8882
+ }
8883
+ }).summary;
8884
+ const providerErrors = {};
8885
+ const providers = new Set;
8886
+ let latestOutcome;
8887
+ let errorCount = 0;
8888
+ for (const event of sorted) {
8889
+ const provider = getString8(event.payload.provider);
8890
+ if (provider) {
8891
+ providers.add(provider);
8892
+ }
8893
+ if (event.type === "session.error" && (event.payload.providerStatus === "error" || typeof event.payload.error === "string")) {
8894
+ errorCount += 1;
8895
+ increment3(providerErrors, provider ?? "unknown");
8896
+ }
8897
+ const outcome = getString8(event.payload.outcome);
8898
+ if (outcome) {
8899
+ latestOutcome = outcome;
8900
+ }
8901
+ }
8902
+ const item = {
8903
+ endedAt: summary.endedAt,
8904
+ errorCount,
8905
+ eventCount: summary.eventCount,
8906
+ latestOutcome,
8907
+ providerErrors,
8908
+ providers: [...providers].sort(),
8909
+ sessionId,
8910
+ startedAt: summary.startedAt,
8911
+ status: errorCount > 0 ? "failed" : "healthy",
8912
+ transcriptCount: summary.transcriptCount,
8913
+ turnCount: summary.turnCount
8914
+ };
8915
+ const replayHref = options.replayHref === false ? "" : typeof options.replayHref === "function" ? options.replayHref(item) : `${options.replayHref ?? "/api/voice-sessions"}/${encodeURIComponent(sessionId)}/replay/htmx`;
8916
+ return {
8917
+ ...item,
8918
+ replayHref
8919
+ };
8920
+ });
8921
+ const search = options.q?.trim().toLowerCase();
8922
+ return sessions.filter((session) => {
8923
+ if (options.status && options.status !== "all" && session.status !== options.status) {
8924
+ return false;
8925
+ }
8926
+ if (options.provider && !session.providers.includes(options.provider)) {
8927
+ return false;
8928
+ }
8929
+ if (!search) {
8930
+ return true;
8931
+ }
8932
+ return [
8933
+ session.sessionId,
8934
+ session.latestOutcome,
8935
+ session.status,
8936
+ ...session.providers
8937
+ ].some((value) => value?.toLowerCase().includes(search));
8938
+ }).sort((left, right) => (right.endedAt ?? right.startedAt ?? 0) - (left.endedAt ?? left.startedAt ?? 0)).slice(0, options.limit ?? 50);
8939
+ };
8940
+ var renderVoiceSessionsHTML = (sessions) => sessions.length === 0 ? '<p class="voice-sessions-empty">No voice sessions found.</p>' : [
8941
+ '<div class="voice-sessions-list">',
8942
+ ...sessions.map((session) => [
8943
+ `<article class="voice-session-card ${escapeHtml11(session.status)}">`,
8944
+ '<div class="voice-session-card-header">',
8945
+ `<strong>${escapeHtml11(session.sessionId)}</strong>`,
8946
+ `<span>${escapeHtml11(session.status)}</span>`,
8947
+ "</div>",
8948
+ "<dl>",
8949
+ `<div><dt>Events</dt><dd>${String(session.eventCount)}</dd></div>`,
8950
+ `<div><dt>Turns</dt><dd>${String(session.turnCount)}</dd></div>`,
8951
+ `<div><dt>Transcripts</dt><dd>${String(session.transcriptCount)}</dd></div>`,
8952
+ `<div><dt>Errors</dt><dd>${String(session.errorCount)}</dd></div>`,
8953
+ "</dl>",
8954
+ session.latestOutcome ? `<p>Outcome: ${escapeHtml11(session.latestOutcome)}</p>` : "",
8955
+ session.providers.length ? `<p>Providers: ${session.providers.map(escapeHtml11).join(", ")}</p>` : "",
8956
+ session.replayHref ? `<p><a href="${escapeHtml11(session.replayHref)}">Open replay</a></p>` : "",
8957
+ "</article>"
8958
+ ].join("")),
8959
+ "</div>"
8960
+ ].join("");
8961
+ var createVoiceSessionsJSONHandler = (options = {}) => async ({ query }) => summarizeVoiceSessions({
8962
+ ...options,
8963
+ limit: typeof query?.limit === "string" ? Number(query.limit) : options.limit,
8964
+ provider: query?.provider ?? options.provider,
8965
+ q: query?.q ?? options.q,
8966
+ status: query?.status === "failed" || query?.status === "healthy" || query?.status === "all" ? query.status : options.status
8967
+ });
8968
+ var createVoiceSessionsHTMLHandler = (options = {}) => async ({ query }) => {
8969
+ const sessions = await summarizeVoiceSessions({
8970
+ ...options,
8971
+ limit: typeof query?.limit === "string" ? Number(query.limit) : options.limit,
8972
+ provider: query?.provider ?? options.provider,
8973
+ q: query?.q ?? options.q,
8974
+ status: query?.status === "failed" || query?.status === "healthy" || query?.status === "all" ? query.status : options.status
8975
+ });
8976
+ const body = await (options.render?.(sessions) ?? renderVoiceSessionsHTML(sessions));
8977
+ return new Response(body, {
8978
+ headers: {
8979
+ "Content-Type": "text/html; charset=utf-8",
8980
+ ...options.headers
8981
+ }
8982
+ });
8983
+ };
8984
+ var createVoiceSessionListRoutes = (options = {}) => {
8985
+ const path = options.path ?? "/api/voice-sessions";
8986
+ const htmlPath = options.htmlPath === undefined ? `${path}/htmx` : options.htmlPath;
8987
+ const routes = new Elysia9({
8988
+ name: options.name ?? "absolutejs-voice-session-list"
8989
+ }).get(path, createVoiceSessionsJSONHandler(options));
8990
+ if (htmlPath) {
8991
+ routes.get(htmlPath, createVoiceSessionsHTMLHandler(options));
8992
+ }
8993
+ return routes;
8994
+ };
8995
+ var createVoiceSessionReplayJSONHandler = (options) => async ({ params }) => summarizeVoiceSessionReplay({
8996
+ ...options,
8997
+ sessionId: params.sessionId ?? ""
8998
+ });
8999
+ var createVoiceSessionReplayHTMLHandler = (options) => async ({ params }) => {
9000
+ const replay = await summarizeVoiceSessionReplay({
9001
+ ...options,
9002
+ sessionId: params.sessionId ?? ""
9003
+ });
9004
+ const body = await (options.render?.(replay) ?? replay.html);
9005
+ return new Response(body, {
9006
+ headers: {
9007
+ "Content-Type": "text/html; charset=utf-8",
9008
+ ...options.headers
9009
+ }
9010
+ });
9011
+ };
9012
+ var createVoiceSessionReplayRoutes = (options) => {
9013
+ const path = options.path ?? "/api/voice-sessions/:sessionId/replay";
9014
+ const htmlPath = options.htmlPath === undefined ? `${path}/htmx` : options.htmlPath;
9015
+ const routes = new Elysia9({
9016
+ name: options.name ?? "absolutejs-voice-session-replay"
9017
+ }).get(path, createVoiceSessionReplayJSONHandler(options));
9018
+ if (htmlPath) {
9019
+ routes.get(htmlPath, createVoiceSessionReplayHTMLHandler(options));
9020
+ }
9021
+ return routes;
9022
+ };
9023
+
9024
+ // src/opsConsoleRoutes.ts
9025
+ var DEFAULT_LINKS = [
9026
+ {
9027
+ description: "Quality gates for CI, deploy checks, and production readiness.",
9028
+ href: "/quality",
9029
+ label: "Quality",
9030
+ statusHref: "/quality/status"
9031
+ },
9032
+ {
9033
+ description: "Replay stored sessions against acceptance gates over time.",
9034
+ href: "/evals",
9035
+ label: "Evals",
9036
+ statusHref: "/evals/status"
9037
+ },
9038
+ {
9039
+ description: "Provider health, fallback paths, and failure simulation.",
9040
+ href: "/resilience",
9041
+ label: "Resilience"
9042
+ },
9043
+ {
9044
+ description: "Redacted trace exports for debugging and support handoffs.",
9045
+ href: "/diagnostics",
9046
+ label: "Diagnostics"
9047
+ },
9048
+ {
9049
+ description: "Recent sessions with replay links.",
9050
+ href: "/sessions",
9051
+ label: "Sessions"
9052
+ },
9053
+ {
9054
+ description: "Transfer and webhook delivery health.",
9055
+ href: "/handoffs",
9056
+ label: "Handoffs"
9057
+ }
9058
+ ];
9059
+ var escapeHtml12 = (value) => value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
9060
+ var countProviderStatuses = (providers) => {
9061
+ const degradedStatuses = new Set(["degraded", "rate-limited", "suppressed"]);
9062
+ const healthy = providers.filter((provider) => provider.status === "healthy").length;
9063
+ const degraded = providers.filter((provider) => degradedStatuses.has(provider.status)).length;
9064
+ return {
9065
+ degraded,
9066
+ healthy,
9067
+ total: providers.length
9068
+ };
9069
+ };
9070
+ var buildVoiceOpsConsoleReport = async (options) => {
9071
+ const events = await options.store.list();
9072
+ const providers = [
9073
+ ...await summarizeVoiceProviderHealth({
9074
+ events,
9075
+ providers: options.llmProviders
9076
+ }),
9077
+ ...await summarizeVoiceProviderHealth({
9078
+ events,
9079
+ providers: options.sttProviders
9080
+ }),
9081
+ ...await summarizeVoiceProviderHealth({
9082
+ events,
9083
+ providers: options.ttsProviders
9084
+ })
9085
+ ];
9086
+ const handoffs = await summarizeVoiceHandoffHealth({ events });
9087
+ const sessions = await summarizeVoiceSessions({
9088
+ events,
9089
+ limit: 8,
9090
+ status: "all"
9091
+ });
9092
+ const quality = await evaluateVoiceQuality({ events });
9093
+ const routingEvents = listVoiceRoutingEvents(events).slice(0, 10);
9094
+ const trace = summarizeVoiceTrace(events);
9095
+ return {
9096
+ checkedAt: Date.now(),
9097
+ eventCount: events.length,
9098
+ handoffs: {
9099
+ failed: handoffs.failed,
9100
+ total: handoffs.total
9101
+ },
9102
+ links: options.links ?? DEFAULT_LINKS,
9103
+ providers: countProviderStatuses(providers),
9104
+ quality,
9105
+ recentRoutingEvents: routingEvents,
9106
+ recentSessions: sessions,
9107
+ sessions: {
9108
+ failed: sessions.filter((session) => session.status === "failed").length,
9109
+ healthy: sessions.filter((session) => session.status === "healthy").length,
9110
+ total: sessions.length
9111
+ },
9112
+ trace
9113
+ };
9114
+ };
9115
+ var renderMetricCard = (input) => `<article class="metric"><span>${escapeHtml12(input.label)}</span><strong>${escapeHtml12(String(input.value))}</strong>${input.status ? `<p class="${escapeHtml12(input.status)}">${escapeHtml12(input.status)}</p>` : ""}${input.href ? `<a href="${escapeHtml12(input.href)}">Open</a>` : ""}</article>`;
9116
+ var renderVoiceOpsConsoleHTML = (report, options = {}) => {
9117
+ const links = report.links.map((link) => `<article class="surface">
9118
+ <div><h2>${escapeHtml12(link.label)}</h2>${link.description ? `<p>${escapeHtml12(link.description)}</p>` : ""}</div>
9119
+ <p><a href="${escapeHtml12(link.href)}">Open ${escapeHtml12(link.label)}</a>${link.statusHref ? ` \xB7 <a href="${escapeHtml12(link.statusHref)}">Status</a>` : ""}</p>
9120
+ </article>`).join("");
9121
+ const sessions = report.recentSessions.length ? report.recentSessions.map((session) => `<tr><td>${escapeHtml12(session.sessionId)}</td><td>${escapeHtml12(session.status)}</td><td>${session.turnCount}</td><td>${session.errorCount}</td><td>${session.replayHref ? `<a href="${escapeHtml12(session.replayHref)}">Replay</a>` : ""}</td></tr>`).join("") : '<tr><td colspan="5">No sessions yet.</td></tr>';
9122
+ const routing = report.recentRoutingEvents.length ? report.recentRoutingEvents.map((event) => `<tr><td>${escapeHtml12(event.kind)}</td><td>${escapeHtml12(event.provider ?? "unknown")}</td><td>${escapeHtml12(event.status ?? "unknown")}</td><td>${event.elapsedMs ?? 0}ms</td><td>${escapeHtml12(event.sessionId)}</td></tr>`).join("") : '<tr><td colspan="5">No provider routing events yet.</td></tr>';
9123
+ const title = options.title ?? "AbsoluteJS Voice Ops Console";
9124
+ return `<!doctype html><html lang="en"><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /><title>${escapeHtml12(title)}</title><style>body{font-family:ui-sans-serif,system-ui,sans-serif;background:#101316;color:#f6f2e8;margin:0}main{max-width:1180px;margin:auto;padding:32px}a{color:#fbbf24}header{display:flex;justify-content:space-between;gap:24px;align-items:flex-start;margin-bottom:24px}.eyebrow{color:#fbbf24;font-weight:800;letter-spacing:.08em;text-transform:uppercase}h1{font-size:clamp(2.2rem,5vw,4.5rem);line-height:.95;margin:.2rem 0 1rem}.muted{color:#a8b0b8}.grid{display:grid;gap:14px;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));margin:20px 0}.metric,.surface{background:#181d22;border:1px solid #2a323a;border-radius:20px;padding:18px}.metric strong{display:block;font-size:2.2rem;margin:.25rem 0}.pass,.healthy{color:#86efac}.fail,.failed,.degraded{color:#fca5a5}.surfaces{display:grid;gap:16px;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));margin:24px 0}table{width:100%;border-collapse:collapse;background:#181d22;border-radius:16px;overflow:hidden;margin:12px 0 28px}td,th{border-bottom:1px solid #2a323a;padding:12px;text-align:left}section{margin-top:30px}@media(max-width:700px){main{padding:20px}header{display:block}}</style></head><body><main><header><div><p class="eyebrow">Self-hosted voice operations</p><h1>${escapeHtml12(title)}</h1><p class="muted">One deployable control plane for quality gates, failover, traces, sessions, handoffs, and provider health.</p></div><p class="muted">Checked ${escapeHtml12(new Date(report.checkedAt).toLocaleString())}</p></header><div class="grid">${renderMetricCard({ label: "Quality", value: report.quality.status, status: report.quality.status, href: "/quality" })}${renderMetricCard({ label: "Events", value: report.eventCount, href: "/diagnostics" })}${renderMetricCard({ label: "Sessions", value: report.sessions.total, status: report.sessions.failed > 0 ? "failed" : "healthy", href: "/sessions" })}${renderMetricCard({ label: "Handoffs failed", value: report.handoffs.failed, status: report.handoffs.failed > 0 ? "failed" : "healthy", href: "/handoffs" })}${renderMetricCard({ label: "Providers degraded", value: report.providers.degraded, status: report.providers.degraded > 0 ? "degraded" : "healthy", href: "/resilience" })}</div><section><h2>Operational Surfaces</h2><div class="surfaces">${links}</div></section><section><h2>Recent Sessions</h2><table><thead><tr><th>Session</th><th>Status</th><th>Turns</th><th>Errors</th><th>Replay</th></tr></thead><tbody>${sessions}</tbody></table></section><section><h2>Recent Provider Routing</h2><table><thead><tr><th>Kind</th><th>Provider</th><th>Status</th><th>Elapsed</th><th>Session</th></tr></thead><tbody>${routing}</tbody></table></section></main></body></html>`;
9125
+ };
9126
+ var createVoiceOpsConsoleRoutes = (options) => {
9127
+ const path = options.path ?? "/ops-console";
9128
+ const routes = new Elysia10({
9129
+ name: options.name ?? "absolutejs-voice-ops-console"
9130
+ });
9131
+ const getReport = () => buildVoiceOpsConsoleReport(options);
9132
+ routes.get(path, async () => {
9133
+ const report = await getReport();
9134
+ return new Response(renderVoiceOpsConsoleHTML(report, { title: options.title }), {
9135
+ headers: {
9136
+ "Content-Type": "text/html; charset=utf-8",
9137
+ ...options.headers
9138
+ }
9139
+ });
9140
+ });
9141
+ routes.get(`${path}/json`, async () => getReport());
9142
+ return routes;
9143
+ };
9144
+
9145
+ // src/appKit.ts
9146
+ var DEFAULT_LINKS2 = [
9147
+ {
9148
+ description: "Integrated voice operations console.",
9149
+ href: "/ops-console",
9150
+ label: "Ops Console"
9151
+ },
9152
+ {
9153
+ description: "Production quality gates.",
9154
+ href: "/quality",
9155
+ label: "Quality",
9156
+ statusHref: "/quality/status"
9157
+ },
9158
+ {
9159
+ description: "Replay sessions against evals and workflow contracts.",
9160
+ href: "/evals",
9161
+ label: "Evals",
9162
+ statusHref: "/evals/status"
9163
+ },
9164
+ {
9165
+ description: "Provider routing, fallback, and resilience controls.",
9166
+ href: "/resilience",
9167
+ label: "Resilience"
9168
+ },
9169
+ {
9170
+ description: "Recent sessions and replay links.",
9171
+ href: "/sessions",
9172
+ label: "Sessions"
9173
+ },
9174
+ {
9175
+ description: "Handoff delivery health.",
9176
+ href: "/handoffs",
9177
+ label: "Handoffs"
9178
+ },
9179
+ {
9180
+ description: "Redacted traces and bug-report exports.",
9181
+ href: "/diagnostics",
9182
+ label: "Diagnostics"
9183
+ }
9184
+ ];
9185
+ var resolveLinks = (links) => links ?? DEFAULT_LINKS2;
9186
+ var toBasicLinks = (links) => links.map(({ href, label }) => ({ href, label }));
9187
+ var toOpsLinks = (links) => links.map((link) => ({
9188
+ description: link.description ?? link.label,
9189
+ href: link.href,
9190
+ label: link.label,
9191
+ statusHref: link.statusHref
9192
+ }));
9193
+ var toResilienceLinks = (links) => links.map(({ href, label }) => ({ href, label }));
9194
+ var createVoiceAppKitRoutes = (options) => {
9195
+ const routes = new Elysia11({
9196
+ name: options.name ?? "absolutejs-voice-app-kit"
9197
+ });
9198
+ const links = resolveLinks(options.links);
9199
+ const common = {
9200
+ headers: options.headers,
9201
+ store: options.store
9202
+ };
9203
+ const surfaces = [];
9204
+ if (options.providerHealth !== false) {
9205
+ surfaces.push("providerHealth");
9206
+ routes.use(createVoiceProviderHealthRoutes({
9207
+ ...common,
9208
+ providers: options.llmProviders,
9209
+ ...options.providerHealth
9210
+ }));
9211
+ }
9212
+ if (options.assistantHealth !== false) {
9213
+ surfaces.push("assistantHealth");
9214
+ routes.use(createVoiceAssistantHealthRoutes({
9215
+ ...common,
9216
+ providers: options.llmProviders,
9217
+ ...options.assistantHealth
9218
+ }));
9219
+ }
9220
+ if (options.quality !== false) {
9221
+ surfaces.push("quality");
9222
+ routes.use(createVoiceQualityRoutes({
9223
+ ...common,
9224
+ links: toBasicLinks(links),
9225
+ ...options.quality
9226
+ }));
9227
+ }
9228
+ if (options.evals !== false) {
9229
+ surfaces.push("evals");
9230
+ routes.use(createVoiceEvalRoutes({
9231
+ ...common,
9232
+ links: toBasicLinks(links),
9233
+ title: options.title ? `${options.title} Evals` : undefined,
9234
+ ...options.evals
9235
+ }));
9236
+ }
9237
+ if (options.sessions !== false) {
9238
+ surfaces.push("sessions");
9239
+ routes.use(createVoiceSessionListRoutes({
9240
+ ...common,
9241
+ htmlPath: "/sessions",
9242
+ path: "/api/voice-sessions",
9243
+ replayHref: "/sessions/:sessionId",
9244
+ ...options.sessions
9245
+ }));
9246
+ }
9247
+ if (options.sessionReplay !== false) {
9248
+ surfaces.push("sessionReplay");
9249
+ routes.use(createVoiceSessionReplayRoutes({
9250
+ ...common,
9251
+ htmlPath: "/sessions/:sessionId",
9252
+ path: "/api/voice-sessions/:sessionId/replay",
9253
+ ...options.sessionReplay
9254
+ }));
9255
+ }
9256
+ if (options.handoffs !== false) {
9257
+ surfaces.push("handoffs");
9258
+ routes.use(createVoiceHandoffHealthRoutes({
9259
+ ...common,
9260
+ htmlPath: "/handoffs",
9261
+ path: "/api/voice-handoffs",
9262
+ ...options.handoffs
9263
+ }));
9264
+ }
9265
+ if (options.diagnostics !== false) {
9266
+ surfaces.push("diagnostics");
9267
+ routes.use(createVoiceDiagnosticsRoutes({
9268
+ ...common,
9269
+ path: "/diagnostics",
9270
+ title: options.title ? `${options.title} Diagnostics` : undefined,
9271
+ ...options.diagnostics
9272
+ }));
9273
+ }
9274
+ if (options.resilience !== false) {
9275
+ surfaces.push("resilience");
9276
+ routes.use(createVoiceResilienceRoutes({
9277
+ ...common,
9278
+ links: toResilienceLinks(links),
9279
+ llmProviders: options.llmProviders,
9280
+ sttProviders: options.sttProviders,
9281
+ title: options.title ? `${options.title} Resilience` : undefined,
9282
+ ttsProviders: options.ttsProviders,
9283
+ ...options.resilience
9284
+ }));
9285
+ }
9286
+ if (options.opsConsole !== false) {
9287
+ surfaces.push("opsConsole");
9288
+ routes.use(createVoiceOpsConsoleRoutes({
9289
+ ...common,
9290
+ links: toOpsLinks(links),
9291
+ llmProviders: options.llmProviders,
9292
+ sttProviders: options.sttProviders,
9293
+ title: options.title,
9294
+ ttsProviders: options.ttsProviders,
9295
+ ...options.opsConsole
9296
+ }));
9297
+ }
9298
+ return {
9299
+ links,
9300
+ routes,
9301
+ surfaces,
9302
+ use: routes.use.bind(routes)
9303
+ };
9304
+ };
9305
+ var createVoiceAppKit = createVoiceAppKitRoutes;
9306
+ // src/workflowContract.ts
9307
+ var getObject2 = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : undefined;
9308
+ var getPathValue2 = (value, path) => {
9309
+ let current = value;
9310
+ for (const part of path.split(".").filter(Boolean)) {
9311
+ const record = getObject2(current);
9312
+ if (!record || !(part in record)) {
9313
+ return;
9314
+ }
9315
+ current = record[part];
9316
+ }
9317
+ return current;
9318
+ };
9319
+ var hasValue = (value, match) => {
9320
+ switch (match) {
9321
+ case "boolean":
9322
+ return typeof value === "boolean";
9323
+ case "number":
9324
+ return typeof value === "number" && Number.isFinite(value);
9325
+ case "string":
9326
+ return typeof value === "string";
9327
+ case "truthy":
9328
+ return Boolean(value);
9329
+ case "non-empty":
9330
+ default:
9331
+ return Array.isArray(value) ? value.length > 0 : typeof value === "string" ? value.trim().length > 0 : value !== undefined && value !== null;
9332
+ }
9333
+ };
9334
+ var resolveOutcome2 = (routeResult) => {
9335
+ if (routeResult.complete)
9336
+ return "complete";
9337
+ if (routeResult.transfer)
9338
+ return "transfer";
9339
+ if (routeResult.escalate)
9340
+ return "escalate";
9341
+ if (routeResult.voicemail)
9342
+ return "voicemail";
9343
+ if (routeResult.noAnswer)
9344
+ return "no-answer";
9345
+ return;
9346
+ };
9347
+ var validateVoiceWorkflowRouteResult = (definition, routeResult) => {
9348
+ const issues = [];
9349
+ const requiredFields = (definition.fields ?? []).filter((field) => field.required !== false).map((field) => field.path);
9350
+ const missingFields = [];
9351
+ const outcome = resolveOutcome2(routeResult);
9352
+ if (definition.outcome && outcome !== definition.outcome) {
9353
+ issues.push({
9354
+ code: "workflow.outcome_mismatch",
9355
+ message: `Expected workflow outcome ${definition.outcome}, saw ${outcome ?? "none"}.`
9356
+ });
9357
+ }
9358
+ for (const field of definition.fields ?? []) {
9359
+ if (field.required === false)
9360
+ continue;
9361
+ const paths = [field.path, ...field.aliases ?? []];
9362
+ const present = paths.some((path) => hasValue(getPathValue2(routeResult.result, path), field.match ?? "non-empty"));
9363
+ if (!present) {
9364
+ missingFields.push(field.path);
9365
+ issues.push({
9366
+ code: "workflow.missing_field",
9367
+ field: field.path,
9368
+ message: `Missing required workflow field: ${field.label ?? field.path}.`
9369
+ });
9370
+ }
9371
+ }
9372
+ issues.push(...definition.validate?.({
9373
+ result: routeResult.result,
9374
+ routeResult
9375
+ }) ?? []);
9376
+ return {
9377
+ contractId: definition.id,
9378
+ issues,
9379
+ missingFields,
9380
+ outcome,
9381
+ pass: issues.length === 0,
9382
+ requiredFields
9383
+ };
9384
+ };
9385
+ var createVoiceWorkflowScenario = (definition, overrides = {}) => ({
9386
+ description: definition.description,
9387
+ forbiddenHandoffActions: definition.forbiddenHandoffActions,
9388
+ id: definition.id,
9389
+ label: definition.label,
9390
+ maxProviderErrors: definition.maxProviderErrors,
9391
+ maxSessionErrors: definition.maxSessionErrors,
9392
+ minSessions: definition.minSessions,
9393
+ minTurns: definition.minTurns,
9394
+ requiredAssistantIncludes: definition.requiredAssistantIncludes,
9395
+ requiredDisposition: definition.requiredDisposition,
9396
+ requiredHandoffActions: definition.requiredHandoffActions,
9397
+ requiredLifecycleTypes: definition.requiredLifecycleTypes,
9398
+ requiredTranscriptIncludes: definition.requiredTranscriptIncludes,
9399
+ requiredWorkflowContracts: [definition.id],
9400
+ scenarioId: definition.scenarioId,
9401
+ ...overrides
9402
+ });
9403
+ var createVoiceWorkflowContract = (definition) => ({
9404
+ assertRouteResult: (routeResult) => {
9405
+ const validation = validateVoiceWorkflowRouteResult(definition, routeResult);
9406
+ if (!validation.pass) {
9407
+ throw new Error(`Voice workflow contract ${definition.id} failed: ${validation.issues.map((issue) => issue.message).join(" ")}`);
9408
+ }
9409
+ },
9410
+ definition,
9411
+ toScenarioEval: (overrides) => createVoiceWorkflowScenario(definition, overrides),
9412
+ validateRouteResult: (routeResult) => validateVoiceWorkflowRouteResult(definition, routeResult)
9413
+ });
9414
+ var presetDefinitions = {
9415
+ "appointment-booking": {
9416
+ description: "Appointment booking should complete with enough identity, appointment, and follow-up details to act on.",
9417
+ fields: [
9418
+ { aliases: ["name", "customer.name"], label: "Caller name", path: "caller.name" },
9419
+ {
9420
+ aliases: ["phone", "customer.phone"],
9421
+ label: "Caller phone",
9422
+ path: "caller.phone"
9423
+ },
9424
+ {
9425
+ aliases: ["appointment.start", "appointment.time", "scheduledAt"],
9426
+ label: "Appointment time",
9427
+ path: "appointment.startsAt"
9428
+ },
9429
+ {
9430
+ aliases: ["summary", "assistantSummary"],
9431
+ label: "Summary",
9432
+ path: "appointment.summary"
9433
+ }
9434
+ ],
9435
+ id: "appointment-booking",
9436
+ label: "Appointment booking",
9437
+ outcome: "complete",
9438
+ requiredDisposition: "completed"
9439
+ },
9440
+ "lead-qualification": {
9441
+ description: "Lead qualification should complete with contact, need, qualification, and next-step fields.",
9442
+ fields: [
9443
+ { aliases: ["name", "lead.name"], label: "Lead name", path: "contact.name" },
9444
+ {
9445
+ aliases: ["email", "lead.email"],
9446
+ label: "Lead email",
9447
+ path: "contact.email"
9448
+ },
9449
+ {
9450
+ aliases: ["need", "pain", "summary"],
9451
+ label: "Need",
9452
+ path: "qualification.need"
9453
+ },
9454
+ {
9455
+ aliases: ["qualified", "qualification.qualified"],
9456
+ label: "Qualified",
9457
+ match: "boolean",
9458
+ path: "qualification.isQualified"
8610
9459
  },
8611
9460
  {
8612
9461
  aliases: ["nextStep", "followUp"],
@@ -8668,327 +9517,85 @@ var presetDefinitions = {
8668
9517
  }
8669
9518
  ],
8670
9519
  id: "transfer-handoff",
8671
- label: "Transfer handoff",
8672
- outcome: "transfer",
8673
- requiredDisposition: "transferred",
8674
- requiredHandoffActions: ["transfer"]
8675
- },
8676
- "voicemail-callback": {
8677
- description: "Voicemail callback should preserve enough caller and callback context for follow-up.",
8678
- fields: [
8679
- {
8680
- aliases: ["name", "caller.name"],
8681
- label: "Caller name",
8682
- path: "voicemail.callerName"
8683
- },
8684
- {
8685
- aliases: ["phone", "caller.phone"],
8686
- label: "Callback phone",
8687
- path: "voicemail.callbackPhone"
8688
- },
8689
- {
8690
- aliases: ["message", "summary", "assistantSummary"],
8691
- label: "Voicemail summary",
8692
- path: "voicemail.summary"
8693
- }
8694
- ],
8695
- id: "voicemail-callback",
8696
- label: "Voicemail callback",
8697
- outcome: "voicemail",
8698
- requiredDisposition: "voicemail",
8699
- requiredHandoffActions: ["voicemail"]
8700
- }
8701
- };
8702
- var createVoiceWorkflowContractPreset = (name, options = {}) => {
8703
- const preset = presetDefinitions[name];
8704
- return createVoiceWorkflowContract({
8705
- ...preset,
8706
- ...options,
8707
- fields: options.fields ?? preset.fields,
8708
- id: options.id ?? preset.id
8709
- });
8710
- };
8711
- var recordVoiceWorkflowContractTrace = async (input) => input.store.append({
8712
- at: input.at ?? Date.now(),
8713
- payload: {
8714
- contractId: input.contractId ?? input.validation.contractId,
8715
- issues: input.validation.issues,
8716
- missingFields: input.validation.missingFields,
8717
- outcome: input.validation.outcome,
8718
- requiredFields: input.validation.requiredFields,
8719
- status: input.validation.pass ? "pass" : "fail"
8720
- },
8721
- scenarioId: input.scenarioId,
8722
- sessionId: input.sessionId,
8723
- traceId: input.traceId,
8724
- turnId: input.turnId,
8725
- type: "workflow.contract"
8726
- });
8727
- var createVoiceWorkflowContractHandler = (input) => {
8728
- return async (session, turn, api, context) => {
8729
- const legacyHandler = input.handler;
8730
- const objectHandler = input.handler;
8731
- const result = input.handler.length >= 4 ? await legacyHandler(session, turn, api, context) : await objectHandler({ api, context, session, turn });
8732
- if (!result)
8733
- return result;
8734
- const resolved = input.resolveContract?.({ context, result, session, turn }) ?? input.contract;
8735
- if (!resolved)
8736
- return result;
8737
- const contract = "validateRouteResult" in resolved ? resolved : createVoiceWorkflowContract(resolved);
8738
- const validation = contract.validateRouteResult(result);
8739
- if (input.store) {
8740
- await recordVoiceWorkflowContractTrace({
8741
- scenarioId: session.scenarioId,
8742
- sessionId: session.id,
8743
- store: input.store,
8744
- turnId: turn.id,
8745
- validation
8746
- });
8747
- }
8748
- return result;
8749
- };
8750
- };
8751
- // src/sessionReplay.ts
8752
- import { Elysia as Elysia8 } from "elysia";
8753
- var getString7 = (value) => typeof value === "string" ? value : undefined;
8754
- var escapeHtml10 = (value) => value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
8755
- var increment3 = (record, key) => {
8756
- record[key] = (record[key] ?? 0) + 1;
8757
- };
8758
- var buildReplayTurns = (events) => {
8759
- const turns = new Map;
8760
- const getTurn = (turnId) => {
8761
- const existing = turns.get(turnId);
8762
- if (existing) {
8763
- return existing;
8764
- }
8765
- const turn = {
8766
- assistantReplies: [],
8767
- errors: [],
8768
- id: turnId,
8769
- modelCalls: [],
8770
- tools: [],
8771
- transcripts: []
8772
- };
8773
- turns.set(turnId, turn);
8774
- return turn;
8775
- };
8776
- for (const event of events) {
8777
- const turnId = event.turnId ?? "session";
8778
- const turn = getTurn(turnId);
8779
- switch (event.type) {
8780
- case "turn.transcript":
8781
- turn.transcripts.push({
8782
- isFinal: event.payload.isFinal === true,
8783
- text: getString7(event.payload.text)
8784
- });
8785
- break;
8786
- case "turn.committed":
8787
- turn.committedText = getString7(event.payload.text);
8788
- break;
8789
- case "turn.assistant": {
8790
- const text = getString7(event.payload.text);
8791
- if (text) {
8792
- turn.assistantReplies.push(text);
8793
- }
8794
- break;
8795
- }
8796
- case "agent.model":
8797
- case "assistant.run":
8798
- turn.modelCalls.push(event.payload);
8799
- break;
8800
- case "agent.tool":
8801
- turn.tools.push(event.payload);
8802
- break;
8803
- case "session.error":
8804
- turn.errors.push(event.payload);
8805
- break;
8806
- }
8807
- }
8808
- return [...turns.values()];
8809
- };
8810
- var summarizeVoiceSessionReplay = async (options) => {
8811
- const sourceEvents = options.events ?? await options.store?.list({ sessionId: options.sessionId }) ?? [];
8812
- const events = filterVoiceTraceEvents(sourceEvents, {
8813
- sessionId: options.sessionId
8814
- });
8815
- const replay = buildVoiceTraceReplay(events, {
8816
- evaluation: options.evaluation,
8817
- redact: options.redact,
8818
- title: options.title ?? `Voice Session ${options.sessionId}`
8819
- });
8820
- const startedAt = replay.summary.startedAt;
8821
- return {
8822
- evaluation: replay.evaluation,
8823
- events,
8824
- html: replay.html,
8825
- markdown: replay.markdown,
8826
- sessionId: options.sessionId,
8827
- summary: replay.summary,
8828
- timeline: events.map((event) => ({
8829
- at: event.at,
8830
- offsetMs: startedAt === undefined ? undefined : Math.max(0, event.at - startedAt),
8831
- payload: event.payload,
8832
- turnId: event.turnId,
8833
- type: event.type
8834
- })),
8835
- turns: buildReplayTurns(events)
8836
- };
8837
- };
8838
- var summarizeVoiceSessions = async (options = {}) => {
8839
- const events = options.events ?? await options.store?.list() ?? [];
8840
- const grouped = new Map;
8841
- for (const event of events) {
8842
- grouped.set(event.sessionId, [...grouped.get(event.sessionId) ?? [], event]);
8843
- }
8844
- const sessions = [...grouped.entries()].map(([sessionId, sessionEvents]) => {
8845
- const sorted = filterVoiceTraceEvents(sessionEvents);
8846
- const summary = buildVoiceTraceReplay(sorted, {
8847
- evaluation: {
8848
- requireAssistantReply: false,
8849
- requireCompletedCall: false,
8850
- requireTranscript: false,
8851
- requireTurn: false
8852
- }
8853
- }).summary;
8854
- const providerErrors = {};
8855
- const providers = new Set;
8856
- let latestOutcome;
8857
- let errorCount = 0;
8858
- for (const event of sorted) {
8859
- const provider = getString7(event.payload.provider);
8860
- if (provider) {
8861
- providers.add(provider);
8862
- }
8863
- if (event.type === "session.error" && (event.payload.providerStatus === "error" || typeof event.payload.error === "string")) {
8864
- errorCount += 1;
8865
- increment3(providerErrors, provider ?? "unknown");
8866
- }
8867
- const outcome = getString7(event.payload.outcome);
8868
- if (outcome) {
8869
- latestOutcome = outcome;
8870
- }
8871
- }
8872
- const item = {
8873
- endedAt: summary.endedAt,
8874
- errorCount,
8875
- eventCount: summary.eventCount,
8876
- latestOutcome,
8877
- providerErrors,
8878
- providers: [...providers].sort(),
8879
- sessionId,
8880
- startedAt: summary.startedAt,
8881
- status: errorCount > 0 ? "failed" : "healthy",
8882
- transcriptCount: summary.transcriptCount,
8883
- turnCount: summary.turnCount
8884
- };
8885
- const replayHref = options.replayHref === false ? "" : typeof options.replayHref === "function" ? options.replayHref(item) : `${options.replayHref ?? "/api/voice-sessions"}/${encodeURIComponent(sessionId)}/replay/htmx`;
8886
- return {
8887
- ...item,
8888
- replayHref
8889
- };
8890
- });
8891
- const search = options.q?.trim().toLowerCase();
8892
- return sessions.filter((session) => {
8893
- if (options.status && options.status !== "all" && session.status !== options.status) {
8894
- return false;
8895
- }
8896
- if (options.provider && !session.providers.includes(options.provider)) {
8897
- return false;
8898
- }
8899
- if (!search) {
8900
- return true;
8901
- }
8902
- return [
8903
- session.sessionId,
8904
- session.latestOutcome,
8905
- session.status,
8906
- ...session.providers
8907
- ].some((value) => value?.toLowerCase().includes(search));
8908
- }).sort((left, right) => (right.endedAt ?? right.startedAt ?? 0) - (left.endedAt ?? left.startedAt ?? 0)).slice(0, options.limit ?? 50);
9520
+ label: "Transfer handoff",
9521
+ outcome: "transfer",
9522
+ requiredDisposition: "transferred",
9523
+ requiredHandoffActions: ["transfer"]
9524
+ },
9525
+ "voicemail-callback": {
9526
+ description: "Voicemail callback should preserve enough caller and callback context for follow-up.",
9527
+ fields: [
9528
+ {
9529
+ aliases: ["name", "caller.name"],
9530
+ label: "Caller name",
9531
+ path: "voicemail.callerName"
9532
+ },
9533
+ {
9534
+ aliases: ["phone", "caller.phone"],
9535
+ label: "Callback phone",
9536
+ path: "voicemail.callbackPhone"
9537
+ },
9538
+ {
9539
+ aliases: ["message", "summary", "assistantSummary"],
9540
+ label: "Voicemail summary",
9541
+ path: "voicemail.summary"
9542
+ }
9543
+ ],
9544
+ id: "voicemail-callback",
9545
+ label: "Voicemail callback",
9546
+ outcome: "voicemail",
9547
+ requiredDisposition: "voicemail",
9548
+ requiredHandoffActions: ["voicemail"]
9549
+ }
8909
9550
  };
8910
- var renderVoiceSessionsHTML = (sessions) => sessions.length === 0 ? '<p class="voice-sessions-empty">No voice sessions found.</p>' : [
8911
- '<div class="voice-sessions-list">',
8912
- ...sessions.map((session) => [
8913
- `<article class="voice-session-card ${escapeHtml10(session.status)}">`,
8914
- '<div class="voice-session-card-header">',
8915
- `<strong>${escapeHtml10(session.sessionId)}</strong>`,
8916
- `<span>${escapeHtml10(session.status)}</span>`,
8917
- "</div>",
8918
- "<dl>",
8919
- `<div><dt>Events</dt><dd>${String(session.eventCount)}</dd></div>`,
8920
- `<div><dt>Turns</dt><dd>${String(session.turnCount)}</dd></div>`,
8921
- `<div><dt>Transcripts</dt><dd>${String(session.transcriptCount)}</dd></div>`,
8922
- `<div><dt>Errors</dt><dd>${String(session.errorCount)}</dd></div>`,
8923
- "</dl>",
8924
- session.latestOutcome ? `<p>Outcome: ${escapeHtml10(session.latestOutcome)}</p>` : "",
8925
- session.providers.length ? `<p>Providers: ${session.providers.map(escapeHtml10).join(", ")}</p>` : "",
8926
- session.replayHref ? `<p><a href="${escapeHtml10(session.replayHref)}">Open replay</a></p>` : "",
8927
- "</article>"
8928
- ].join("")),
8929
- "</div>"
8930
- ].join("");
8931
- var createVoiceSessionsJSONHandler = (options = {}) => async ({ query }) => summarizeVoiceSessions({
8932
- ...options,
8933
- limit: typeof query?.limit === "string" ? Number(query.limit) : options.limit,
8934
- provider: query?.provider ?? options.provider,
8935
- q: query?.q ?? options.q,
8936
- status: query?.status === "failed" || query?.status === "healthy" || query?.status === "all" ? query.status : options.status
8937
- });
8938
- var createVoiceSessionsHTMLHandler = (options = {}) => async ({ query }) => {
8939
- const sessions = await summarizeVoiceSessions({
9551
+ var createVoiceWorkflowContractPreset = (name, options = {}) => {
9552
+ const preset = presetDefinitions[name];
9553
+ return createVoiceWorkflowContract({
9554
+ ...preset,
8940
9555
  ...options,
8941
- limit: typeof query?.limit === "string" ? Number(query.limit) : options.limit,
8942
- provider: query?.provider ?? options.provider,
8943
- q: query?.q ?? options.q,
8944
- status: query?.status === "failed" || query?.status === "healthy" || query?.status === "all" ? query.status : options.status
8945
- });
8946
- const body = await (options.render?.(sessions) ?? renderVoiceSessionsHTML(sessions));
8947
- return new Response(body, {
8948
- headers: {
8949
- "Content-Type": "text/html; charset=utf-8",
8950
- ...options.headers
8951
- }
9556
+ fields: options.fields ?? preset.fields,
9557
+ id: options.id ?? preset.id
8952
9558
  });
8953
9559
  };
8954
- var createVoiceSessionListRoutes = (options = {}) => {
8955
- const path = options.path ?? "/api/voice-sessions";
8956
- const htmlPath = options.htmlPath === undefined ? `${path}/htmx` : options.htmlPath;
8957
- const routes = new Elysia8({
8958
- name: options.name ?? "absolutejs-voice-session-list"
8959
- }).get(path, createVoiceSessionsJSONHandler(options));
8960
- if (htmlPath) {
8961
- routes.get(htmlPath, createVoiceSessionsHTMLHandler(options));
8962
- }
8963
- return routes;
8964
- };
8965
- var createVoiceSessionReplayJSONHandler = (options) => async ({ params }) => summarizeVoiceSessionReplay({
8966
- ...options,
8967
- sessionId: params.sessionId ?? ""
9560
+ var recordVoiceWorkflowContractTrace = async (input) => input.store.append({
9561
+ at: input.at ?? Date.now(),
9562
+ payload: {
9563
+ contractId: input.contractId ?? input.validation.contractId,
9564
+ issues: input.validation.issues,
9565
+ missingFields: input.validation.missingFields,
9566
+ outcome: input.validation.outcome,
9567
+ requiredFields: input.validation.requiredFields,
9568
+ status: input.validation.pass ? "pass" : "fail"
9569
+ },
9570
+ scenarioId: input.scenarioId,
9571
+ sessionId: input.sessionId,
9572
+ traceId: input.traceId,
9573
+ turnId: input.turnId,
9574
+ type: "workflow.contract"
8968
9575
  });
8969
- var createVoiceSessionReplayHTMLHandler = (options) => async ({ params }) => {
8970
- const replay = await summarizeVoiceSessionReplay({
8971
- ...options,
8972
- sessionId: params.sessionId ?? ""
8973
- });
8974
- const body = await (options.render?.(replay) ?? replay.html);
8975
- return new Response(body, {
8976
- headers: {
8977
- "Content-Type": "text/html; charset=utf-8",
8978
- ...options.headers
9576
+ var createVoiceWorkflowContractHandler = (input) => {
9577
+ return async (session, turn, api, context) => {
9578
+ const legacyHandler = input.handler;
9579
+ const objectHandler = input.handler;
9580
+ const result = input.handler.length >= 4 ? await legacyHandler(session, turn, api, context) : await objectHandler({ api, context, session, turn });
9581
+ if (!result)
9582
+ return result;
9583
+ const resolved = input.resolveContract?.({ context, result, session, turn }) ?? input.contract;
9584
+ if (!resolved)
9585
+ return result;
9586
+ const contract = "validateRouteResult" in resolved ? resolved : createVoiceWorkflowContract(resolved);
9587
+ const validation = contract.validateRouteResult(result);
9588
+ if (input.store) {
9589
+ await recordVoiceWorkflowContractTrace({
9590
+ scenarioId: session.scenarioId,
9591
+ sessionId: session.id,
9592
+ store: input.store,
9593
+ turnId: turn.id,
9594
+ validation
9595
+ });
8979
9596
  }
8980
- });
8981
- };
8982
- var createVoiceSessionReplayRoutes = (options) => {
8983
- const path = options.path ?? "/api/voice-sessions/:sessionId/replay";
8984
- const htmlPath = options.htmlPath === undefined ? `${path}/htmx` : options.htmlPath;
8985
- const routes = new Elysia8({
8986
- name: options.name ?? "absolutejs-voice-session-replay"
8987
- }).get(path, createVoiceSessionReplayJSONHandler(options));
8988
- if (htmlPath) {
8989
- routes.get(htmlPath, createVoiceSessionReplayHTMLHandler(options));
8990
- }
8991
- return routes;
9597
+ return result;
9598
+ };
8992
9599
  };
8993
9600
  // src/fileStore.ts
8994
9601
  import { mkdir as mkdir2, readFile, readdir, rename, rm, writeFile } from "fs/promises";
@@ -9989,571 +10596,134 @@ var createAnthropicVoiceAssistantModel = (options) => {
9989
10596
  method: "POST"
9990
10597
  });
9991
10598
  if (!response.ok) {
9992
- throw createHTTPError("Anthropic", response);
9993
- }
9994
- const body = await response.json();
9995
- if (body.usage && typeof body.usage === "object") {
9996
- await options.onUsage?.(body.usage);
9997
- }
9998
- const toolCalls = extractAnthropicToolCalls(body);
9999
- if (toolCalls.length) {
10000
- return {
10001
- assistantText: extractAnthropicText(body) || undefined,
10002
- toolCalls
10003
- };
10004
- }
10005
- return normalizeRouteOutput(parseJSON(extractAnthropicText(body)));
10006
- }
10007
- };
10008
- };
10009
- var extractGeminiCandidateParts = (response) => {
10010
- const candidates = Array.isArray(response.candidates) ? response.candidates : [];
10011
- const first = candidates[0];
10012
- if (!first || typeof first !== "object") {
10013
- return [];
10014
- }
10015
- const content = first.content;
10016
- if (!content || typeof content !== "object") {
10017
- return [];
10018
- }
10019
- const parts = content.parts;
10020
- return Array.isArray(parts) ? parts : [];
10021
- };
10022
- var extractGeminiText = (response) => extractGeminiCandidateParts(response).map((part) => part && typeof part === "object" && typeof part.text === "string" ? part.text : "").filter(Boolean).join(`
10023
- `);
10024
- var extractGeminiToolCalls = (response) => {
10025
- const toolCalls = [];
10026
- for (const part of extractGeminiCandidateParts(response)) {
10027
- if (!part || typeof part !== "object") {
10028
- continue;
10029
- }
10030
- const functionCall = part.functionCall;
10031
- if (!functionCall || typeof functionCall !== "object") {
10032
- continue;
10033
- }
10034
- const record = functionCall;
10035
- if (typeof record.name !== "string") {
10036
- continue;
10037
- }
10038
- toolCalls.push({
10039
- args: record.args && typeof record.args === "object" ? record.args : {},
10040
- id: typeof record.id === "string" ? record.id : undefined,
10041
- name: record.name
10042
- });
10043
- }
10044
- return toolCalls;
10045
- };
10046
- var createGeminiVoiceAssistantModel = (options) => {
10047
- const fetchImpl = options.fetch ?? globalThis.fetch;
10048
- const baseUrl = options.baseUrl ?? "https://generativelanguage.googleapis.com/v1beta";
10049
- const model = options.model ?? "gemini-2.5-flash";
10050
- const maxRetries = Math.max(0, options.maxRetries ?? 2);
10051
- return {
10052
- generate: async (input) => {
10053
- const endpoint = `${baseUrl.replace(/\/$/, "")}/models/${encodeURIComponent(model)}:generateContent?key=${encodeURIComponent(options.apiKey)}`;
10054
- let response;
10055
- for (let attempt = 0;attempt <= maxRetries; attempt += 1) {
10056
- response = await fetchImpl(endpoint, {
10057
- body: JSON.stringify({
10058
- contents: input.messages.map(messageToGeminiContent).filter(Boolean),
10059
- generationConfig: {
10060
- maxOutputTokens: options.maxOutputTokens,
10061
- ...input.tools.length ? {} : {
10062
- responseMimeType: "application/json",
10063
- responseSchema: toGeminiSchema(OUTPUT_SCHEMA)
10064
- },
10065
- temperature: options.temperature
10066
- },
10067
- systemInstruction: {
10068
- parts: [
10069
- {
10070
- text: [input.system, ROUTE_RESULT_INSTRUCTION].filter(Boolean).join(`
10071
-
10072
- `)
10073
- }
10074
- ]
10075
- },
10076
- tools: input.tools.length ? [
10077
- {
10078
- functionDeclarations: input.tools.map((tool) => ({
10079
- description: tool.description,
10080
- name: tool.name,
10081
- parameters: toGeminiSchema(tool.parameters ?? {
10082
- additionalProperties: true,
10083
- type: "object"
10084
- })
10085
- }))
10086
- }
10087
- ] : undefined
10088
- }),
10089
- headers: {
10090
- "content-type": "application/json"
10091
- },
10092
- method: "POST"
10093
- });
10094
- if (response.ok || response.status !== 429 && response.status < 500 || attempt === maxRetries) {
10095
- break;
10096
- }
10097
- const retryAfter = Number(response.headers.get("retry-after"));
10098
- await sleep4(Number.isFinite(retryAfter) && retryAfter > 0 ? retryAfter * 1000 : 500 * 2 ** attempt);
10099
- }
10100
- if (!response) {
10101
- throw new Error("Gemini voice assistant model failed: no response");
10102
- }
10103
- if (!response.ok) {
10104
- throw createHTTPError("Gemini", response);
10599
+ throw createHTTPError("Anthropic", response);
10105
10600
  }
10106
10601
  const body = await response.json();
10107
- if (body.usageMetadata && typeof body.usageMetadata === "object") {
10108
- await options.onUsage?.(body.usageMetadata);
10602
+ if (body.usage && typeof body.usage === "object") {
10603
+ await options.onUsage?.(body.usage);
10109
10604
  }
10110
- const toolCalls = extractGeminiToolCalls(body);
10605
+ const toolCalls = extractAnthropicToolCalls(body);
10111
10606
  if (toolCalls.length) {
10112
10607
  return {
10113
- assistantText: extractGeminiText(body) || undefined,
10608
+ assistantText: extractAnthropicText(body) || undefined,
10114
10609
  toolCalls
10115
10610
  };
10116
10611
  }
10117
- return normalizeRouteOutput(parseJSON(extractGeminiText(body)));
10118
- }
10119
- };
10120
- };
10121
- // src/opsConsoleRoutes.ts
10122
- import { Elysia as Elysia10 } from "elysia";
10123
-
10124
- // src/resilienceRoutes.ts
10125
- import { Elysia as Elysia9 } from "elysia";
10126
- var escapeHtml11 = (value) => value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
10127
- var getString8 = (value) => typeof value === "string" ? value : undefined;
10128
- var getNumber4 = (value) => typeof value === "number" && Number.isFinite(value) ? value : undefined;
10129
- var getBoolean2 = (value) => value === true;
10130
- var isProviderStatus2 = (value) => value === "error" || value === "fallback" || value === "success";
10131
- var listVoiceRoutingEvents = (events) => {
10132
- const routingEvents = [];
10133
- for (const event of events) {
10134
- if (event.type !== "session.error") {
10135
- continue;
10136
- }
10137
- const provider = getString8(event.payload.provider);
10138
- const providerStatus = isProviderStatus2(event.payload.providerStatus) ? event.payload.providerStatus : undefined;
10139
- if (!provider || !providerStatus) {
10140
- continue;
10141
- }
10142
- const kind = getString8(event.payload.kind);
10143
- routingEvents.push({
10144
- at: event.at,
10145
- attempt: getNumber4(event.payload.attempt),
10146
- elapsedMs: getNumber4(event.payload.elapsedMs),
10147
- error: getString8(event.payload.error),
10148
- fallbackProvider: getString8(event.payload.fallbackProvider),
10149
- kind: kind === "stt" || kind === "tts" ? kind : "llm",
10150
- latencyBudgetMs: getNumber4(event.payload.latencyBudgetMs),
10151
- operation: getString8(event.payload.operation),
10152
- provider,
10153
- selectedProvider: getString8(event.payload.selectedProvider),
10154
- sessionId: event.sessionId,
10155
- status: providerStatus,
10156
- timedOut: getBoolean2(event.payload.timedOut),
10157
- turnId: event.turnId
10158
- });
10159
- }
10160
- return routingEvents.sort((left, right) => right.at - left.at);
10161
- };
10162
- var summarizeRoutingEvents = (events) => {
10163
- const byKind = new Map;
10164
- let errors = 0;
10165
- let fallbacks = 0;
10166
- let timeouts = 0;
10167
- for (const event of events) {
10168
- byKind.set(event.kind, (byKind.get(event.kind) ?? 0) + 1);
10169
- if (event.status === "error") {
10170
- errors += 1;
10171
- }
10172
- if (event.status === "fallback") {
10173
- fallbacks += 1;
10174
- }
10175
- if (event.timedOut) {
10176
- timeouts += 1;
10612
+ return normalizeRouteOutput(parseJSON(extractAnthropicText(body)));
10177
10613
  }
10178
- }
10179
- return {
10180
- byKind,
10181
- errors,
10182
- fallbacks,
10183
- timeouts,
10184
- total: events.length
10185
10614
  };
10186
10615
  };
10187
- var renderProviderCards = (title, providers) => {
10188
- if (providers.length === 0) {
10189
- return `<p class="muted">No ${escapeHtml11(title)} provider health yet.</p>`;
10190
- }
10191
- return `<div class="provider-grid">${providers.map((provider) => `
10192
- <article class="card provider ${escapeHtml11(provider.status)}">
10193
- <div class="card-header">
10194
- <strong>${escapeHtml11(provider.provider)}</strong>
10195
- <span>${escapeHtml11(provider.status)}${provider.recommended ? " \xB7 recommended" : ""}</span>
10196
- </div>
10197
- <dl>
10198
- <div><dt>Runs</dt><dd>${provider.runCount}</dd></div>
10199
- <div><dt>Avg latency</dt><dd>${provider.averageElapsedMs ?? 0}ms</dd></div>
10200
- <div><dt>Errors</dt><dd>${provider.errorCount}</dd></div>
10201
- <div><dt>Timeouts</dt><dd>${provider.timeoutCount}</dd></div>
10202
- <div><dt>Fallbacks</dt><dd>${provider.fallbackCount}</dd></div>
10203
- </dl>
10204
- ${provider.lastError ? `<p class="muted">${escapeHtml11(provider.lastError)}</p>` : ""}
10205
- </article>
10206
- `).join("")}</div>`;
10207
- };
10208
- var renderTimeline2 = (events) => {
10209
- if (events.length === 0) {
10210
- return '<p class="muted">No provider routing events yet. Run the app or simulate provider failover.</p>';
10211
- }
10212
- return `<div class="timeline">${events.slice(0, 40).map((event) => `
10213
- <article class="card event ${escapeHtml11(event.status ?? "unknown")}">
10214
- <div class="card-header">
10215
- <strong>${escapeHtml11(event.kind.toUpperCase())} ${escapeHtml11(event.operation ?? "generate")}</strong>
10216
- <span>${new Date(event.at).toLocaleString()}</span>
10217
- </div>
10218
- <p>
10219
- <span class="pill">${escapeHtml11(event.status ?? "unknown")}</span>
10220
- <span class="pill">provider: ${escapeHtml11(event.provider ?? "unknown")}</span>
10221
- ${event.fallbackProvider ? `<span class="pill">fallback: ${escapeHtml11(event.fallbackProvider)}</span>` : ""}
10222
- ${event.timedOut ? '<span class="pill danger">timed out</span>' : ""}
10223
- </p>
10224
- <dl>
10225
- <div><dt>Attempt</dt><dd>${event.attempt ?? 0}</dd></div>
10226
- <div><dt>Elapsed</dt><dd>${event.elapsedMs ?? 0}ms</dd></div>
10227
- <div><dt>Budget</dt><dd>${event.latencyBudgetMs ?? 0}ms</dd></div>
10228
- <div><dt>Session</dt><dd>${escapeHtml11(event.sessionId)}</dd></div>
10229
- </dl>
10230
- ${event.error ? `<p class="muted">${escapeHtml11(event.error)}</p>` : ""}
10231
- </article>
10232
- `).join("")}</div>`;
10233
- };
10234
- var renderSimulationControls = (kind, simulation) => {
10235
- if (!simulation) {
10236
- return "";
10616
+ var extractGeminiCandidateParts = (response) => {
10617
+ const candidates = Array.isArray(response.candidates) ? response.candidates : [];
10618
+ const first = candidates[0];
10619
+ if (!first || typeof first !== "object") {
10620
+ return [];
10237
10621
  }
10238
- const configuredProviders = simulation.providers.filter((provider) => provider.configured !== false);
10239
- if (configuredProviders.length === 0) {
10240
- return `<p class="muted">No ${kind.toUpperCase()} providers are configured for simulation.</p>`;
10622
+ const content = first.content;
10623
+ if (!content || typeof content !== "object") {
10624
+ return [];
10241
10625
  }
10242
- const pathPrefix = simulation.pathPrefix ?? `/api/${kind}-simulate`;
10243
- const failureProviders = simulation.failureProviders ?? configuredProviders.map(({ provider }) => provider);
10244
- const canFail = (provider) => configuredProviders.some((entry) => entry.provider === provider) && (!simulation.fallbackRequiredProvider || configuredProviders.some((entry) => entry.provider === simulation.fallbackRequiredProvider));
10245
- return `<div class="simulate-panel" data-sim-kind="${kind}" data-sim-prefix="${escapeHtml11(pathPrefix)}">
10246
- <p class="muted">${escapeHtml11(simulation.failureMessage ?? `Simulate ${kind.toUpperCase()} provider failure without changing provider credentials.`)}</p>
10247
- <div class="simulate-actions">
10248
- ${failureProviders.map((provider) => `<button type="button" data-provider-fail="${escapeHtml11(provider)}"${canFail(provider) ? "" : " disabled"}>Simulate ${escapeHtml11(provider)} ${kind.toUpperCase()} failure</button>`).join("")}
10249
- ${configuredProviders.map((provider) => `<button type="button" data-provider-recover="${escapeHtml11(provider.provider)}">Mark ${escapeHtml11(provider.provider)} recovered</button>`).join("")}
10250
- </div>
10251
- ${simulation.fallbackRequiredProvider && !configuredProviders.some((entry) => entry.provider === simulation.fallbackRequiredProvider) ? `<p class="muted">${escapeHtml11(simulation.fallbackRequiredMessage ?? `Configure ${simulation.fallbackRequiredProvider} to enable fallback simulation.`)}</p>` : ""}
10252
- <pre class="simulate-output" hidden></pre>
10253
- </div>`;
10254
- };
10255
- var renderVoiceResilienceHTML = (input) => {
10256
- const summary = summarizeRoutingEvents(input.routingEvents);
10257
- const kindCounts = [...summary.byKind.entries()].map(([kind, count]) => `<span class="pill">${escapeHtml11(kind)}: ${String(count)}</span>`).join("");
10258
- const links = input.links?.length ? input.links.map((link) => `<a href="${escapeHtml11(link.href)}">${escapeHtml11(link.label)}</a>`).join(" \xB7 ") : "";
10259
- return `<!doctype html>
10260
- <html lang="en">
10261
- <head>
10262
- <meta charset="utf-8" />
10263
- <meta name="viewport" content="width=device-width, initial-scale=1" />
10264
- <title>${escapeHtml11(input.title ?? "AbsoluteJS Voice Resilience")}</title>
10265
- <style>
10266
- :root { color-scheme: dark; }
10267
- body { background: radial-gradient(circle at top left, #172554, #09090b 36%, #050505); color: #f4f4f5; font-family: ui-sans-serif, system-ui, sans-serif; margin: 0; padding: 24px; }
10268
- main { display: grid; gap: 16px; margin: 0 auto; max-width: 1180px; }
10269
- section, .card { background: rgba(19, 22, 27, 0.92); border: 1px solid #27272a; border-radius: 20px; padding: 20px; }
10270
- .hero { background: linear-gradient(135deg, rgba(14, 165, 233, 0.18), rgba(245, 158, 11, 0.12)); }
10271
- .grid, .provider-grid { display: grid; gap: 14px; grid-template-columns: repeat(4, minmax(0, 1fr)); }
10272
- .timeline { display: grid; gap: 12px; }
10273
- .card-header { align-items: center; display: flex; gap: 12px; justify-content: space-between; }
10274
- .card-header strong { font-size: 1.05rem; }
10275
- .metric strong { display: block; font-size: 2rem; margin-top: 6px; }
10276
- .muted, dt, span { color: #a1a1aa; }
10277
- dl { display: grid; gap: 8px; grid-template-columns: repeat(4, minmax(0, 1fr)); }
10278
- dl div { background: #0f1217; border: 1px solid #27272a; border-radius: 12px; padding: 10px; }
10279
- dd { font-weight: 800; margin: 4px 0 0; }
10280
- .pill { background: #0f1217; border: 1px solid #3f3f46; border-radius: 999px; color: #d4d4d8; display: inline-flex; margin: 3px 4px 3px 0; padding: 5px 9px; }
10281
- .danger { border-color: rgba(239, 68, 68, 0.75); color: #fecaca; }
10282
- .event.error { border-color: rgba(239, 68, 68, 0.7); }
10283
- .event.fallback { border-color: rgba(245, 158, 11, 0.7); }
10284
- .event.success, .provider.healthy { border-color: rgba(34, 197, 94, 0.5); }
10285
- .provider.suppressed, .provider.degraded, .provider.rate-limited { border-color: rgba(239, 68, 68, 0.7); }
10286
- .provider.recoverable { border-color: rgba(59, 130, 246, 0.7); }
10287
- button { background: #f59e0b; border: 0; border-radius: 999px; color: #111827; cursor: pointer; font-weight: 800; padding: 10px 14px; }
10288
- button:disabled { cursor: not-allowed; opacity: 0.45; }
10289
- .simulate-actions { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 12px; }
10290
- .simulate-output { background: #050505; border: 1px solid #27272a; border-radius: 14px; color: #d4d4d8; overflow: auto; padding: 12px; white-space: pre-wrap; }
10291
- a { color: #f59e0b; }
10292
- @media (max-width: 850px) { .grid, .provider-grid, dl { grid-template-columns: 1fr; } }
10293
- </style>
10294
- </head>
10295
- <body>
10296
- <main>
10297
- <section class="hero">
10298
- <h1>Provider routing and resilience</h1>
10299
- <p>One view for the production reliability story: LLM failover, STT/TTS routing, latency budgets, timeouts, and fallback decisions.</p>
10300
- ${links ? `<p>${links}</p>` : ""}
10301
- <p>${kindCounts || '<span class="pill">No routing events yet</span>'}</p>
10302
- </section>
10303
- <section class="grid">
10304
- <article class="card metric"><span>Total routing events</span><strong>${summary.total}</strong></article>
10305
- <article class="card metric"><span>Fallbacks</span><strong>${summary.fallbacks}</strong></article>
10306
- <article class="card metric"><span>Errors</span><strong>${summary.errors}</strong></article>
10307
- <article class="card metric"><span>Timeouts</span><strong>${summary.timeouts}</strong></article>
10308
- </section>
10309
- <section>
10310
- <h2>LLM provider health</h2>
10311
- ${renderProviderCards("LLM", input.llmProviderHealth)}
10312
- </section>
10313
- <section>
10314
- <h2>STT provider health</h2>
10315
- ${renderSimulationControls("stt", input.sttSimulation)}
10316
- ${renderProviderCards("STT", input.sttProviderHealth)}
10317
- </section>
10318
- <section>
10319
- <h2>TTS provider health</h2>
10320
- ${renderSimulationControls("tts", input.ttsSimulation)}
10321
- ${renderProviderCards("TTS", input.ttsProviderHealth)}
10322
- </section>
10323
- <section>
10324
- <h2>Routing timeline</h2>
10325
- ${renderTimeline2(input.routingEvents)}
10326
- </section>
10327
- </main>
10328
- <script>
10329
- const showResult = (panel, result) => {
10330
- const output = panel.querySelector(".simulate-output");
10331
- if (!output) return;
10332
- output.hidden = false;
10333
- output.textContent = JSON.stringify(result, null, 2);
10334
- };
10335
- document.querySelectorAll("[data-sim-prefix]").forEach((panel) => {
10336
- const prefix = panel.getAttribute("data-sim-prefix");
10337
- panel.querySelectorAll("[data-provider-fail]").forEach((button) => {
10338
- button.addEventListener("click", async () => {
10339
- const provider = button.getAttribute("data-provider-fail");
10340
- const response = await fetch(prefix + "/failure?provider=" + encodeURIComponent(provider || ""), { method: "POST" });
10341
- showResult(panel, await response.json());
10342
- if (response.ok) window.setTimeout(() => window.location.reload(), 450);
10343
- });
10344
- });
10345
- panel.querySelectorAll("[data-provider-recover]").forEach((button) => {
10346
- button.addEventListener("click", async () => {
10347
- const provider = button.getAttribute("data-provider-recover");
10348
- const response = await fetch(prefix + "/recovery?provider=" + encodeURIComponent(provider || ""), { method: "POST" });
10349
- showResult(panel, await response.json());
10350
- if (response.ok) window.setTimeout(() => window.location.reload(), 450);
10351
- });
10352
- });
10353
- });
10354
- </script>
10355
- </body>
10356
- </html>`;
10626
+ const parts = content.parts;
10627
+ return Array.isArray(parts) ? parts : [];
10357
10628
  };
10358
- var providerFromQuery = (value, providers) => typeof value === "string" && providers.some((provider) => provider.provider === value && provider.configured !== false) ? value : undefined;
10359
- var registerSimulationRoutes = (routes, simulation, defaultPathPrefix) => {
10360
- if (!simulation) {
10361
- return routes;
10362
- }
10363
- const pathPrefix = simulation.pathPrefix ?? defaultPathPrefix;
10364
- routes.post(`${pathPrefix}/failure`, async ({ query, set }) => {
10365
- const provider = providerFromQuery(query.provider, simulation.providers);
10366
- if (!provider) {
10367
- set.status = 400;
10368
- return {
10369
- error: "Provider is not configured for simulation."
10370
- };
10371
- }
10372
- if (simulation.failureProviders && !simulation.failureProviders.includes(provider)) {
10373
- set.status = 400;
10374
- return {
10375
- error: `${provider} is not configured for failure simulation.`
10376
- };
10629
+ var extractGeminiText = (response) => extractGeminiCandidateParts(response).map((part) => part && typeof part === "object" && typeof part.text === "string" ? part.text : "").filter(Boolean).join(`
10630
+ `);
10631
+ var extractGeminiToolCalls = (response) => {
10632
+ const toolCalls = [];
10633
+ for (const part of extractGeminiCandidateParts(response)) {
10634
+ if (!part || typeof part !== "object") {
10635
+ continue;
10377
10636
  }
10378
- if (simulation.fallbackRequiredProvider && !simulation.providers.some((entry) => entry.provider === simulation.fallbackRequiredProvider && entry.configured !== false)) {
10379
- set.status = 400;
10380
- return {
10381
- error: simulation.fallbackRequiredMessage ?? `Configure ${simulation.fallbackRequiredProvider} before simulating fallback.`
10382
- };
10637
+ const functionCall = part.functionCall;
10638
+ if (!functionCall || typeof functionCall !== "object") {
10639
+ continue;
10383
10640
  }
10384
- return simulation.run(provider, "failure");
10385
- });
10386
- routes.post(`${pathPrefix}/recovery`, async ({ query, set }) => {
10387
- const provider = providerFromQuery(query.provider, simulation.providers);
10388
- if (!provider) {
10389
- set.status = 400;
10390
- return {
10391
- error: "Provider is not configured for simulation."
10392
- };
10641
+ const record = functionCall;
10642
+ if (typeof record.name !== "string") {
10643
+ continue;
10393
10644
  }
10394
- return simulation.run(provider, "recovery");
10395
- });
10396
- return routes;
10397
- };
10398
- var createVoiceResilienceRoutes = (options) => {
10399
- const path = options.path ?? "/resilience";
10400
- const routes = new Elysia9({
10401
- name: options.name ?? "absolutejs-voice-resilience"
10402
- }).get(path, async () => {
10403
- const events = await options.store.list();
10404
- const sttEvents = events.filter((event) => event.payload.kind === "stt");
10405
- const ttsEvents = events.filter((event) => event.payload.kind === "tts");
10406
- const data = {
10407
- links: options.links,
10408
- llmProviderHealth: await summarizeVoiceProviderHealth({
10409
- events,
10410
- providers: options.llmProviders ?? []
10411
- }),
10412
- routingEvents: listVoiceRoutingEvents(events),
10413
- sttProviderHealth: await summarizeVoiceProviderHealth({
10414
- events: sttEvents,
10415
- providers: options.sttProviders ?? []
10416
- }),
10417
- sttSimulation: options.sttSimulation,
10418
- title: options.title,
10419
- ttsProviderHealth: await summarizeVoiceProviderHealth({
10420
- events: ttsEvents,
10421
- providers: options.ttsProviders ?? []
10422
- }),
10423
- ttsSimulation: options.ttsSimulation
10424
- };
10425
- const body = await (options.render ?? renderVoiceResilienceHTML)(data);
10426
- return new Response(body, {
10427
- headers: {
10428
- "Content-Type": "text/html; charset=utf-8",
10429
- ...options.headers
10430
- }
10645
+ toolCalls.push({
10646
+ args: record.args && typeof record.args === "object" ? record.args : {},
10647
+ id: typeof record.id === "string" ? record.id : undefined,
10648
+ name: record.name
10431
10649
  });
10432
- });
10433
- registerSimulationRoutes(routes, options.sttSimulation, "/api/stt-simulate");
10434
- registerSimulationRoutes(routes, options.ttsSimulation, "/api/tts-simulate");
10435
- return routes;
10436
- };
10437
-
10438
- // src/opsConsoleRoutes.ts
10439
- var DEFAULT_LINKS = [
10440
- {
10441
- description: "Quality gates for CI, deploy checks, and production readiness.",
10442
- href: "/quality",
10443
- label: "Quality",
10444
- statusHref: "/quality/status"
10445
- },
10446
- {
10447
- description: "Replay stored sessions against acceptance gates over time.",
10448
- href: "/evals",
10449
- label: "Evals",
10450
- statusHref: "/evals/status"
10451
- },
10452
- {
10453
- description: "Provider health, fallback paths, and failure simulation.",
10454
- href: "/resilience",
10455
- label: "Resilience"
10456
- },
10457
- {
10458
- description: "Redacted trace exports for debugging and support handoffs.",
10459
- href: "/diagnostics",
10460
- label: "Diagnostics"
10461
- },
10462
- {
10463
- description: "Recent sessions with replay links.",
10464
- href: "/sessions",
10465
- label: "Sessions"
10466
- },
10467
- {
10468
- description: "Transfer and webhook delivery health.",
10469
- href: "/handoffs",
10470
- label: "Handoffs"
10471
10650
  }
10472
- ];
10473
- var escapeHtml12 = (value) => value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
10474
- var countProviderStatuses = (providers) => {
10475
- const degradedStatuses = new Set(["degraded", "rate-limited", "suppressed"]);
10476
- const healthy = providers.filter((provider) => provider.status === "healthy").length;
10477
- const degraded = providers.filter((provider) => degradedStatuses.has(provider.status)).length;
10478
- return {
10479
- degraded,
10480
- healthy,
10481
- total: providers.length
10482
- };
10651
+ return toolCalls;
10483
10652
  };
10484
- var buildVoiceOpsConsoleReport = async (options) => {
10485
- const events = await options.store.list();
10486
- const providers = [
10487
- ...await summarizeVoiceProviderHealth({
10488
- events,
10489
- providers: options.llmProviders
10490
- }),
10491
- ...await summarizeVoiceProviderHealth({
10492
- events,
10493
- providers: options.sttProviders
10494
- }),
10495
- ...await summarizeVoiceProviderHealth({
10496
- events,
10497
- providers: options.ttsProviders
10498
- })
10499
- ];
10500
- const handoffs = await summarizeVoiceHandoffHealth({ events });
10501
- const sessions = await summarizeVoiceSessions({
10502
- events,
10503
- limit: 8,
10504
- status: "all"
10505
- });
10506
- const quality = await evaluateVoiceQuality({ events });
10507
- const routingEvents = listVoiceRoutingEvents(events).slice(0, 10);
10508
- const trace = summarizeVoiceTrace(events);
10653
+ var createGeminiVoiceAssistantModel = (options) => {
10654
+ const fetchImpl = options.fetch ?? globalThis.fetch;
10655
+ const baseUrl = options.baseUrl ?? "https://generativelanguage.googleapis.com/v1beta";
10656
+ const model = options.model ?? "gemini-2.5-flash";
10657
+ const maxRetries = Math.max(0, options.maxRetries ?? 2);
10509
10658
  return {
10510
- checkedAt: Date.now(),
10511
- eventCount: events.length,
10512
- handoffs: {
10513
- failed: handoffs.failed,
10514
- total: handoffs.total
10515
- },
10516
- links: options.links ?? DEFAULT_LINKS,
10517
- providers: countProviderStatuses(providers),
10518
- quality,
10519
- recentRoutingEvents: routingEvents,
10520
- recentSessions: sessions,
10521
- sessions: {
10522
- failed: sessions.filter((session) => session.status === "failed").length,
10523
- healthy: sessions.filter((session) => session.status === "healthy").length,
10524
- total: sessions.length
10525
- },
10526
- trace
10527
- };
10528
- };
10529
- var renderMetricCard = (input) => `<article class="metric"><span>${escapeHtml12(input.label)}</span><strong>${escapeHtml12(String(input.value))}</strong>${input.status ? `<p class="${escapeHtml12(input.status)}">${escapeHtml12(input.status)}</p>` : ""}${input.href ? `<a href="${escapeHtml12(input.href)}">Open</a>` : ""}</article>`;
10530
- var renderVoiceOpsConsoleHTML = (report, options = {}) => {
10531
- const links = report.links.map((link) => `<article class="surface">
10532
- <div><h2>${escapeHtml12(link.label)}</h2>${link.description ? `<p>${escapeHtml12(link.description)}</p>` : ""}</div>
10533
- <p><a href="${escapeHtml12(link.href)}">Open ${escapeHtml12(link.label)}</a>${link.statusHref ? ` \xB7 <a href="${escapeHtml12(link.statusHref)}">Status</a>` : ""}</p>
10534
- </article>`).join("");
10535
- const sessions = report.recentSessions.length ? report.recentSessions.map((session) => `<tr><td>${escapeHtml12(session.sessionId)}</td><td>${escapeHtml12(session.status)}</td><td>${session.turnCount}</td><td>${session.errorCount}</td><td>${session.replayHref ? `<a href="${escapeHtml12(session.replayHref)}">Replay</a>` : ""}</td></tr>`).join("") : '<tr><td colspan="5">No sessions yet.</td></tr>';
10536
- const routing = report.recentRoutingEvents.length ? report.recentRoutingEvents.map((event) => `<tr><td>${escapeHtml12(event.kind)}</td><td>${escapeHtml12(event.provider ?? "unknown")}</td><td>${escapeHtml12(event.status ?? "unknown")}</td><td>${event.elapsedMs ?? 0}ms</td><td>${escapeHtml12(event.sessionId)}</td></tr>`).join("") : '<tr><td colspan="5">No provider routing events yet.</td></tr>';
10537
- const title = options.title ?? "AbsoluteJS Voice Ops Console";
10538
- return `<!doctype html><html lang="en"><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /><title>${escapeHtml12(title)}</title><style>body{font-family:ui-sans-serif,system-ui,sans-serif;background:#101316;color:#f6f2e8;margin:0}main{max-width:1180px;margin:auto;padding:32px}a{color:#fbbf24}header{display:flex;justify-content:space-between;gap:24px;align-items:flex-start;margin-bottom:24px}.eyebrow{color:#fbbf24;font-weight:800;letter-spacing:.08em;text-transform:uppercase}h1{font-size:clamp(2.2rem,5vw,4.5rem);line-height:.95;margin:.2rem 0 1rem}.muted{color:#a8b0b8}.grid{display:grid;gap:14px;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));margin:20px 0}.metric,.surface{background:#181d22;border:1px solid #2a323a;border-radius:20px;padding:18px}.metric strong{display:block;font-size:2.2rem;margin:.25rem 0}.pass,.healthy{color:#86efac}.fail,.failed,.degraded{color:#fca5a5}.surfaces{display:grid;gap:16px;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));margin:24px 0}table{width:100%;border-collapse:collapse;background:#181d22;border-radius:16px;overflow:hidden;margin:12px 0 28px}td,th{border-bottom:1px solid #2a323a;padding:12px;text-align:left}section{margin-top:30px}@media(max-width:700px){main{padding:20px}header{display:block}}</style></head><body><main><header><div><p class="eyebrow">Self-hosted voice operations</p><h1>${escapeHtml12(title)}</h1><p class="muted">One deployable control plane for quality gates, failover, traces, sessions, handoffs, and provider health.</p></div><p class="muted">Checked ${escapeHtml12(new Date(report.checkedAt).toLocaleString())}</p></header><div class="grid">${renderMetricCard({ label: "Quality", value: report.quality.status, status: report.quality.status, href: "/quality" })}${renderMetricCard({ label: "Events", value: report.eventCount, href: "/diagnostics" })}${renderMetricCard({ label: "Sessions", value: report.sessions.total, status: report.sessions.failed > 0 ? "failed" : "healthy", href: "/sessions" })}${renderMetricCard({ label: "Handoffs failed", value: report.handoffs.failed, status: report.handoffs.failed > 0 ? "failed" : "healthy", href: "/handoffs" })}${renderMetricCard({ label: "Providers degraded", value: report.providers.degraded, status: report.providers.degraded > 0 ? "degraded" : "healthy", href: "/resilience" })}</div><section><h2>Operational Surfaces</h2><div class="surfaces">${links}</div></section><section><h2>Recent Sessions</h2><table><thead><tr><th>Session</th><th>Status</th><th>Turns</th><th>Errors</th><th>Replay</th></tr></thead><tbody>${sessions}</tbody></table></section><section><h2>Recent Provider Routing</h2><table><thead><tr><th>Kind</th><th>Provider</th><th>Status</th><th>Elapsed</th><th>Session</th></tr></thead><tbody>${routing}</tbody></table></section></main></body></html>`;
10539
- };
10540
- var createVoiceOpsConsoleRoutes = (options) => {
10541
- const path = options.path ?? "/ops-console";
10542
- const routes = new Elysia10({
10543
- name: options.name ?? "absolutejs-voice-ops-console"
10544
- });
10545
- const getReport = () => buildVoiceOpsConsoleReport(options);
10546
- routes.get(path, async () => {
10547
- const report = await getReport();
10548
- return new Response(renderVoiceOpsConsoleHTML(report, { title: options.title }), {
10549
- headers: {
10550
- "Content-Type": "text/html; charset=utf-8",
10551
- ...options.headers
10659
+ generate: async (input) => {
10660
+ const endpoint = `${baseUrl.replace(/\/$/, "")}/models/${encodeURIComponent(model)}:generateContent?key=${encodeURIComponent(options.apiKey)}`;
10661
+ let response;
10662
+ for (let attempt = 0;attempt <= maxRetries; attempt += 1) {
10663
+ response = await fetchImpl(endpoint, {
10664
+ body: JSON.stringify({
10665
+ contents: input.messages.map(messageToGeminiContent).filter(Boolean),
10666
+ generationConfig: {
10667
+ maxOutputTokens: options.maxOutputTokens,
10668
+ ...input.tools.length ? {} : {
10669
+ responseMimeType: "application/json",
10670
+ responseSchema: toGeminiSchema(OUTPUT_SCHEMA)
10671
+ },
10672
+ temperature: options.temperature
10673
+ },
10674
+ systemInstruction: {
10675
+ parts: [
10676
+ {
10677
+ text: [input.system, ROUTE_RESULT_INSTRUCTION].filter(Boolean).join(`
10678
+
10679
+ `)
10680
+ }
10681
+ ]
10682
+ },
10683
+ tools: input.tools.length ? [
10684
+ {
10685
+ functionDeclarations: input.tools.map((tool) => ({
10686
+ description: tool.description,
10687
+ name: tool.name,
10688
+ parameters: toGeminiSchema(tool.parameters ?? {
10689
+ additionalProperties: true,
10690
+ type: "object"
10691
+ })
10692
+ }))
10693
+ }
10694
+ ] : undefined
10695
+ }),
10696
+ headers: {
10697
+ "content-type": "application/json"
10698
+ },
10699
+ method: "POST"
10700
+ });
10701
+ if (response.ok || response.status !== 429 && response.status < 500 || attempt === maxRetries) {
10702
+ break;
10703
+ }
10704
+ const retryAfter = Number(response.headers.get("retry-after"));
10705
+ await sleep4(Number.isFinite(retryAfter) && retryAfter > 0 ? retryAfter * 1000 : 500 * 2 ** attempt);
10552
10706
  }
10553
- });
10554
- });
10555
- routes.get(`${path}/json`, async () => getReport());
10556
- return routes;
10707
+ if (!response) {
10708
+ throw new Error("Gemini voice assistant model failed: no response");
10709
+ }
10710
+ if (!response.ok) {
10711
+ throw createHTTPError("Gemini", response);
10712
+ }
10713
+ const body = await response.json();
10714
+ if (body.usageMetadata && typeof body.usageMetadata === "object") {
10715
+ await options.onUsage?.(body.usageMetadata);
10716
+ }
10717
+ const toolCalls = extractGeminiToolCalls(body);
10718
+ if (toolCalls.length) {
10719
+ return {
10720
+ assistantText: extractGeminiText(body) || undefined,
10721
+ toolCalls
10722
+ };
10723
+ }
10724
+ return normalizeRouteOutput(parseJSON(extractGeminiText(body)));
10725
+ }
10726
+ };
10557
10727
  };
10558
10728
  // src/providerAdapters.ts
10559
10729
  class VoiceIOProviderTimeoutError extends Error {
@@ -11399,7 +11569,7 @@ var createVoiceMemoryStore = () => {
11399
11569
  return { get, getOrCreate, list, remove, set };
11400
11570
  };
11401
11571
  // src/opsWebhook.ts
11402
- import { Elysia as Elysia11 } from "elysia";
11572
+ import { Elysia as Elysia12 } from "elysia";
11403
11573
  var toHex5 = (bytes) => Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
11404
11574
  var signVoiceOpsWebhookBody = async (input) => {
11405
11575
  const encoder = new TextEncoder;
@@ -11529,7 +11699,7 @@ var verifyVoiceOpsWebhookSignature = async (input) => {
11529
11699
  };
11530
11700
  var createVoiceOpsWebhookReceiverRoutes = (options = {}) => {
11531
11701
  const path = options.path ?? "/api/voice-ops/webhook";
11532
- return new Elysia11().post(path, async ({ body, request, set }) => {
11702
+ return new Elysia12().post(path, async ({ body, request, set }) => {
11533
11703
  const bodyText = typeof body === "string" ? body : JSON.stringify(body);
11534
11704
  if (options.signingSecret) {
11535
11705
  const verification = await verifyVoiceOpsWebhookSignature({
@@ -13866,6 +14036,8 @@ export {
13866
14036
  createVoiceAssistantHealthJSONHandler,
13867
14037
  createVoiceAssistantHealthHTMLHandler,
13868
14038
  createVoiceAssistant,
14039
+ createVoiceAppKitRoutes,
14040
+ createVoiceAppKit,
13869
14041
  createVoiceAgentTool,
13870
14042
  createVoiceAgentSquad,
13871
14043
  createVoiceAgent,