@absolutejs/voice 0.0.22-beta.17 → 0.0.22-beta.18

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.
@@ -0,0 +1,79 @@
1
+ import { Elysia } from 'elysia';
2
+ import { type VoiceAssistantRunsSummary } from './assistant';
3
+ import { type VoiceProviderHealthSummary } from './providerHealth';
4
+ import type { StoredVoiceTraceEvent, VoiceTraceEventStore } from './trace';
5
+ export type VoiceAssistantHealthFailure = {
6
+ at: number;
7
+ assistantId?: string;
8
+ error?: string;
9
+ provider?: string;
10
+ rateLimited?: boolean;
11
+ sessionId: string;
12
+ status?: string;
13
+ turnId?: string;
14
+ type: StoredVoiceTraceEvent['type'];
15
+ };
16
+ export type VoiceAssistantHealthSummary<TProvider extends string = string> = {
17
+ assistantRuns: VoiceAssistantRunsSummary;
18
+ providerHealth: VoiceProviderHealthSummary<TProvider>[];
19
+ recentFailures: VoiceAssistantHealthFailure[];
20
+ };
21
+ export type VoiceAssistantHealthSummaryOptions<TProvider extends string = string> = {
22
+ events?: StoredVoiceTraceEvent[];
23
+ maxFailures?: number;
24
+ providers?: readonly TProvider[];
25
+ store?: VoiceTraceEventStore;
26
+ };
27
+ export type VoiceAssistantHealthHTMLHandlerOptions<TProvider extends string = string> = VoiceAssistantHealthSummaryOptions<TProvider> & {
28
+ headers?: HeadersInit;
29
+ render?: (summary: VoiceAssistantHealthSummary<TProvider>) => string | Promise<string>;
30
+ };
31
+ export type VoiceAssistantHealthRoutesOptions<TProvider extends string = string> = VoiceAssistantHealthHTMLHandlerOptions<TProvider> & {
32
+ htmlPath?: false | string;
33
+ name?: string;
34
+ path?: string;
35
+ };
36
+ export declare const summarizeVoiceAssistantHealth: <TProvider extends string = string>(options: VoiceAssistantHealthSummaryOptions<TProvider>) => Promise<VoiceAssistantHealthSummary<TProvider>>;
37
+ export declare const renderVoiceAssistantHealthHTML: <TProvider extends string = string>(summary: VoiceAssistantHealthSummary<TProvider>) => string;
38
+ export declare const createVoiceAssistantHealthJSONHandler: <TProvider extends string = string>(options: VoiceAssistantHealthSummaryOptions<TProvider>) => () => Promise<VoiceAssistantHealthSummary<TProvider>>;
39
+ export declare const createVoiceAssistantHealthHTMLHandler: <TProvider extends string = string>(options: VoiceAssistantHealthHTMLHandlerOptions<TProvider>) => () => Promise<Response>;
40
+ export declare const createVoiceAssistantHealthRoutes: <TProvider extends string = string>(options: VoiceAssistantHealthRoutesOptions<TProvider>) => Elysia<"", {
41
+ decorator: {};
42
+ store: {};
43
+ derive: {};
44
+ resolve: {};
45
+ }, {
46
+ typebox: {};
47
+ error: {};
48
+ }, {
49
+ schema: {};
50
+ standaloneSchema: {};
51
+ macro: {};
52
+ macroFn: {};
53
+ parser: {};
54
+ response: {};
55
+ }, {
56
+ [x: string]: {
57
+ get: {
58
+ body: unknown;
59
+ params: {};
60
+ query: unknown;
61
+ headers: unknown;
62
+ response: {
63
+ 200: VoiceAssistantHealthSummary<TProvider>;
64
+ };
65
+ };
66
+ };
67
+ }, {
68
+ derive: {};
69
+ resolve: {};
70
+ schema: {};
71
+ standaloneSchema: {};
72
+ response: {};
73
+ }, {
74
+ derive: {};
75
+ resolve: {};
76
+ schema: {};
77
+ standaloneSchema: {};
78
+ response: {};
79
+ }>;
package/dist/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  export { voice } from './plugin';
2
2
  export { createVoiceAssistant, createVoiceExperiment, summarizeVoiceAssistantRuns } from './assistant';
3
+ export { createVoiceAssistantHealthHTMLHandler, createVoiceAssistantHealthJSONHandler, createVoiceAssistantHealthRoutes, renderVoiceAssistantHealthHTML, summarizeVoiceAssistantHealth } from './assistantHealth';
3
4
  export { createVoiceAgent, createVoiceAgentSquad, createVoiceAgentTool } from './agent';
4
5
  export { createStoredVoiceCallReviewArtifact, createStoredVoiceExternalObjectMap, createStoredVoiceIntegrationEvent, createStoredVoiceOpsTask, createVoiceFileExternalObjectMapStore, createVoiceFileAssistantMemoryStore, createVoiceFileIntegrationEventStore, createVoiceFileReviewStore, createVoiceFileRuntimeStorage, createVoiceFileSessionStore, createVoiceFileTaskStore, createVoiceFileTraceSinkDeliveryStore, createVoiceFileTraceEventStore } from './fileStore';
