@absolutejs/voice 0.0.22-beta.41 → 0.0.22-beta.42

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
@@ -7623,230 +7623,186 @@ var createVoiceDiagnosticsRoutes = (options) => {
7623
7623
  });
7624
7624
  return routes;
7625
7625
  };
7626
- // src/sessionReplay.ts
7626
+ // src/evalRoutes.ts
7627
+ import { Elysia as Elysia7 } from "elysia";
7628
+
7629
+ // src/qualityRoutes.ts
7630
+ import { Elysia as Elysia6 } from "elysia";
7631
+
7632
+ // src/handoffHealth.ts
7627
7633
  import { Elysia as Elysia5 } from "elysia";
7628
- var getString4 = (value) => typeof value === "string" ? value : undefined;
7629
7634
  var escapeHtml7 = (value) => value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
7635
+ var getString4 = (value) => typeof value === "string" && value.length > 0 ? value : undefined;
7636
+ var isStatus = (value) => value === "delivered" || value === "failed" || value === "skipped";
7630
7637
  var increment2 = (record, key) => {
7631
7638
  record[key] = (record[key] ?? 0) + 1;
7632
7639
  };
7633
- var buildReplayTurns = (events) => {
7634
- const turns = new Map;
7635
- const getTurn = (turnId) => {
7636
- const existing = turns.get(turnId);
7637
- if (existing) {
7638
- return existing;
7639
- }
7640
- const turn = {
7641
- assistantReplies: [],
7642
- errors: [],
7643
- id: turnId,
7644
- modelCalls: [],
7645
- tools: [],
7646
- transcripts: []
7647
- };
7648
- turns.set(turnId, turn);
7649
- return turn;
7650
- };
7651
- for (const event of events) {
7652
- const turnId = event.turnId ?? "session";
7653
- const turn = getTurn(turnId);
7654
- switch (event.type) {
7655
- case "turn.transcript":
7656
- turn.transcripts.push({
7657
- isFinal: event.payload.isFinal === true,
7658
- text: getString4(event.payload.text)
7659
- });
7660
- break;
7661
- case "turn.committed":
7662
- turn.committedText = getString4(event.payload.text);
7663
- break;
7664
- case "turn.assistant": {
7665
- const text = getString4(event.payload.text);
7666
- if (text) {
7667
- turn.assistantReplies.push(text);
7668
- }
7669
- break;
7670
- }
7671
- case "agent.model":
7672
- case "assistant.run":
7673
- turn.modelCalls.push(event.payload);
7674
- break;
7675
- case "agent.tool":
7676
- turn.tools.push(event.payload);
7677
- break;
7678
- case "session.error":
7679
- turn.errors.push(event.payload);
7680
- break;
7681
- }
7682
- }
7683
- return [...turns.values()];
7684
- };
7685
- var summarizeVoiceSessionReplay = async (options) => {
7686
- const sourceEvents = options.events ?? await options.store?.list({ sessionId: options.sessionId }) ?? [];
7687
- const events = filterVoiceTraceEvents(sourceEvents, {
7688
- sessionId: options.sessionId
7689
- });
7690
- const replay = buildVoiceTraceReplay(events, {
7691
- evaluation: options.evaluation,
7692
- redact: options.redact,
7693
- title: options.title ?? `Voice Session ${options.sessionId}`
7694
- });
7695
- const startedAt = replay.summary.startedAt;
7640
+ var normalizeDelivery = (adapterId, value) => {
7641
+ const record = value && typeof value === "object" ? value : {};
7696
7642
  return {
7697
- evaluation: replay.evaluation,
7698
- events,
7699
- html: replay.html,
7700
- markdown: replay.markdown,
7701
- sessionId: options.sessionId,
7702
- summary: replay.summary,
7703
- timeline: events.map((event) => ({
7704
- at: event.at,
7705
- offsetMs: startedAt === undefined ? undefined : Math.max(0, event.at - startedAt),
7706
- payload: event.payload,
7707
- turnId: event.turnId,
7708
- type: event.type
7709
- })),
7710
- turns: buildReplayTurns(events)
7643
+ adapterId: getString4(record.adapterId) ?? adapterId,
7644
+ adapterKind: getString4(record.adapterKind),
7645
+ deliveredAt: typeof record.deliveredAt === "number" ? record.deliveredAt : undefined,
7646
+ deliveredTo: getString4(record.deliveredTo),
7647
+ error: getString4(record.error),
7648
+ status: isStatus(record.status) ? record.status : "failed"
7711
7649
  };
7712
7650
  };
7713
- var summarizeVoiceSessions = async (options = {}) => {
7714
- const events = options.events ?? await options.store?.list() ?? [];
7715
- const grouped = new Map;
7716
- for (const event of events) {
7717
- grouped.set(event.sessionId, [...grouped.get(event.sessionId) ?? [], event]);
7651
+ var normalizeDeliveries = (payload) => {
7652
+ const deliveries = payload.deliveries;
7653
+ if (!deliveries || typeof deliveries !== "object") {
7654
+ return [];
7718
7655
  }
7719
- const sessions = [...grouped.entries()].map(([sessionId, sessionEvents]) => {
7720
- const sorted = filterVoiceTraceEvents(sessionEvents);
7721
- const summary = buildVoiceTraceReplay(sorted, {
7722
- evaluation: {
7723
- requireAssistantReply: false,
7724
- requireCompletedCall: false,
7725
- requireTranscript: false,
7726
- requireTurn: false
7727
- }
7728
- }).summary;
7729
- const providerErrors = {};
7730
- const providers = new Set;
7731
- let latestOutcome;
7732
- let errorCount = 0;
7733
- for (const event of sorted) {
7734
- const provider = getString4(event.payload.provider);
7735
- if (provider) {
7736
- providers.add(provider);
7737
- }
7738
- if (event.type === "session.error" && (event.payload.providerStatus === "error" || typeof event.payload.error === "string")) {
7739
- errorCount += 1;
7740
- increment2(providerErrors, provider ?? "unknown");
7741
- }
7742
- const outcome = getString4(event.payload.outcome);
7743
- if (outcome) {
7744
- latestOutcome = outcome;
7745
- }
7746
- }
7656
+ return Object.entries(deliveries).map(([adapterId, value]) => normalizeDelivery(adapterId, value));
7657
+ };
7658
+ var resolveReplayHref = (event, replayHref) => {
7659
+ if (replayHref === false) {
7660
+ return;
7661
+ }
7662
+ if (typeof replayHref === "function") {
7663
+ return replayHref(event);
7664
+ }
7665
+ return `${replayHref ?? "/api/voice-sessions"}/${encodeURIComponent(event.sessionId)}/replay/htmx`;
7666
+ };
7667
+ var summarizeVoiceHandoffHealth = async (options = {}) => {
7668
+ const sourceEvents = options.events ?? await options.store?.list() ?? [];
7669
+ const search = options.q?.trim().toLowerCase();
7670
+ const byAction = {};
7671
+ const byAdapter = {};
7672
+ const byStatus = {
7673
+ delivered: 0,
7674
+ failed: 0,
7675
+ skipped: 0
7676
+ };
7677
+ const events = sourceEvents.filter((event) => event.type === "call.handoff").map((event) => {
7678
+ const status = isStatus(event.payload.status) ? event.payload.status : "failed";
7679
+ const deliveries = normalizeDeliveries(event.payload);
7747
7680
  const item = {
7748
- endedAt: summary.endedAt,
7749
- errorCount,
7750
- eventCount: summary.eventCount,
7751
- latestOutcome,
7752
- providerErrors,
7753
- providers: [...providers].sort(),
7754
- sessionId,
7755
- startedAt: summary.startedAt,
7756
- status: errorCount > 0 ? "failed" : "healthy",
7757
- transcriptCount: summary.transcriptCount,
7758
- turnCount: summary.turnCount
7681
+ action: getString4(event.payload.action),
7682
+ at: event.at,
7683
+ deliveries,
7684
+ reason: getString4(event.payload.reason),
7685
+ sessionId: event.sessionId,
7686
+ status,
7687
+ target: getString4(event.payload.target)
7759
7688
  };
7760
- const replayHref = options.replayHref === false ? "" : typeof options.replayHref === "function" ? options.replayHref(item) : `${options.replayHref ?? "/api/voice-sessions"}/${encodeURIComponent(sessionId)}/replay/htmx`;
7761
7689
  return {
7762
7690
  ...item,
7763
- replayHref
7691
+ replayHref: resolveReplayHref(item, options.replayHref)
7764
7692
  };
7765
- });
7766
- const search = options.q?.trim().toLowerCase();
7767
- return sessions.filter((session) => {
7768
- if (options.status && options.status !== "all" && session.status !== options.status) {
7769
- return false;
7770
- }
7771
- if (options.provider && !session.providers.includes(options.provider)) {
7693
+ }).filter((event) => {
7694
+ if (options.status && options.status !== "all" && event.status !== options.status) {
7772
7695
  return false;
7773
7696
  }
7774
7697
  if (!search) {
7775
7698
  return true;
7776
7699
  }
7777
7700
  return [
7778
- session.sessionId,
7779
- session.latestOutcome,
7780
- session.status,
7781
- ...session.providers
7701
+ event.action,
7702
+ event.reason,
7703
+ event.sessionId,
7704
+ event.status,
7705
+ event.target,
7706
+ ...event.deliveries.flatMap((delivery) => [
7707
+ delivery.adapterId,
7708
+ delivery.adapterKind,
7709
+ delivery.deliveredTo,
7710
+ delivery.error,
7711
+ delivery.status
7712
+ ])
7782
7713
  ].some((value) => value?.toLowerCase().includes(search));
7783
- }).sort((left, right) => (right.endedAt ?? right.startedAt ?? 0) - (left.endedAt ?? left.startedAt ?? 0)).slice(0, options.limit ?? 50);
7784
- };
7785
- var renderVoiceSessionsHTML = (sessions) => sessions.length === 0 ? '<p class="voice-sessions-empty">No voice sessions found.</p>' : [
7786
- '<div class="voice-sessions-list">',
7787
- ...sessions.map((session) => [
7788
- `<article class="voice-session-card ${escapeHtml7(session.status)}">`,
7789
- '<div class="voice-session-card-header">',
7790
- `<strong>${escapeHtml7(session.sessionId)}</strong>`,
7791
- `<span>${escapeHtml7(session.status)}</span>`,
7792
- "</div>",
7793
- "<dl>",
7794
- `<div><dt>Events</dt><dd>${String(session.eventCount)}</dd></div>`,
7795
- `<div><dt>Turns</dt><dd>${String(session.turnCount)}</dd></div>`,
7796
- `<div><dt>Transcripts</dt><dd>${String(session.transcriptCount)}</dd></div>`,
7797
- `<div><dt>Errors</dt><dd>${String(session.errorCount)}</dd></div>`,
7798
- "</dl>",
7799
- session.latestOutcome ? `<p>Outcome: ${escapeHtml7(session.latestOutcome)}</p>` : "",
7800
- session.providers.length ? `<p>Providers: ${session.providers.map(escapeHtml7).join(", ")}</p>` : "",
7801
- session.replayHref ? `<p><a href="${escapeHtml7(session.replayHref)}">Open replay</a></p>` : "",
7802
- "</article>"
7803
- ].join("")),
7804
- "</div>"
7805
- ].join("");
7806
- var createVoiceSessionsJSONHandler = (options = {}) => async ({ query }) => summarizeVoiceSessions({
7807
- ...options,
7808
- limit: typeof query?.limit === "string" ? Number(query.limit) : options.limit,
7809
- provider: query?.provider ?? options.provider,
7810
- q: query?.q ?? options.q,
7811
- status: query?.status === "failed" || query?.status === "healthy" || query?.status === "all" ? query.status : options.status
7812
- });
7813
- var createVoiceSessionsHTMLHandler = (options = {}) => async ({ query }) => {
7814
- const sessions = await summarizeVoiceSessions({
7815
- ...options,
7816
- limit: typeof query?.limit === "string" ? Number(query.limit) : options.limit,
7817
- provider: query?.provider ?? options.provider,
7818
- q: query?.q ?? options.q,
7819
- status: query?.status === "failed" || query?.status === "healthy" || query?.status === "all" ? query.status : options.status
7820
- });
7821
- const body = await (options.render?.(sessions) ?? renderVoiceSessionsHTML(sessions));
7822
- return new Response(body, {
7823
- headers: {
7824
- "Content-Type": "text/html; charset=utf-8",
7825
- ...options.headers
7714
+ }).sort((left, right) => right.at - left.at).slice(0, options.limit ?? 50);
7715
+ for (const event of events) {
7716
+ byStatus[event.status] += 1;
7717
+ if (event.action) {
7718
+ increment2(byAction, event.action);
7719
+ }
7720
+ for (const delivery of event.deliveries) {
7721
+ byAdapter[delivery.adapterId] ??= {
7722
+ delivered: 0,
7723
+ failed: 0,
7724
+ skipped: 0
7725
+ };
7726
+ byAdapter[delivery.adapterId][delivery.status] += 1;
7826
7727
  }
7827
- });
7828
- };
7829
- var createVoiceSessionListRoutes = (options = {}) => {
7830
- const path = options.path ?? "/api/voice-sessions";
7831
- const htmlPath = options.htmlPath === undefined ? `${path}/htmx` : options.htmlPath;
7832
- const routes = new Elysia5({
7833
- name: options.name ?? "absolutejs-voice-session-list"
7834
- }).get(path, createVoiceSessionsJSONHandler(options));
7835
- if (htmlPath) {
7836
- routes.get(htmlPath, createVoiceSessionsHTMLHandler(options));
7837
7728
  }
7838
- return routes;
7729
+ return {
7730
+ byAction,
7731
+ byAdapter,
7732
+ byStatus,
7733
+ events,
7734
+ failed: byStatus.failed,
7735
+ total: events.length
7736
+ };
7839
7737
  };
7840
- var createVoiceSessionReplayJSONHandler = (options) => async ({ params }) => summarizeVoiceSessionReplay({
7738
+ var renderMetricGrid = (summary) => [
7739
+ '<section class="voice-handoff-health-grid">',
7740
+ `<article><span>Total</span><strong>${String(summary.total)}</strong></article>`,
7741
+ `<article><span>Delivered</span><strong>${String(summary.byStatus.delivered)}</strong></article>`,
7742
+ `<article><span>Failed</span><strong>${String(summary.byStatus.failed)}</strong></article>`,
7743
+ `<article><span>Skipped</span><strong>${String(summary.byStatus.skipped)}</strong></article>`,
7744
+ "</section>"
7745
+ ].join("");
7746
+ var renderActionSummary = (summary) => {
7747
+ const actions = Object.entries(summary.byAction).sort((left, right) => right[1] - left[1]);
7748
+ const adapters = Object.entries(summary.byAdapter).sort(([left], [right]) => left.localeCompare(right));
7749
+ return [
7750
+ '<section class="voice-handoff-health-columns">',
7751
+ "<article><h3>Actions</h3>",
7752
+ actions.length === 0 ? "<p>No handoff actions yet.</p>" : `<ul>${actions.map(([action, count]) => `<li>${escapeHtml7(action)}: ${String(count)}</li>`).join("")}</ul>`,
7753
+ "</article>",
7754
+ "<article><h3>Adapters</h3>",
7755
+ 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>`,
7756
+ "</article>",
7757
+ "</section>"
7758
+ ].join("");
7759
+ };
7760
+ var renderVoiceHandoffHealthHTML = (summary) => [
7761
+ '<div class="voice-handoff-health">',
7762
+ renderMetricGrid(summary),
7763
+ renderActionSummary(summary),
7764
+ "<section>",
7765
+ "<h3>Recent Handoffs</h3>",
7766
+ summary.events.length === 0 ? '<p class="voice-handoff-health-empty">No handoffs found.</p>' : [
7767
+ '<div class="voice-handoff-health-events">',
7768
+ ...summary.events.map((event) => [
7769
+ `<article class="${escapeHtml7(event.status)}">`,
7770
+ '<div class="voice-handoff-health-event-header">',
7771
+ `<strong>${escapeHtml7(event.action ?? "handoff")}</strong>`,
7772
+ `<span>${escapeHtml7(event.status)}</span>`,
7773
+ "</div>",
7774
+ `<p><small>${escapeHtml7(event.sessionId)}</small></p>`,
7775
+ event.target ? `<p>Target: ${escapeHtml7(event.target)}</p>` : "",
7776
+ event.reason ? `<p>Reason: ${escapeHtml7(event.reason)}</p>` : "",
7777
+ event.deliveries.length ? `<ul>${event.deliveries.map((delivery) => [
7778
+ "<li>",
7779
+ `${escapeHtml7(delivery.adapterId)}: ${escapeHtml7(delivery.status)}`,
7780
+ delivery.deliveredTo ? ` to ${escapeHtml7(delivery.deliveredTo)}` : "",
7781
+ delivery.error ? ` (${escapeHtml7(delivery.error)})` : "",
7782
+ "</li>"
7783
+ ].join("")).join("")}</ul>` : "",
7784
+ event.replayHref ? `<p><a href="${escapeHtml7(event.replayHref)}">Open replay</a></p>` : "",
7785
+ "</article>"
7786
+ ].join("")),
7787
+ "</div>"
7788
+ ].join(""),
7789
+ "</section>",
7790
+ "</div>"
7791
+ ].join("");
7792
+ var createVoiceHandoffHealthJSONHandler = (options = {}) => async ({ query }) => summarizeVoiceHandoffHealth({
7841
7793
  ...options,
7842
- sessionId: params.sessionId ?? ""
7794
+ limit: typeof query?.limit === "string" ? Number(query.limit) : options.limit,
7795
+ q: query?.q ?? options.q,
7796
+ status: query?.status === "delivered" || query?.status === "failed" || query?.status === "skipped" || query?.status === "all" ? query.status : options.status
7843
7797
  });
7844
- var createVoiceSessionReplayHTMLHandler = (options) => async ({ params }) => {
7845
- const replay = await summarizeVoiceSessionReplay({
7798
+ var createVoiceHandoffHealthHTMLHandler = (options = {}) => async ({ query }) => {
7799
+ const summary = await summarizeVoiceHandoffHealth({
7846
7800
  ...options,
7847
- sessionId: params.sessionId ?? ""
7801
+ limit: typeof query?.limit === "string" ? Number(query.limit) : options.limit,
7802
+ q: query?.q ?? options.q,
7803
+ status: query?.status === "delivered" || query?.status === "failed" || query?.status === "skipped" || query?.status === "all" ? query.status : options.status
7848
7804
  });
7849
- const body = await (options.render?.(replay) ?? replay.html);
7805
+ const body = await (options.render?.(summary) ?? renderVoiceHandoffHealthHTML(summary));
7850
7806
  return new Response(body, {
7851
7807
  headers: {
7852
7808
  "Content-Type": "text/html; charset=utf-8",
@@ -7854,110 +7810,619 @@ var createVoiceSessionReplayHTMLHandler = (options) => async ({ params }) => {
7854
7810
  }
7855
7811
  });
7856
7812
  };
7857
- var createVoiceSessionReplayRoutes = (options) => {
7858
- const path = options.path ?? "/api/voice-sessions/:sessionId/replay";
7813
+ var createVoiceHandoffHealthRoutes = (options = {}) => {
7814
+ const path = options.path ?? "/api/voice-handoffs";
7859
7815
  const htmlPath = options.htmlPath === undefined ? `${path}/htmx` : options.htmlPath;
7860
7816
  const routes = new Elysia5({
7861
- name: options.name ?? "absolutejs-voice-session-replay"
7862
- }).get(path, createVoiceSessionReplayJSONHandler(options));
7817
+ name: options.name ?? "absolutejs-voice-handoff-health"
7818
+ }).get(path, createVoiceHandoffHealthJSONHandler(options));
7863
7819
  if (htmlPath) {
7864
- routes.get(htmlPath, createVoiceSessionReplayHTMLHandler(options));
7820
+ routes.get(htmlPath, createVoiceHandoffHealthHTMLHandler(options));
7865
7821
  }
7866
7822
  return routes;
7867
7823
  };
7868
- // src/fileStore.ts
7869
- import { mkdir, readFile, readdir, rename, rm, writeFile } from "fs/promises";
7870
- import { join } from "path";
7871
- var listJsonFiles = async (directory) => {
7872
- try {
7873
- const entries = await readdir(directory, {
7874
- withFileTypes: true
7875
- });
7876
- return entries.filter((entry) => entry.isFile() && entry.name.endsWith(".json")).map((entry) => join(directory, entry.name));
7877
- } catch (error) {
7878
- if (error.code === "ENOENT") {
7879
- return [];
7880
- }
7881
- throw error;
7882
- }
7883
- };
7884
- var encodeStoreId = (id) => `${encodeURIComponent(id)}.json`;
7885
- var resolveFilePath = (directory, id) => join(directory, encodeStoreId(id));
7886
- var createMemoryStoreId = (input) => `${input.assistantId}:${input.namespace}:${input.key}`;
7887
- var readJsonFile = async (path) => JSON.parse(await readFile(path, "utf8"));
7888
- var writeJsonFile = async (path, value, options) => {
7889
- await mkdir(options.directory, {
7890
- recursive: true
7891
- });
7892
- const tempPath = `${path}.${crypto.randomUUID()}.tmp`;
7893
- await writeFile(tempPath, JSON.stringify(value, null, options.pretty === false ? undefined : 2));
7894
- await rename(tempPath, path);
7824
+
7825
+ // src/qualityRoutes.ts
7826
+ var DEFAULT_THRESHOLDS = {
7827
+ maxDuplicateTurnRate: 0,
7828
+ maxEmptyTurnRate: 0.02,
7829
+ maxHandoffFailureRate: 0,
7830
+ maxMissingAssistantReplyRate: 0.05,
7831
+ maxProviderAverageLatencyMs: 3000,
7832
+ maxProviderErrorRate: 0.05,
7833
+ maxProviderFallbackRate: 0.25,
7834
+ maxProviderTimeoutRate: 0.03
7895
7835
  };
7896
- var createVoiceFileSessionStore = (options) => {
7897
- const get = async (id) => {
7898
- const path = resolveFilePath(options.directory, id);
7899
- try {
7900
- return await readJsonFile(path);
7901
- } catch (error) {
7902
- if (error.code === "ENOENT") {
7903
- return;
7904
- }
7905
- throw error;
7906
- }
7836
+ var getString5 = (value) => typeof value === "string" ? value : undefined;
7837
+ var getNumber3 = (value) => typeof value === "number" && Number.isFinite(value) ? value : undefined;
7838
+ var rate = (count, total) => count / Math.max(1, total);
7839
+ var roundMetric2 = (value) => Math.round(value * 1e4) / 1e4;
7840
+ var createMetric = (input) => ({
7841
+ ...input,
7842
+ actual: roundMetric2(input.actual),
7843
+ pass: input.actual <= input.threshold
7844
+ });
7845
+ var evaluateVoiceQuality = async (input) => {
7846
+ const events = filterVoiceTraceEvents(input.events ?? await input.store?.list() ?? []);
7847
+ const thresholds = {
7848
+ ...DEFAULT_THRESHOLDS,
7849
+ ...input.thresholds
7907
7850
  };
7908
- const getOrCreate = async (id) => {
7909
- const existing = await get(id);
7910
- if (existing) {
7911
- return existing;
7851
+ const committedTurns = events.filter((event) => event.type === "turn.committed");
7852
+ const assistantReplies = events.filter((event) => event.type === "turn.assistant");
7853
+ const sessionIdsWithAssistantReply = new Set(assistantReplies.map((event) => event.sessionId));
7854
+ const sessionsWithTurns = new Set(committedTurns.map((event) => event.sessionId));
7855
+ const emptyTurns = committedTurns.filter((event) => !getString5(event.payload.text)?.trim());
7856
+ const turnTextsBySession = new Map;
7857
+ let duplicateTurns = 0;
7858
+ for (const turn of committedTurns) {
7859
+ const normalized = getString5(turn.payload.text)?.trim().toLowerCase();
7860
+ if (!normalized) {
7861
+ continue;
7912
7862
  }
7913
- const session = createVoiceSessionRecord(id);
7914
- await writeJsonFile(resolveFilePath(options.directory, id), session, options);
7915
- return session;
7916
- };
7917
- const set = async (id, value) => {
7918
- await writeJsonFile(resolveFilePath(options.directory, id), value, options);
7919
- };
7920
- const list = async () => {
7921
- const files = await listJsonFiles(options.directory);
7922
- const sessions = await Promise.all(files.map((file) => readJsonFile(file)));
7923
- return sessions.map((session) => toVoiceSessionSummary(session)).sort((first, second) => (second.lastActivityAt ?? second.createdAt) - (first.lastActivityAt ?? first.createdAt));
7924
- };
7925
- const remove = async (id) => {
7926
- await rm(resolveFilePath(options.directory, id), {
7927
- force: true
7928
- });
7929
- };
7930
- return { get, getOrCreate, list, remove, set };
7931
- };
7932
- var createVoiceFileReviewStore = (options) => {
7933
- const get = async (id) => {
7934
- const path = resolveFilePath(options.directory, id);
7935
- try {
7936
- return await readJsonFile(path);
7937
- } catch (error) {
7938
- if (error.code === "ENOENT") {
7939
- return;
7940
- }
7941
- throw error;
7863
+ const seen = turnTextsBySession.get(turn.sessionId) ?? new Set;
7864
+ if (seen.has(normalized)) {
7865
+ duplicateTurns += 1;
7942
7866
  }
7867
+ seen.add(normalized);
7868
+ turnTextsBySession.set(turn.sessionId, seen);
7869
+ }
7870
+ const missingAssistantReplySessions = [...sessionsWithTurns].filter((sessionId) => !sessionIdsWithAssistantReply.has(sessionId)).length;
7871
+ const providerEvents = events.filter((event) => event.type === "session.error" && typeof event.payload.provider === "string" && typeof event.payload.providerStatus === "string");
7872
+ const providerErrors = providerEvents.filter((event) => event.payload.providerStatus === "error");
7873
+ const providerFallbacks = providerEvents.filter((event) => event.payload.providerStatus === "fallback");
7874
+ const providerTimeouts = providerEvents.filter((event) => event.payload.timedOut === true);
7875
+ const providerLatencies = providerEvents.map((event) => getNumber3(event.payload.elapsedMs)).filter((value) => value !== undefined);
7876
+ const averageProviderLatencyMs = providerLatencies.length > 0 ? providerLatencies.reduce((sum, value) => sum + value, 0) / providerLatencies.length : 0;
7877
+ const handoffHealth = await summarizeVoiceHandoffHealth({ events });
7878
+ const metrics = {
7879
+ duplicateTurnRate: createMetric({
7880
+ actual: rate(duplicateTurns, committedTurns.length),
7881
+ label: "Duplicate turn rate",
7882
+ threshold: thresholds.maxDuplicateTurnRate,
7883
+ unit: "rate"
7884
+ }),
7885
+ emptyTurnRate: createMetric({
7886
+ actual: rate(emptyTurns.length, committedTurns.length),
7887
+ label: "Empty turn rate",
7888
+ threshold: thresholds.maxEmptyTurnRate,
7889
+ unit: "rate"
7890
+ }),
7891
+ handoffFailureRate: createMetric({
7892
+ actual: rate(handoffHealth.failed, handoffHealth.total),
7893
+ label: "Handoff failure rate",
7894
+ threshold: thresholds.maxHandoffFailureRate,
7895
+ unit: "rate"
7896
+ }),
7897
+ missingAssistantReplyRate: createMetric({
7898
+ actual: rate(missingAssistantReplySessions, sessionsWithTurns.size),
7899
+ label: "Missing assistant reply rate",
7900
+ threshold: thresholds.maxMissingAssistantReplyRate,
7901
+ unit: "rate"
7902
+ }),
7903
+ providerAverageLatencyMs: createMetric({
7904
+ actual: averageProviderLatencyMs,
7905
+ label: "Average provider latency",
7906
+ threshold: thresholds.maxProviderAverageLatencyMs,
7907
+ unit: "ms"
7908
+ }),
7909
+ providerErrorRate: createMetric({
7910
+ actual: rate(providerErrors.length, providerEvents.length),
7911
+ label: "Provider error rate",
7912
+ threshold: thresholds.maxProviderErrorRate,
7913
+ unit: "rate"
7914
+ }),
7915
+ providerFallbackRate: createMetric({
7916
+ actual: rate(providerFallbacks.length, providerEvents.length),
7917
+ label: "Provider fallback rate",
7918
+ threshold: thresholds.maxProviderFallbackRate,
7919
+ unit: "rate"
7920
+ }),
7921
+ providerTimeoutRate: createMetric({
7922
+ actual: rate(providerTimeouts.length, providerEvents.length),
7923
+ label: "Provider timeout rate",
7924
+ threshold: thresholds.maxProviderTimeoutRate,
7925
+ unit: "rate"
7926
+ })
7943
7927
  };
7944
- const list = async () => {
7945
- const files = await listJsonFiles(options.directory);
7946
- const reviews = await Promise.all(files.map((file) => readJsonFile(file)));
7947
- return reviews.sort((left, right) => (right.generatedAt ?? 0) - (left.generatedAt ?? 0));
7948
- };
7949
- const set = async (id, artifact) => {
7950
- await writeJsonFile(resolveFilePath(options.directory, id), withVoiceCallReviewId(id, artifact), options);
7951
- };
7952
- const remove = async (id) => {
7953
- await rm(resolveFilePath(options.directory, id), {
7954
- force: true
7955
- });
7928
+ const status = Object.values(metrics).every((metric) => metric.pass) ? "pass" : "fail";
7929
+ return {
7930
+ checkedAt: Date.now(),
7931
+ eventCount: events.length,
7932
+ metrics,
7933
+ status,
7934
+ thresholds
7956
7935
  };
7957
- return { get, list, remove, set };
7958
7936
  };
7959
- var createVoiceFileTaskStore = (options) => {
7960
- const get = async (id) => {
7937
+ var escapeHtml8 = (value) => value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
7938
+ var formatMetricValue = (metric) => metric.unit === "rate" ? `${(metric.actual * 100).toFixed(2)}%` : metric.unit === "ms" ? `${Math.round(metric.actual)}ms` : String(metric.actual);
7939
+ var formatThreshold = (metric) => metric.unit === "rate" ? `${(metric.threshold * 100).toFixed(2)}%` : metric.unit === "ms" ? `${Math.round(metric.threshold)}ms` : String(metric.threshold);
7940
+ var renderVoiceQualityHTML = (report, options = {}) => {
7941
+ const rows = Object.entries(report.metrics).map(([key, metric]) => `<tr class="${metric.pass ? "pass" : "fail"}"><td>${escapeHtml8(metric.label)}</td><td>${escapeHtml8(formatMetricValue(metric))}</td><td>${escapeHtml8(formatThreshold(metric))}</td><td>${metric.pass ? "pass" : "fail"}</td><td><code>${escapeHtml8(key)}</code></td></tr>`).join("");
7942
+ const links = options.links?.length ? `<nav>${options.links.map((link) => `<a href="${escapeHtml8(link.href)}">${escapeHtml8(link.label)}</a>`).join("")}</nav>` : "";
7943
+ return `<!doctype html><html lang="en"><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /><title>AbsoluteJS Voice Quality</title><style>body{font-family:ui-sans-serif,system-ui,sans-serif;margin:2rem;background:#f8f7f2;color:#181713}main{max-width:1100px;margin:auto}nav{display:flex;flex-wrap:wrap;gap:.5rem;margin:0 0 1.25rem}nav a{background:#181713;border-radius:999px;color:white;padding:.35rem .7rem;text-decoration:none}.status{border-radius:999px;display:inline-flex;padding:.35rem .75rem;font-weight:800}.status.pass{background:#dcfce7;color:#166534}.status.fail{background:#fee2e2;color:#991b1b}table{border-collapse:collapse;width:100%;background:white;margin-top:1rem}td,th{border-bottom:1px solid #eee;padding:.75rem;text-align:left}.pass td{border-left:4px solid #16a34a}.fail td{border-left:4px solid #dc2626}code{background:#f3f4f6;padding:.15rem .3rem;border-radius:.3rem}</style></head><body><main>${links}<h1>Voice quality gates</h1><p class="status ${report.status}">${report.status}</p><p>${report.eventCount} event(s) checked.</p><table><thead><tr><th>Metric</th><th>Actual</th><th>Threshold</th><th>Status</th><th>Key</th></tr></thead><tbody>${rows}</tbody></table></main></body></html>`;
7944
+ };
7945
+ var createVoiceQualityRoutes = (options) => {
7946
+ const path = options.path ?? "/quality";
7947
+ const routes = new Elysia6({
7948
+ name: options.name ?? "absolutejs-voice-quality"
7949
+ });
7950
+ const getReport = () => evaluateVoiceQuality({
7951
+ events: options.events,
7952
+ store: options.store,
7953
+ thresholds: options.thresholds
7954
+ });
7955
+ routes.get(path, async () => {
7956
+ const report = await getReport();
7957
+ return new Response(renderVoiceQualityHTML(report, { links: options.links }), {
7958
+ headers: {
7959
+ "Content-Type": "text/html; charset=utf-8",
7960
+ ...options.headers
7961
+ }
7962
+ });
7963
+ });
7964
+ routes.get(`${path}/json`, async () => getReport());
7965
+ routes.get(`${path}/status`, async ({ set }) => {
7966
+ const report = await getReport();
7967
+ if (report.status === "fail") {
7968
+ set.status = 503;
7969
+ }
7970
+ return report;
7971
+ });
7972
+ return routes;
7973
+ };
7974
+
7975
+ // src/evalRoutes.ts
7976
+ var escapeHtml9 = (value) => value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
7977
+ var sessionTime = (events) => {
7978
+ const sorted = filterVoiceTraceEvents(events);
7979
+ return {
7980
+ endedAt: sorted.at(-1)?.at,
7981
+ startedAt: sorted[0]?.at
7982
+ };
7983
+ };
7984
+ var bucketKey = (timestamp) => new Date(timestamp).toISOString().slice(0, 10);
7985
+ var buildTrend = (sessions) => {
7986
+ const buckets = new Map;
7987
+ for (const session of sessions) {
7988
+ const endedAt = session.endedAt ?? session.startedAt ?? session.quality.checkedAt;
7989
+ const key = bucketKey(endedAt);
7990
+ const bucket = buckets.get(key) ?? {
7991
+ endedAt,
7992
+ failed: 0,
7993
+ key,
7994
+ passed: 0,
7995
+ total: 0
7996
+ };
7997
+ bucket.endedAt = Math.max(bucket.endedAt, endedAt);
7998
+ bucket.total += 1;
7999
+ if (session.status === "pass") {
8000
+ bucket.passed += 1;
8001
+ } else {
8002
+ bucket.failed += 1;
8003
+ }
8004
+ buckets.set(key, bucket);
8005
+ }
8006
+ return [...buckets.values()].sort((left, right) => right.endedAt - left.endedAt);
8007
+ };
8008
+ var runVoiceSessionEvals = async (options = {}) => {
8009
+ const events = filterVoiceTraceEvents(options.events ?? await options.store?.list() ?? []);
8010
+ const grouped = new Map;
8011
+ for (const event of events) {
8012
+ grouped.set(event.sessionId, [...grouped.get(event.sessionId) ?? [], event]);
8013
+ }
8014
+ const sessions = await Promise.all([...grouped.entries()].map(async ([sessionId, sessionEvents]) => {
8015
+ const sorted = filterVoiceTraceEvents(sessionEvents);
8016
+ const quality = await evaluateVoiceQuality({
8017
+ events: sorted,
8018
+ thresholds: options.thresholds
8019
+ });
8020
+ const { endedAt, startedAt } = sessionTime(sorted);
8021
+ const summary = summarizeVoiceTrace(sorted);
8022
+ const scenarioId = sorted.find((event) => event.scenarioId)?.scenarioId;
8023
+ return {
8024
+ endedAt,
8025
+ eventCount: sorted.length,
8026
+ quality,
8027
+ scenarioId,
8028
+ sessionId,
8029
+ startedAt,
8030
+ status: quality.status,
8031
+ summary
8032
+ };
8033
+ }));
8034
+ const limitedSessions = sessions.sort((left, right) => (right.endedAt ?? right.startedAt ?? 0) - (left.endedAt ?? left.startedAt ?? 0)).slice(0, options.limit ?? 100);
8035
+ const failed = limitedSessions.filter((session) => session.status === "fail").length;
8036
+ const passed = limitedSessions.length - failed;
8037
+ return {
8038
+ checkedAt: Date.now(),
8039
+ failed,
8040
+ passed,
8041
+ sessions: limitedSessions,
8042
+ status: failed > 0 ? "fail" : "pass",
8043
+ total: limitedSessions.length,
8044
+ trend: buildTrend(limitedSessions)
8045
+ };
8046
+ };
8047
+ var formatTime = (value) => value === undefined ? "unknown" : new Date(value).toLocaleString();
8048
+ var renderVoiceEvalHTML = (report, options = {}) => {
8049
+ const title = options.title ?? "AbsoluteJS Voice Evals";
8050
+ const links = options.links?.length ? `<nav>${options.links.map((link) => `<a href="${escapeHtml9(link.href)}">${escapeHtml9(link.label)}</a>`).join("")}</nav>` : "";
8051
+ const trend = report.trend.length ? report.trend.map((bucket) => `<tr><td>${escapeHtml9(bucket.key)}</td><td>${bucket.total}</td><td>${bucket.passed}</td><td>${bucket.failed}</td></tr>`).join("") : '<tr><td colspan="4">No eval buckets yet.</td></tr>';
8052
+ const sessions = report.sessions.length ? report.sessions.map((session) => {
8053
+ const failedMetrics = Object.entries(session.quality.metrics).filter(([, metric]) => !metric.pass).map(([, metric]) => metric.label).join(", ");
8054
+ return `<tr class="${session.status}"><td>${escapeHtml9(session.sessionId)}</td><td>${escapeHtml9(session.status)}</td><td>${session.eventCount}</td><td>${session.summary.turnCount}</td><td>${session.summary.errorCount}</td><td>${escapeHtml9(formatTime(session.endedAt))}</td><td>${escapeHtml9(failedMetrics || "none")}</td></tr>`;
8055
+ }).join("") : '<tr><td colspan="7">No sessions found.</td></tr>';
8056
+ return `<!doctype html><html lang="en"><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /><title>${escapeHtml9(title)}</title><style>body{font-family:ui-sans-serif,system-ui,sans-serif;margin:2rem;background:#f8f7f2;color:#181713}main{max-width:1180px;margin:auto}nav{display:flex;gap:.5rem;flex-wrap:wrap;margin-bottom:1rem}nav a{background:#181713;border-radius:999px;color:white;padding:.35rem .7rem;text-decoration:none}.status{border-radius:999px;display:inline-flex;font-weight:800;padding:.35rem .75rem}.pass{color:#166534}.fail{color:#991b1b}.status.pass{background:#dcfce7}.status.fail{background:#fee2e2}.grid{display:grid;gap:1rem;grid-template-columns:repeat(auto-fit,minmax(160px,1fr));margin:1rem 0}.card{background:white;border:1px solid #e7e5e4;border-radius:1rem;padding:1rem}.card strong{display:block;font-size:2rem}table{border-collapse:collapse;background:white;width:100%;margin:1rem 0 2rem}td,th{border-bottom:1px solid #eee;padding:.75rem;text-align:left}tr.fail td{border-left:4px solid #dc2626}tr.pass td{border-left:4px solid #16a34a}</style></head><body><main>${links}<h1>${escapeHtml9(title)}</h1><p class="status ${report.status}">${report.status}</p><div class="grid"><article class="card"><span>Total</span><strong>${report.total}</strong></article><article class="card"><span>Passed</span><strong>${report.passed}</strong></article><article class="card"><span>Failed</span><strong>${report.failed}</strong></article></div><h2>Trend</h2><table><thead><tr><th>Day</th><th>Total</th><th>Passed</th><th>Failed</th></tr></thead><tbody>${trend}</tbody></table><h2>Session Eval Results</h2><table><thead><tr><th>Session</th><th>Status</th><th>Events</th><th>Turns</th><th>Errors</th><th>Last event</th><th>Failed metrics</th></tr></thead><tbody>${sessions}</tbody></table></main></body></html>`;
8057
+ };
8058
+ var createVoiceEvalRoutes = (options) => {
8059
+ const path = options.path ?? "/evals";
8060
+ const routes = new Elysia7({
8061
+ name: options.name ?? "absolutejs-voice-evals"
8062
+ });
8063
+ const getReport = () => runVoiceSessionEvals({
8064
+ events: options.events,
8065
+ limit: options.limit,
8066
+ store: options.store,
8067
+ thresholds: options.thresholds
8068
+ });
8069
+ routes.get(path, async () => {
8070
+ const report = await getReport();
8071
+ return new Response(renderVoiceEvalHTML(report, {
8072
+ links: options.links,
8073
+ title: options.title
8074
+ }), {
8075
+ headers: {
8076
+ "Content-Type": "text/html; charset=utf-8",
8077
+ ...options.headers
8078
+ }
8079
+ });
8080
+ });
8081
+ routes.get(`${path}/json`, async () => getReport());
8082
+ routes.get(`${path}/status`, async ({ set }) => {
8083
+ const report = await getReport();
8084
+ if (report.status === "fail") {
8085
+ set.status = 503;
8086
+ }
8087
+ return report;
8088
+ });
8089
+ return routes;
8090
+ };
8091
+ // src/sessionReplay.ts
8092
+ import { Elysia as Elysia8 } from "elysia";
8093
+ var getString6 = (value) => typeof value === "string" ? value : undefined;
8094
+ var escapeHtml10 = (value) => value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
8095
+ var increment3 = (record, key) => {
8096
+ record[key] = (record[key] ?? 0) + 1;
8097
+ };
8098
+ var buildReplayTurns = (events) => {
8099
+ const turns = new Map;
8100
+ const getTurn = (turnId) => {
8101
+ const existing = turns.get(turnId);
8102
+ if (existing) {
8103
+ return existing;
8104
+ }
8105
+ const turn = {
8106
+ assistantReplies: [],
8107
+ errors: [],
8108
+ id: turnId,
8109
+ modelCalls: [],
8110
+ tools: [],
8111
+ transcripts: []
8112
+ };
8113
+ turns.set(turnId, turn);
8114
+ return turn;
8115
+ };
8116
+ for (const event of events) {
8117
+ const turnId = event.turnId ?? "session";
8118
+ const turn = getTurn(turnId);
8119
+ switch (event.type) {
8120
+ case "turn.transcript":
8121
+ turn.transcripts.push({
8122
+ isFinal: event.payload.isFinal === true,
8123
+ text: getString6(event.payload.text)
8124
+ });
8125
+ break;
8126
+ case "turn.committed":
8127
+ turn.committedText = getString6(event.payload.text);
8128
+ break;
8129
+ case "turn.assistant": {
8130
+ const text = getString6(event.payload.text);
8131
+ if (text) {
8132
+ turn.assistantReplies.push(text);
8133
+ }
8134
+ break;
8135
+ }
8136
+ case "agent.model":
8137
+ case "assistant.run":
8138
+ turn.modelCalls.push(event.payload);
8139
+ break;
8140
+ case "agent.tool":
8141
+ turn.tools.push(event.payload);
8142
+ break;
8143
+ case "session.error":
8144
+ turn.errors.push(event.payload);
8145
+ break;
8146
+ }
8147
+ }
8148
+ return [...turns.values()];
8149
+ };
8150
+ var summarizeVoiceSessionReplay = async (options) => {
8151
+ const sourceEvents = options.events ?? await options.store?.list({ sessionId: options.sessionId }) ?? [];
8152
+ const events = filterVoiceTraceEvents(sourceEvents, {
8153
+ sessionId: options.sessionId
8154
+ });
8155
+ const replay = buildVoiceTraceReplay(events, {
8156
+ evaluation: options.evaluation,
8157
+ redact: options.redact,
8158
+ title: options.title ?? `Voice Session ${options.sessionId}`
8159
+ });
8160
+ const startedAt = replay.summary.startedAt;
8161
+ return {
8162
+ evaluation: replay.evaluation,
8163
+ events,
8164
+ html: replay.html,
8165
+ markdown: replay.markdown,
8166
+ sessionId: options.sessionId,
8167
+ summary: replay.summary,
8168
+ timeline: events.map((event) => ({
8169
+ at: event.at,
8170
+ offsetMs: startedAt === undefined ? undefined : Math.max(0, event.at - startedAt),
8171
+ payload: event.payload,
8172
+ turnId: event.turnId,
8173
+ type: event.type
8174
+ })),
8175
+ turns: buildReplayTurns(events)
8176
+ };
8177
+ };
8178
+ var summarizeVoiceSessions = async (options = {}) => {
8179
+ const events = options.events ?? await options.store?.list() ?? [];
8180
+ const grouped = new Map;
8181
+ for (const event of events) {
8182
+ grouped.set(event.sessionId, [...grouped.get(event.sessionId) ?? [], event]);
8183
+ }
8184
+ const sessions = [...grouped.entries()].map(([sessionId, sessionEvents]) => {
8185
+ const sorted = filterVoiceTraceEvents(sessionEvents);
8186
+ const summary = buildVoiceTraceReplay(sorted, {
8187
+ evaluation: {
8188
+ requireAssistantReply: false,
8189
+ requireCompletedCall: false,
8190
+ requireTranscript: false,
8191
+ requireTurn: false
8192
+ }
8193
+ }).summary;
8194
+ const providerErrors = {};
8195
+ const providers = new Set;
8196
+ let latestOutcome;
8197
+ let errorCount = 0;
8198
+ for (const event of sorted) {
8199
+ const provider = getString6(event.payload.provider);
8200
+ if (provider) {
8201
+ providers.add(provider);
8202
+ }
8203
+ if (event.type === "session.error" && (event.payload.providerStatus === "error" || typeof event.payload.error === "string")) {
8204
+ errorCount += 1;
8205
+ increment3(providerErrors, provider ?? "unknown");
8206
+ }
8207
+ const outcome = getString6(event.payload.outcome);
8208
+ if (outcome) {
8209
+ latestOutcome = outcome;
8210
+ }
8211
+ }
8212
+ const item = {
8213
+ endedAt: summary.endedAt,
8214
+ errorCount,
8215
+ eventCount: summary.eventCount,
8216
+ latestOutcome,
8217
+ providerErrors,
8218
+ providers: [...providers].sort(),
8219
+ sessionId,
8220
+ startedAt: summary.startedAt,
8221
+ status: errorCount > 0 ? "failed" : "healthy",
8222
+ transcriptCount: summary.transcriptCount,
8223
+ turnCount: summary.turnCount
8224
+ };
8225
+ const replayHref = options.replayHref === false ? "" : typeof options.replayHref === "function" ? options.replayHref(item) : `${options.replayHref ?? "/api/voice-sessions"}/${encodeURIComponent(sessionId)}/replay/htmx`;
8226
+ return {
8227
+ ...item,
8228
+ replayHref
8229
+ };
8230
+ });
8231
+ const search = options.q?.trim().toLowerCase();
8232
+ return sessions.filter((session) => {
8233
+ if (options.status && options.status !== "all" && session.status !== options.status) {
8234
+ return false;
8235
+ }
8236
+ if (options.provider && !session.providers.includes(options.provider)) {
8237
+ return false;
8238
+ }
8239
+ if (!search) {
8240
+ return true;
8241
+ }
8242
+ return [
8243
+ session.sessionId,
8244
+ session.latestOutcome,
8245
+ session.status,
8246
+ ...session.providers
8247
+ ].some((value) => value?.toLowerCase().includes(search));
8248
+ }).sort((left, right) => (right.endedAt ?? right.startedAt ?? 0) - (left.endedAt ?? left.startedAt ?? 0)).slice(0, options.limit ?? 50);
8249
+ };
8250
+ var renderVoiceSessionsHTML = (sessions) => sessions.length === 0 ? '<p class="voice-sessions-empty">No voice sessions found.</p>' : [
8251
+ '<div class="voice-sessions-list">',
8252
+ ...sessions.map((session) => [
8253
+ `<article class="voice-session-card ${escapeHtml10(session.status)}">`,
8254
+ '<div class="voice-session-card-header">',
8255
+ `<strong>${escapeHtml10(session.sessionId)}</strong>`,
8256
+ `<span>${escapeHtml10(session.status)}</span>`,
8257
+ "</div>",
8258
+ "<dl>",
8259
+ `<div><dt>Events</dt><dd>${String(session.eventCount)}</dd></div>`,
8260
+ `<div><dt>Turns</dt><dd>${String(session.turnCount)}</dd></div>`,
8261
+ `<div><dt>Transcripts</dt><dd>${String(session.transcriptCount)}</dd></div>`,
8262
+ `<div><dt>Errors</dt><dd>${String(session.errorCount)}</dd></div>`,
8263
+ "</dl>",
8264
+ session.latestOutcome ? `<p>Outcome: ${escapeHtml10(session.latestOutcome)}</p>` : "",
8265
+ session.providers.length ? `<p>Providers: ${session.providers.map(escapeHtml10).join(", ")}</p>` : "",
8266
+ session.replayHref ? `<p><a href="${escapeHtml10(session.replayHref)}">Open replay</a></p>` : "",
8267
+ "</article>"
8268
+ ].join("")),
8269
+ "</div>"
8270
+ ].join("");
8271
+ var createVoiceSessionsJSONHandler = (options = {}) => async ({ query }) => summarizeVoiceSessions({
8272
+ ...options,
8273
+ limit: typeof query?.limit === "string" ? Number(query.limit) : options.limit,
8274
+ provider: query?.provider ?? options.provider,
8275
+ q: query?.q ?? options.q,
8276
+ status: query?.status === "failed" || query?.status === "healthy" || query?.status === "all" ? query.status : options.status
8277
+ });
8278
+ var createVoiceSessionsHTMLHandler = (options = {}) => async ({ query }) => {
8279
+ const sessions = await summarizeVoiceSessions({
8280
+ ...options,
8281
+ limit: typeof query?.limit === "string" ? Number(query.limit) : options.limit,
8282
+ provider: query?.provider ?? options.provider,
8283
+ q: query?.q ?? options.q,
8284
+ status: query?.status === "failed" || query?.status === "healthy" || query?.status === "all" ? query.status : options.status
8285
+ });
8286
+ const body = await (options.render?.(sessions) ?? renderVoiceSessionsHTML(sessions));
8287
+ return new Response(body, {
8288
+ headers: {
8289
+ "Content-Type": "text/html; charset=utf-8",
8290
+ ...options.headers
8291
+ }
8292
+ });
8293
+ };
8294
+ var createVoiceSessionListRoutes = (options = {}) => {
8295
+ const path = options.path ?? "/api/voice-sessions";
8296
+ const htmlPath = options.htmlPath === undefined ? `${path}/htmx` : options.htmlPath;
8297
+ const routes = new Elysia8({
8298
+ name: options.name ?? "absolutejs-voice-session-list"
8299
+ }).get(path, createVoiceSessionsJSONHandler(options));
8300
+ if (htmlPath) {
8301
+ routes.get(htmlPath, createVoiceSessionsHTMLHandler(options));
8302
+ }
8303
+ return routes;
8304
+ };
8305
+ var createVoiceSessionReplayJSONHandler = (options) => async ({ params }) => summarizeVoiceSessionReplay({
8306
+ ...options,
8307
+ sessionId: params.sessionId ?? ""
8308
+ });
8309
+ var createVoiceSessionReplayHTMLHandler = (options) => async ({ params }) => {
8310
+ const replay = await summarizeVoiceSessionReplay({
8311
+ ...options,
8312
+ sessionId: params.sessionId ?? ""
8313
+ });
8314
+ const body = await (options.render?.(replay) ?? replay.html);
8315
+ return new Response(body, {
8316
+ headers: {
8317
+ "Content-Type": "text/html; charset=utf-8",
8318
+ ...options.headers
8319
+ }
8320
+ });
8321
+ };
8322
+ var createVoiceSessionReplayRoutes = (options) => {
8323
+ const path = options.path ?? "/api/voice-sessions/:sessionId/replay";
8324
+ const htmlPath = options.htmlPath === undefined ? `${path}/htmx` : options.htmlPath;
8325
+ const routes = new Elysia8({
8326
+ name: options.name ?? "absolutejs-voice-session-replay"
8327
+ }).get(path, createVoiceSessionReplayJSONHandler(options));
8328
+ if (htmlPath) {
8329
+ routes.get(htmlPath, createVoiceSessionReplayHTMLHandler(options));
8330
+ }
8331
+ return routes;
8332
+ };
8333
+ // src/fileStore.ts
8334
+ import { mkdir, readFile, readdir, rename, rm, writeFile } from "fs/promises";
8335
+ import { join } from "path";
8336
+ var listJsonFiles = async (directory) => {
8337
+ try {
8338
+ const entries = await readdir(directory, {
8339
+ withFileTypes: true
8340
+ });
8341
+ return entries.filter((entry) => entry.isFile() && entry.name.endsWith(".json")).map((entry) => join(directory, entry.name));
8342
+ } catch (error) {
8343
+ if (error.code === "ENOENT") {
8344
+ return [];
8345
+ }
8346
+ throw error;
8347
+ }
8348
+ };
8349
+ var encodeStoreId = (id) => `${encodeURIComponent(id)}.json`;
8350
+ var resolveFilePath = (directory, id) => join(directory, encodeStoreId(id));
8351
+ var createMemoryStoreId = (input) => `${input.assistantId}:${input.namespace}:${input.key}`;
8352
+ var readJsonFile = async (path) => JSON.parse(await readFile(path, "utf8"));
8353
+ var writeJsonFile = async (path, value, options) => {
8354
+ await mkdir(options.directory, {
8355
+ recursive: true
8356
+ });
8357
+ const tempPath = `${path}.${crypto.randomUUID()}.tmp`;
8358
+ await writeFile(tempPath, JSON.stringify(value, null, options.pretty === false ? undefined : 2));
8359
+ await rename(tempPath, path);
8360
+ };
8361
+ var createVoiceFileSessionStore = (options) => {
8362
+ const get = async (id) => {
8363
+ const path = resolveFilePath(options.directory, id);
8364
+ try {
8365
+ return await readJsonFile(path);
8366
+ } catch (error) {
8367
+ if (error.code === "ENOENT") {
8368
+ return;
8369
+ }
8370
+ throw error;
8371
+ }
8372
+ };
8373
+ const getOrCreate = async (id) => {
8374
+ const existing = await get(id);
8375
+ if (existing) {
8376
+ return existing;
8377
+ }
8378
+ const session = createVoiceSessionRecord(id);
8379
+ await writeJsonFile(resolveFilePath(options.directory, id), session, options);
8380
+ return session;
8381
+ };
8382
+ const set = async (id, value) => {
8383
+ await writeJsonFile(resolveFilePath(options.directory, id), value, options);
8384
+ };
8385
+ const list = async () => {
8386
+ const files = await listJsonFiles(options.directory);
8387
+ const sessions = await Promise.all(files.map((file) => readJsonFile(file)));
8388
+ return sessions.map((session) => toVoiceSessionSummary(session)).sort((first, second) => (second.lastActivityAt ?? second.createdAt) - (first.lastActivityAt ?? first.createdAt));
8389
+ };
8390
+ const remove = async (id) => {
8391
+ await rm(resolveFilePath(options.directory, id), {
8392
+ force: true
8393
+ });
8394
+ };
8395
+ return { get, getOrCreate, list, remove, set };
8396
+ };
8397
+ var createVoiceFileReviewStore = (options) => {
8398
+ const get = async (id) => {
8399
+ const path = resolveFilePath(options.directory, id);
8400
+ try {
8401
+ return await readJsonFile(path);
8402
+ } catch (error) {
8403
+ if (error.code === "ENOENT") {
8404
+ return;
8405
+ }
8406
+ throw error;
8407
+ }
8408
+ };
8409
+ const list = async () => {
8410
+ const files = await listJsonFiles(options.directory);
8411
+ const reviews = await Promise.all(files.map((file) => readJsonFile(file)));
8412
+ return reviews.sort((left, right) => (right.generatedAt ?? 0) - (left.generatedAt ?? 0));
8413
+ };
8414
+ const set = async (id, artifact) => {
8415
+ await writeJsonFile(resolveFilePath(options.directory, id), withVoiceCallReviewId(id, artifact), options);
8416
+ };
8417
+ const remove = async (id) => {
8418
+ await rm(resolveFilePath(options.directory, id), {
8419
+ force: true
8420
+ });
8421
+ };
8422
+ return { get, list, remove, set };
8423
+ };
8424
+ var createVoiceFileTaskStore = (options) => {
8425
+ const get = async (id) => {
7961
8426
  const path = resolveFilePath(options.directory, id);
7962
8427
  try {
7963
8428
  return await readJsonFile(path);
@@ -8778,571 +9243,227 @@ var createOpenAIVoiceAssistantModel = (options) => {
8778
9243
  name: tool.name,
8779
9244
  parameters: tool.parameters ?? {
8780
9245
  additionalProperties: true,
8781
- type: "object"
8782
- },
8783
- strict: false,
8784
- type: "function"
8785
- }))
8786
- }),
8787
- headers: {
8788
- authorization: `Bearer ${options.apiKey}`,
8789
- "content-type": "application/json"
8790
- },
8791
- method: "POST"
8792
- });
8793
- if (!response.ok) {
8794
- throw createHTTPError("OpenAI", response);
8795
- }
8796
- const body = await response.json();
8797
- if (body.usage && typeof body.usage === "object") {
8798
- await options.onUsage?.(body.usage);
8799
- }
8800
- const toolCalls = extractToolCalls(body);
8801
- if (toolCalls.length) {
8802
- return {
8803
- toolCalls
8804
- };
8805
- }
8806
- return normalizeRouteOutput(parseJSON(extractText(body)));
8807
- }
8808
- };
8809
- };
8810
- var extractAnthropicText = (response) => {
8811
- const content = Array.isArray(response.content) ? response.content : [];
8812
- return content.map((item) => item && typeof item === "object" && item.type === "text" && typeof item.text === "string" ? item.text : "").filter(Boolean).join(`
8813
- `);
8814
- };
8815
- var extractAnthropicToolCalls = (response) => {
8816
- const content = Array.isArray(response.content) ? response.content : [];
8817
- const toolCalls = [];
8818
- for (const item of content) {
8819
- if (!item || typeof item !== "object") {
8820
- continue;
8821
- }
8822
- const record = item;
8823
- if (record.type !== "tool_use" || typeof record.name !== "string") {
8824
- continue;
8825
- }
8826
- toolCalls.push({
8827
- args: record.input && typeof record.input === "object" ? record.input : {},
8828
- id: typeof record.id === "string" ? record.id : undefined,
8829
- name: record.name
8830
- });
8831
- }
8832
- return toolCalls;
8833
- };
8834
- var createAnthropicVoiceAssistantModel = (options) => {
8835
- const fetchImpl = options.fetch ?? globalThis.fetch;
8836
- const baseUrl = options.baseUrl ?? "https://api.anthropic.com/v1";
8837
- const model = options.model ?? "claude-sonnet-4-5";
8838
- return {
8839
- generate: async (input) => {
8840
- const response = await fetchImpl(`${baseUrl.replace(/\/$/, "")}/messages`, {
8841
- body: JSON.stringify({
8842
- max_tokens: options.maxOutputTokens ?? 1024,
8843
- messages: input.messages.map(messageToAnthropicMessage).filter(Boolean),
8844
- model,
8845
- system: [input.system, ROUTE_RESULT_INSTRUCTION].filter(Boolean).join(`
8846
-
8847
- `),
8848
- temperature: options.temperature,
8849
- tool_choice: input.tools.length ? { type: "auto" } : { type: "none" },
8850
- tools: input.tools.map((tool) => ({
8851
- description: tool.description,
8852
- input_schema: tool.parameters ?? {
8853
- additionalProperties: true,
8854
- type: "object"
8855
- },
8856
- name: tool.name
8857
- }))
8858
- }),
8859
- headers: {
8860
- "anthropic-version": options.version ?? "2023-06-01",
8861
- "content-type": "application/json",
8862
- "x-api-key": options.apiKey
8863
- },
8864
- method: "POST"
8865
- });
8866
- if (!response.ok) {
8867
- throw createHTTPError("Anthropic", response);
8868
- }
8869
- const body = await response.json();
8870
- if (body.usage && typeof body.usage === "object") {
8871
- await options.onUsage?.(body.usage);
8872
- }
8873
- const toolCalls = extractAnthropicToolCalls(body);
8874
- if (toolCalls.length) {
8875
- return {
8876
- assistantText: extractAnthropicText(body) || undefined,
8877
- toolCalls
8878
- };
8879
- }
8880
- return normalizeRouteOutput(parseJSON(extractAnthropicText(body)));
8881
- }
8882
- };
8883
- };
8884
- var extractGeminiCandidateParts = (response) => {
8885
- const candidates = Array.isArray(response.candidates) ? response.candidates : [];
8886
- const first = candidates[0];
8887
- if (!first || typeof first !== "object") {
8888
- return [];
8889
- }
8890
- const content = first.content;
8891
- if (!content || typeof content !== "object") {
8892
- return [];
8893
- }
8894
- const parts = content.parts;
8895
- return Array.isArray(parts) ? parts : [];
8896
- };
8897
- var extractGeminiText = (response) => extractGeminiCandidateParts(response).map((part) => part && typeof part === "object" && typeof part.text === "string" ? part.text : "").filter(Boolean).join(`
8898
- `);
8899
- var extractGeminiToolCalls = (response) => {
8900
- const toolCalls = [];
8901
- for (const part of extractGeminiCandidateParts(response)) {
8902
- if (!part || typeof part !== "object") {
8903
- continue;
8904
- }
8905
- const functionCall = part.functionCall;
8906
- if (!functionCall || typeof functionCall !== "object") {
8907
- continue;
8908
- }
8909
- const record = functionCall;
8910
- if (typeof record.name !== "string") {
8911
- continue;
8912
- }
8913
- toolCalls.push({
8914
- args: record.args && typeof record.args === "object" ? record.args : {},
8915
- id: typeof record.id === "string" ? record.id : undefined,
8916
- name: record.name
8917
- });
8918
- }
8919
- return toolCalls;
8920
- };
8921
- var createGeminiVoiceAssistantModel = (options) => {
8922
- const fetchImpl = options.fetch ?? globalThis.fetch;
8923
- const baseUrl = options.baseUrl ?? "https://generativelanguage.googleapis.com/v1beta";
8924
- const model = options.model ?? "gemini-2.5-flash";
8925
- const maxRetries = Math.max(0, options.maxRetries ?? 2);
8926
- return {
8927
- generate: async (input) => {
8928
- const endpoint = `${baseUrl.replace(/\/$/, "")}/models/${encodeURIComponent(model)}:generateContent?key=${encodeURIComponent(options.apiKey)}`;
8929
- let response;
8930
- for (let attempt = 0;attempt <= maxRetries; attempt += 1) {
8931
- response = await fetchImpl(endpoint, {
8932
- body: JSON.stringify({
8933
- contents: input.messages.map(messageToGeminiContent).filter(Boolean),
8934
- generationConfig: {
8935
- maxOutputTokens: options.maxOutputTokens,
8936
- ...input.tools.length ? {} : {
8937
- responseMimeType: "application/json",
8938
- responseSchema: toGeminiSchema(OUTPUT_SCHEMA)
8939
- },
8940
- temperature: options.temperature
8941
- },
8942
- systemInstruction: {
8943
- parts: [
8944
- {
8945
- text: [input.system, ROUTE_RESULT_INSTRUCTION].filter(Boolean).join(`
8946
-
8947
- `)
8948
- }
8949
- ]
8950
- },
8951
- tools: input.tools.length ? [
8952
- {
8953
- functionDeclarations: input.tools.map((tool) => ({
8954
- description: tool.description,
8955
- name: tool.name,
8956
- parameters: toGeminiSchema(tool.parameters ?? {
8957
- additionalProperties: true,
8958
- type: "object"
8959
- })
8960
- }))
8961
- }
8962
- ] : undefined
8963
- }),
8964
- headers: {
8965
- "content-type": "application/json"
8966
- },
8967
- method: "POST"
8968
- });
8969
- if (response.ok || response.status !== 429 && response.status < 500 || attempt === maxRetries) {
8970
- break;
8971
- }
8972
- const retryAfter = Number(response.headers.get("retry-after"));
8973
- await sleep4(Number.isFinite(retryAfter) && retryAfter > 0 ? retryAfter * 1000 : 500 * 2 ** attempt);
8974
- }
8975
- if (!response) {
8976
- throw new Error("Gemini voice assistant model failed: no response");
8977
- }
9246
+ type: "object"
9247
+ },
9248
+ strict: false,
9249
+ type: "function"
9250
+ }))
9251
+ }),
9252
+ headers: {
9253
+ authorization: `Bearer ${options.apiKey}`,
9254
+ "content-type": "application/json"
9255
+ },
9256
+ method: "POST"
9257
+ });
8978
9258
  if (!response.ok) {
8979
- throw createHTTPError("Gemini", response);
9259
+ throw createHTTPError("OpenAI", response);
8980
9260
  }
