@absolutejs/voice 0.0.22-beta.35 → 0.0.22-beta.37

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -7,6 +7,7 @@ export { createStoredVoiceCallReviewArtifact, createStoredVoiceExternalObjectMap
7
7
  export { createVoiceAssistantMemoryHandle, createVoiceAssistantMemoryRecord, createVoiceMemoryAssistantMemoryStore, resolveVoiceAssistantMemoryNamespace } from './assistantMemory';
8
8
  export { createAnthropicVoiceAssistantModel, createGeminiVoiceAssistantModel, createJSONVoiceAssistantModel, createOpenAIVoiceAssistantModel, createVoiceProviderRouter } from './modelAdapters';
9
9
  export { createVoiceProviderHealthHTMLHandler, createVoiceProviderHealthJSONHandler, createVoiceProviderHealthRoutes, renderVoiceProviderHealthHTML, summarizeVoiceProviderHealth } from './providerHealth';
10
+ export { createVoiceResilienceRoutes, listVoiceRoutingEvents, renderVoiceResilienceHTML } from './resilienceRoutes';
10
11
  export { createVoiceSTTProviderRouter, createVoiceTTSProviderRouter } from './providerAdapters';
11
12
  export { buildVoiceTraceReplay, createVoiceMemoryTraceSinkDeliveryStore, createVoiceTraceHTTPSink, createVoiceMemoryTraceEventStore, createVoiceTraceSinkDeliveryId, createVoiceTraceSinkDeliveryRecord, createVoiceTraceSinkStore, createVoiceTraceEvent, createVoiceTraceEventId, deliverVoiceTraceEventsToSinks, evaluateVoiceTrace, exportVoiceTrace, filterVoiceTraceEvents, pruneVoiceTraceEvents, redactVoiceTraceEvent, redactVoiceTraceEvents, redactVoiceTraceText, renderVoiceTraceHTML, renderVoiceTraceMarkdown, resolveVoiceTraceRedactionOptions, selectVoiceTraceEventsForPrune, summarizeVoiceTrace } from './trace';
12
13
  export { createVoiceSQLiteExternalObjectMapStore, createVoiceSQLiteIntegrationEventStore, createVoiceSQLiteReviewStore, createVoiceSQLiteRuntimeStorage, createVoiceSQLiteSessionStore, createVoiceSQLiteTaskStore, createVoiceSQLiteTraceSinkDeliveryStore, createVoiceSQLiteTraceEventStore } from './sqliteStore';
@@ -37,6 +38,7 @@ export type { VoiceAssistantMemoryBinding, VoiceAssistantMemoryHandle, VoiceAssi
37
38
  export type { VoiceSessionListHTMLHandlerOptions, VoiceSessionListItem, VoiceSessionListOptions, VoiceSessionListRoutesOptions, VoiceSessionListStatus, VoiceSessionReplay, VoiceSessionReplayHTMLHandlerOptions, VoiceSessionReplayOptions, VoiceSessionReplayRoutesOptions, VoiceSessionReplayTurn } from './sessionReplay';
38
39
  export type { AnthropicVoiceAssistantModelOptions, GeminiVoiceAssistantModelOptions, OpenAIVoiceAssistantModelOptions, VoiceProviderRouterEvent, VoiceProviderRouterFallbackMode, VoiceProviderRouterHealthOptions, VoiceProviderRouterOptions, VoiceProviderRouterPolicy, VoiceProviderRouterProviderHealth, VoiceProviderRouterProviderProfile, VoiceJSONAssistantModelHandler, VoiceJSONAssistantModelOptions } from './modelAdapters';
39
40
  export type { VoiceProviderHealthStatus, VoiceProviderHealthSummary, VoiceProviderHealthSummaryOptions } from './providerHealth';
41
+ export type { VoiceResilienceIOSimulator, VoiceResilienceLink, VoiceResiliencePageData, VoiceResilienceRoutesOptions, VoiceResilienceSimulationProvider, VoiceRoutingEvent, VoiceRoutingEventKind } from './resilienceRoutes';
40
42
  export type { VoiceIOProviderRouterEvent, VoiceSTTProviderRouterOptions, VoiceTTSProviderRouterOptions } from './providerAdapters';
41
43
  export type { VoiceAgent, VoiceAgentMessage, VoiceAgentMessageRole, VoiceAgentModel, VoiceAgentModelInput, VoiceAgentModelOutput, VoiceAgentOptions, VoiceAgentRunResult, VoiceAgentSquadOptions, VoiceAgentTool, VoiceAgentToolCall, VoiceAgentToolResult } from './agent';
42
44
  export type { VoiceOpsRuntime, VoiceOpsRuntimeConfig, VoiceOpsRuntimeSummary, VoiceOpsRuntimeSinkWorkerConfig, VoiceOpsRuntimeTaskWorkerConfig, VoiceOpsRuntimeTickResult, VoiceOpsRuntimeWebhookWorkerConfig } from './opsRuntime';
package/dist/index.js CHANGED
@@ -8858,6 +8858,319 @@ var createGeminiVoiceAssistantModel = (options) => {
8858
8858
  }
8859
8859
  };
8860
8860
  };