5
6
  export { createVoiceAssistantMemoryHandle, createVoiceAssistantMemoryRecord, createVoiceMemoryAssistantMemoryStore, resolveVoiceAssistantMemoryNamespace } from './assistantMemory';
@@ -26,6 +27,7 @@ export { resolveVoiceRuntimePreset } from './presets';
26
27
  export { resolveTurnDetectionConfig, TURN_PROFILE_DEFAULTS } from './turnProfiles';
27
28
  export { createVoiceCallReviewFromLiveTelephonyReport, createVoiceCallReviewRecorder, renderVoiceCallReviewHTML, renderVoiceCallReviewMarkdown } from './testing/review';
28
29
  export type { VoiceAssistant, VoiceAssistantArtifactPlan, VoiceAssistantExperiment, VoiceAssistantExperimentOptions, VoiceAssistantGuardrailInput, VoiceAssistantGuardrails, VoiceAssistantMemoryLifecycle, VoiceAssistantMemoryLifecycleInput, VoiceAssistantOptions, VoiceAssistantOutputGuardrailInput, VoiceAssistantPreset, VoiceAssistantRunsSummary, VoiceAssistantRunSummary, VoiceAssistantVariant } from './assistant';
30
+ export type { VoiceAssistantHealthFailure, VoiceAssistantHealthHTMLHandlerOptions, VoiceAssistantHealthRoutesOptions, VoiceAssistantHealthSummary, VoiceAssistantHealthSummaryOptions } from './assistantHealth';
29
31
  export type { VoiceAssistantMemoryBinding, VoiceAssistantMemoryHandle, VoiceAssistantMemoryOptions, VoiceAssistantMemoryRecord, VoiceAssistantMemoryStore } from './assistantMemory';
30
32
  export type { AnthropicVoiceAssistantModelOptions, GeminiVoiceAssistantModelOptions, OpenAIVoiceAssistantModelOptions, VoiceProviderRouterEvent, VoiceProviderRouterFallbackMode, VoiceProviderRouterHealthOptions, VoiceProviderRouterOptions, VoiceProviderRouterPolicy, VoiceProviderRouterProviderHealth, VoiceProviderRouterProviderProfile, VoiceJSONAssistantModelHandler, VoiceJSONAssistantModelOptions } from './modelAdapters';
31
33
  export type { VoiceProviderHealthStatus, VoiceProviderHealthSummary, VoiceProviderHealthSummaryOptions } from './providerHealth';
package/dist/index.js CHANGED
@@ -6101,6 +6101,292 @@ var summarizeVoiceAssistantRuns = async (input) => {
6101
6101
  totalRuns: assistantRuns.length
6102
6102
  };
6103
6103
  };