8981
9261
  const body = await response.json();
8982
- if (body.usageMetadata && typeof body.usageMetadata === "object") {
8983
- await options.onUsage?.(body.usageMetadata);
9262
+ if (body.usage && typeof body.usage === "object") {
9263
+ await options.onUsage?.(body.usage);
8984
9264
  }
8985
- const toolCalls = extractGeminiToolCalls(body);
9265
+ const toolCalls = extractToolCalls(body);
8986
9266
  if (toolCalls.length) {
8987
9267
  return {
8988
- assistantText: extractGeminiText(body) || undefined,
8989
9268
  toolCalls
8990
9269
  };
8991
9270
  }
8992
- return normalizeRouteOutput(parseJSON(extractGeminiText(body)));
9271
+ return normalizeRouteOutput(parseJSON(extractText(body)));
8993
9272
  }
8994
9273
  };
8995
9274
  };
8996
- // src/opsConsoleRoutes.ts
8997
- import { Elysia as Elysia9 } from "elysia";
8998
-
8999
- // src/handoffHealth.ts
9000
- import { Elysia as Elysia6 } from "elysia";
9001
- var escapeHtml8 = (value) => value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
9002
- var getString5 = (value) => typeof value === "string" && value.length > 0 ? value : undefined;
9003
- var isStatus = (value) => value === "delivered" || value === "failed" || value === "skipped";
9004
- var increment3 = (record, key) => {
9005
- record[key] = (record[key] ?? 0) + 1;
9006
- };
9007
- var normalizeDelivery = (adapterId, value) => {
9008
- const record = value && typeof value === "object" ? value : {};
9009
- return {
9010
- adapterId: getString5(record.adapterId) ?? adapterId,
9011
- adapterKind: getString5(record.adapterKind),
9012
- deliveredAt: typeof record.deliveredAt === "number" ? record.deliveredAt : undefined,
9013
- deliveredTo: getString5(record.deliveredTo),
9014
- error: getString5(record.error),
9015
- status: isStatus(record.status) ? record.status : "failed"
9016
- };
9017
- };
9018
- var normalizeDeliveries = (payload) => {
9019
- const deliveries = payload.deliveries;
9020
- if (!deliveries || typeof deliveries !== "object") {
9021
- return [];
9022
- }
9023
- return Object.entries(deliveries).map(([adapterId, value]) => normalizeDelivery(adapterId, value));
9024
- };
9025
- var resolveReplayHref = (event, replayHref) => {
9026
- if (replayHref === false) {
9027
- return;
9028
- }
9029
- if (typeof replayHref === "function") {
9030
- return replayHref(event);
9031
- }
9032
- return `${replayHref ?? "/api/voice-sessions"}/${encodeURIComponent(event.sessionId)}/replay/htmx`;
9275
+ var extractAnthropicText = (response) => {
9276
+ const content = Array.isArray(response.content) ? response.content : [];
9277
+ return content.map((item) => item && typeof item === "object" && item.type === "text" && typeof item.text === "string" ? item.text : "").filter(Boolean).join(`
9278
+ `);
9033
9279
  };
9034
- var summarizeVoiceHandoffHealth = async (options = {}) => {
9035
- const sourceEvents = options.events ?? await options.store?.list() ?? [];
9036
- const search = options.q?.trim().toLowerCase();
9037
- const byAction = {};
9038
- const byAdapter = {};
9039
- const byStatus = {
9040
- delivered: 0,
9041
- failed: 0,
9042
- skipped: 0
9043
- };
9044
- const events = sourceEvents.filter((event) => event.type === "call.handoff").map((event) => {
9045
- const status = isStatus(event.payload.status) ? event.payload.status : "failed";
9046
- const deliveries = normalizeDeliveries(event.payload);
9047
- const item = {
9048
- action: getString5(event.payload.action),
9049
- at: event.at,
9050
- deliveries,
9051
- reason: getString5(event.payload.reason),
9052
- sessionId: event.sessionId,
9053
- status,
9054
- target: getString5(event.payload.target)
9055
- };
9056
- return {
9057
- ...item,
9058
- replayHref: resolveReplayHref(item, options.replayHref)
9059
- };
9060
- }).filter((event) => {
9061
- if (options.status && options.status !== "all" && event.status !== options.status) {
9062
- return false;
9063
- }
9064
- if (!search) {
9065
- return true;
9066
- }
9067
- return [
9068
- event.action,
9069
- event.reason,
9070
- event.sessionId,
9071
- event.status,
9072
- event.target,
9073
- ...event.deliveries.flatMap((delivery) => [
9074
- delivery.adapterId,
9075
- delivery.adapterKind,
9076
- delivery.deliveredTo,
9077
- delivery.error,
9078
- delivery.status
9079
- ])
9080
- ].some((value) => value?.toLowerCase().includes(search));
9081
- }).sort((left, right) => right.at - left.at).slice(0, options.limit ?? 50);
9082
- for (const event of events) {
9083
- byStatus[event.status] += 1;
9084
- if (event.action) {
9085
- increment3(byAction, event.action);
9280
+ var extractAnthropicToolCalls = (response) => {
9281
+ const content = Array.isArray(response.content) ? response.content : [];
9282
+ const toolCalls = [];
9283
+ for (const item of content) {
9284
+ if (!item || typeof item !== "object") {
9285
+ continue;
9086
9286
  }
9087
- for (const delivery of event.deliveries) {
9088
- byAdapter[delivery.adapterId] ??= {
9089
- delivered: 0,
9090
- failed: 0,
9091
- skipped: 0
9092
- };
9093
- byAdapter[delivery.adapterId][delivery.status] += 1;
9287
+ const record = item;
9288
+ if (record.type !== "tool_use" || typeof record.name !== "string") {
9289
+ continue;
9094
9290
  }
9291
+ toolCalls.push({
9292
+ args: record.input && typeof record.input === "object" ? record.input : {},
9293
+ id: typeof record.id === "string" ? record.id : undefined,
9294
+ name: record.name
9295
+ });
9095
9296
  }
9096
- return {
9097
- byAction,
9098
- byAdapter,
9099
- byStatus,
9100
- events,
9101
- failed: byStatus.failed,
9102
- total: events.length
9103
- };
9104
- };
9105
- var renderMetricGrid = (summary) => [
9106
- '<section class="voice-handoff-health-grid">',
9107
- `<article><span>Total</span><strong>${String(summary.total)}</strong></article>`,
9108
- `<article><span>Delivered</span><strong>${String(summary.byStatus.delivered)}</strong></article>`,
9109
- `<article><span>Failed</span><strong>${String(summary.byStatus.failed)}</strong></article>`,
9110
- `<article><span>Skipped</span><strong>${String(summary.byStatus.skipped)}</strong></article>`,
9111
- "</section>"
9112
- ].join("");
9113
- var renderActionSummary = (summary) => {
9114
- const actions = Object.entries(summary.byAction).sort((left, right) => right[1] - left[1]);
9115
- const adapters = Object.entries(summary.byAdapter).sort(([left], [right]) => left.localeCompare(right));
9116
- return [
9117
- '<section class="voice-handoff-health-columns">',
9118
- "<article><h3>Actions</h3>",
9119
- actions.length === 0 ? "<p>No handoff actions yet.</p>" : `<ul>${actions.map(([action, count]) => `<li>${escapeHtml8(action)}: ${String(count)}</li>`).join("")}</ul>`,
9120
- "</article>",
9121
- "<article><h3>Adapters</h3>",
9122
- 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>`,
9123
- "</article>",
9124
- "</section>"
9125
- ].join("");
9297
+ return toolCalls;
9126
9298
  };
9127
- var renderVoiceHandoffHealthHTML = (summary) => [
9128
- '<div class="voice-handoff-health">',
9129
- renderMetricGrid(summary),
9130
- renderActionSummary(summary),
9131
- "<section>",
9132
- "<h3>Recent Handoffs</h3>",
9133
- summary.events.length === 0 ? '<p class="voice-handoff-health-empty">No handoffs found.</p>' : [
9134
- '<div class="voice-handoff-health-events">',
9135
- ...summary.events.map((event) => [
9136
- `<article class="${escapeHtml8(event.status)}">`,
9137
- '<div class="voice-handoff-health-event-header">',
9138
- `<strong>${escapeHtml8(event.action ?? "handoff")}</strong>`,
9139
- `<span>${escapeHtml8(event.status)}</span>`,
9140
- "</div>",
9141
- `<p><small>${escapeHtml8(event.sessionId)}</small></p>`,
9142
- event.target ? `<p>Target: ${escapeHtml8(event.target)}</p>` : "",
9143
- event.reason ? `<p>Reason: ${escapeHtml8(event.reason)}</p>` : "",
9144
- event.deliveries.length ? `<ul>${event.deliveries.map((delivery) => [
9145
- "<li>",
9146
- `${escapeHtml8(delivery.adapterId)}: ${escapeHtml8(delivery.status)}`,
9147
- delivery.deliveredTo ? ` to ${escapeHtml8(delivery.deliveredTo)}` : "",
9148
- delivery.error ? ` (${escapeHtml8(delivery.error)})` : "",
9149
- "</li>"
9150
- ].join("")).join("")}</ul>` : "",
9151
- event.replayHref ? `<p><a href="${escapeHtml8(event.replayHref)}">Open replay</a></p>` : "",
9152
- "</article>"
9153
- ].join("")),
9154
- "</div>"
9155
- ].join(""),
9156
- "</section>",
9157
- "</div>"
9158
- ].join("");
9159
- var createVoiceHandoffHealthJSONHandler = (options = {}) => async ({ query }) => summarizeVoiceHandoffHealth({
9160
- ...options,
9161
- limit: typeof query?.limit === "string" ? Number(query.limit) : options.limit,
9162
- q: query?.q ?? options.q,
9163
- status: query?.status === "delivered" || query?.status === "failed" || query?.status === "skipped" || query?.status === "all" ? query.status : options.status
9164
- });
9165
- var createVoiceHandoffHealthHTMLHandler = (options = {}) => async ({ query }) => {
9166
- const summary = await summarizeVoiceHandoffHealth({
9167
- ...options,
9168
- limit: typeof query?.limit === "string" ? Number(query.limit) : options.limit,
9169
- q: query?.q ?? options.q,
9170
- status: query?.status === "delivered" || query?.status === "failed" || query?.status === "skipped" || query?.status === "all" ? query.status : options.status
9171
- });
9172
- const body = await (options.render?.(summary) ?? renderVoiceHandoffHealthHTML(summary));
9173
- return new Response(body, {
9174
- headers: {
9175
- "Content-Type": "text/html; charset=utf-8",
9176
- ...options.headers
9299
+ var createAnthropicVoiceAssistantModel = (options) => {
9300
+ const fetchImpl = options.fetch ?? globalThis.fetch;
9301
+ const baseUrl = options.baseUrl ?? "https://api.anthropic.com/v1";
9302
+ const model = options.model ?? "claude-sonnet-4-5";
9303
+ return {
9304
+ generate: async (input) => {
9305
+ const response = await fetchImpl(`${baseUrl.replace(/\/$/, "")}/messages`, {
9306
+ body: JSON.stringify({
9307
+ max_tokens: options.maxOutputTokens ?? 1024,
9308
+ messages: input.messages.map(messageToAnthropicMessage).filter(Boolean),
9309
+ model,
9310
+ system: [input.system, ROUTE_RESULT_INSTRUCTION].filter(Boolean).join(`
9311
+
9312
+ `),
9313
+ temperature: options.temperature,
9314
+ tool_choice: input.tools.length ? { type: "auto" } : { type: "none" },
9315
+ tools: input.tools.map((tool) => ({
9316
+ description: tool.description,
9317
+ input_schema: tool.parameters ?? {
9318
+ additionalProperties: true,
9319
+ type: "object"
9320
+ },
9321
+ name: tool.name
9322
+ }))
9323
+ }),
9324
+ headers: {
9325
+ "anthropic-version": options.version ?? "2023-06-01",
9326
+ "content-type": "application/json",
9327
+ "x-api-key": options.apiKey
9328
+ },
9329
+ method: "POST"
9330
+ });
9331
+ if (!response.ok) {
9332
+ throw createHTTPError("Anthropic", response);
9333
+ }
9334
+ const body = await response.json();
9335
+ if (body.usage && typeof body.usage === "object") {
9336
+ await options.onUsage?.(body.usage);
9337
+ }
9338
+ const toolCalls = extractAnthropicToolCalls(body);
9339
+ if (toolCalls.length) {
9340
+ return {
9341
+ assistantText: extractAnthropicText(body) || undefined,
9342
+ toolCalls
9343
+ };
9344
+ }
9345
+ return normalizeRouteOutput(parseJSON(extractAnthropicText(body)));
9177
9346
  }
9178
- });
9179
- };
9180
- var createVoiceHandoffHealthRoutes = (options = {}) => {
9181
- const path = options.path ?? "/api/voice-handoffs";
9182
- const htmlPath = options.htmlPath === undefined ? `${path}/htmx` : options.htmlPath;
9183
- const routes = new Elysia6({
9184
- name: options.name ?? "absolutejs-voice-handoff-health"
9185
- }).get(path, createVoiceHandoffHealthJSONHandler(options));
9186
- if (htmlPath) {
9187
- routes.get(htmlPath, createVoiceHandoffHealthHTMLHandler(options));
9188
- }
9189
- return routes;
9347
+ };
9190
9348
  };
9191
-
9192
- // src/qualityRoutes.ts
9193
- import { Elysia as Elysia7 } from "elysia";
9194
- var DEFAULT_THRESHOLDS = {
9195
- maxDuplicateTurnRate: 0,
9196
- maxEmptyTurnRate: 0.02,
9197
- maxHandoffFailureRate: 0,
9198
- maxMissingAssistantReplyRate: 0.05,
9199
- maxProviderAverageLatencyMs: 3000,
9200
- maxProviderErrorRate: 0.05,
9201
- maxProviderFallbackRate: 0.25,
9202
- maxProviderTimeoutRate: 0.03
9349
+ var extractGeminiCandidateParts = (response) => {
9350
+ const candidates = Array.isArray(response.candidates) ? response.candidates : [];
9351
+ const first = candidates[0];
9352
+ if (!first || typeof first !== "object") {
9353
+ return [];
9354
+ }
9355
+ const content = first.content;
9356
+ if (!content || typeof content !== "object") {
9357
+ return [];
9358
+ }
9359
+ const parts = content.parts;
9360
+ return Array.isArray(parts) ? parts : [];
9203
9361
  };
9204
- var getString6 = (value) => typeof value === "string" ? value : undefined;
9205
- var getNumber3 = (value) => typeof value === "number" && Number.isFinite(value) ? value : undefined;
9206
- var rate = (count, total) => count / Math.max(1, total);
9207
- var roundMetric2 = (value) => Math.round(value * 1e4) / 1e4;
9208
- var createMetric = (input) => ({
9209
- ...input,
9210
- actual: roundMetric2(input.actual),
9211
- pass: input.actual <= input.threshold
9212
- });
9213
- var evaluateVoiceQuality = async (input) => {
9214
- const events = filterVoiceTraceEvents(input.events ?? await input.store?.list() ?? []);
9215
- const thresholds = {
9216
- ...DEFAULT_THRESHOLDS,
9217
- ...input.thresholds
9218
- };
9219
- const committedTurns = events.filter((event) => event.type === "turn.committed");
9220
- const assistantReplies = events.filter((event) => event.type === "turn.assistant");
9221
- const sessionIdsWithAssistantReply = new Set(assistantReplies.map((event) => event.sessionId));
9222
- const sessionsWithTurns = new Set(committedTurns.map((event) => event.sessionId));
9223
- const emptyTurns = committedTurns.filter((event) => !getString6(event.payload.text)?.trim());
9224
- const turnTextsBySession = new Map;
9225
- let duplicateTurns = 0;
9226
- for (const turn of committedTurns) {
9227
- const normalized = getString6(turn.payload.text)?.trim().toLowerCase();
9228
- if (!normalized) {
9362
+ var extractGeminiText = (response) => extractGeminiCandidateParts(response).map((part) => part && typeof part === "object" && typeof part.text === "string" ? part.text : "").filter(Boolean).join(`
9363
+ `);
9364
+ var extractGeminiToolCalls = (response) => {
9365
+ const toolCalls = [];
9366
+ for (const part of extractGeminiCandidateParts(response)) {
9367
+ if (!part || typeof part !== "object") {
9229
9368
  continue;
9230
9369
  }
9231
- const seen = turnTextsBySession.get(turn.sessionId) ?? new Set;
9232
- if (seen.has(normalized)) {
9233
- duplicateTurns += 1;
9370
+ const functionCall = part.functionCall;
9371
+ if (!functionCall || typeof functionCall !== "object") {
9372
+ continue;
9234
9373
  }
9235
- seen.add(normalized);
9236
- turnTextsBySession.set(turn.sessionId, seen);
9374
+ const record = functionCall;
9375
+ if (typeof record.name !== "string") {
9376
+ continue;
9377
+ }
9378
+ toolCalls.push({
9379
+ args: record.args && typeof record.args === "object" ? record.args : {},
9380
+ id: typeof record.id === "string" ? record.id : undefined,
9381
+ name: record.name
9382
+ });
9237
9383
  }
9238
- const missingAssistantReplySessions = [...sessionsWithTurns].filter((sessionId) => !sessionIdsWithAssistantReply.has(sessionId)).length;
9239
- const providerEvents = events.filter((event) => event.type === "session.error" && typeof event.payload.provider === "string" && typeof event.payload.providerStatus === "string");
9240
- const providerErrors = providerEvents.filter((event) => event.payload.providerStatus === "error");
9241
- const providerFallbacks = providerEvents.filter((event) => event.payload.providerStatus === "fallback");
9242
- const providerTimeouts = providerEvents.filter((event) => event.payload.timedOut === true);
9243
- const providerLatencies = providerEvents.map((event) => getNumber3(event.payload.elapsedMs)).filter((value) => value !== undefined);
9244
- const averageProviderLatencyMs = providerLatencies.length > 0 ? providerLatencies.reduce((sum, value) => sum + value, 0) / providerLatencies.length : 0;
9245
- const handoffHealth = await summarizeVoiceHandoffHealth({ events });
9246
- const metrics = {
9247
- duplicateTurnRate: createMetric({
9248
- actual: rate(duplicateTurns, committedTurns.length),
9249
- label: "Duplicate turn rate",
9250
- threshold: thresholds.maxDuplicateTurnRate,
9251
- unit: "rate"
9252
- }),
9253
- emptyTurnRate: createMetric({
9254
- actual: rate(emptyTurns.length, committedTurns.length),
9255
- label: "Empty turn rate",
9256
- threshold: thresholds.maxEmptyTurnRate,
9257
- unit: "rate"
9258
- }),
9259
- handoffFailureRate: createMetric({
9260
- actual: rate(handoffHealth.failed, handoffHealth.total),
9261
- label: "Handoff failure rate",
9262
- threshold: thresholds.maxHandoffFailureRate,
9263
- unit: "rate"
9264
- }),
9265
- missingAssistantReplyRate: createMetric({
9266
- actual: rate(missingAssistantReplySessions, sessionsWithTurns.size),
9267
- label: "Missing assistant reply rate",
9268
- threshold: thresholds.maxMissingAssistantReplyRate,
9269
- unit: "rate"
9270
- }),
9271
- providerAverageLatencyMs: createMetric({
9272
- actual: averageProviderLatencyMs,
9273
- label: "Average provider latency",
9274
- threshold: thresholds.maxProviderAverageLatencyMs,
9275
- unit: "ms"
9276
- }),
9277
- providerErrorRate: createMetric({
9278
- actual: rate(providerErrors.length, providerEvents.length),
9279
- label: "Provider error rate",
9280
- threshold: thresholds.maxProviderErrorRate,
9281
- unit: "rate"
9282
- }),
9283
- providerFallbackRate: createMetric({
9284
- actual: rate(providerFallbacks.length, providerEvents.length),
9285
- label: "Provider fallback rate",
9286
- threshold: thresholds.maxProviderFallbackRate,
9287
- unit: "rate"
9288
- }),
9289
- providerTimeoutRate: createMetric({
9290
- actual: rate(providerTimeouts.length, providerEvents.length),
9291
- label: "Provider timeout rate",
9292
- threshold: thresholds.maxProviderTimeoutRate,
9293
- unit: "rate"
9294
- })
9295
- };
9296
- const status = Object.values(metrics).every((metric) => metric.pass) ? "pass" : "fail";
9297
- return {
9298
- checkedAt: Date.now(),
9299
- eventCount: events.length,
9300
- metrics,
9301
- status,
9302
- thresholds
9303
- };
9304
- };
9305
- var escapeHtml9 = (value) => value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
9306
- var formatMetricValue = (metric) => metric.unit === "rate" ? `${(metric.actual * 100).toFixed(2)}%` : metric.unit === "ms" ? `${Math.round(metric.actual)}ms` : String(metric.actual);
9307
- var formatThreshold = (metric) => metric.unit === "rate" ? `${(metric.threshold * 100).toFixed(2)}%` : metric.unit === "ms" ? `${Math.round(metric.threshold)}ms` : String(metric.threshold);
9308
- var renderVoiceQualityHTML = (report, options = {}) => {
9309
- const rows = Object.entries(report.metrics).map(([key, metric]) => `<tr class="${metric.pass ? "pass" : "fail"}"><td>${escapeHtml9(metric.label)}</td><td>${escapeHtml9(formatMetricValue(metric))}</td><td>${escapeHtml9(formatThreshold(metric))}</td><td>${metric.pass ? "pass" : "fail"}</td><td><code>${escapeHtml9(key)}</code></td></tr>`).join("");
9310
- const links = options.links?.length ? `<nav>${options.links.map((link) => `<a href="${escapeHtml9(link.href)}">${escapeHtml9(link.label)}</a>`).join("")}</nav>` : "";
9311
- return `<!doctype html><html lang="en"><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /><title>AbsoluteJS Voice Quality</title><style>body{font-family:ui-sans-serif,system-ui,sans-serif;margin:2rem;background:#f8f7f2;color:#181713}main{max-width:1100px;margin:auto}nav{display:flex;flex-wrap:wrap;gap:.5rem;margin:0 0 1.25rem}nav a{background:#181713;border-radius:999px;color:white;padding:.35rem .7rem;text-decoration:none}.status{border-radius:999px;display:inline-flex;padding:.35rem .75rem;font-weight:800}.status.pass{background:#dcfce7;color:#166534}.status.fail{background:#fee2e2;color:#991b1b}table{border-collapse:collapse;width:100%;background:white;margin-top:1rem}td,th{border-bottom:1px solid #eee;padding:.75rem;text-align:left}.pass td{border-left:4px solid #16a34a}.fail td{border-left:4px solid #dc2626}code{background:#f3f4f6;padding:.15rem .3rem;border-radius:.3rem}</style></head><body><main>${links}<h1>Voice quality gates</h1><p class="status ${report.status}">${report.status}</p><p>${report.eventCount} event(s) checked.</p><table><thead><tr><th>Metric</th><th>Actual</th><th>Threshold</th><th>Status</th><th>Key</th></tr></thead><tbody>${rows}</tbody></table></main></body></html>`;
9384
+ return toolCalls;
9312
9385
  };
9313
- var createVoiceQualityRoutes = (options) => {
9314
- const path = options.path ?? "/quality";
9315
- const routes = new Elysia7({
9316
- name: options.name ?? "absolutejs-voice-quality"
9317
- });
9318
- const getReport = () => evaluateVoiceQuality({
9319
- events: options.events,
9320
- store: options.store,
9321
- thresholds: options.thresholds
9322
- });
9323
- routes.get(path, async () => {
9324
- const report = await getReport();
9325
- return new Response(renderVoiceQualityHTML(report, { links: options.links }), {
9326
- headers: {
9327
- "Content-Type": "text/html; charset=utf-8",
9328
- ...options.headers
9386
+ var createGeminiVoiceAssistantModel = (options) => {
9387
+ const fetchImpl = options.fetch ?? globalThis.fetch;
9388
+ const baseUrl = options.baseUrl ?? "https://generativelanguage.googleapis.com/v1beta";
9389
+ const model = options.model ?? "gemini-2.5-flash";
9390
+ const maxRetries = Math.max(0, options.maxRetries ?? 2);
9391
+ return {
9392
+ generate: async (input) => {
9393
+ const endpoint = `${baseUrl.replace(/\/$/, "")}/models/${encodeURIComponent(model)}:generateContent?key=${encodeURIComponent(options.apiKey)}`;
9394
+ let response;
9395
+ for (let attempt = 0;attempt <= maxRetries; attempt += 1) {
9396
+ response = await fetchImpl(endpoint, {
9397
+ body: JSON.stringify({
9398
+ contents: input.messages.map(messageToGeminiContent).filter(Boolean),
9399
+ generationConfig: {
9400
+ maxOutputTokens: options.maxOutputTokens,
9401
+ ...input.tools.length ? {} : {
9402
+ responseMimeType: "application/json",
9403
+ responseSchema: toGeminiSchema(OUTPUT_SCHEMA)
9404
+ },
9405
+ temperature: options.temperature
9406
+ },
9407
+ systemInstruction: {
9408
+ parts: [
9409
+ {
9410
+ text: [input.system, ROUTE_RESULT_INSTRUCTION].filter(Boolean).join(`
9411
+
9412
+ `)
9413
+ }
9414
+ ]
9415
+ },
9416
+ tools: input.tools.length ? [
9417
+ {
9418
+ functionDeclarations: input.tools.map((tool) => ({
9419
+ description: tool.description,
9420
+ name: tool.name,
9421
+ parameters: toGeminiSchema(tool.parameters ?? {
9422
+ additionalProperties: true,
9423
+ type: "object"
9424
+ })
9425
+ }))
9426
+ }
9427
+ ] : undefined
9428
+ }),
9429
+ headers: {
9430
+ "content-type": "application/json"
9431
+ },
9432
+ method: "POST"
9433
+ });
9434
+ if (response.ok || response.status !== 429 && response.status < 500 || attempt === maxRetries) {
9435
+ break;
9436
+ }
9437
+ const retryAfter = Number(response.headers.get("retry-after"));
9438
+ await sleep4(Number.isFinite(retryAfter) && retryAfter > 0 ? retryAfter * 1000 : 500 * 2 ** attempt);
9329
9439
  }
9330
- });
9331
- });
9332
- routes.get(`${path}/json`, async () => getReport());
9333
- routes.get(`${path}/status`, async ({ set }) => {
9334
- const report = await getReport();
9335
- if (report.status === "fail") {
9336
- set.status = 503;
9440
+ if (!response) {
9441
+ throw new Error("Gemini voice assistant model failed: no response");
9442
+ }
9443
+ if (!response.ok) {
9444
+ throw createHTTPError("Gemini", response);
9445
+ }
9446
+ const body = await response.json();
9447
+ if (body.usageMetadata && typeof body.usageMetadata === "object") {
9448
+ await options.onUsage?.(body.usageMetadata);
9449
+ }
9450
+ const toolCalls = extractGeminiToolCalls(body);
9451
+ if (toolCalls.length) {
9452
+ return {
9453
+ assistantText: extractGeminiText(body) || undefined,
9454
+ toolCalls
9455
+ };
9456
+ }
9457
+ return normalizeRouteOutput(parseJSON(extractGeminiText(body)));
9337
9458
  }
9338
- return report;
9339
- });
9340
- return routes;
9459
+ };
9341
9460
  };
9461
+ // src/opsConsoleRoutes.ts
9462
+ import { Elysia as Elysia10 } from "elysia";
9342
9463
 
9343
9464
  // src/resilienceRoutes.ts
9344
- import { Elysia as Elysia8 } from "elysia";
9345
- var escapeHtml10 = (value) => value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
9465
+ import { Elysia as Elysia9 } from "elysia";
9466
+ var escapeHtml11 = (value) => value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
9346
9467
  var getString7 = (value) => typeof value === "string" ? value : undefined;
9347
9468
  var getNumber4 = (value) => typeof value === "number" && Number.isFinite(value) ? value : undefined;
9348
9469
  var getBoolean2 = (value) => value === true;
@@ -9405,13 +9526,13 @@ var summarizeRoutingEvents = (events) => {
9405
9526
  };
9406
9527
  var renderProviderCards = (title, providers) => {
9407
9528
  if (providers.length === 0) {
9408
- return `<p class="muted">No ${escapeHtml10(title)} provider health yet.</p>`;
9529
+ return `<p class="muted">No ${escapeHtml11(title)} provider health yet.</p>`;
9409
9530
  }
9410
9531
  return `<div class="provider-grid">${providers.map((provider) => `
9411
- <article class="card provider ${escapeHtml10(provider.status)}">
9532
+ <article class="card provider ${escapeHtml11(provider.status)}">
9412
9533
  <div class="card-header">
9413
- <strong>${escapeHtml10(provider.provider)}</strong>
9414
- <span>${escapeHtml10(provider.status)}${provider.recommended ? " \xB7 recommended" : ""}</span>
9534
+ <strong>${escapeHtml11(provider.provider)}</strong>
9535
+ <span>${escapeHtml11(provider.status)}${provider.recommended ? " \xB7 recommended" : ""}</span>
9415
9536
  </div>
9416
9537
  <dl>
9417
9538
  <div><dt>Runs</dt><dd>${provider.runCount}</dd></div>
@@ -9420,7 +9541,7 @@ var renderProviderCards = (title, providers) => {
9420
9541
  <div><dt>Timeouts</dt><dd>${provider.timeoutCount}</dd></div>
9421
9542
  <div><dt>Fallbacks</dt><dd>${provider.fallbackCount}</dd></div>
9422
9543
  </dl>
9423
- ${provider.lastError ? `<p class="muted">${escapeHtml10(provider.lastError)}</p>` : ""}
9544
+ ${provider.lastError ? `<p class="muted">${escapeHtml11(provider.lastError)}</p>` : ""}
9424
9545
  </article>
9425
9546
  `).join("")}</div>`;
9426
9547
  };
@@ -9429,24 +9550,24 @@ var renderTimeline2 = (events) => {
9429
9550
  return '<p class="muted">No provider routing events yet. Run the app or simulate provider failover.</p>';
9430
9551
  }
9431
9552
  return `<div class="timeline">${events.slice(0, 40).map((event) => `
9432
- <article class="card event ${escapeHtml10(event.status ?? "unknown")}">
9553
+ <article class="card event ${escapeHtml11(event.status ?? "unknown")}">
9433
9554
  <div class="card-header">
9434
- <strong>${escapeHtml10(event.kind.toUpperCase())} ${escapeHtml10(event.operation ?? "generate")}</strong>
9555
+ <strong>${escapeHtml11(event.kind.toUpperCase())} ${escapeHtml11(event.operation ?? "generate")}</strong>
9435
9556
  <span>${new Date(event.at).toLocaleString()}</span>
9436
9557
  </div>
9437
9558
  <p>
9438
- <span class="pill">${escapeHtml10(event.status ?? "unknown")}</span>
9439
- <span class="pill">provider: ${escapeHtml10(event.provider ?? "unknown")}</span>
9440
- ${event.fallbackProvider ? `<span class="pill">fallback: ${escapeHtml10(event.fallbackProvider)}</span>` : ""}
9559
+ <span class="pill">${escapeHtml11(event.status ?? "unknown")}</span>
9560
+ <span class="pill">provider: ${escapeHtml11(event.provider ?? "unknown")}</span>
9561
+ ${event.fallbackProvider ? `<span class="pill">fallback: ${escapeHtml11(event.fallbackProvider)}</span>` : ""}
9441
9562
  ${event.timedOut ? '<span class="pill danger">timed out</span>' : ""}
9442
9563
  </p>
9443
9564
  <dl>
9444
9565
  <div><dt>Attempt</dt><dd>${event.attempt ?? 0}</dd></div>
9445
9566
  <div><dt>Elapsed</dt><dd>${event.elapsedMs ?? 0}ms</dd></div>
9446
9567
  <div><dt>Budget</dt><dd>${event.latencyBudgetMs ?? 0}ms</dd></div>
9447
- <div><dt>Session</dt><dd>${escapeHtml10(event.sessionId)}</dd></div>
9568
+ <div><dt>Session</dt><dd>${escapeHtml11(event.sessionId)}</dd></div>
9448
9569
  </dl>
9449
- ${event.error ? `<p class="muted">${escapeHtml10(event.error)}</p>` : ""}
9570
+ ${event.error ? `<p class="muted">${escapeHtml11(event.error)}</p>` : ""}
9450
9571
  </article>
9451
9572
  `).join("")}</div>`;
9452
9573
  };
@@ -9461,26 +9582,26 @@ var renderSimulationControls = (kind, simulation) => {
9461
9582
  const pathPrefix = simulation.pathPrefix ?? `/api/${kind}-simulate`;
9462
9583
  const failureProviders = simulation.failureProviders ?? configuredProviders.map(({ provider }) => provider);
9463
9584
  const canFail = (provider) => configuredProviders.some((entry) => entry.provider === provider) && (!simulation.fallbackRequiredProvider || configuredProviders.some((entry) => entry.provider === simulation.fallbackRequiredProvider));
9464
- return `<div class="simulate-panel" data-sim-kind="${kind}" data-sim-prefix="${escapeHtml10(pathPrefix)}">
9465
- <p class="muted">${escapeHtml10(simulation.failureMessage ?? `Simulate ${kind.toUpperCase()} provider failure without changing provider credentials.`)}</p>
9585
+ return `<div class="simulate-panel" data-sim-kind="${kind}" data-sim-prefix="${escapeHtml11(pathPrefix)}">
9586
+ <p class="muted">${escapeHtml11(simulation.failureMessage ?? `Simulate ${kind.toUpperCase()} provider failure without changing provider credentials.`)}</p>
9466
9587
  <div class="simulate-actions">
9467
- ${failureProviders.map((provider) => `<button type="button" data-provider-fail="${escapeHtml10(provider)}"${canFail(provider) ? "" : " disabled"}>Simulate ${escapeHtml10(provider)} ${kind.toUpperCase()} failure</button>`).join("")}
9468
- ${configuredProviders.map((provider) => `<button type="button" data-provider-recover="${escapeHtml10(provider.provider)}">Mark ${escapeHtml10(provider.provider)} recovered</button>`).join("")}
9588
+ ${failureProviders.map((provider) => `<button type="button" data-provider-fail="${escapeHtml11(provider)}"${canFail(provider) ? "" : " disabled"}>Simulate ${escapeHtml11(provider)} ${kind.toUpperCase()} failure</button>`).join("")}
9589
+ ${configuredProviders.map((provider) => `<button type="button" data-provider-recover="${escapeHtml11(provider.provider)}">Mark ${escapeHtml11(provider.provider)} recovered</button>`).join("")}
9469
9590
  </div>
9470
- ${simulation.fallbackRequiredProvider && !configuredProviders.some((entry) => entry.provider === simulation.fallbackRequiredProvider) ? `<p class="muted">${escapeHtml10(simulation.fallbackRequiredMessage ?? `Configure ${simulation.fallbackRequiredProvider} to enable fallback simulation.`)}</p>` : ""}
9591
+ ${simulation.fallbackRequiredProvider && !configuredProviders.some((entry) => entry.provider === simulation.fallbackRequiredProvider) ? `<p class="muted">${escapeHtml11(simulation.fallbackRequiredMessage ?? `Configure ${simulation.fallbackRequiredProvider} to enable fallback simulation.`)}</p>` : ""}
9471
9592
  <pre class="simulate-output" hidden></pre>
9472
9593
  </div>`;
9473
9594
  };
9474
9595
  var renderVoiceResilienceHTML = (input) => {
9475
9596
  const summary = summarizeRoutingEvents(input.routingEvents);
9476
- const kindCounts = [...summary.byKind.entries()].map(([kind, count]) => `<span class="pill">${escapeHtml10(kind)}: ${String(count)}</span>`).join("");
9477
- const links = input.links?.length ? input.links.map((link) => `<a href="${escapeHtml10(link.href)}">${escapeHtml10(link.label)}</a>`).join(" \xB7 ") : "";
9597
+ const kindCounts = [...summary.byKind.entries()].map(([kind, count]) => `<span class="pill">${escapeHtml11(kind)}: ${String(count)}</span>`).join("");
9598
+ const links = input.links?.length ? input.links.map((link) => `<a href="${escapeHtml11(link.href)}">${escapeHtml11(link.label)}</a>`).join(" \xB7 ") : "";
9478
9599
  return `<!doctype html>
9479
9600
  <html lang="en">
9480
9601
  <head>
9481
9602
  <meta charset="utf-8" />
9482
9603
  <meta name="viewport" content="width=device-width, initial-scale=1" />
9483
- <title>${escapeHtml10(input.title ?? "AbsoluteJS Voice Resilience")}</title>
9604
+ <title>${escapeHtml11(input.title ?? "AbsoluteJS Voice Resilience")}</title>
9484
9605
  <style>
9485
9606
  :root { color-scheme: dark; }
9486
9607
  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; }
@@ -9616,7 +9737,7 @@ var registerSimulationRoutes = (routes, simulation, defaultPathPrefix) => {
9616
9737
  };
9617
9738
  var createVoiceResilienceRoutes = (options) => {
9618
9739
  const path = options.path ?? "/resilience";
9619
- const routes = new Elysia8({
9740
+ const routes = new Elysia9({
9620
9741
  name: options.name ?? "absolutejs-voice-resilience"
9621
9742
  }).get(path, async () => {
9622
9743
  const events = await options.store.list();
@@ -9662,6 +9783,12 @@ var DEFAULT_LINKS = [
9662
9783
  label: "Quality",
9663
9784
  statusHref: "/quality/status"
9664
9785
  },
9786
+ {
9787
+ description: "Replay stored sessions against acceptance gates over time.",
9788
+ href: "/evals",
9789
+ label: "Evals",
9790
+ statusHref: "/evals/status"
9791
+ },
9665
9792
  {
9666
9793
  description: "Provider health, fallback paths, and failure simulation.",
9667
9794
  href: "/resilience",
@@ -9683,7 +9810,7 @@ var DEFAULT_LINKS = [
9683
9810
  label: "Handoffs"
9684
9811
  }
9685
9812
  ];
9686
- var escapeHtml11 = (value) => value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
9813
+ var escapeHtml12 = (value) => value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
9687
9814
  var countProviderStatuses = (providers) => {
9688
9815
  const degradedStatuses = new Set(["degraded", "rate-limited", "suppressed"]);
9689
9816
  const healthy = providers.filter((provider) => provider.status === "healthy").length;
@@ -9739,20 +9866,20 @@ var buildVoiceOpsConsoleReport = async (options) => {
9739
9866
  trace
9740
9867
  };
9741
9868
  };
9742
- var renderMetricCard = (input) => `<article class="metric"><span>${escapeHtml11(input.label)}</span><strong>${escapeHtml11(String(input.value))}</strong>${input.status ? `<p class="${escapeHtml11(input.status)}">${escapeHtml11(input.status)}</p>` : ""}${input.href ? `<a href="${escapeHtml11(input.href)}">Open</a>` : ""}</article>`;
9869
+ 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>`;
9743
9870
  var renderVoiceOpsConsoleHTML = (report, options = {}) => {
9744
9871
  const links = report.links.map((link) => `<article class="surface">
9745
- <div><h2>${escapeHtml11(link.label)}</h2>${link.description ? `<p>${escapeHtml11(link.description)}</p>` : ""}</div>
9746
- <p><a href="${escapeHtml11(link.href)}">Open ${escapeHtml11(link.label)}</a>${link.statusHref ? ` \xB7 <a href="${escapeHtml11(link.statusHref)}">Status</a>` : ""}</p>
9872
+ <div><h2>${escapeHtml12(link.label)}</h2>${link.description ? `<p>${escapeHtml12(link.description)}</p>` : ""}</div>
9873
+ <p><a href="${escapeHtml12(link.href)}">Open ${escapeHtml12(link.label)}</a>${link.statusHref ? ` \xB7 <a href="${escapeHtml12(link.statusHref)}">Status</a>` : ""}</p>
9747
9874
  </article>`).join("");
9748
- const sessions = report.recentSessions.length ? report.recentSessions.map((session) => `<tr><td>${escapeHtml11(session.sessionId)}</td><td>${escapeHtml11(session.status)}</td><td>${session.turnCount}</td><td>${session.errorCount}</td><td>${session.replayHref ? `<a href="${escapeHtml11(session.replayHref)}">Replay</a>` : ""}</td></tr>`).join("") : '<tr><td colspan="5">No sessions yet.</td></tr>';
9749
- const routing = report.recentRoutingEvents.length ? report.recentRoutingEvents.map((event) => `<tr><td>${escapeHtml11(event.kind)}</td><td>${escapeHtml11(event.provider ?? "unknown")}</td><td>${escapeHtml11(event.status ?? "unknown")}</td><td>${event.elapsedMs ?? 0}ms</td><td>${escapeHtml11(event.sessionId)}</td></tr>`).join("") : '<tr><td colspan="5">No provider routing events yet.</td></tr>';
9875
+ 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>';
9876
+ 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>';
9750
9877
  const title = options.title ?? "AbsoluteJS Voice Ops Console";
9751
- return `<!doctype html><html lang="en"><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /><title>${escapeHtml11(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>${escapeHtml11(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 ${escapeHtml11(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>`;
9878
+ 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>`;
9752
9879
  };
9753
9880
  var createVoiceOpsConsoleRoutes = (options) => {
9754
9881
  const path = options.path ?? "/ops-console";
9755
- const routes = new Elysia9({
9882
+ const routes = new Elysia10({
9756
9883
  name: options.name ?? "absolutejs-voice-ops-console"
9757
9884
  });
9758
9885
  const getReport = () => buildVoiceOpsConsoleReport(options);
@@ -10612,7 +10739,7 @@ var createVoiceMemoryStore = () => {
10612
10739
  return { get, getOrCreate, list, remove, set };
10613
10740
  };
10614
10741
  // src/opsWebhook.ts
10615
- import { Elysia as Elysia10 } from "elysia";
10742
+ import { Elysia as Elysia11 } from "elysia";
10616
10743
  var toHex5 = (bytes) => Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
10617
10744
  var signVoiceOpsWebhookBody = async (input) => {
10618
10745
  const encoder = new TextEncoder;
@@ -10742,7 +10869,7 @@ var verifyVoiceOpsWebhookSignature = async (input) => {
10742
10869
  };
10743
10870
  var createVoiceOpsWebhookReceiverRoutes = (options = {}) => {
10744
10871
  const path = options.path ?? "/api/voice-ops/webhook";
10745
- return new Elysia10().post(path, async ({ body, request, set }) => {
10872
+ return new Elysia11().post(path, async ({ body, request, set }) => {
10746
10873
  const bodyText = typeof body === "string" ? body : JSON.stringify(body);
10747
10874
  if (options.signingSecret) {
10748
10875
  const verification = await verifyVoiceOpsWebhookSignature({
@@ -12904,6 +13031,7 @@ export {
12904
13031
  startVoiceOpsTask,
12905
13032
  shapeTelephonyAssistantText,
12906
13033
  selectVoiceTraceEventsForPrune,
13034
+ runVoiceSessionEvals,
12907
13035
  resolveVoiceTraceRedactionOptions,
12908
13036
  resolveVoiceSTTRoutingStrategy,
12909
13037
  resolveVoiceRuntimePreset,
@@ -12926,6 +13054,7 @@ export {
12926
13054
  renderVoiceProviderHealthHTML,
12927
13055
  renderVoiceOpsConsoleHTML,
12928
13056
  renderVoiceHandoffHealthHTML,
13057
+ renderVoiceEvalHTML,
12929
13058
  renderVoiceCallReviewMarkdown,
12930
13059
  renderVoiceCallReviewHTML,
12931
13060
  renderVoiceAssistantHealthHTML,
@@ -13051,6 +13180,7 @@ export {
13051
13180
  createVoiceExternalObjectMapId,
13052
13181
  createVoiceExternalObjectMap,
13053
13182
  createVoiceExperiment,
13183
+ createVoiceEvalRoutes,
13054
13184
  createVoiceDiagnosticsRoutes,
13055
13185
  createVoiceCallReviewRecorder,
13056
13186
  createVoiceCallReviewFromSession,