8861
+ // src/resilienceRoutes.ts
8862
+ import { Elysia as Elysia5 } from "elysia";
8863
+ var escapeHtml7 = (value) => value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
8864
+ var getString4 = (value) => typeof value === "string" ? value : undefined;
8865
+ var getNumber2 = (value) => typeof value === "number" && Number.isFinite(value) ? value : undefined;
8866
+ var getBoolean = (value) => value === true;
8867
+ var isProviderStatus2 = (value) => value === "error" || value === "fallback" || value === "success";
8868
+ var listVoiceRoutingEvents = (events) => {
8869
+ const routingEvents = [];
8870
+ for (const event of events) {
8871
+ if (event.type !== "session.error") {
8872
+ continue;
8873
+ }
8874
+ const provider = getString4(event.payload.provider);
8875
+ const providerStatus = isProviderStatus2(event.payload.providerStatus) ? event.payload.providerStatus : undefined;
8876
+ if (!provider || !providerStatus) {
8877
+ continue;
8878
+ }
8879
+ const kind = getString4(event.payload.kind);
8880
+ routingEvents.push({
8881
+ at: event.at,
8882
+ attempt: getNumber2(event.payload.attempt),
8883
+ elapsedMs: getNumber2(event.payload.elapsedMs),
8884
+ error: getString4(event.payload.error),
8885
+ fallbackProvider: getString4(event.payload.fallbackProvider),
8886
+ kind: kind === "stt" || kind === "tts" ? kind : "llm",
8887
+ latencyBudgetMs: getNumber2(event.payload.latencyBudgetMs),
8888
+ operation: getString4(event.payload.operation),
8889
+ provider,
8890
+ selectedProvider: getString4(event.payload.selectedProvider),
8891
+ sessionId: event.sessionId,
8892
+ status: providerStatus,
8893
+ timedOut: getBoolean(event.payload.timedOut),
8894
+ turnId: event.turnId
8895
+ });
8896
+ }
8897
+ return routingEvents.sort((left, right) => right.at - left.at);
8898
+ };
8899
+ var summarizeRoutingEvents = (events) => {
8900
+ const byKind = new Map;
8901
+ let errors = 0;
8902
+ let fallbacks = 0;
8903
+ let timeouts = 0;
8904
+ for (const event of events) {
8905
+ byKind.set(event.kind, (byKind.get(event.kind) ?? 0) + 1);
8906
+ if (event.status === "error") {
8907
+ errors += 1;
8908
+ }
8909
+ if (event.status === "fallback") {
8910
+ fallbacks += 1;
8911
+ }
8912
+ if (event.timedOut) {
8913
+ timeouts += 1;
8914
+ }
8915
+ }
8916
+ return {
8917
+ byKind,
8918
+ errors,
8919
+ fallbacks,
8920
+ timeouts,
8921
+ total: events.length
8922
+ };
8923
+ };
8924
+ var renderProviderCards = (title, providers) => {
8925
+ if (providers.length === 0) {
8926
+ return `<p class="muted">No ${escapeHtml7(title)} provider health yet.</p>`;
8927
+ }
8928
+ return `<div class="provider-grid">${providers.map((provider) => `
8929
+ <article class="card provider ${escapeHtml7(provider.status)}">
8930
+ <div class="card-header">
8931
+ <strong>${escapeHtml7(provider.provider)}</strong>
8932
+ <span>${escapeHtml7(provider.status)}${provider.recommended ? " \xB7 recommended" : ""}</span>
8933
+ </div>
8934
+ <dl>
8935
+ <div><dt>Runs</dt><dd>${provider.runCount}</dd></div>
8936
+ <div><dt>Avg latency</dt><dd>${provider.averageElapsedMs ?? 0}ms</dd></div>
8937
+ <div><dt>Errors</dt><dd>${provider.errorCount}</dd></div>
8938
+ <div><dt>Timeouts</dt><dd>${provider.timeoutCount}</dd></div>
8939
+ <div><dt>Fallbacks</dt><dd>${provider.fallbackCount}</dd></div>
8940
+ </dl>
8941
+ ${provider.lastError ? `<p class="muted">${escapeHtml7(provider.lastError)}</p>` : ""}
8942
+ </article>
8943
+ `).join("")}</div>`;
8944
+ };
8945
+ var renderTimeline2 = (events) => {
8946
+ if (events.length === 0) {
8947
+ return '<p class="muted">No provider routing events yet. Run the app or simulate provider failover.</p>';
8948
+ }
8949
+ return `<div class="timeline">${events.slice(0, 40).map((event) => `
8950
+ <article class="card event ${escapeHtml7(event.status ?? "unknown")}">
8951
+ <div class="card-header">
8952
+ <strong>${escapeHtml7(event.kind.toUpperCase())} ${escapeHtml7(event.operation ?? "generate")}</strong>
8953
+ <span>${new Date(event.at).toLocaleString()}</span>
8954
+ </div>
8955
+ <p>
8956
+ <span class="pill">${escapeHtml7(event.status ?? "unknown")}</span>
8957
+ <span class="pill">provider: ${escapeHtml7(event.provider ?? "unknown")}</span>
8958
+ ${event.fallbackProvider ? `<span class="pill">fallback: ${escapeHtml7(event.fallbackProvider)}</span>` : ""}
8959
+ ${event.timedOut ? '<span class="pill danger">timed out</span>' : ""}
8960
+ </p>
8961
+ <dl>
8962
+ <div><dt>Attempt</dt><dd>${event.attempt ?? 0}</dd></div>
8963
+ <div><dt>Elapsed</dt><dd>${event.elapsedMs ?? 0}ms</dd></div>
8964
+ <div><dt>Budget</dt><dd>${event.latencyBudgetMs ?? 0}ms</dd></div>
8965
+ <div><dt>Session</dt><dd>${escapeHtml7(event.sessionId)}</dd></div>
8966
+ </dl>
8967
+ ${event.error ? `<p class="muted">${escapeHtml7(event.error)}</p>` : ""}
8968
+ </article>
8969
+ `).join("")}</div>`;
8970
+ };
8971
+ var renderSimulationControls = (kind, simulation) => {
8972
+ if (!simulation) {
8973
+ return "";
8974
+ }
8975
+ const configuredProviders = simulation.providers.filter((provider) => provider.configured !== false);
8976
+ if (configuredProviders.length === 0) {
8977
+ return `<p class="muted">No ${kind.toUpperCase()} providers are configured for simulation.</p>`;
8978
+ }
8979
+ const pathPrefix = simulation.pathPrefix ?? `/api/${kind}-simulate`;
8980
+ const failureProviders = simulation.failureProviders ?? configuredProviders.map(({ provider }) => provider);
8981
+ const canFail = (provider) => configuredProviders.some((entry) => entry.provider === provider) && (!simulation.fallbackRequiredProvider || configuredProviders.some((entry) => entry.provider === simulation.fallbackRequiredProvider));
8982
+ return `<div class="simulate-panel" data-sim-kind="${kind}" data-sim-prefix="${escapeHtml7(pathPrefix)}">
8983
+ <p class="muted">${escapeHtml7(simulation.failureMessage ?? `Simulate ${kind.toUpperCase()} provider failure without changing provider credentials.`)}</p>
8984
+ <div class="simulate-actions">
8985
+ ${failureProviders.map((provider) => `<button type="button" data-provider-fail="${escapeHtml7(provider)}"${canFail(provider) ? "" : " disabled"}>Simulate ${escapeHtml7(provider)} ${kind.toUpperCase()} failure</button>`).join("")}
8986
+ ${configuredProviders.map((provider) => `<button type="button" data-provider-recover="${escapeHtml7(provider.provider)}">Mark ${escapeHtml7(provider.provider)} recovered</button>`).join("")}
8987
+ </div>
8988
+ ${simulation.fallbackRequiredProvider && !configuredProviders.some((entry) => entry.provider === simulation.fallbackRequiredProvider) ? `<p class="muted">${escapeHtml7(simulation.fallbackRequiredMessage ?? `Configure ${simulation.fallbackRequiredProvider} to enable fallback simulation.`)}</p>` : ""}
8989
+ <pre class="simulate-output" hidden></pre>
8990
+ </div>`;
8991
+ };
8992
+ var renderVoiceResilienceHTML = (input) => {
8993
+ const summary = summarizeRoutingEvents(input.routingEvents);
8994
+ const kindCounts = [...summary.byKind.entries()].map(([kind, count]) => `<span class="pill">${escapeHtml7(kind)}: ${String(count)}</span>`).join("");
8995
+ const links = input.links?.length ? input.links.map((link) => `<a href="${escapeHtml7(link.href)}">${escapeHtml7(link.label)}</a>`).join(" \xB7 ") : "";
8996
+ return `<!doctype html>
8997
+ <html lang="en">
8998
+ <head>
8999
+ <meta charset="utf-8" />
9000
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
9001
+ <title>${escapeHtml7(input.title ?? "AbsoluteJS Voice Resilience")}</title>
9002
+ <style>
9003
+ :root { color-scheme: dark; }
9004
+ 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; }
9005
+ main { display: grid; gap: 16px; margin: 0 auto; max-width: 1180px; }
9006
+ section, .card { background: rgba(19, 22, 27, 0.92); border: 1px solid #27272a; border-radius: 20px; padding: 20px; }
9007
+ .hero { background: linear-gradient(135deg, rgba(14, 165, 233, 0.18), rgba(245, 158, 11, 0.12)); }
9008
+ .grid, .provider-grid { display: grid; gap: 14px; grid-template-columns: repeat(4, minmax(0, 1fr)); }
9009
+ .timeline { display: grid; gap: 12px; }
9010
+ .card-header { align-items: center; display: flex; gap: 12px; justify-content: space-between; }
9011
+ .card-header strong { font-size: 1.05rem; }
9012
+ .metric strong { display: block; font-size: 2rem; margin-top: 6px; }
9013
+ .muted, dt, span { color: #a1a1aa; }
9014
+ dl { display: grid; gap: 8px; grid-template-columns: repeat(4, minmax(0, 1fr)); }
9015
+ dl div { background: #0f1217; border: 1px solid #27272a; border-radius: 12px; padding: 10px; }
9016
+ dd { font-weight: 800; margin: 4px 0 0; }
9017
+ .pill { background: #0f1217; border: 1px solid #3f3f46; border-radius: 999px; color: #d4d4d8; display: inline-flex; margin: 3px 4px 3px 0; padding: 5px 9px; }
9018
+ .danger { border-color: rgba(239, 68, 68, 0.75); color: #fecaca; }
9019
+ .event.error { border-color: rgba(239, 68, 68, 0.7); }
9020
+ .event.fallback { border-color: rgba(245, 158, 11, 0.7); }
9021
+ .event.success, .provider.healthy { border-color: rgba(34, 197, 94, 0.5); }
9022
+ .provider.suppressed, .provider.degraded, .provider.rate-limited { border-color: rgba(239, 68, 68, 0.7); }
9023
+ .provider.recoverable { border-color: rgba(59, 130, 246, 0.7); }
9024
+ button { background: #f59e0b; border: 0; border-radius: 999px; color: #111827; cursor: pointer; font-weight: 800; padding: 10px 14px; }
9025
+ button:disabled { cursor: not-allowed; opacity: 0.45; }
9026
+ .simulate-actions { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 12px; }
9027
+ .simulate-output { background: #050505; border: 1px solid #27272a; border-radius: 14px; color: #d4d4d8; overflow: auto; padding: 12px; white-space: pre-wrap; }
9028
+ a { color: #f59e0b; }
9029
+ @media (max-width: 850px) { .grid, .provider-grid, dl { grid-template-columns: 1fr; } }
9030
+ </style>
9031
+ </head>
9032
+ <body>
9033
+ <main>
9034
+ <section class="hero">
9035
+ <h1>Provider routing and resilience</h1>
9036
+ <p>One view for the production reliability story: LLM failover, STT/TTS routing, latency budgets, timeouts, and fallback decisions.</p>
9037
+ ${links ? `<p>${links}</p>` : ""}
9038
+ <p>${kindCounts || '<span class="pill">No routing events yet</span>'}</p>
9039
+ </section>
9040
+ <section class="grid">
9041
+ <article class="card metric"><span>Total routing events</span><strong>${summary.total}</strong></article>
9042
+ <article class="card metric"><span>Fallbacks</span><strong>${summary.fallbacks}</strong></article>
9043
+ <article class="card metric"><span>Errors</span><strong>${summary.errors}</strong></article>
9044
+ <article class="card metric"><span>Timeouts</span><strong>${summary.timeouts}</strong></article>
9045
+ </section>
9046
+ <section>
9047
+ <h2>LLM provider health</h2>
9048
+ ${renderProviderCards("LLM", input.llmProviderHealth)}
9049
+ </section>
9050
+ <section>
9051
+ <h2>STT provider health</h2>
9052
+ ${renderSimulationControls("stt", input.sttSimulation)}
9053
+ ${renderProviderCards("STT", input.sttProviderHealth)}
9054
+ </section>
9055
+ <section>
9056
+ <h2>TTS provider health</h2>
9057
+ ${renderSimulationControls("tts", input.ttsSimulation)}
9058
+ ${renderProviderCards("TTS", input.ttsProviderHealth)}
9059
+ </section>
9060
+ <section>
9061
+ <h2>Routing timeline</h2>
9062
+ ${renderTimeline2(input.routingEvents)}
9063
+ </section>
9064
+ </main>
9065
+ <script>
9066
+ const showResult = (panel, result) => {
9067
+ const output = panel.querySelector(".simulate-output");
9068
+ if (!output) return;
9069
+ output.hidden = false;
9070
+ output.textContent = JSON.stringify(result, null, 2);
9071
+ };
9072
+ document.querySelectorAll("[data-sim-prefix]").forEach((panel) => {
9073
+ const prefix = panel.getAttribute("data-sim-prefix");
9074
+ panel.querySelectorAll("[data-provider-fail]").forEach((button) => {
9075
+ button.addEventListener("click", async () => {
9076
+ const provider = button.getAttribute("data-provider-fail");
9077
+ const response = await fetch(prefix + "/failure?provider=" + encodeURIComponent(provider || ""), { method: "POST" });
9078
+ showResult(panel, await response.json());
9079
+ if (response.ok) window.setTimeout(() => window.location.reload(), 450);
9080
+ });
9081
+ });
9082
+ panel.querySelectorAll("[data-provider-recover]").forEach((button) => {
9083
+ button.addEventListener("click", async () => {
9084
+ const provider = button.getAttribute("data-provider-recover");
9085
+ const response = await fetch(prefix + "/recovery?provider=" + encodeURIComponent(provider || ""), { method: "POST" });
9086
+ showResult(panel, await response.json());
9087
+ if (response.ok) window.setTimeout(() => window.location.reload(), 450);
9088
+ });
9089
+ });
9090
+ });
9091
+ </script>
9092
+ </body>
9093
+ </html>`;
9094
+ };
9095
+ var providerFromQuery = (value, providers) => typeof value === "string" && providers.some((provider) => provider.provider === value && provider.configured !== false) ? value : undefined;
9096
+ var registerSimulationRoutes = (routes, simulation, defaultPathPrefix) => {
9097
+ if (!simulation) {
9098
+ return routes;
9099
+ }
9100
+ const pathPrefix = simulation.pathPrefix ?? defaultPathPrefix;
9101
+ routes.post(`${pathPrefix}/failure`, async ({ query, set }) => {
9102
+ const provider = providerFromQuery(query.provider, simulation.providers);
9103
+ if (!provider) {
9104
+ set.status = 400;
9105
+ return {
9106
+ error: "Provider is not configured for simulation."
9107
+ };
9108
+ }
9109
+ if (simulation.failureProviders && !simulation.failureProviders.includes(provider)) {
9110
+ set.status = 400;
9111
+ return {
9112
+ error: `${provider} is not configured for failure simulation.`
9113
+ };
9114
+ }
9115
+ if (simulation.fallbackRequiredProvider && !simulation.providers.some((entry) => entry.provider === simulation.fallbackRequiredProvider && entry.configured !== false)) {
9116
+ set.status = 400;
9117
+ return {
9118
+ error: simulation.fallbackRequiredMessage ?? `Configure ${simulation.fallbackRequiredProvider} before simulating fallback.`
9119
+ };
9120
+ }
9121
+ return simulation.run(provider, "failure");
9122
+ });
9123
+ routes.post(`${pathPrefix}/recovery`, async ({ query, set }) => {
9124
+ const provider = providerFromQuery(query.provider, simulation.providers);
9125
+ if (!provider) {
9126
+ set.status = 400;
9127
+ return {
9128
+ error: "Provider is not configured for simulation."
9129
+ };
9130
+ }
9131
+ return simulation.run(provider, "recovery");
9132
+ });
9133
+ return routes;
9134
+ };
9135
+ var createVoiceResilienceRoutes = (options) => {
9136
+ const path = options.path ?? "/resilience";
9137
+ const routes = new Elysia5({
9138
+ name: options.name ?? "absolutejs-voice-resilience"
9139
+ }).get(path, async () => {
9140
+ const events = await options.store.list();
9141
+ const sttEvents = events.filter((event) => event.payload.kind === "stt");
9142
+ const ttsEvents = events.filter((event) => event.payload.kind === "tts");
9143
+ const data = {
9144
+ links: options.links,
9145
+ llmProviderHealth: await summarizeVoiceProviderHealth({
9146
+ events,
9147
+ providers: options.llmProviders ?? []
9148
+ }),
9149
+ routingEvents: listVoiceRoutingEvents(events),
9150
+ sttProviderHealth: await summarizeVoiceProviderHealth({
9151
+ events: sttEvents,
9152
+ providers: options.sttProviders ?? []
9153
+ }),
9154
+ sttSimulation: options.sttSimulation,
9155
+ title: options.title,
9156
+ ttsProviderHealth: await summarizeVoiceProviderHealth({
9157
+ events: ttsEvents,
9158
+ providers: options.ttsProviders ?? []
9159
+ }),
9160
+ ttsSimulation: options.ttsSimulation
9161
+ };
9162
+ const body = await (options.render ?? renderVoiceResilienceHTML)(data);
9163
+ return new Response(body, {
9164
+ headers: {
9165
+ "Content-Type": "text/html; charset=utf-8",
9166
+ ...options.headers
9167
+ }
9168
+ });
9169
+ });
9170
+ registerSimulationRoutes(routes, options.sttSimulation, "/api/stt-simulate");
9171
+ registerSimulationRoutes(routes, options.ttsSimulation, "/api/tts-simulate");
9172
+ return routes;
9173
+ };
8861
9174
  // src/providerAdapters.ts
