@absolutejs/voice 0.0.22-beta.43 → 0.0.22-beta.45

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.
@@ -59,6 +59,73 @@ export type VoiceEvalBaselineStore = {
59
59
  get: () => Promise<VoiceEvalReport | undefined>;
60
60
  set: (report: VoiceEvalReport) => Promise<void>;
61
61
  };
62
+ export type VoiceScenarioEvalDefinition = {
63
+ description?: string;
64
+ forbiddenHandoffActions?: string[];
65
+ forbiddenLifecycleTypes?: string[];
66
+ id: string;
67
+ label?: string;
68
+ maxProviderErrors?: number;
69
+ maxSessionErrors?: number;
70
+ minSessions?: number;
71
+ minTurns?: number;
72
+ requiredAssistantIncludes?: string[];
73
+ requiredDisposition?: string;
74
+ requiredHandoffActions?: string[];
75
+ requiredLifecycleTypes?: string[];
76
+ requiredPayloadPaths?: string[];
77
+ requiredTranscriptIncludes?: string[];
78
+ scenarioId?: string;
79
+ };
80
+ export type VoiceScenarioEvalSessionResult = {
81
+ eventCount: number;
82
+ issues: string[];
83
+ sessionId: string;
84
+ status: VoiceEvalStatus;
85
+ };
86
+ export type VoiceScenarioEvalResult = {
87
+ description?: string;
88
+ failed: number;
89
+ id: string;
90
+ issues: string[];
91
+ label: string;
92
+ matchedSessions: number;
93
+ passed: number;
94
+ sessions: VoiceScenarioEvalSessionResult[];
95
+ status: VoiceEvalStatus;
96
+ };
97
+ export type VoiceScenarioEvalReport = {
98
+ checkedAt: number;
99
+ failed: number;
100
+ passed: number;
101
+ scenarios: VoiceScenarioEvalResult[];
102
+ status: VoiceEvalStatus;
103
+ total: number;
104
+ };
105
+ export type VoiceScenarioFixture = {
106
+ description?: string;
107
+ events: StoredVoiceTraceEvent[];
108
+ id: string;
109
+ label?: string;
110
+ };
111
+ export type VoiceScenarioFixtureStore = {
112
+ list: () => Promise<VoiceScenarioFixture[]>;
113
+ };
114
+ export type VoiceScenarioFixtureEvalResult = {
115
+ description?: string;
116
+ fixtureId: string;
117
+ label: string;
118
+ report: VoiceScenarioEvalReport;
119
+ status: VoiceEvalStatus;
120
+ };
121
+ export type VoiceScenarioFixtureEvalReport = {
122
+ checkedAt: number;
123
+ failed: number;
124
+ fixtures: VoiceScenarioFixtureEvalResult[];
125
+ passed: number;
126
+ status: VoiceEvalStatus;
127
+ total: number;
128
+ };
62
129
  export type VoiceEvalLink = {
63
130
  href: string;
64
131
  label: string;
@@ -68,11 +135,14 @@ export type VoiceEvalRoutesOptions = {
68
135
  baselineComparison?: VoiceEvalBaselineComparisonOptions;
69
136
  baselineStore?: VoiceEvalBaselineStore;
70
137
  events?: StoredVoiceTraceEvent[];
138
+ fixtures?: VoiceScenarioFixture[];
139
+ fixtureStore?: VoiceScenarioFixtureStore;
71
140
  headers?: HeadersInit;
72
141
  links?: VoiceEvalLink[];
73
142
  limit?: number;
74
143
  name?: string;
75
144
  path?: string;
145
+ scenarios?: VoiceScenarioEvalDefinition[];
76
146
  store?: VoiceTraceEventStore;
77
147
  thresholds?: VoiceQualityThresholds;
78
148
  title?: string;
@@ -83,8 +153,19 @@ export declare const runVoiceSessionEvals: (options?: {
83
153
  store?: VoiceTraceEventStore;
84
154
  thresholds?: VoiceQualityThresholds;
85
155
  }) => Promise<VoiceEvalReport>;
156
+ export declare const runVoiceScenarioEvals: (options?: {
157
+ events?: StoredVoiceTraceEvent[];
158
+ scenarios?: VoiceScenarioEvalDefinition[];
159
+ store?: VoiceTraceEventStore;
160
+ }) => Promise<VoiceScenarioEvalReport>;
161
+ export declare const runVoiceScenarioFixtureEvals: (options?: {
162
+ fixtures?: VoiceScenarioFixture[];
163
+ fixtureStore?: VoiceScenarioFixtureStore;
164
+ scenarios?: VoiceScenarioEvalDefinition[];
165
+ }) => Promise<VoiceScenarioFixtureEvalReport>;
86
166
  export declare const compareVoiceEvalBaseline: (currentReport: VoiceEvalReport, baselineReport: VoiceEvalReport, options?: VoiceEvalBaselineComparisonOptions) => VoiceEvalBaselineComparison;
87
167
  export declare const createVoiceFileEvalBaselineStore: (filePath: string) => VoiceEvalBaselineStore;
168
+ export declare const createVoiceFileScenarioFixtureStore: (filePath: string) => VoiceScenarioFixtureStore;
88
169
  export declare const renderVoiceEvalHTML: (report: VoiceEvalReport, options?: {
89
170
  links?: VoiceEvalLink[];
90
171
  title?: string;
@@ -93,6 +174,14 @@ export declare const renderVoiceEvalBaselineHTML: (comparison: VoiceEvalBaseline
93
174
  links?: VoiceEvalLink[];
94
175
  title?: string;
95
176
  }) => string;
177
+ export declare const renderVoiceScenarioEvalHTML: (report: VoiceScenarioEvalReport, options?: {
178
+ links?: VoiceEvalLink[];
179
+ title?: string;
180
+ }) => string;
181
+ export declare const renderVoiceScenarioFixtureEvalHTML: (report: VoiceScenarioFixtureEvalReport, options?: {
182
+ links?: VoiceEvalLink[];
183
+ title?: string;
184
+ }) => string;
96
185
  export declare const createVoiceEvalRoutes: (options: VoiceEvalRoutesOptions) => Elysia<"", {
97
186
  decorator: {};
98
187
  store: {};
package/dist/index.d.ts CHANGED
@@ -2,7 +2,7 @@ export { voice } from './plugin';
2
2
  export { createVoiceAssistant, createVoiceExperiment, summarizeVoiceAssistantRuns } from './assistant';
3
3
  export { createVoiceAssistantHealthHTMLHandler, createVoiceAssistantHealthJSONHandler, createVoiceAssistantHealthRoutes, renderVoiceAssistantHealthHTML, summarizeVoiceAssistantHealth } from './assistantHealth';
4
4
  export { buildVoiceDiagnosticsMarkdown, createVoiceDiagnosticsRoutes, resolveVoiceDiagnosticsTraceFilter } from './diagnosticsRoutes';
5
- export { compareVoiceEvalBaseline, createVoiceFileEvalBaselineStore, createVoiceEvalRoutes, renderVoiceEvalBaselineHTML, renderVoiceEvalHTML, runVoiceSessionEvals } from './evalRoutes';
5
+ export { compareVoiceEvalBaseline, createVoiceFileEvalBaselineStore, createVoiceFileScenarioFixtureStore, createVoiceEvalRoutes, renderVoiceEvalBaselineHTML, renderVoiceEvalHTML, renderVoiceScenarioEvalHTML, renderVoiceScenarioFixtureEvalHTML, runVoiceScenarioEvals, runVoiceScenarioFixtureEvals, runVoiceSessionEvals } from './evalRoutes';
6
6
  export { createVoiceSessionListRoutes, createVoiceSessionReplayHTMLHandler, createVoiceSessionReplayJSONHandler, createVoiceSessionReplayRoutes, createVoiceSessionsHTMLHandler, createVoiceSessionsJSONHandler, renderVoiceSessionsHTML, summarizeVoiceSessions, summarizeVoiceSessionReplay } from './sessionReplay';
7
7
  export { createVoiceAgent, createVoiceAgentSquad, createVoiceAgentTool } from './agent';
8
8
  export { createStoredVoiceCallReviewArtifact, createStoredVoiceExternalObjectMap, createStoredVoiceIntegrationEvent, createStoredVoiceOpsTask, createVoiceFileExternalObjectMapStore, createVoiceFileAssistantMemoryStore, createVoiceFileIntegrationEventStore, createVoiceFileReviewStore, createVoiceFileRuntimeStorage, createVoiceFileSessionStore, createVoiceFileTaskStore, createVoiceFileTraceSinkDeliveryStore, createVoiceFileTraceEventStore } from './fileStore';
@@ -40,7 +40,7 @@ export type { VoiceAssistant, VoiceAssistantArtifactPlan, VoiceAssistantExperime
40
40
  export type { VoiceAssistantHealthFailure, VoiceAssistantHealthHTMLHandlerOptions, VoiceAssistantHealthRoutesOptions, VoiceAssistantHealthSummary, VoiceAssistantHealthSummaryOptions } from './assistantHealth';
41
41
  export type { VoiceAssistantMemoryBinding, VoiceAssistantMemoryHandle, VoiceAssistantMemoryOptions, VoiceAssistantMemoryRecord, VoiceAssistantMemoryStore } from './assistantMemory';
42
42
  export type { VoiceDiagnosticsRoutesOptions } from './diagnosticsRoutes';
43
- export type { VoiceEvalBaselineComparison, VoiceEvalBaselineComparisonOptions, VoiceEvalBaselineStore, VoiceEvalBaselineSummary, VoiceEvalLink, VoiceEvalReport, VoiceEvalRoutesOptions, VoiceEvalSessionReport, VoiceEvalStatus, VoiceEvalTrendBucket } from './evalRoutes';
43
+ export type { VoiceEvalBaselineComparison, VoiceEvalBaselineComparisonOptions, VoiceEvalBaselineStore, VoiceEvalBaselineSummary, VoiceEvalLink, VoiceEvalReport, VoiceEvalRoutesOptions, VoiceEvalSessionReport, VoiceEvalStatus, VoiceEvalTrendBucket, VoiceScenarioEvalDefinition, VoiceScenarioEvalReport, VoiceScenarioEvalResult, VoiceScenarioEvalSessionResult, VoiceScenarioFixture, VoiceScenarioFixtureEvalReport, VoiceScenarioFixtureEvalResult, VoiceScenarioFixtureStore } from './evalRoutes';
44
44
  export type { VoiceSessionListHTMLHandlerOptions, VoiceSessionListItem, VoiceSessionListOptions, VoiceSessionListRoutesOptions, VoiceSessionListStatus, VoiceSessionReplay, VoiceSessionReplayHTMLHandlerOptions, VoiceSessionReplayOptions, VoiceSessionReplayRoutesOptions, VoiceSessionReplayTurn } from './sessionReplay';
45
45
  export type { AnthropicVoiceAssistantModelOptions, GeminiVoiceAssistantModelOptions, OpenAIVoiceAssistantModelOptions, VoiceProviderRouterEvent, VoiceProviderRouterFallbackMode, VoiceProviderRouterHealthOptions, VoiceProviderRouterOptions, VoiceProviderRouterPolicy, VoiceProviderRouterProviderHealth, VoiceProviderRouterProviderProfile, VoiceJSONAssistantModelHandler, VoiceJSONAssistantModelOptions } from './modelAdapters';
46
46
  export type { VoiceProviderHealthStatus, VoiceProviderHealthSummary, VoiceProviderHealthSummaryOptions } from './providerHealth';
package/dist/index.js CHANGED
@@ -7977,6 +7977,24 @@ var createVoiceQualityRoutes = (options) => {
7977
7977
  // src/evalRoutes.ts
7978
7978
  var escapeHtml9 = (value) => value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
7979
7979
  var rate2 = (count, total) => count / Math.max(1, total);
7980
+ var normalizeSearchText = (value) => value.trim().toLowerCase();
7981
+ var getString6 = (value) => typeof value === "string" ? value : undefined;
7982
+ var getObject = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : undefined;
7983
+ var getPathValue = (value, path) => {
7984
+ let current = value;
7985
+ for (const part of path.split(".").filter(Boolean)) {
7986
+ const record = getObject(current);
7987
+ if (!record || !(part in record)) {
7988
+ return;
7989
+ }
7990
+ current = record[part];
7991
+ }
7992
+ return current;
7993
+ };
7994
+ var includesAll = (haystack, needles) => {
7995
+ const normalized = normalizeSearchText(haystack);
7996
+ return needles.filter((needle) => !normalized.includes(normalizeSearchText(needle)));
7997
+ };
7980
7998
  var sessionTime = (events) => {
7981
7999
  const sorted = filterVoiceTraceEvents(events);
7982
8000
  return {
@@ -8047,6 +8065,135 @@ var runVoiceSessionEvals = async (options = {}) => {
8047
8065
  trend: buildTrend(limitedSessions)
8048
8066
  };
8049
8067
  };
8068
+ var getSessionText = (events, type) => events.filter((event) => event.type === type).map((event) => getString6(event.payload.text)).filter((text) => Boolean(text?.trim())).join(`
8069
+ `);
8070
+ var countProviderErrors = (events) => events.filter((event) => event.type === "session.error" && (event.payload.providerStatus === "error" || typeof event.payload.provider === "string")).length;
8071
+ var evaluateScenarioSession = (scenario, sessionId, events) => {
8072
+ const issues = [];
8073
+ const committedText = getSessionText(events, "turn.committed");
8074
+ const assistantText = getSessionText(events, "turn.assistant");
8075
+ const lifecycleTypes = events.filter((event) => event.type === "call.lifecycle").map((event) => getString6(event.payload.type)).filter((type) => Boolean(type));
8076
+ const dispositions = events.filter((event) => event.type === "call.lifecycle").map((event) => getString6(event.payload.disposition)).filter((disposition) => Boolean(disposition));
8077
+ const handoffActions = events.filter((event) => event.type === "call.handoff").map((event) => getString6(event.payload.action)).filter((action) => Boolean(action));
8078
+ const turnCount = events.filter((event) => event.type === "turn.committed").length;
8079
+ const sessionErrorCount = events.filter((event) => event.type === "session.error").length;
8080
+ const providerErrorCount = countProviderErrors(events);
8081
+ for (const missing of includesAll(committedText, scenario.requiredTranscriptIncludes ?? [])) {
8082
+ issues.push(`Missing transcript text: ${missing}`);
8083
+ }
8084
+ for (const missing of includesAll(assistantText, scenario.requiredAssistantIncludes ?? [])) {
8085
+ issues.push(`Missing assistant text: ${missing}`);
8086
+ }
8087
+ for (const type of scenario.requiredLifecycleTypes ?? []) {
8088
+ if (!lifecycleTypes.includes(type)) {
8089
+ issues.push(`Missing lifecycle event: ${type}`);
8090
+ }
8091
+ }
8092
+ for (const type of scenario.forbiddenLifecycleTypes ?? []) {
8093
+ if (lifecycleTypes.includes(type)) {
8094
+ issues.push(`Forbidden lifecycle event occurred: ${type}`);
8095
+ }
8096
+ }
8097
+ for (const action of scenario.requiredHandoffActions ?? []) {
8098
+ if (!handoffActions.includes(action)) {
8099
+ issues.push(`Missing handoff action: ${action}`);
8100
+ }
8101
+ }
8102
+ for (const action of scenario.forbiddenHandoffActions ?? []) {
8103
+ if (handoffActions.includes(action)) {
8104
+ issues.push(`Forbidden handoff action occurred: ${action}`);
8105
+ }
8106
+ }
8107
+ if (scenario.requiredDisposition && !dispositions.includes(scenario.requiredDisposition)) {
8108
+ issues.push(`Missing disposition: ${scenario.requiredDisposition}`);
8109
+ }
8110
+ if (scenario.minTurns !== undefined && turnCount < scenario.minTurns) {
8111
+ issues.push(`Expected at least ${scenario.minTurns} turn(s), saw ${turnCount}.`);
8112
+ }
8113
+ if (scenario.maxSessionErrors !== undefined && sessionErrorCount > scenario.maxSessionErrors) {
8114
+ issues.push(`Expected at most ${scenario.maxSessionErrors} session error(s), saw ${sessionErrorCount}.`);
8115
+ }
8116
+ if (scenario.maxProviderErrors !== undefined && providerErrorCount > scenario.maxProviderErrors) {
8117
+ issues.push(`Expected at most ${scenario.maxProviderErrors} provider error(s), saw ${providerErrorCount}.`);
8118
+ }
8119
+ for (const path of scenario.requiredPayloadPaths ?? []) {
8120
+ if (events.every((event) => getPathValue(event.payload, path) === undefined)) {
8121
+ issues.push(`Missing payload path: ${path}`);
8122
+ }
8123
+ }
8124
+ return {
8125
+ eventCount: events.length,
8126
+ issues,
8127
+ sessionId,
8128
+ status: issues.length > 0 ? "fail" : "pass"
8129
+ };
8130
+ };
8131
+ var runVoiceScenarioEvals = async (options = {}) => {
8132
+ const scenarios = options.scenarios ?? [];
8133
+ const events = filterVoiceTraceEvents(options.events ?? await options.store?.list() ?? []);
8134
+ const grouped = new Map;
8135
+ for (const event of events) {
8136
+ grouped.set(event.sessionId, [...grouped.get(event.sessionId) ?? [], event]);
8137
+ }
8138
+ const results = scenarios.map((scenario) => {
8139
+ const sessions = [...grouped.entries()].filter(([, sessionEvents]) => scenario.scenarioId ? sessionEvents.some((event) => event.scenarioId === scenario.scenarioId) : true).map(([sessionId, sessionEvents]) => evaluateScenarioSession(scenario, sessionId, filterVoiceTraceEvents(sessionEvents))).sort((left, right) => left.sessionId.localeCompare(right.sessionId));
8140
+ const issues = [];
8141
+ const minSessions = scenario.minSessions ?? 1;
8142
+ if (sessions.length < minSessions) {
8143
+ issues.push(`Expected at least ${minSessions} matching session(s), saw ${sessions.length}.`);
8144
+ }
8145
+ const failed2 = sessions.filter((session) => session.status === "fail").length;
8146
+ const passed2 = sessions.length - failed2;
8147
+ return {
8148
+ description: scenario.description,
8149
+ failed: failed2,
8150
+ id: scenario.id,
8151
+ issues,
8152
+ label: scenario.label ?? scenario.id,
8153
+ matchedSessions: sessions.length,
8154
+ passed: passed2,
8155
+ sessions,
8156
+ status: issues.length > 0 || failed2 > 0 ? "fail" : "pass"
8157
+ };
8158
+ });
8159
+ const failed = results.filter((scenario) => scenario.status === "fail").length;
8160
+ const passed = results.length - failed;
8161
+ return {
8162
+ checkedAt: Date.now(),
8163
+ failed,
8164
+ passed,
8165
+ scenarios: results,
8166
+ status: failed > 0 ? "fail" : "pass",
8167
+ total: results.length
8168
+ };
8169
+ };
8170
+ var resolveScenarioFixtures = async (options) => [...options.fixtures ?? [], ...await options.fixtureStore?.list() ?? []];
8171
+ var runVoiceScenarioFixtureEvals = async (options = {}) => {
8172
+ const fixtures = await resolveScenarioFixtures(options);
8173
+ const results = await Promise.all(fixtures.map(async (fixture) => {
8174
+ const report = await runVoiceScenarioEvals({
8175
+ events: fixture.events,
8176
+ scenarios: options.scenarios
8177
+ });
8178
+ return {
8179
+ description: fixture.description,
8180
+ fixtureId: fixture.id,
8181
+ label: fixture.label ?? fixture.id,
8182
+ report,
8183
+ status: report.status
8184
+ };
8185
+ }));
8186
+ const failed = results.filter((fixture) => fixture.status === "fail").length;
8187
+ const passed = results.length - failed;
8188
+ return {
8189
+ checkedAt: Date.now(),
8190
+ failed,
8191
+ fixtures: results,
8192
+ passed,
8193
+ status: failed > 0 ? "fail" : "pass",
8194
+ total: results.length
8195
+ };
8196
+ };
8050
8197
  var summarizeEvalBaseline = (report) => {
8051
8198
  const failedSessionIds = report.sessions.filter((session) => session.status === "fail").map((session) => session.sessionId).sort();
8052
8199
  return {
@@ -8108,6 +8255,20 @@ var createVoiceFileEvalBaselineStore = (filePath) => ({
8108
8255
  await Bun.write(filePath, JSON.stringify(report, null, 2));
8109
8256
  }
8110
8257
  });
8258
+ var createVoiceFileScenarioFixtureStore = (filePath) => ({
8259
+ list: async () => {
8260
+ const file = Bun.file(filePath);
8261
+ if (!await file.exists()) {
8262
+ return [];
8263
+ }
8264
+ const text = await file.text();
8265
+ if (!text.trim()) {
8266
+ return [];
8267
+ }
8268
+ const parsed = JSON.parse(text);
8269
+ return Array.isArray(parsed) ? parsed : parsed.fixtures ?? [];
8270
+ }
8271
+ });
8111
8272
  var formatTime = (value) => value === undefined ? "unknown" : new Date(value).toLocaleString();
8112
8273
  var formatPercent = (value) => `${(value * 100).toFixed(2)}%`;
8113
8274
  var renderVoiceEvalHTML = (report, options = {}) => {
@@ -8128,6 +8289,25 @@ var renderVoiceEvalBaselineHTML = (comparison, options = {}) => {
8128
8289
  const recovered = comparison.recoveredSessionIds.length ? comparison.recoveredSessionIds.map((id) => `<li>${escapeHtml9(id)}</li>`).join("") : "<li>none</li>";
8129
8290
  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:1000px;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{background:#dcfce7;color:#166534}.fail{background:#fee2e2;color:#991b1b}.grid{display:grid;gap:1rem;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));margin:1rem 0}.card{background:white;border:1px solid #e7e5e4;border-radius:1rem;padding:1rem}.card strong{display:block;font-size:2rem}section{background:white;border:1px solid #e7e5e4;border-radius:1rem;margin:1rem 0;padding:1rem}</style></head><body><main>${links}<h1>${escapeHtml9(title)}</h1><p class="status ${comparison.status}">${comparison.status}</p><div class="grid"><article class="card"><span>Baseline pass rate</span><strong>${escapeHtml9(formatPercent(comparison.baseline.passRate))}</strong></article><article class="card"><span>Current pass rate</span><strong>${escapeHtml9(formatPercent(comparison.current.passRate))}</strong></article><article class="card"><span>Failed delta</span><strong>${comparison.deltas.failed}</strong></article><article class="card"><span>Pass rate delta</span><strong>${escapeHtml9(formatPercent(comparison.deltas.passRate))}</strong></article></div><section><h2>Regression Reasons</h2><ul>${reasons}</ul></section><section><h2>New Failed Sessions</h2><ul>${newFailures}</ul></section><section><h2>Recovered Sessions</h2><ul>${recovered}</ul></section></main></body></html>`;
8130
8291
  };
8292
+ var renderVoiceScenarioEvalHTML = (report, options = {}) => {
8293
+ const title = options.title ?? "AbsoluteJS Voice Scenario Evals";
8294
+ const links = options.links?.length ? `<nav>${options.links.map((link) => `<a href="${escapeHtml9(link.href)}">${escapeHtml9(link.label)}</a>`).join("")}</nav>` : "";
8295
+ const scenarios = report.scenarios.length ? report.scenarios.map((scenario) => {
8296
+ const scenarioIssues = scenario.issues.length ? `<ul>${scenario.issues.map((issue) => `<li>${escapeHtml9(issue)}</li>`).join("")}</ul>` : "";
8297
+ const sessions = scenario.sessions.length ? scenario.sessions.map((session) => `<tr class="${session.status}"><td>${escapeHtml9(session.sessionId)}</td><td>${escapeHtml9(session.status)}</td><td>${session.eventCount}</td><td>${escapeHtml9(session.issues.join(", ") || "none")}</td></tr>`).join("") : '<tr><td colspan="4">No matching sessions.</td></tr>';
8298
+ return `<section class="scenario ${scenario.status}"><h2>${escapeHtml9(scenario.label)}</h2>${scenario.description ? `<p>${escapeHtml9(scenario.description)}</p>` : ""}<p class="status ${scenario.status}">${scenario.status}</p><p>${scenario.passed} passed, ${scenario.failed} failed, ${scenario.matchedSessions} matched.</p>${scenarioIssues}<table><thead><tr><th>Session</th><th>Status</th><th>Events</th><th>Issues</th></tr></thead><tbody>${sessions}</tbody></table></section>`;
8299
+ }).join("") : "<section><p>No scenarios configured.</p></section>";
8300
+ 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}.status.pass{background:#dcfce7;color:#166534}.status.fail{background:#fee2e2;color:#991b1b}.grid{display:grid;gap:1rem;grid-template-columns:repeat(auto-fit,minmax(160px,1fr));margin:1rem 0}.card,section{background:white;border:1px solid #e7e5e4;border-radius:1rem;padding:1rem}.card strong{display:block;font-size:2rem}section{margin:1rem 0}table{border-collapse:collapse;width:100%;margin-top:1rem}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>${scenarios}</main></body></html>`;
8301
+ };
8302
+ var renderVoiceScenarioFixtureEvalHTML = (report, options = {}) => {
8303
+ const title = options.title ?? "AbsoluteJS Voice Fixture Evals";
8304
+ const links = options.links?.length ? `<nav>${options.links.map((link) => `<a href="${escapeHtml9(link.href)}">${escapeHtml9(link.label)}</a>`).join("")}</nav>` : "";
8305
+ const fixtures = report.fixtures.length ? report.fixtures.map((fixture) => {
8306
+ const scenarios = fixture.report.scenarios.map((scenario) => `<tr class="${scenario.status}"><td>${escapeHtml9(scenario.label)}</td><td>${escapeHtml9(scenario.status)}</td><td>${scenario.matchedSessions}</td><td>${escapeHtml9([...scenario.issues, ...scenario.sessions.flatMap((session) => session.issues)].join(", ") || "none")}</td></tr>`).join("");
8307
+ return `<section class="${fixture.status}"><h2>${escapeHtml9(fixture.label)}</h2>${fixture.description ? `<p>${escapeHtml9(fixture.description)}</p>` : ""}<p class="status ${fixture.status}">${fixture.status}</p><table><thead><tr><th>Scenario</th><th>Status</th><th>Sessions</th><th>Issues</th></tr></thead><tbody>${scenarios}</tbody></table></section>`;
8308
+ }).join("") : "<section><p>No scenario fixtures configured.</p></section>";
8309
+ 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}.status.pass{background:#dcfce7;color:#166534}.status.fail{background:#fee2e2;color:#991b1b}.grid{display:grid;gap:1rem;grid-template-columns:repeat(auto-fit,minmax(160px,1fr));margin:1rem 0}.card,section{background:white;border:1px solid #e7e5e4;border-radius:1rem;padding:1rem}.card strong{display:block;font-size:2rem}section{margin:1rem 0}table{border-collapse:collapse;width:100%;margin-top:1rem}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>${fixtures}</main></body></html>`;
8310
+ };
8131
8311
  var createVoiceEvalRoutes = (options) => {
8132
8312
  const path = options.path ?? "/evals";
8133
8313
  const routes = new Elysia7({
@@ -8144,6 +8324,16 @@ var createVoiceEvalRoutes = (options) => {
8144
8324
  const [current, baseline] = await Promise.all([getReport(), getBaseline()]);
8145
8325
  return baseline ? compareVoiceEvalBaseline(current, baseline, options.baselineComparison) : undefined;
8146
8326
  };
8327
+ const getScenarioReport = () => runVoiceScenarioEvals({
8328
+ events: options.events,
8329
+ scenarios: options.scenarios,
8330
+ store: options.store
8331
+ });
8332
+ const getFixtureReport = () => runVoiceScenarioFixtureEvals({
8333
+ fixtures: options.fixtures,
8334
+ fixtureStore: options.fixtureStore,
8335
+ scenarios: options.scenarios
8336
+ });
8147
8337
  routes.get(path, async () => {
8148
8338
  const report = await getReport();
8149
8339
  return new Response(renderVoiceEvalHTML(report, {
@@ -8211,11 +8401,51 @@ var createVoiceEvalRoutes = (options) => {
8211
8401
  status: "saved"
8212
8402
  };
8213
8403
  });
8404
+ routes.get(`${path}/scenarios`, async () => {
8405
+ const report = await getScenarioReport();
8406
+ return new Response(renderVoiceScenarioEvalHTML(report, {
8407
+ links: options.links,
8408
+ title: `${options.title ?? "AbsoluteJS Voice Evals"} Scenarios`
8409
+ }), {
8410
+ headers: {
8411
+ "Content-Type": "text/html; charset=utf-8",
8412
+ ...options.headers
8413
+ }
8414
+ });
8415
+ });
8416
+ routes.get(`${path}/scenarios/json`, async () => getScenarioReport());
8417
+ routes.get(`${path}/scenarios/status`, async ({ set }) => {
8418
+ const report = await getScenarioReport();
8419
+ if (report.status === "fail") {
8420
+ set.status = 503;
8421
+ }
8422
+ return report;
8423
+ });
8424
+ routes.get(`${path}/fixtures`, async () => {
8425
+ const report = await getFixtureReport();
8426
+ return new Response(renderVoiceScenarioFixtureEvalHTML(report, {
8427
+ links: options.links,
8428
+ title: `${options.title ?? "AbsoluteJS Voice Evals"} Fixtures`
8429
+ }), {
8430
+ headers: {
8431
+ "Content-Type": "text/html; charset=utf-8",
8432
+ ...options.headers
8433
+ }
8434
+ });
8435
+ });
8436
+ routes.get(`${path}/fixtures/json`, async () => getFixtureReport());
8437
+ routes.get(`${path}/fixtures/status`, async ({ set }) => {
8438
+ const report = await getFixtureReport();
8439
+ if (report.status === "fail") {
8440
+ set.status = 503;
8441
+ }
8442
+ return report;
8443
+ });
8214
8444
  return routes;
8215
8445
  };
8216
8446
  // src/sessionReplay.ts
8217
8447
  import { Elysia as Elysia8 } from "elysia";
8218
- var getString6 = (value) => typeof value === "string" ? value : undefined;
8448
+ var getString7 = (value) => typeof value === "string" ? value : undefined;
8219
8449
  var escapeHtml10 = (value) => value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
8220
8450
  var increment3 = (record, key) => {
8221
8451
  record[key] = (record[key] ?? 0) + 1;
@@ -8245,14 +8475,14 @@ var buildReplayTurns = (events) => {
8245
8475
  case "turn.transcript":
8246
8476
  turn.transcripts.push({
8247
8477
  isFinal: event.payload.isFinal === true,
8248
- text: getString6(event.payload.text)
8478
+ text: getString7(event.payload.text)
8249
8479
  });
8250
8480
  break;
8251
8481
  case "turn.committed":
8252
- turn.committedText = getString6(event.payload.text);
8482
+ turn.committedText = getString7(event.payload.text);
8253
8483
  break;
8254
8484
  case "turn.assistant": {
8255
- const text = getString6(event.payload.text);
8485
+ const text = getString7(event.payload.text);
8256
8486
  if (text) {
8257
8487
  turn.assistantReplies.push(text);
8258
8488
  }
@@ -8321,7 +8551,7 @@ var summarizeVoiceSessions = async (options = {}) => {
8321
8551
  let latestOutcome;
8322
8552
  let errorCount = 0;
8323
8553
  for (const event of sorted) {
8324
- const provider = getString6(event.payload.provider);
8554
+ const provider = getString7(event.payload.provider);
8325
8555
  if (provider) {
8326
8556
  providers.add(provider);
8327
8557
  }
@@ -8329,7 +8559,7 @@ var summarizeVoiceSessions = async (options = {}) => {
8329
8559
  errorCount += 1;
8330
8560
  increment3(providerErrors, provider ?? "unknown");
8331
8561
  }
8332
- const outcome = getString6(event.payload.outcome);
8562
+ const outcome = getString7(event.payload.outcome);
8333
8563
  if (outcome) {
8334
8564
  latestOutcome = outcome;
8335
8565
  }
@@ -9589,7 +9819,7 @@ import { Elysia as Elysia10 } from "elysia";
9589
9819
  // src/resilienceRoutes.ts
9590
9820
  import { Elysia as Elysia9 } from "elysia";
9591
9821
  var escapeHtml11 = (value) => value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
9592
- var getString7 = (value) => typeof value === "string" ? value : undefined;
9822
+ var getString8 = (value) => typeof value === "string" ? value : undefined;
9593
9823
  var getNumber4 = (value) => typeof value === "number" && Number.isFinite(value) ? value : undefined;
9594
9824
  var getBoolean2 = (value) => value === true;
9595
9825
  var isProviderStatus2 = (value) => value === "error" || value === "fallback" || value === "success";
@@ -9599,23 +9829,23 @@ var listVoiceRoutingEvents = (events) => {
9599
9829
  if (event.type !== "session.error") {
9600
9830
  continue;
9601
9831
  }
9602
- const provider = getString7(event.payload.provider);
9832
+ const provider = getString8(event.payload.provider);
9603
9833
  const providerStatus = isProviderStatus2(event.payload.providerStatus) ? event.payload.providerStatus : undefined;
9604
9834
  if (!provider || !providerStatus) {
9605
9835
  continue;
9606
9836
  }
9607
- const kind = getString7(event.payload.kind);
9837
+ const kind = getString8(event.payload.kind);
9608
9838
  routingEvents.push({
9609
9839
  at: event.at,
9610
9840
  attempt: getNumber4(event.payload.attempt),
9611
9841
  elapsedMs: getNumber4(event.payload.elapsedMs),
9612
- error: getString7(event.payload.error),
9613
- fallbackProvider: getString7(event.payload.fallbackProvider),
9842
+ error: getString8(event.payload.error),
9843
+ fallbackProvider: getString8(event.payload.fallbackProvider),
9614
9844
  kind: kind === "stt" || kind === "tts" ? kind : "llm",
9615
9845
  latencyBudgetMs: getNumber4(event.payload.latencyBudgetMs),
9616
- operation: getString7(event.payload.operation),
9846
+ operation: getString8(event.payload.operation),
9617
9847
  provider,
9618
- selectedProvider: getString7(event.payload.selectedProvider),
9848
+ selectedProvider: getString8(event.payload.selectedProvider),
9619
9849
  sessionId: event.sessionId,
9620
9850
  status: providerStatus,
9621
9851
  timedOut: getBoolean2(event.payload.timedOut),
@@ -13157,6 +13387,8 @@ export {
13157
13387
  shapeTelephonyAssistantText,
13158
13388
  selectVoiceTraceEventsForPrune,
13159
13389
  runVoiceSessionEvals,
13390
+ runVoiceScenarioFixtureEvals,
13391
+ runVoiceScenarioEvals,
13160
13392
  resolveVoiceTraceRedactionOptions,
13161
13393
  resolveVoiceSTTRoutingStrategy,
13162
13394
  resolveVoiceRuntimePreset,
@@ -13174,6 +13406,8 @@ export {
13174
13406
  renderVoiceTraceMarkdown,
13175
13407
  renderVoiceTraceHTML,
13176
13408
  renderVoiceSessionsHTML,
13409
+ renderVoiceScenarioFixtureEvalHTML,
13410
+ renderVoiceScenarioEvalHTML,
13177
13411
  renderVoiceResilienceHTML,
13178
13412
  renderVoiceQualityHTML,
13179
13413
  renderVoiceProviderHealthHTML,
@@ -13298,6 +13532,7 @@ export {
13298
13532
  createVoiceFileTraceEventStore,
13299
13533
  createVoiceFileTaskStore,
13300
13534
  createVoiceFileSessionStore,
13535
+ createVoiceFileScenarioFixtureStore,
13301
13536
  createVoiceFileRuntimeStorage,
13302
13537
  createVoiceFileReviewStore,
13303
13538
  createVoiceFileIntegrationEventStore,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@absolutejs/voice",
3
- "version": "0.0.22-beta.43",
3
+ "version": "0.0.22-beta.45",
4
4
  "description": "Voice primitives and Elysia plugin for AbsoluteJS",
5
5
  "repository": {
6
6
  "type": "git",