6104
+ // src/assistantHealth.ts
6105
+ import { Elysia as Elysia3 } from "elysia";
6106
+
6107
+ // src/providerHealth.ts
6108
+ import { Elysia as Elysia2 } from "elysia";
6109
+ var getString = (value) => typeof value === "string" ? value : undefined;
6110
+ var getNumber = (value) => typeof value === "number" && Number.isFinite(value) ? value : undefined;
6111
+ var isProviderStatus = (value) => value === "success" || value === "fallback" || value === "error";
6112
+ var summarizeVoiceProviderHealth = async (input) => {
6113
+ const options = Array.isArray(input) ? { events: input } : input;
6114
+ const events = options.events ?? await options.store?.list() ?? [];
6115
+ const providers = options.providers ?? [];
6116
+ const providerSet = new Set(providers);
6117
+ const now = options.now ?? Date.now();
6118
+ const entries = new Map;
6119
+ const isAllowedProvider = (value) => typeof value === "string" && (providerSet.size === 0 || providerSet.has(value));
6120
+ const getEntry = (provider) => {
6121
+ const existing = entries.get(provider);
6122
+ if (existing) {
6123
+ return existing;
6124
+ }
6125
+ const entry = {
6126
+ elapsedCount: 0,
6127
+ elapsedTotal: 0,
6128
+ errorCount: 0,
6129
+ fallbackCount: 0,
6130
+ provider,
6131
+ rateLimited: false,
6132
+ recommended: false,
6133
+ runCount: 0,
6134
+ status: "idle"
6135
+ };
6136
+ entries.set(provider, entry);
6137
+ return entry;
6138
+ };
6139
+ for (const provider of providers) {
6140
+ getEntry(provider);
6141
+ }
6142
+ const hasProviderRouterEvents = events.some((event) => event.type === "session.error" && isAllowedProvider(event.payload.provider) && isProviderStatus(event.payload.providerStatus));
6143
+ for (const event of events) {
6144
+ if (event.type === "assistant.run") {
6145
+ if (hasProviderRouterEvents) {
6146
+ continue;
6147
+ }
6148
+ const provider2 = event.payload.variantId;
6149
+ if (!isAllowedProvider(provider2)) {
6150
+ continue;
6151
+ }
6152
+ const entry2 = getEntry(provider2);
6153
+ entry2.runCount += 1;
6154
+ const elapsedMs = getNumber(event.payload.elapsedMs);
6155
+ if (elapsedMs !== undefined) {
6156
+ entry2.elapsedCount += 1;
6157
+ entry2.elapsedTotal += elapsedMs;
6158
+ }
6159
+ continue;
6160
+ }
6161
+ if (event.type !== "session.error") {
6162
+ continue;
6163
+ }
6164
+ const provider = event.payload.provider;
6165
+ if (!isAllowedProvider(provider)) {
6166
+ continue;
6167
+ }
6168
+ const providerStatus = isProviderStatus(event.payload.providerStatus) ? event.payload.providerStatus : undefined;
6169
+ const applyProviderHealth = () => {
6170
+ const entry2 = getEntry(provider);
6171
+ const providerHealth = event.payload.providerHealth;
6172
+ if (providerHealth && typeof providerHealth === "object") {
6173
+ const suppressedUntil2 = getNumber(providerHealth.suppressedUntil);
6174
+ if (suppressedUntil2 !== undefined) {
6175
+ entry2.suppressedUntil = suppressedUntil2;
6176
+ }
6177
+ }
6178
+ const suppressedUntil = getNumber(event.payload.suppressedUntil);
6179
+ if (suppressedUntil !== undefined) {
6180
+ entry2.suppressedUntil = suppressedUntil;
6181
+ }
6182
+ const suppressionRemainingMs = getNumber(event.payload.suppressionRemainingMs);
6183
+ if (suppressionRemainingMs !== undefined) {
6184
+ entry2.suppressionRemainingMs = suppressionRemainingMs;
6185
+ }
6186
+ return entry2;
6187
+ };
6188
+ if (providerStatus === "success" || providerStatus === "fallback") {
6189
+ const entry2 = applyProviderHealth();
6190
+ entry2.runCount += 1;
6191
+ entry2.lastSuccessAt = event.at;
6192
+ if (providerStatus === "success") {
6193
+ entry2.lastError = undefined;
6194
+ entry2.rateLimited = false;
6195
+ entry2.suppressedUntil = undefined;
6196
+ entry2.suppressionRemainingMs = undefined;
6197
+ }
6198
+ const elapsedMs = getNumber(event.payload.elapsedMs);
6199
+ if (elapsedMs !== undefined) {
6200
+ entry2.elapsedCount += 1;
6201
+ entry2.elapsedTotal += elapsedMs;
6202
+ }
6203
+ const selectedProvider = event.payload.selectedProvider;
6204
+ if (providerStatus === "fallback" && isAllowedProvider(selectedProvider) && selectedProvider !== provider) {
6205
+ getEntry(selectedProvider).fallbackCount += 1;
6206
+ }
6207
+ continue;
6208
+ }
6209
+ const entry = applyProviderHealth();
6210
+ entry.errorCount += 1;
6211
+ entry.lastError = getString(event.payload.error);
6212
+ entry.lastErrorAt = event.at;
6213
+ entry.rateLimited ||= event.payload.rateLimited === true;
6214
+ }
6215
+ const summaries = [...entries.values()].map((entry) => {
6216
+ const hadSuppression = typeof entry.suppressedUntil === "number" || typeof entry.suppressionRemainingMs === "number";
6217
+ const suppressionRemainingMs = typeof entry.suppressedUntil === "number" ? Math.max(0, entry.suppressedUntil - now) : entry.suppressionRemainingMs;
6218
+ const activeSuppression = typeof suppressionRemainingMs === "number" && suppressionRemainingMs > 0;
6219
+ const recoverable = hadSuppression && !activeSuppression;
6220
+ const averageElapsedMs = entry.elapsedCount > 0 ? Math.round(entry.elapsedTotal / entry.elapsedCount) : undefined;
6221
+ const status = activeSuppression ? "suppressed" : recoverable ? "recoverable" : entry.rateLimited ? "rate-limited" : entry.errorCount > 0 && (!entry.lastSuccessAt || !entry.lastErrorAt || entry.lastErrorAt > entry.lastSuccessAt) ? "degraded" : entry.runCount > 0 ? "healthy" : "idle";
6222
+ return {
6223
+ averageElapsedMs,
6224
+ errorCount: entry.errorCount,
6225
+ fallbackCount: entry.fallbackCount,
6226
+ lastError: entry.lastError,
6227
+ lastErrorAt: entry.lastErrorAt,
6228
+ lastSuccessAt: entry.lastSuccessAt,
6229
+ provider: entry.provider,
6230
+ rateLimited: entry.rateLimited,
6231
+ recommended: false,
6232
+ runCount: entry.runCount,
6233
+ status,
6234
+ suppressionRemainingMs: activeSuppression ? suppressionRemainingMs : undefined,
6235
+ suppressedUntil: entry.suppressedUntil
6236
+ };
6237
+ });
6238
+ const recommended = summaries.filter((entry) => entry.status === "healthy").sort((left, right) => (left.averageElapsedMs ?? Number.MAX_SAFE_INTEGER) - (right.averageElapsedMs ?? Number.MAX_SAFE_INTEGER))[0];
6239
+ if (recommended) {
6240
+ recommended.recommended = true;
6241
+ }
6242
+ return summaries;
6243
+ };
6244
+ var escapeHtml3 = (value) => value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
6245
+ var renderVoiceProviderHealthHTML = (providers) => providers.length === 0 ? '<p class="voice-provider-empty">No provider status yet.</p>' : [
6246
+ '<div class="voice-provider-health">',
6247
+ ...providers.map((provider) => {
6248
+ const suppressionSeconds = typeof provider.suppressionRemainingMs === "number" ? Math.ceil(provider.suppressionRemainingMs / 1000) : undefined;
6249
+ return [
6250
+ `<article class="voice-provider-card ${escapeHtml3(provider.status)}">`,
6251
+ '<div class="voice-provider-card-header">',
6252
+ `<strong>${escapeHtml3(provider.provider)}</strong>`,
6253
+ `<span>${escapeHtml3(provider.status)}${provider.recommended ? " \xB7 recommended" : ""}</span>`,
6254
+ "</div>",
6255
+ "<dl>",
6256
+ `<div><dt>Runs</dt><dd>${String(provider.runCount)}</dd></div>`,
6257
+ `<div><dt>Avg latency</dt><dd>${String(provider.averageElapsedMs ?? 0)}ms</dd></div>`,
6258
+ `<div><dt>Errors</dt><dd>${String(provider.errorCount)}</dd></div>`,
6259
+ `<div><dt>Fallbacks</dt><dd>${String(provider.fallbackCount)}</dd></div>`,
6260
+ "</dl>",
6261
+ suppressionSeconds ? `<p>Temporarily suppressed for ${String(suppressionSeconds)}s.</p>` : "",
6262
+ provider.lastError ? `<p>${escapeHtml3(provider.lastError)}</p>` : "",
6263
+ "</article>"
6264
+ ].join("");
6265
+ }),
6266
+ "</div>"
6267
+ ].join("");
6268
+ var createVoiceProviderHealthJSONHandler = (options) => async () => summarizeVoiceProviderHealth(options);
6269
+ var createVoiceProviderHealthHTMLHandler = (options) => async () => {
6270
+ const providers = await summarizeVoiceProviderHealth(options);
6271
+ const render = options.render ?? renderVoiceProviderHealthHTML;
6272
+ const body = await render(providers);
6273
+ return new Response(body, {
6274
+ headers: {
6275
+ "Content-Type": "text/html; charset=utf-8",
6276
+ ...options.headers
6277
+ }
6278
+ });
6279
+ };
6280
+ var createVoiceProviderHealthRoutes = (options) => {
6281
+ const path = options.path ?? "/api/provider-status";
6282
+ const htmlPath = options.htmlPath === undefined ? `${path}/htmx` : options.htmlPath;
6283
+ const routes = new Elysia2({
6284
+ name: options.name ?? "absolutejs-voice-provider-health"
6285
+ }).get(path, createVoiceProviderHealthJSONHandler(options));
6286
+ if (htmlPath) {
6287
+ routes.get(htmlPath, createVoiceProviderHealthHTMLHandler(options));
6288
+ }
6289
+ return routes;
6290
+ };
6291
+
6292
+ // src/assistantHealth.ts
6293
+ var escapeHtml4 = (value) => value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
6294
+ var renderCountMap = (values) => {
6295
+ const entries = Object.entries(values).sort((left, right) => right[1] - left[1]);
6296
+ if (entries.length === 0) {
6297
+ return '<p class="voice-assistant-health-empty">No data yet.</p>';
6298
+ }
6299
+ return [
6300
+ '<div class="voice-assistant-health-metrics">',
6301
+ ...entries.map(([label, value]) => `<div><span>${escapeHtml4(label)}</span><strong>${String(value)}</strong></div>`),
6302
+ "</div>"
6303
+ ].join("");
6304
+ };
6305
+ var getString2 = (value) => typeof value === "string" ? value : undefined;
6306
+ var getRecentFailures = (events, maxFailures) => events.filter((event) => event.type === "session.error" || event.type === "assistant.guardrail" && event.payload.action === "blocked").toReversed().slice(0, maxFailures).map((event) => ({
6307
+ at: event.at,
6308
+ assistantId: getString2(event.payload.assistantId),
6309
+ error: getString2(event.payload.error),
6310
+ provider: getString2(event.payload.provider),
6311
+ rateLimited: event.payload.rateLimited === true ? true : undefined,
6312
+ sessionId: event.sessionId,
6313
+ status: getString2(event.payload.providerStatus),
6314
+ turnId: event.turnId,
6315
+ type: event.type
6316
+ }));
6317
+ var summarizeVoiceAssistantHealth = async (options) => {
6318
+ const events = options.events ?? await options.store?.list() ?? [];
6319
+ return {
6320
+ assistantRuns: await summarizeVoiceAssistantRuns({ events }),
6321
+ providerHealth: await summarizeVoiceProviderHealth({
6322
+ events,
6323
+ providers: options.providers
6324
+ }),
6325
+ recentFailures: getRecentFailures(events, options.maxFailures ?? 8)
6326
+ };
6327
+ };
6328
+ var renderVoiceAssistantHealthHTML = (summary) => {
6329
+ const assistant = summary.assistantRuns.assistants[0];
6330
+ const failures = summary.recentFailures;
6331
+ return [
6332
+ '<div class="voice-assistant-health">',
6333
+ '<section class="voice-assistant-health-grid">',
6334
+ `<article><span>Runs</span><strong>${String(assistant?.runCount ?? 0)}</strong></article>`,
6335
+ `<article><span>Sessions</span><strong>${String(assistant?.sessions ?? 0)}</strong></article>`,
6336
+ `<article><span>Guardrails</span><strong>${String(assistant?.guardrailCount ?? 0)}</strong></article>`,
6337
+ `<article><span>Avg latency</span><strong>${String(assistant?.averageElapsedMs ?? 0)}ms</strong></article>`,
6338
+ "</section>",
6339
+ "<section>",
6340
+ "<h3>Provider Health</h3>",
6341
+ renderVoiceProviderHealthHTML(summary.providerHealth),
6342
+ "</section>",
6343
+ '<section class="voice-assistant-health-columns">',
6344
+ `<article><h3>Outcomes</h3>${renderCountMap(assistant?.outcomes ?? {})}</article>`,
6345
+ `<article><h3>Variants</h3>${renderCountMap(assistant?.variants ?? {})}</article>`,
6346
+ `<article><h3>Tools</h3>${renderCountMap(assistant?.toolCalls ?? {})}</article>`,
6347
+ `<article><h3>Artifact Plans</h3>${renderCountMap(assistant?.artifactPlans ?? {})}</article>`,
6348
+ "</section>",
6349
+ "<section>",
6350
+ "<h3>Recent Failures</h3>",
6351
+ failures.length === 0 ? '<p class="voice-assistant-health-empty">No failures yet.</p>' : [
6352
+ '<div class="voice-assistant-health-failures">',
6353
+ ...failures.map((failure) => [
6354
+ "<article>",
6355
+ `<strong>${escapeHtml4(failure.provider ?? failure.assistantId ?? failure.type)}</strong>`,
6356
+ `<span>${escapeHtml4(failure.status ?? (failure.rateLimited ? "rate-limited" : "error"))}</span>`,
6357
+ failure.error ? `<p>${escapeHtml4(failure.error)}</p>` : "",
6358
+ `<small>${escapeHtml4(failure.sessionId)}${failure.turnId ? ` / ${escapeHtml4(failure.turnId)}` : ""}</small>`,
6359
+ "</article>"
6360
+ ].join("")),
6361
+ "</div>"
6362
+ ].join(""),
6363
+ "</section>",
6364
+ "</div>"
6365
+ ].join("");
6366
+ };
6367
+ var createVoiceAssistantHealthJSONHandler = (options) => async () => summarizeVoiceAssistantHealth(options);
6368
+ var createVoiceAssistantHealthHTMLHandler = (options) => async () => {
6369
+ const summary = await summarizeVoiceAssistantHealth(options);
6370
+ const render = options.render ?? renderVoiceAssistantHealthHTML;
6371
+ const body = await render(summary);
6372
+ return new Response(body, {
6373
+ headers: {
6374
+ "Content-Type": "text/html; charset=utf-8",
6375
+ ...options.headers
6376
+ }
6377
+ });
6378
+ };
6379
+ var createVoiceAssistantHealthRoutes = (options) => {
6380
+ const path = options.path ?? "/api/assistant-health";
6381
+ const htmlPath = options.htmlPath === undefined ? `${path}/htmx` : options.htmlPath;
6382
+ const routes = new Elysia3({
6383
+ name: options.name ?? "absolutejs-voice-assistant-health"
6384
+ }).get(path, createVoiceAssistantHealthJSONHandler(options));
6385
+ if (htmlPath) {
6386
+ routes.get(htmlPath, createVoiceAssistantHealthHTMLHandler(options));
6387
+ }
6388
+ return routes;
6389
+ };
6104
6390
  // src/fileStore.ts