8862
9175
  class VoiceIOProviderTimeoutError extends Error {
8863
9176
  provider;
@@ -9702,7 +10015,7 @@ var createVoiceMemoryStore = () => {
9702
10015
  return { get, getOrCreate, list, remove, set };
9703
10016
  };
9704
10017
  // src/opsWebhook.ts
9705
- import { Elysia as Elysia5 } from "elysia";
10018
+ import { Elysia as Elysia6 } from "elysia";
9706
10019
  var toHex5 = (bytes) => Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
9707
10020
  var signVoiceOpsWebhookBody = async (input) => {
9708
10021
  const encoder = new TextEncoder;
@@ -9832,7 +10145,7 @@ var verifyVoiceOpsWebhookSignature = async (input) => {
9832
10145
  };
9833
10146
  var createVoiceOpsWebhookReceiverRoutes = (options = {}) => {
9834
10147
  const path = options.path ?? "/api/voice-ops/webhook";
9835
- return new Elysia5().post(path, async ({ body, request, set }) => {
10148
+ return new Elysia6().post(path, async ({ body, request, set }) => {
9836
10149
  const bodyText = typeof body === "string" ? body : JSON.stringify(body);
9837
10150
  if (options.signingSecret) {
9838
10151
  const verification = await verifyVoiceOpsWebhookSignature({
@@ -9865,9 +10178,9 @@ var createVoiceOpsWebhookReceiverRoutes = (options = {}) => {
9865
10178
  });
9866
10179
  };
9867
10180
  // src/handoffHealth.ts
9868
- import { Elysia as Elysia6 } from "elysia";
9869
- var escapeHtml7 = (value) => value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
9870
- var getString4 = (value) => typeof value === "string" && value.length > 0 ? value : undefined;
10181
+ import { Elysia as Elysia7 } from "elysia";
10182
+ var escapeHtml8 = (value) => value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
10183
+ var getString5 = (value) => typeof value === "string" && value.length > 0 ? value : undefined;
9871
10184
  var isStatus = (value) => value === "delivered" || value === "failed" || value === "skipped";
9872
10185
  var increment3 = (record, key) => {
9873
10186
  record[key] = (record[key] ?? 0) + 1;
@@ -9875,11 +10188,11 @@ var increment3 = (record, key) => {
9875
10188
  var normalizeDelivery = (adapterId, value) => {
9876
10189
  const record = value && typeof value === "object" ? value : {};
9877
10190
  return {
9878
- adapterId: getString4(record.adapterId) ?? adapterId,
9879
- adapterKind: getString4(record.adapterKind),
10191
+ adapterId: getString5(record.adapterId) ?? adapterId,
10192
+ adapterKind: getString5(record.adapterKind),
9880
10193
  deliveredAt: typeof record.deliveredAt === "number" ? record.deliveredAt : undefined,
9881
- deliveredTo: getString4(record.deliveredTo),
9882
- error: getString4(record.error),
10194
+ deliveredTo: getString5(record.deliveredTo),
10195
+ error: getString5(record.error),
9883
10196
  status: isStatus(record.status) ? record.status : "failed"
9884
10197
  };
9885
10198
  };
@@ -9913,13 +10226,13 @@ var summarizeVoiceHandoffHealth = async (options = {}) => {
9913
10226
  const status = isStatus(event.payload.status) ? event.payload.status : "failed";
9914
10227
  const deliveries = normalizeDeliveries(event.payload);
9915
10228
  const item = {
9916
- action: getString4(event.payload.action),
10229
+ action: getString5(event.payload.action),
9917
10230
  at: event.at,
9918
10231
  deliveries,
9919
- reason: getString4(event.payload.reason),
10232
+ reason: getString5(event.payload.reason),
9920
10233
  sessionId: event.sessionId,
9921
10234
  status,
9922
- target: getString4(event.payload.target)
10235
+ target: getString5(event.payload.target)
9923
10236
  };
9924
10237
  return {
9925
10238
  ...item,
@@ -9984,10 +10297,10 @@ var renderActionSummary = (summary) => {
9984
10297
  return [
9985
10298
  '<section class="voice-handoff-health-columns">',
9986
10299
  "<article><h3>Actions</h3>",
9987
- actions.length === 0 ? "<p>No handoff actions yet.</p>" : `<ul>${actions.map(([action, count]) => `<li>${escapeHtml7(action)}: ${String(count)}</li>`).join("")}</ul>`,
10300
+ actions.length === 0 ? "<p>No handoff actions yet.</p>" : `<ul>${actions.map(([action, count]) => `<li>${escapeHtml8(action)}: ${String(count)}</li>`).join("")}</ul>`,
9988
10301
  "</article>",
9989
10302
  "<article><h3>Adapters</h3>",
9990
- adapters.length === 0 ? "<p>No adapter deliveries yet.</p>" : `<ul>${adapters.map(([adapterId, counts]) => `<li>${escapeHtml7(adapterId)}: ${String(counts.delivered)} delivered / ${String(counts.failed)} failed / ${String(counts.skipped)} skipped</li>`).join("")}</ul>`,
10303
+ adapters.length === 0 ? "<p>No adapter deliveries yet.</p>" : `<ul>${adapters.map(([adapterId, counts]) => `<li>${escapeHtml8(adapterId)}: ${String(counts.delivered)} delivered / ${String(counts.failed)} failed / ${String(counts.skipped)} skipped</li>`).join("")}</ul>`,
9991
10304
  "</article>",
9992
10305
  "</section>"
9993
10306
  ].join("");
@@ -10001,22 +10314,22 @@ var renderVoiceHandoffHealthHTML = (summary) => [
10001
10314
  summary.events.length === 0 ? '<p class="voice-handoff-health-empty">No handoffs found.</p>' : [
10002
10315
  '<div class="voice-handoff-health-events">',
10003
10316
  ...summary.events.map((event) => [
10004
- `<article class="${escapeHtml7(event.status)}">`,
10317
+ `<article class="${escapeHtml8(event.status)}">`,
10005
10318
  '<div class="voice-handoff-health-event-header">',
10006
- `<strong>${escapeHtml7(event.action ?? "handoff")}</strong>`,
10007
- `<span>${escapeHtml7(event.status)}</span>`,
10319
+ `<strong>${escapeHtml8(event.action ?? "handoff")}</strong>`,
10320
+ `<span>${escapeHtml8(event.status)}</span>`,
10008
10321
  "</div>",
10009
- `<p><small>${escapeHtml7(event.sessionId)}</small></p>`,
10010
- event.target ? `<p>Target: ${escapeHtml7(event.target)}</p>` : "",
10011
- event.reason ? `<p>Reason: ${escapeHtml7(event.reason)}</p>` : "",
10322
+ `<p><small>${escapeHtml8(event.sessionId)}</small></p>`,
10323
+ event.target ? `<p>Target: ${escapeHtml8(event.target)}</p>` : "",
10324
+ event.reason ? `<p>Reason: ${escapeHtml8(event.reason)}</p>` : "",
10012
10325
  event.deliveries.length ? `<ul>${event.deliveries.map((delivery) => [
10013
10326
  "<li>",
10014
- `${escapeHtml7(delivery.adapterId)}: ${escapeHtml7(delivery.status)}`,
10015
- delivery.deliveredTo ? ` to ${escapeHtml7(delivery.deliveredTo)}` : "",
10016
- delivery.error ? ` (${escapeHtml7(delivery.error)})` : "",
10327
+ `${escapeHtml8(delivery.adapterId)}: ${escapeHtml8(delivery.status)}`,
10328
+ delivery.deliveredTo ? ` to ${escapeHtml8(delivery.deliveredTo)}` : "",
10329
+ delivery.error ? ` (${escapeHtml8(delivery.error)})` : "",
10017
10330
  "</li>"
10018
10331
  ].join("")).join("")}</ul>` : "",
10019
- event.replayHref ? `<p><a href="${escapeHtml7(event.replayHref)}">Open replay</a></p>` : "",
10332
+ event.replayHref ? `<p><a href="${escapeHtml8(event.replayHref)}">Open replay</a></p>` : "",
10020
10333
  "</article>"
10021
10334
  ].join("")),
10022
10335
  "</div>"
@@ -10048,7 +10361,7 @@ var createVoiceHandoffHealthHTMLHandler = (options = {}) => async ({ query }) =>
10048
10361
  var createVoiceHandoffHealthRoutes = (options = {}) => {
10049
10362
  const path = options.path ?? "/api/voice-handoffs";
10050
10363
  const htmlPath = options.htmlPath === undefined ? `${path}/htmx` : options.htmlPath;
10051
- const routes = new Elysia6({
10364
+ const routes = new Elysia7({
10052
10365
  name: options.name ?? "absolutejs-voice-handoff-health"
10053
10366
  }).get(path, createVoiceHandoffHealthJSONHandler(options));
10054
10367
  if (htmlPath) {
@@ -12202,6 +12515,7 @@ export {
12202
12515
  renderVoiceTraceMarkdown,
12203
12516
  renderVoiceTraceHTML,
12204
12517
  renderVoiceSessionsHTML,
12518
+ renderVoiceResilienceHTML,
12205
12519
  renderVoiceProviderHealthHTML,
12206
12520
  renderVoiceHandoffHealthHTML,
12207
12521
  renderVoiceCallReviewMarkdown,
@@ -12214,6 +12528,7 @@ export {
12214
12528
  pruneVoiceTraceEvents,
12215
12529
  matchesVoiceOpsTaskAssignmentRule,
12216
12530
  markVoiceOpsTaskSLABreached,
12531
+ listVoiceRoutingEvents,
12217
12532
  listVoiceOpsTasks,
12218
12533
  isVoiceOpsTaskOverdue,
12219
12534
  heartbeatVoiceOpsTask,
@@ -12269,6 +12584,7 @@ export {
12269
12584
  createVoiceSQLiteExternalObjectMapStore,
12270
12585
  createVoiceS3ReviewStore,
12271
12586
  createVoiceReviewSavedEvent,
12587
+ createVoiceResilienceRoutes,
12272
12588
  createVoiceRedisTaskLeaseCoordinator,
12273
12589
  createVoiceRedisIdempotencyStore,
12274
12590
  createVoiceProviderRouter,
@@ -0,0 +1,106 @@
1
+ import { Elysia } from 'elysia';
2
+ import { type VoiceProviderHealthSummary } from './providerHealth';
3
+ import type { StoredVoiceTraceEvent, VoiceTraceEventStore } from './trace';
4
+ import type { VoiceIOProviderFailureSimulationMode, VoiceIOProviderFailureSimulationResult } from './testing/ioProviderSimulator';
5
+ export type VoiceRoutingEventKind = 'llm' | 'stt' | 'tts';
6
+ export type VoiceRoutingEvent = {
7
+ at: number;
8
+ attempt?: number;
9
+ elapsedMs?: number;
10
+ error?: string;
11
+ fallbackProvider?: string;
12
+ kind: VoiceRoutingEventKind;
13
+ latencyBudgetMs?: number;
14
+ operation?: string;
15
+ provider?: string;
16
+ selectedProvider?: string;
17
+ sessionId: string;
18
+ status?: string;
19
+ timedOut: boolean;
20
+ turnId?: string;
21
+ };
22
+ export type VoiceResilienceLink = {
23
+ href: string;
24
+ label: string;
25
+ };
26
+ export type VoiceResilienceSimulationProvider<TProvider extends string = string> = {
27
+ configured?: boolean;
28
+ provider: TProvider;
29
+ };
30
+ export type VoiceResilienceIOSimulator<TProvider extends string = string> = {
31
+ failureProviders?: readonly TProvider[];
32
+ fallbackRequiredProvider?: TProvider;
33
+ fallbackRequiredMessage?: string;
34
+ failureMessage?: string;
35
+ label?: string;
36
+ pathPrefix?: string;
37
+ providers: readonly VoiceResilienceSimulationProvider<TProvider>[];
38
+ recoveryMessage?: string;
39
+ run: (provider: TProvider, mode: VoiceIOProviderFailureSimulationMode) => Promise<VoiceIOProviderFailureSimulationResult<TProvider>>;
40
+ };
41
+ export type VoiceResiliencePageData = {
42
+ links?: readonly VoiceResilienceLink[];
43
+ llmProviderHealth: VoiceProviderHealthSummary<string>[];
44
+ routingEvents: VoiceRoutingEvent[];
45
+ sttProviderHealth: VoiceProviderHealthSummary<string>[];
46
+ sttSimulation?: VoiceResilienceIOSimulator<string>;
47
+ title?: string;
48
+ ttsProviderHealth: VoiceProviderHealthSummary<string>[];
49
+ ttsSimulation?: VoiceResilienceIOSimulator<string>;
50
+ };
51
+ export type VoiceResilienceRoutesOptions = {
52
+ headers?: HeadersInit;
53
+ links?: readonly VoiceResilienceLink[];
54
+ llmProviders?: readonly string[];
55
+ name?: string;
56
+ path?: string;
57
+ render?: (input: VoiceResiliencePageData) => string | Promise<string>;
58
+ sttProviders?: readonly string[];
59
+ sttSimulation?: VoiceResilienceIOSimulator<string>;
60
+ store: VoiceTraceEventStore;
61
+ title?: string;
62
+ ttsProviders?: readonly string[];
63
+ ttsSimulation?: VoiceResilienceIOSimulator<string>;
64
+ };
65
+ export declare const listVoiceRoutingEvents: (events: StoredVoiceTraceEvent[]) => VoiceRoutingEvent[];
66
+ export declare const renderVoiceResilienceHTML: (input: VoiceResiliencePageData) => string;
67
+ export declare const createVoiceResilienceRoutes: (options: VoiceResilienceRoutesOptions) => Elysia<"", {
68
+ decorator: {};
69
+ store: {};
70
+ derive: {};
71
+ resolve: {};
72
+ }, {
73
+ typebox: {};
74
+ error: {};
75
+ }, {
76
+ schema: {};
77
+ standaloneSchema: {};
78
+ macro: {};
79
+ macroFn: {};
80
+ parser: {};
81
+ response: {};
82
+ }, {
83
+ [x: string]: {
84
+ get: {
85
+ body: unknown;
86
+ params: {};
87
+ query: unknown;
88
+ headers: unknown;
89
+ response: {
90
+ 200: Response;
91
+ };
92
+ };
93
+ };
94
+ }, {
95
+ derive: {};
96
+ resolve: {};
97
+ schema: {};
98
+ standaloneSchema: {};
99
+ response: {};
100
+ }, {
101
+ derive: {};
102
+ resolve: {};
103
+ schema: {};
104
+ standaloneSchema: {};
105
+ response: {};
106
+ }>;
@@ -3536,8 +3536,8 @@ var createVoiceIOProviderFailureSimulator = (options) => {
3536
3536
  const now = options.now ?? Date.now;
3537
3537
  const operation = options.operation ?? "open";
3538
3538
  const cooldownMs = Math.max(0, options.cooldownMs ?? 30000);
3539
- const emit = async (event) => {
3540
- await options.onProviderEvent?.(event);
3539
+ const emit = async (event, input) => {
3540
+ await options.onProviderEvent?.(event, input);
3541
3541
  };
3542
3542
  const run = async (provider, mode) => {
3543
3543
  if (!options.providers.includes(provider)) {
@@ -3561,7 +3561,7 @@ var createVoiceIOProviderFailureSimulator = (options) => {
3561
3561
  }),
3562
3562
  selectedProvider: provider,
3563
3563
  status: "success"
3564
- });
3564
+ }, { mode, provider, sessionId });
3565
3565
  return {
3566
3566
  mode,
3567
3567
  provider,
@@ -3594,7 +3594,7 @@ var createVoiceIOProviderFailureSimulator = (options) => {
3594
3594
  selectedProvider: provider,
3595
3595
  status: "error",
3596
3596
  suppressedUntil
3597
- });
3597
+ }, { mode, provider, sessionId });
3598
3598
  if (fallbackProvider) {
3599
3599
  await emit({
3600
3600
  at: startedAt + 1,
@@ -3612,7 +3612,7 @@ var createVoiceIOProviderFailureSimulator = (options) => {
3612
3612
  }),
3613
3613
  selectedProvider: provider,
3614
3614
  status: "fallback"
3615
- });
3615
+ }, { mode, provider, sessionId });
3616
3616
  }
3617
3617
  return {
3618
3618
  fallbackProvider,
@@ -22,7 +22,11 @@ export type VoiceIOProviderFailureSimulatorOptions<TProvider extends string = st
22
22
  kind: VoiceIOProviderFailureSimulationKind;
23
23
  latencyBudgets?: Partial<Record<TProvider, number>>;
24
24
  now?: () => number;
25
- onProviderEvent?: (event: VoiceIOProviderRouterEvent<TProvider>) => Promise<void> | void;
25
+ onProviderEvent?: (event: VoiceIOProviderRouterEvent<TProvider>, input: {
26
+ mode: VoiceIOProviderFailureSimulationMode;
27
+ provider: TProvider;
28
+ sessionId: string;
29
+ }) => Promise<void> | void;
26
30
  operation?: VoiceIOProviderFailureSimulationOperation;
27
31
  providers: readonly TProvider[];
28
32
  recoveryElapsedMs?: number | Partial<Record<TProvider, number>>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@absolutejs/voice",
3
- "version": "0.0.22-beta.35",
3
+ "version": "0.0.22-beta.37",
4
4
  "description": "Voice primitives and Elysia plugin for AbsoluteJS",
5
5
  "repository": {
6
6
  "type": "git",