6105
6391
  import { mkdir, readFile, readdir, rename, rm, writeFile } from "fs/promises";
6106
6392
  import { join } from "path";
@@ -6437,7 +6723,7 @@ var exportVoiceTrace = async (input) => {
6437
6723
  };
6438
6724
  };
6439
6725
  var toNumber = (value) => typeof value === "number" && Number.isFinite(value) ? value : 0;
6440
- var escapeHtml3 = (value) => value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
6726
+ var escapeHtml5 = (value) => value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
6441
6727
  var formatTraceValue = (value) => {
6442
6728
  if (value === undefined || value === null) {
6443
6729
  return "";
@@ -6715,10 +7001,10 @@ var renderVoiceTraceHTML = (events, options = {}) => {
6715
7001
  const offset = summary.startedAt === undefined ? event.at : Math.max(0, event.at - summary.startedAt);
6716
7002
  return [
6717
7003
  "<tr>",
6718
- `<td>${escapeHtml3(String(offset))}</td>`,
6719
- `<td>${escapeHtml3(event.type)}</td>`,
6720
- `<td>${escapeHtml3(event.turnId ?? "")}</td>`,
6721
- `<td><code>${escapeHtml3(JSON.stringify(event.payload))}</code></td>`,
7004
+ `<td>${escapeHtml5(String(offset))}</td>`,
7005
+ `<td>${escapeHtml5(event.type)}</td>`,
7006
+ `<td>${escapeHtml5(event.turnId ?? "")}</td>`,
7007
+ `<td><code>${escapeHtml5(JSON.stringify(event.payload))}</code></td>`,
6722
7008
  "</tr>"
6723
7009
  ].join("");
6724
7010
  }).join(`
@@ -6729,7 +7015,7 @@ var renderVoiceTraceHTML = (events, options = {}) => {
6729
7015
  "<head>",
6730
7016
  '<meta charset="utf-8" />',
6731
7017
  '<meta name="viewport" content="width=device-width, initial-scale=1" />',
6732
- `<title>${escapeHtml3(options.title ?? "Voice Trace")}</title>`,
7018
+ `<title>${escapeHtml5(options.title ?? "Voice Trace")}</title>`,
6733
7019
  "<style>",
6734
7020
  "body{font-family:ui-sans-serif,system-ui,sans-serif;margin:2rem;line-height:1.45;background:#f8f7f2;color:#181713}",
6735
7021
  "main{max-width:1100px;margin:auto}",
@@ -6743,7 +7029,7 @@ var renderVoiceTraceHTML = (events, options = {}) => {
6743
7029
  "</style>",
6744
7030
  "</head>",
6745
7031
  "<body><main>",
6746
- `<h1>${escapeHtml3(options.title ?? `Voice Trace ${summary.sessionId ?? ""}`.trim())}</h1>`,
7032
+ `<h1>${escapeHtml5(options.title ?? `Voice Trace ${summary.sessionId ?? ""}`.trim())}</h1>`,
6747
7033
  `<p class="${evaluation.pass ? "pass" : "fail"}">QA: ${evaluation.pass ? "pass" : "fail"}</p>`,
6748
7034
  '<section class="summary">',
6749
7035
  `<div class="card"><strong>Events</strong><br>${summary.eventCount}</div>`,
@@ -6757,7 +7043,7 @@ var renderVoiceTraceHTML = (events, options = {}) => {
6757
7043
  eventRows,
6758
7044
  "</tbody></table>",
6759
7045
  "<h2>Markdown Export</h2>",
6760
- `<pre>${escapeHtml3(markdown)}</pre>`,
7046
+ `<pre>${escapeHtml5(markdown)}</pre>`,
6761
7047
  "</main></body></html>"
6762
7048
  ].join(`
6763
7049
  `);
@@ -7855,190 +8141,6 @@ var createGeminiVoiceAssistantModel = (options) => {
7855
8141
  }
7856
8142
  };
7857
8143
  };
7858
- // src/providerHealth.ts
7859
- import { Elysia as Elysia2 } from "elysia";
7860
- var getString = (value) => typeof value === "string" ? value : undefined;
7861
- var getNumber = (value) => typeof value === "number" && Number.isFinite(value) ? value : undefined;
7862
- var isProviderStatus = (value) => value === "success" || value === "fallback" || value === "error";
7863
- var summarizeVoiceProviderHealth = async (input) => {
7864
- const options = Array.isArray(input) ? { events: input } : input;
7865
- const events = options.events ?? await options.store?.list() ?? [];
7866
- const providers = options.providers ?? [];
7867
- const providerSet = new Set(providers);
7868
- const now = options.now ?? Date.now();
7869
- const entries = new Map;
7870
- const isAllowedProvider = (value) => typeof value === "string" && (providerSet.size === 0 || providerSet.has(value));
7871
- const getEntry = (provider) => {
7872
- const existing = entries.get(provider);
7873
- if (existing) {
7874
- return existing;
7875
- }
7876
- const entry = {
7877
- elapsedCount: 0,
7878
- elapsedTotal: 0,
7879
- errorCount: 0,
7880
- fallbackCount: 0,
7881
- provider,
7882
- rateLimited: false,
7883
- recommended: false,
7884
- runCount: 0,
7885
- status: "idle"
7886
- };
7887
- entries.set(provider, entry);
7888
- return entry;
7889
- };
7890
- for (const provider of providers) {
7891
- getEntry(provider);
7892
- }
7893
- const hasProviderRouterEvents = events.some((event) => event.type === "session.error" && isAllowedProvider(event.payload.provider) && isProviderStatus(event.payload.providerStatus));
7894
- for (const event of events) {
7895
- if (event.type === "assistant.run") {
7896
- if (hasProviderRouterEvents) {
7897
- continue;
7898
- }
7899
- const provider2 = event.payload.variantId;
7900
- if (!isAllowedProvider(provider2)) {
7901
- continue;
7902
- }
7903
- const entry2 = getEntry(provider2);
7904
- entry2.runCount += 1;
7905
- const elapsedMs = getNumber(event.payload.elapsedMs);
7906
- if (elapsedMs !== undefined) {
7907
- entry2.elapsedCount += 1;
7908
- entry2.elapsedTotal += elapsedMs;
7909
- }
7910
- continue;
7911
- }
7912
- if (event.type !== "session.error") {
7913
- continue;
7914
- }
7915
- const provider = event.payload.provider;
7916
- if (!isAllowedProvider(provider)) {
7917
- continue;
7918
- }
7919
- const providerStatus = isProviderStatus(event.payload.providerStatus) ? event.payload.providerStatus : undefined;
7920
- const applyProviderHealth = () => {
7921
- const entry2 = getEntry(provider);
7922
- const providerHealth = event.payload.providerHealth;
7923
- if (providerHealth && typeof providerHealth === "object") {
7924
- const suppressedUntil2 = getNumber(providerHealth.suppressedUntil);
7925
- if (suppressedUntil2 !== undefined) {
7926
- entry2.suppressedUntil = suppressedUntil2;
7927
- }
7928
- }
7929
- const suppressedUntil = getNumber(event.payload.suppressedUntil);
7930
- if (suppressedUntil !== undefined) {
7931
- entry2.suppressedUntil = suppressedUntil;
7932
- }
7933
- const suppressionRemainingMs = getNumber(event.payload.suppressionRemainingMs);
7934
- if (suppressionRemainingMs !== undefined) {
7935
- entry2.suppressionRemainingMs = suppressionRemainingMs;
7936
- }
7937
- return entry2;
7938
- };
7939
- if (providerStatus === "success" || providerStatus === "fallback") {
7940
- const entry2 = applyProviderHealth();
7941
- entry2.runCount += 1;
7942
- entry2.lastSuccessAt = event.at;
7943
- if (providerStatus === "success") {
7944
- entry2.lastError = undefined;
7945
- entry2.rateLimited = false;
7946
- entry2.suppressedUntil = undefined;
7947
- entry2.suppressionRemainingMs = undefined;
7948
- }
7949
- const elapsedMs = getNumber(event.payload.elapsedMs);
7950
- if (elapsedMs !== undefined) {
7951
- entry2.elapsedCount += 1;
7952
- entry2.elapsedTotal += elapsedMs;
7953
- }
7954
- const selectedProvider = event.payload.selectedProvider;
7955
- if (providerStatus === "fallback" && isAllowedProvider(selectedProvider) && selectedProvider !== provider) {
7956
- getEntry(selectedProvider).fallbackCount += 1;
7957
- }
7958
- continue;
7959
- }
7960
- const entry = applyProviderHealth();
7961
- entry.errorCount += 1;
7962
- entry.lastError = getString(event.payload.error);
7963
- entry.lastErrorAt = event.at;
7964
- entry.rateLimited ||= event.payload.rateLimited === true;
7965
- }
7966
- const summaries = [...entries.values()].map((entry) => {
7967
- const hadSuppression = typeof entry.suppressedUntil === "number" || typeof entry.suppressionRemainingMs === "number";
7968
- const suppressionRemainingMs = typeof entry.suppressedUntil === "number" ? Math.max(0, entry.suppressedUntil - now) : entry.suppressionRemainingMs;
7969
- const activeSuppression = typeof suppressionRemainingMs === "number" && suppressionRemainingMs > 0;
7970
- const recoverable = hadSuppression && !activeSuppression;
7971
- const averageElapsedMs = entry.elapsedCount > 0 ? Math.round(entry.elapsedTotal / entry.elapsedCount) : undefined;
7972
- const status = activeSuppression ? "suppressed" : recoverable ? "recoverable" : entry.rateLimited ? "rate-limited" : entry.errorCount > 0 && (!entry.lastSuccessAt || !entry.lastErrorAt || entry.lastErrorAt > entry.lastSuccessAt) ? "degraded" : entry.runCount > 0 ? "healthy" : "idle";
7973
- return {
7974
- averageElapsedMs,
7975
- errorCount: entry.errorCount,
7976
- fallbackCount: entry.fallbackCount,
7977
- lastError: entry.lastError,
7978
- lastErrorAt: entry.lastErrorAt,
7979
- lastSuccessAt: entry.lastSuccessAt,
7980
- provider: entry.provider,
7981
- rateLimited: entry.rateLimited,
7982
- recommended: false,
7983
- runCount: entry.runCount,
7984
- status,
7985
- suppressionRemainingMs: activeSuppression ? suppressionRemainingMs : undefined,
7986
- suppressedUntil: entry.suppressedUntil
7987
- };
7988
- });
7989
- const recommended = summaries.filter((entry) => entry.status === "healthy").sort((left, right) => (left.averageElapsedMs ?? Number.MAX_SAFE_INTEGER) - (right.averageElapsedMs ?? Number.MAX_SAFE_INTEGER))[0];
7990
- if (recommended) {
7991
- recommended.recommended = true;
7992
- }
7993
- return summaries;
7994
- };
7995
- var escapeHtml4 = (value) => value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
7996
- var renderVoiceProviderHealthHTML = (providers) => providers.length === 0 ? '<p class="voice-provider-empty">No provider status yet.</p>' : [
7997
- '<div class="voice-provider-health">',
7998
- ...providers.map((provider) => {
7999
- const suppressionSeconds = typeof provider.suppressionRemainingMs === "number" ? Math.ceil(provider.suppressionRemainingMs / 1000) : undefined;
8000
- return [
8001
- `<article class="voice-provider-card ${escapeHtml4(provider.status)}">`,
8002
- '<div class="voice-provider-card-header">',
8003
- `<strong>${escapeHtml4(provider.provider)}</strong>`,
8004
- `<span>${escapeHtml4(provider.status)}${provider.recommended ? " \xB7 recommended" : ""}</span>`,
8005
- "</div>",
8006
- "<dl>",
8007
- `<div><dt>Runs</dt><dd>${String(provider.runCount)}</dd></div>`,
8008
- `<div><dt>Avg latency</dt><dd>${String(provider.averageElapsedMs ?? 0)}ms</dd></div>`,
8009
- `<div><dt>Errors</dt><dd>${String(provider.errorCount)}</dd></div>`,
8010
- `<div><dt>Fallbacks</dt><dd>${String(provider.fallbackCount)}</dd></div>`,
8011
- "</dl>",
8012
- suppressionSeconds ? `<p>Temporarily suppressed for ${String(suppressionSeconds)}s.</p>` : "",
8013
- provider.lastError ? `<p>${escapeHtml4(provider.lastError)}</p>` : "",
8014
- "</article>"
8015
- ].join("");
8016
- }),
8017
- "</div>"
8018
- ].join("");
8019
- var createVoiceProviderHealthJSONHandler = (options) => async () => summarizeVoiceProviderHealth(options);
8020
- var createVoiceProviderHealthHTMLHandler = (options) => async () => {
8021
- const providers = await summarizeVoiceProviderHealth(options);
8022
- const render = options.render ?? renderVoiceProviderHealthHTML;
8023
- const body = await render(providers);
8024
- return new Response(body, {
8025
- headers: {
8026
- "Content-Type": "text/html; charset=utf-8",
8027
- ...options.headers
8028
- }
8029
- });
8030
- };
8031
- var createVoiceProviderHealthRoutes = (options) => {
8032
- const path = options.path ?? "/api/provider-status";
8033
- const htmlPath = options.htmlPath === undefined ? `${path}/htmx` : options.htmlPath;
8034
- const routes = new Elysia2({
8035
- name: options.name ?? "absolutejs-voice-provider-health"
8036
- }).get(path, createVoiceProviderHealthJSONHandler(options));
8037
- if (htmlPath) {
8038
- routes.get(htmlPath, createVoiceProviderHealthHTMLHandler(options));
8039
- }
8040
- return routes;
8041
- };
8042
8144
  // src/sqliteStore.ts
8043
8145
  import { Database } from "bun:sqlite";
8044
8146
  var normalizeTableNameSegment = (value) => value.trim().replace(/[^a-zA-Z0-9_]+/g, "_").replace(/^_+|_+$/g, "") || "voice";
@@ -10495,6 +10597,7 @@ export {
10495
10597
  summarizeVoiceOpsTaskAnalytics,
10496
10598
  summarizeVoiceIntegrationEvents,
10497
10599
  summarizeVoiceAssistantRuns,
10600
+ summarizeVoiceAssistantHealth,
10498
10601
  startVoiceOpsTask,
10499
10602
  shapeTelephonyAssistantText,
10500
10603
  selectVoiceTraceEventsForPrune,
@@ -10516,6 +10619,7 @@ export {
10516
10619
  renderVoiceProviderHealthHTML,
10517
10620
  renderVoiceCallReviewMarkdown,
10518
10621
  renderVoiceCallReviewHTML,
10622
+ renderVoiceAssistantHealthHTML,
10519
10623
  redactVoiceTraceText,
10520
10624
  redactVoiceTraceEvents,
10521
10625
  redactVoiceTraceEvent,
@@ -10618,6 +10722,9 @@ export {
10618
10722
  createVoiceCRMActivitySink,
10619
10723
  createVoiceAssistantMemoryRecord,
10620
10724
  createVoiceAssistantMemoryHandle,
10725
+ createVoiceAssistantHealthRoutes,
10726
+ createVoiceAssistantHealthJSONHandler,
10727
+ createVoiceAssistantHealthHTMLHandler,
10621
10728
  createVoiceAssistant,
10622
10729
  createVoiceAgentTool,
10623
10730
  createVoiceAgentSquad,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@absolutejs/voice",
3
- "version": "0.0.22-beta.17",
3
+ "version": "0.0.22-beta.18",
4
4
  "description": "Voice primitives and Elysia plugin for AbsoluteJS",
5
5
  "repository": {
6
6
  "type": "git",