@absolutejs/voice 0.0.22-beta.6 → 0.0.22-beta.60

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.
Files changed (74) hide show
  1. package/README.md +205 -0
  2. package/dist/angular/index.d.ts +4 -0
  3. package/dist/angular/index.js +587 -43
  4. package/dist/angular/voice-app-kit-status.service.d.ts +12 -0
  5. package/dist/angular/voice-ops-status.component.d.ts +15 -0
  6. package/dist/angular/voice-provider-status.service.d.ts +12 -0
  7. package/dist/angular/voice-routing-status.service.d.ts +11 -0
  8. package/dist/angular/voice-stream.service.d.ts +2 -0
  9. package/dist/angular/voice-workflow-status.service.d.ts +12 -0
  10. package/dist/appKit.d.ts +92 -0
  11. package/dist/assistantHealth.d.ts +81 -0
  12. package/dist/client/actions.d.ts +22 -0
  13. package/dist/client/appKitStatus.d.ts +19 -0
  14. package/dist/client/connection.d.ts +3 -0
  15. package/dist/client/htmxBootstrap.js +44 -2
  16. package/dist/client/index.d.ts +14 -0
  17. package/dist/client/index.js +713 -2
  18. package/dist/client/opsStatusWidget.d.ts +40 -0
  19. package/dist/client/providerStatus.d.ts +19 -0
  20. package/dist/client/providerStatusWidget.d.ts +32 -0
  21. package/dist/client/routingStatus.d.ts +19 -0
  22. package/dist/client/routingStatusWidget.d.ts +28 -0
  23. package/dist/client/workflowStatus.d.ts +19 -0
  24. package/dist/diagnosticsRoutes.d.ts +44 -0
  25. package/dist/evalRoutes.d.ts +213 -0
  26. package/dist/handoff.d.ts +54 -0
  27. package/dist/handoffHealth.d.ts +94 -0
  28. package/dist/index.d.ts +32 -4
  29. package/dist/index.js +4222 -133
  30. package/dist/modelAdapters.d.ts +75 -0
  31. package/dist/opsConsoleRoutes.d.ts +77 -0
  32. package/dist/opsWebhook.d.ts +126 -0
  33. package/dist/providerAdapters.d.ts +48 -0
  34. package/dist/providerHealth.d.ts +79 -0
  35. package/dist/qualityRoutes.d.ts +76 -0
  36. package/dist/queue.d.ts +52 -0
  37. package/dist/react/VoiceOpsStatus.d.ts +6 -0
  38. package/dist/react/VoiceProviderStatus.d.ts +6 -0
  39. package/dist/react/VoiceRoutingStatus.d.ts +6 -0
  40. package/dist/react/index.d.ts +7 -0
  41. package/dist/react/index.js +1024 -11
  42. package/dist/react/useVoiceAppKitStatus.d.ts +8 -0
  43. package/dist/react/useVoiceController.d.ts +2 -0
  44. package/dist/react/useVoiceProviderStatus.d.ts +8 -0
  45. package/dist/react/useVoiceRoutingStatus.d.ts +8 -0
  46. package/dist/react/useVoiceStream.d.ts +2 -0
  47. package/dist/react/useVoiceWorkflowStatus.d.ts +8 -0
  48. package/dist/resilienceRoutes.d.ts +117 -0
  49. package/dist/sessionReplay.d.ts +175 -0
  50. package/dist/svelte/createVoiceAppKitStatus.d.ts +8 -0
  51. package/dist/svelte/createVoiceOpsStatus.d.ts +9 -0
  52. package/dist/svelte/createVoiceProviderStatus.d.ts +10 -0
  53. package/dist/svelte/createVoiceRoutingStatus.d.ts +10 -0
  54. package/dist/svelte/createVoiceWorkflowStatus.d.ts +8 -0
  55. package/dist/svelte/index.d.ts +5 -0
  56. package/dist/svelte/index.js +736 -3
  57. package/dist/testing/index.d.ts +2 -0
  58. package/dist/testing/index.js +1537 -7
  59. package/dist/testing/ioProviderSimulator.d.ts +41 -0
  60. package/dist/testing/providerSimulator.d.ts +44 -0
  61. package/dist/trace.d.ts +1 -1
  62. package/dist/types.d.ts +84 -2
  63. package/dist/vue/VoiceOpsStatus.d.ts +30 -0
  64. package/dist/vue/VoiceProviderStatus.d.ts +51 -0
  65. package/dist/vue/VoiceRoutingStatus.d.ts +51 -0
  66. package/dist/vue/index.d.ts +7 -0
  67. package/dist/vue/index.js +1062 -25
  68. package/dist/vue/useVoiceAppKitStatus.d.ts +9 -0
  69. package/dist/vue/useVoiceProviderStatus.d.ts +9 -0
  70. package/dist/vue/useVoiceRoutingStatus.d.ts +8 -0
  71. package/dist/vue/useVoiceStream.d.ts +2 -0
  72. package/dist/vue/useVoiceWorkflowStatus.d.ts +9 -0
  73. package/dist/workflowContract.d.ts +91 -0
  74. package/package.json +1 -1
package/dist/vue/index.js CHANGED
@@ -69,8 +69,879 @@ var __decorateElement = (array, flags, name, decorators, target, extra) => {
69
69
  return k || __decoratorMetadata(array, target), desc && __defProp(target, name, desc), p ? k ^ 4 ? extra : desc : target;
70
70
  };
71
71
 
72
- // src/vue/useVoiceStream.ts
72
+ // src/vue/VoiceOpsStatus.ts
73
+ import { defineComponent, h } from "vue";
74
+
75
+ // src/client/appKitStatus.ts
76
+ var fetchVoiceAppKitStatus = async (path = "/app-kit/status", options = {}) => {
77
+ const fetchImpl = options.fetch ?? globalThis.fetch;
78
+ const response = await fetchImpl(path);
79
+ if (!response.ok) {
80
+ throw new Error(`Voice app kit status failed: HTTP ${response.status}`);
81
+ }
82
+ return await response.json();
83
+ };
84
+ var createVoiceAppKitStatusStore = (path = "/app-kit/status", options = {}) => {
85
+ const listeners = new Set;
86
+ let closed = false;
87
+ let timer;
88
+ let snapshot = {
89
+ error: null,
90
+ isLoading: false
91
+ };
92
+ const emit = () => {
93
+ for (const listener of listeners) {
94
+ listener();
95
+ }
96
+ };
97
+ const refresh = async () => {
98
+ if (closed) {
99
+ return snapshot.report;
100
+ }
101
+ snapshot = {
102
+ ...snapshot,
103
+ error: null,
104
+ isLoading: true
105
+ };
106
+ emit();
107
+ try {
108
+ const report = await fetchVoiceAppKitStatus(path, options);
109
+ snapshot = {
110
+ error: null,
111
+ isLoading: false,
112
+ report,
113
+ updatedAt: Date.now()
114
+ };
115
+ emit();
116
+ return report;
117
+ } catch (error) {
118
+ snapshot = {
119
+ ...snapshot,
120
+ error: error instanceof Error ? error.message : String(error),
121
+ isLoading: false
122
+ };
123
+ emit();
124
+ throw error;
125
+ }
126
+ };
127
+ const close = () => {
128
+ closed = true;
129
+ if (timer) {
130
+ clearInterval(timer);
131
+ timer = undefined;
132
+ }
133
+ listeners.clear();
134
+ };
135
+ if (typeof window !== "undefined" && options.intervalMs && options.intervalMs > 0) {
136
+ timer = setInterval(() => {
137
+ refresh().catch(() => {});
138
+ }, options.intervalMs);
139
+ }
140
+ return {
141
+ close,
142
+ getServerSnapshot: () => snapshot,
143
+ getSnapshot: () => snapshot,
144
+ refresh,
145
+ subscribe: (listener) => {
146
+ listeners.add(listener);
147
+ return () => {
148
+ listeners.delete(listener);
149
+ };
150
+ }
151
+ };
152
+ };
153
+
154
+ // src/client/opsStatusWidget.ts
155
+ var DEFAULT_TITLE = "Voice Ops Status";
156
+ var DEFAULT_DESCRIPTION = "Certified workflow, provider, and handoff readiness from the AbsoluteJS voice app kit.";
157
+ var SURFACE_LABELS = {
158
+ handoffs: "Handoffs",
159
+ providers: "Providers",
160
+ quality: "Quality",
161
+ sessions: "Sessions",
162
+ workflows: "Workflows"
163
+ };
164
+ var escapeHtml = (value) => value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
165
+ var readNumber = (value, key) => value && typeof value === "object" && (key in value) ? Number(value[key] ?? 0) : 0;
166
+ var surfaceDetail = (surface) => {
167
+ const total = readNumber(surface, "total");
168
+ const failed = readNumber(surface, "failed");
169
+ const degraded = readNumber(surface, "degraded");
170
+ const source = surface && typeof surface === "object" && "source" in surface && typeof surface.source === "string" ? ` from ${surface.source}` : "";
171
+ if (degraded > 0) {
172
+ return `${degraded} degraded of ${total}`;
173
+ }
174
+ if (failed > 0) {
175
+ return `${failed} failing of ${total}${source}`;
176
+ }
177
+ return total > 0 ? `${total} passing${source}` : `No failures${source}`;
178
+ };
179
+ var getVoiceOpsStatusLabel = (report, error) => {
180
+ if (error) {
181
+ return "Unavailable";
182
+ }
183
+ if (!report) {
184
+ return "Checking";
185
+ }
186
+ return report.status === "pass" ? "Passing" : "Needs attention";
187
+ };
188
+ var createVoiceOpsStatusViewModel = (snapshot, options = {}) => {
189
+ const report = snapshot.report;
190
+ const surfaces = Object.entries(report?.surfaces ?? {}).map(([id, surface]) => {
191
+ const failed = readNumber(surface, "failed") || readNumber(surface, "degraded");
192
+ const total = readNumber(surface, "total");
193
+ const status = surface && typeof surface === "object" && "status" in surface ? surface.status ?? "pass" : "pass";
194
+ return {
195
+ detail: surfaceDetail(surface),
196
+ failed,
197
+ id,
198
+ label: SURFACE_LABELS[id] ?? id,
199
+ status,
200
+ total
201
+ };
202
+ });
203
+ return {
204
+ description: options.description ?? DEFAULT_DESCRIPTION,
205
+ error: snapshot.error,
206
+ isLoading: snapshot.isLoading,
207
+ label: getVoiceOpsStatusLabel(report, snapshot.error),
208
+ links: options.includeLinks === false ? [] : report?.links ?? [],
209
+ passed: report?.passed ?? 0,
210
+ status: snapshot.error ? "error" : report ? report.status : snapshot.isLoading ? "loading" : "loading",
211
+ surfaces,
212
+ title: options.title ?? DEFAULT_TITLE,
213
+ total: report?.total ?? 0,
214
+ updatedAt: snapshot.updatedAt
215
+ };
216
+ };
217
+ var renderVoiceOpsStatusHTML = (snapshot, options = {}) => {
218
+ const model = createVoiceOpsStatusViewModel(snapshot, options);
219
+ const surfaces = model.surfaces.length ? model.surfaces.map((surface) => `<li class="absolute-voice-ops-status__surface absolute-voice-ops-status__surface--${escapeHtml(surface.status)}">
220
+ <span>${escapeHtml(surface.label)}</span>
221
+ <strong>${escapeHtml(surface.detail)}</strong>
222
+ </li>`).join("") : '<li class="absolute-voice-ops-status__surface"><span>Status</span><strong>Waiting for first check</strong></li>';
223
+ const links = model.links.length ? `<nav class="absolute-voice-ops-status__links">${model.links.slice(0, 4).map((link) => `<a href="${escapeHtml(link.href)}">${escapeHtml(link.label)}</a>`).join("")}</nav>` : "";
224
+ return `<section class="absolute-voice-ops-status absolute-voice-ops-status--${escapeHtml(model.status)}">
225
+ <header class="absolute-voice-ops-status__header">
226
+ <span class="absolute-voice-ops-status__eyebrow">${escapeHtml(model.title)}</span>
227
+ <strong class="absolute-voice-ops-status__label">${escapeHtml(model.label)}</strong>
228
+ </header>
229
+ <p class="absolute-voice-ops-status__description">${escapeHtml(model.description)}</p>
230
+ <div class="absolute-voice-ops-status__summary">
231
+ <span>${model.passed} passing</span>
232
+ <span>${Math.max(model.total - model.passed, 0)} failing</span>
233
+ <span>${model.total} checks</span>
234
+ </div>
235
+ <ul class="absolute-voice-ops-status__surfaces">${surfaces}</ul>
236
+ ${model.error ? `<p class="absolute-voice-ops-status__error">${escapeHtml(model.error)}</p>` : ""}
237
+ ${links}
238
+ </section>`;
239
+ };
240
+ var getVoiceOpsStatusCSS = () => `.absolute-voice-ops-status{border:1px solid #d8d2c4;border-radius:20px;background:#fffaf0;color:#16130d;padding:18px;box-shadow:0 18px 40px rgba(47,37,18,.12);font-family:inherit}.absolute-voice-ops-status--fail,.absolute-voice-ops-status--error{border-color:#f2a7a7;background:#fff5f3}.absolute-voice-ops-status__header{align-items:start;display:flex;gap:12px;justify-content:space-between}.absolute-voice-ops-status__eyebrow{color:#73664f;font-size:12px;font-weight:800;letter-spacing:.08em;text-transform:uppercase}.absolute-voice-ops-status__label{font-size:28px;line-height:1}.absolute-voice-ops-status__description{color:#514733;margin:12px 0 0}.absolute-voice-ops-status__summary,.absolute-voice-ops-status__links{display:flex;flex-wrap:wrap;gap:8px;margin-top:14px}.absolute-voice-ops-status__summary span,.absolute-voice-ops-status__links a{border:1px solid #e6ddca;border-radius:999px;color:inherit;padding:6px 10px;text-decoration:none}.absolute-voice-ops-status__surfaces{display:grid;gap:8px;list-style:none;margin:16px 0 0;padding:0}.absolute-voice-ops-status__surface{align-items:center;background:#fff;border:1px solid #eee4d2;border-radius:14px;display:flex;gap:12px;justify-content:space-between;padding:10px 12px}.absolute-voice-ops-status__surface--fail{border-color:#f2a7a7}.absolute-voice-ops-status__surface span{color:#655944}.absolute-voice-ops-status__error{color:#9f1239;font-weight:700}`;
241
+ var mountVoiceOpsStatus = (element, path = "/app-kit/status", options = {}) => {
242
+ const store = createVoiceAppKitStatusStore(path, options);
243
+ const render = () => {
244
+ element.innerHTML = renderVoiceOpsStatusHTML(store.getSnapshot(), options);
245
+ };
246
+ const unsubscribe = store.subscribe(render);
247
+ render();
248
+ store.refresh().catch(() => {});
249
+ return {
250
+ close: () => {
251
+ unsubscribe();
252
+ store.close();
253
+ },
254
+ refresh: store.refresh
255
+ };
256
+ };
257
+ var defineVoiceOpsStatusElement = (tagName = "absolute-voice-ops-status") => {
258
+ if (typeof window === "undefined" || typeof customElements === "undefined" || customElements.get(tagName)) {
259
+ return;
260
+ }
261
+ customElements.define(tagName, class AbsoluteVoiceOpsStatusElement extends HTMLElement {
262
+ mounted;
263
+ connectedCallback() {
264
+ const intervalMs = Number(this.getAttribute("interval-ms") ?? 5000);
265
+ this.mounted = mountVoiceOpsStatus(this, this.getAttribute("path") ?? "/app-kit/status", {
266
+ description: this.getAttribute("description") ?? undefined,
267
+ includeLinks: this.getAttribute("include-links") !== "false",
268
+ intervalMs: Number.isFinite(intervalMs) ? intervalMs : 5000,
269
+ title: this.getAttribute("title") ?? undefined
270
+ });
271
+ }
272
+ disconnectedCallback() {
273
+ this.mounted?.close();
274
+ this.mounted = undefined;
275
+ }
276
+ });
277
+ };
278
+
279
+ // src/vue/useVoiceAppKitStatus.ts
73
280
  import { onUnmounted, ref, shallowRef } from "vue";
281
+ var useVoiceAppKitStatus = (path = "/app-kit/status", options = {}) => {
282
+ const store = createVoiceAppKitStatusStore(path, options);
283
+ const error = ref(null);
284
+ const isLoading = ref(false);
285
+ const report = shallowRef(undefined);
286
+ const updatedAt = ref(undefined);
287
+ const sync = () => {
288
+ const snapshot = store.getSnapshot();
289
+ error.value = snapshot.error;
290
+ isLoading.value = snapshot.isLoading;
291
+ report.value = snapshot.report;
292
+ updatedAt.value = snapshot.updatedAt;
293
+ };
294
+ const unsubscribe = store.subscribe(sync);
295
+ sync();
296
+ if (typeof window !== "undefined") {
297
+ store.refresh().catch(() => {});
298
+ }
299
+ onUnmounted(() => {
300
+ unsubscribe();
301
+ store.close();
302
+ });
303
+ return {
304
+ error,
305
+ isLoading,
306
+ refresh: store.refresh,
307
+ report,
308
+ updatedAt
309
+ };
310
+ };
311
+
312
+ // src/vue/VoiceOpsStatus.ts
313
+ var VoiceOpsStatus = defineComponent({
314
+ name: "VoiceOpsStatus",
315
+ props: {
316
+ description: String,
317
+ includeLinks: {
318
+ default: true,
319
+ type: Boolean
320
+ },
321
+ intervalMs: Number,
322
+ path: {
323
+ default: "/app-kit/status",
324
+ type: String
325
+ },
326
+ title: String
327
+ },
328
+ setup(props) {
329
+ const options = {
330
+ description: props.description,
331
+ includeLinks: props.includeLinks,
332
+ intervalMs: props.intervalMs,
333
+ title: props.title
334
+ };
335
+ const status = useVoiceAppKitStatus(props.path, options);
336
+ return () => {
337
+ const model = createVoiceOpsStatusViewModel({
338
+ error: status.error.value,
339
+ isLoading: status.isLoading.value,
340
+ report: status.report.value,
341
+ updatedAt: status.updatedAt.value
342
+ }, options);
343
+ return h("section", {
344
+ class: [
345
+ "absolute-voice-ops-status",
346
+ `absolute-voice-ops-status--${model.status}`
347
+ ]
348
+ }, [
349
+ h("header", { class: "absolute-voice-ops-status__header" }, [
350
+ h("span", { class: "absolute-voice-ops-status__eyebrow" }, model.title),
351
+ h("strong", { class: "absolute-voice-ops-status__label" }, model.label)
352
+ ]),
353
+ h("p", { class: "absolute-voice-ops-status__description" }, model.description),
354
+ h("div", { class: "absolute-voice-ops-status__summary" }, [
355
+ h("span", `${model.passed} passing`),
356
+ h("span", `${Math.max(model.total - model.passed, 0)} failing`),
357
+ h("span", `${model.total} checks`)
358
+ ]),
359
+ h("ul", { class: "absolute-voice-ops-status__surfaces" }, model.surfaces.length > 0 ? model.surfaces.map((surface) => h("li", {
360
+ class: [
361
+ "absolute-voice-ops-status__surface",
362
+ `absolute-voice-ops-status__surface--${surface.status}`
363
+ ],
364
+ key: surface.id
365
+ }, [h("span", surface.label), h("strong", surface.detail)])) : [
366
+ h("li", { class: "absolute-voice-ops-status__surface" }, [
367
+ h("span", "Status"),
368
+ h("strong", "Waiting for first check")
369
+ ])
370
+ ]),
371
+ model.error ? h("p", { class: "absolute-voice-ops-status__error" }, model.error) : null,
372
+ model.links.length > 0 ? h("nav", { class: "absolute-voice-ops-status__links" }, model.links.slice(0, 4).map((link) => h("a", { href: link.href, key: link.href }, link.label))) : null
373
+ ]);
374
+ };
375
+ }
376
+ });
377
+ // src/vue/VoiceProviderStatus.ts
378
+ import { computed, defineComponent as defineComponent2, h as h2 } from "vue";
379
+
380
+ // src/client/providerStatus.ts
381
+ var fetchVoiceProviderStatus = async (path = "/api/provider-status", options = {}) => {
382
+ const fetchImpl = options.fetch ?? globalThis.fetch;
383
+ const response = await fetchImpl(path);
384
+ if (!response.ok) {
385
+ throw new Error(`Voice provider status failed: HTTP ${response.status}`);
386
+ }
387
+ return await response.json();
388
+ };
389
+ var createVoiceProviderStatusStore = (path = "/api/provider-status", options = {}) => {
390
+ const listeners = new Set;
391
+ let closed = false;
392
+ let timer;
393
+ let snapshot = {
394
+ error: null,
395
+ isLoading: false,
396
+ providers: []
397
+ };
398
+ const emit = () => {
399
+ for (const listener of listeners) {
400
+ listener();
401
+ }
402
+ };
403
+ const refresh = async () => {
404
+ if (closed) {
405
+ return snapshot.providers;
406
+ }
407
+ snapshot = {
408
+ ...snapshot,
409
+ error: null,
410
+ isLoading: true
411
+ };
412
+ emit();
413
+ try {
414
+ const providers = await fetchVoiceProviderStatus(path, options);
415
+ snapshot = {
416
+ error: null,
417
+ isLoading: false,
418
+ providers,
419
+ updatedAt: Date.now()
420
+ };
421
+ emit();
422
+ return providers;
423
+ } catch (error) {
424
+ snapshot = {
425
+ ...snapshot,
426
+ error: error instanceof Error ? error.message : String(error),
427
+ isLoading: false
428
+ };
429
+ emit();
430
+ throw error;
431
+ }
432
+ };
433
+ const close = () => {
434
+ closed = true;
435
+ if (timer) {
436
+ clearInterval(timer);
437
+ timer = undefined;
438
+ }
439
+ listeners.clear();
440
+ };
441
+ if (options.intervalMs && options.intervalMs > 0) {
442
+ timer = setInterval(() => {
443
+ refresh().catch(() => {});
444
+ }, options.intervalMs);
445
+ }
446
+ return {
447
+ close,
448
+ getServerSnapshot: () => snapshot,
449
+ getSnapshot: () => snapshot,
450
+ refresh,
451
+ subscribe: (listener) => {
452
+ listeners.add(listener);
453
+ return () => {
454
+ listeners.delete(listener);
455
+ };
456
+ }
457
+ };
458
+ };
459
+
460
+ // src/client/providerStatusWidget.ts
461
+ var DEFAULT_TITLE2 = "Voice Providers";
462
+ var DEFAULT_DESCRIPTION2 = "Live provider health, fallback counts, latency, and suppression state from your self-hosted trace store.";
463
+ var escapeHtml2 = (value) => value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
464
+ var formatProvider = (provider) => provider.split(/[-_\s]+/).filter(Boolean).map((part) => `${part[0]?.toUpperCase() ?? ""}${part.slice(1)}`).join(" ") || provider;
465
+ var formatStatus = (status) => status.split("-").map((part) => `${part[0]?.toUpperCase() ?? ""}${part.slice(1)}`).join(" ");
466
+ var formatLatency = (value) => typeof value === "number" ? `${value}ms` : "No samples";
467
+ var formatSuppression = (value) => typeof value === "number" ? `${Math.ceil(value / 1000)}s` : "None";
468
+ var getProviderDetail = (provider) => {
469
+ if (provider.status === "suppressed") {
470
+ return provider.lastError ? `Suppressed for ${formatSuppression(provider.suppressionRemainingMs)} after ${provider.lastError}.` : `Suppressed for ${formatSuppression(provider.suppressionRemainingMs)}.`;
471
+ }
472
+ if (provider.status === "recoverable") {
473
+ return "Cooldown expired; ready for recovery traffic.";
474
+ }
475
+ if (provider.status === "rate-limited") {
476
+ return "Rate limit detected; router should avoid this provider.";
477
+ }
478
+ if (provider.status === "degraded") {
479
+ return provider.lastError ?? "Recent provider errors detected.";
480
+ }
481
+ if (provider.status === "healthy") {
482
+ return provider.recommended ? "Healthy and currently recommended." : "Healthy and available for routing.";
483
+ }
484
+ return "No provider traffic observed yet.";
485
+ };
486
+ var isWarningStatus = (status) => status === "degraded" || status === "rate-limited" || status === "recoverable" || status === "suppressed";
487
+ var createVoiceProviderStatusViewModel = (snapshot, options = {}) => {
488
+ const providers = snapshot.providers.map((provider) => ({
489
+ ...provider,
490
+ detail: getProviderDetail(provider),
491
+ label: `${formatProvider(provider.provider)}${provider.recommended ? " recommended" : ""}`,
492
+ rows: [
493
+ { label: "Runs", value: String(provider.runCount) },
494
+ { label: "Avg latency", value: formatLatency(provider.averageElapsedMs) },
495
+ { label: "Errors", value: String(provider.errorCount) },
496
+ { label: "Timeouts", value: String(provider.timeoutCount) },
497
+ { label: "Fallbacks", value: String(provider.fallbackCount) },
498
+ {
499
+ label: "Suppression",
500
+ value: formatSuppression(provider.suppressionRemainingMs)
501
+ }
502
+ ]
503
+ }));
504
+ const warningCount = providers.filter((provider) => isWarningStatus(provider.status)).length;
505
+ const healthyCount = providers.filter((provider) => provider.status === "healthy").length;
506
+ return {
507
+ description: options.description ?? DEFAULT_DESCRIPTION2,
508
+ error: snapshot.error,
509
+ isLoading: snapshot.isLoading,
510
+ label: snapshot.error ? "Unavailable" : providers.length ? warningCount > 0 ? `${warningCount} needs attention` : `${healthyCount} healthy` : snapshot.isLoading ? "Checking" : "No provider traffic",
511
+ providers,
512
+ status: snapshot.error ? "error" : providers.length ? warningCount > 0 ? "warning" : "ready" : snapshot.isLoading ? "loading" : "empty",
513
+ title: options.title ?? DEFAULT_TITLE2,
514
+ updatedAt: snapshot.updatedAt
515
+ };
516
+ };
517
+ var renderVoiceProviderStatusHTML = (snapshot, options = {}) => {
518
+ const model = createVoiceProviderStatusViewModel(snapshot, options);
519
+ const providers = model.providers.length ? `<div class="absolute-voice-provider-status__providers">${model.providers.map((provider) => `<article class="absolute-voice-provider-status__provider absolute-voice-provider-status__provider--${escapeHtml2(provider.status)}">
520
+ <header>
521
+ <strong>${escapeHtml2(provider.label)}</strong>
522
+ <span>${escapeHtml2(formatStatus(provider.status))}</span>
523
+ </header>
524
+ <p>${escapeHtml2(provider.detail)}</p>
525
+ <dl>${provider.rows.map((row) => `<div>
526
+ <dt>${escapeHtml2(row.label)}</dt>
527
+ <dd>${escapeHtml2(row.value)}</dd>
528
+ </div>`).join("")}</dl>
529
+ </article>`).join("")}</div>` : '<p class="absolute-voice-provider-status__empty">Run voice traffic to see provider health.</p>';
530
+ return `<section class="absolute-voice-provider-status absolute-voice-provider-status--${escapeHtml2(model.status)}">
531
+ <header class="absolute-voice-provider-status__header">
532
+ <span class="absolute-voice-provider-status__eyebrow">${escapeHtml2(model.title)}</span>
533
+ <strong class="absolute-voice-provider-status__label">${escapeHtml2(model.label)}</strong>
534
+ </header>
535
+ <p class="absolute-voice-provider-status__description">${escapeHtml2(model.description)}</p>
536
+ ${providers}
537
+ ${model.error ? `<p class="absolute-voice-provider-status__error">${escapeHtml2(model.error)}</p>` : ""}
538
+ </section>`;
539
+ };
540
+ var getVoiceProviderStatusCSS = () => `.absolute-voice-provider-status{border:1px solid #d8d2c4;border-radius:20px;background:#fffaf0;color:#16130d;padding:18px;box-shadow:0 18px 40px rgba(47,37,18,.12);font-family:inherit}.absolute-voice-provider-status--error,.absolute-voice-provider-status--warning{border-color:#f2a7a7;background:#fff5f3}.absolute-voice-provider-status__header,.absolute-voice-provider-status__provider header{align-items:start;display:flex;gap:12px;justify-content:space-between}.absolute-voice-provider-status__eyebrow{color:#73664f;font-size:12px;font-weight:800;letter-spacing:.08em;text-transform:uppercase}.absolute-voice-provider-status__label{font-size:24px;line-height:1}.absolute-voice-provider-status__description,.absolute-voice-provider-status__provider p,.absolute-voice-provider-status__provider dt,.absolute-voice-provider-status__empty{color:#514733}.absolute-voice-provider-status__providers{display:grid;gap:12px;margin-top:14px}.absolute-voice-provider-status__provider{background:#fff;border:1px solid #eee4d2;border-radius:16px;padding:14px}.absolute-voice-provider-status__provider--degraded,.absolute-voice-provider-status__provider--rate-limited,.absolute-voice-provider-status__provider--suppressed{border-color:#f2a7a7}.absolute-voice-provider-status__provider--recoverable{border-color:#fbbf24}.absolute-voice-provider-status__provider p{margin:10px 0}.absolute-voice-provider-status__provider dl{display:grid;gap:8px;grid-template-columns:repeat(2,minmax(0,1fr));margin:0}.absolute-voice-provider-status__provider div{background:#fffaf0;border:1px solid #eee4d2;border-radius:12px;padding:8px}.absolute-voice-provider-status__provider dt{font-size:12px}.absolute-voice-provider-status__provider dd{font-weight:800;margin:4px 0 0}.absolute-voice-provider-status__empty{margin:14px 0 0}.absolute-voice-provider-status__error{color:#9f1239;font-weight:700}`;
541
+ var mountVoiceProviderStatus = (element, path = "/api/provider-status", options = {}) => {
542
+ const store = createVoiceProviderStatusStore(path, options);
543
+ const render = () => {
544
+ element.innerHTML = renderVoiceProviderStatusHTML(store.getSnapshot(), options);
545
+ };
546
+ const unsubscribe = store.subscribe(render);
547
+ render();
548
+ store.refresh().catch(() => {});
549
+ return {
550
+ close: () => {
551
+ unsubscribe();
552
+ store.close();
553
+ },
554
+ refresh: store.refresh
555
+ };
556
+ };
557
+ var defineVoiceProviderStatusElement = (tagName = "absolute-voice-provider-status") => {
558
+ if (typeof window === "undefined" || typeof customElements === "undefined" || customElements.get(tagName)) {
559
+ return;
560
+ }
561
+ customElements.define(tagName, class AbsoluteVoiceProviderStatusElement extends HTMLElement {
562
+ mounted;
563
+ connectedCallback() {
564
+ const intervalMs = Number(this.getAttribute("interval-ms") ?? 5000);
565
+ this.mounted = mountVoiceProviderStatus(this, this.getAttribute("path") ?? "/api/provider-status", {
566
+ description: this.getAttribute("description") ?? undefined,
567
+ intervalMs: Number.isFinite(intervalMs) ? intervalMs : 5000,
568
+ title: this.getAttribute("title") ?? undefined
569
+ });
570
+ }
571
+ disconnectedCallback() {
572
+ this.mounted?.close();
573
+ this.mounted = undefined;
574
+ }
575
+ });
576
+ };
577
+
578
+ // src/vue/useVoiceProviderStatus.ts
579
+ import { onUnmounted as onUnmounted2, ref as ref2, shallowRef as shallowRef2 } from "vue";
580
+ var useVoiceProviderStatus = (path = "/api/provider-status", options = {}) => {
581
+ const store = createVoiceProviderStatusStore(path, options);
582
+ const error = ref2(null);
583
+ const isLoading = ref2(false);
584
+ const providers = shallowRef2([]);
585
+ const updatedAt = ref2(undefined);
586
+ const sync = () => {
587
+ const snapshot = store.getSnapshot();
588
+ error.value = snapshot.error;
589
+ isLoading.value = snapshot.isLoading;
590
+ providers.value = [...snapshot.providers];
591
+ updatedAt.value = snapshot.updatedAt;
592
+ };
593
+ const unsubscribe = store.subscribe(sync);
594
+ sync();
595
+ store.refresh().catch(() => {});
596
+ onUnmounted2(() => {
597
+ unsubscribe();
598
+ store.close();
599
+ });
600
+ return {
601
+ error,
602
+ isLoading,
603
+ providers,
604
+ refresh: store.refresh,
605
+ updatedAt
606
+ };
607
+ };
608
+
609
+ // src/vue/VoiceProviderStatus.ts
610
+ var VoiceProviderStatus = defineComponent2({
611
+ name: "VoiceProviderStatus",
612
+ props: {
613
+ class: {
614
+ default: "",
615
+ type: String
616
+ },
617
+ description: {
618
+ default: undefined,
619
+ type: String
620
+ },
621
+ intervalMs: {
622
+ default: 5000,
623
+ type: Number
624
+ },
625
+ path: {
626
+ default: "/api/provider-status",
627
+ type: String
628
+ },
629
+ title: {
630
+ default: undefined,
631
+ type: String
632
+ }
633
+ },
634
+ setup(props) {
635
+ const options = {
636
+ description: props.description,
637
+ intervalMs: props.intervalMs,
638
+ title: props.title
639
+ };
640
+ const status = useVoiceProviderStatus(props.path, options);
641
+ const model = computed(() => createVoiceProviderStatusViewModel({
642
+ error: status.error.value,
643
+ isLoading: status.isLoading.value,
644
+ providers: status.providers.value,
645
+ updatedAt: status.updatedAt.value
646
+ }, options));
647
+ return () => h2("section", {
648
+ class: [
649
+ "absolute-voice-provider-status",
650
+ `absolute-voice-provider-status--${model.value.status}`,
651
+ props.class
652
+ ]
653
+ }, [
654
+ h2("header", { class: "absolute-voice-provider-status__header" }, [
655
+ h2("span", { class: "absolute-voice-provider-status__eyebrow" }, model.value.title),
656
+ h2("strong", { class: "absolute-voice-provider-status__label" }, model.value.label)
657
+ ]),
658
+ h2("p", { class: "absolute-voice-provider-status__description" }, model.value.description),
659
+ model.value.providers.length ? h2("div", { class: "absolute-voice-provider-status__providers" }, model.value.providers.map((provider) => h2("article", {
660
+ class: [
661
+ "absolute-voice-provider-status__provider",
662
+ `absolute-voice-provider-status__provider--${provider.status}`
663
+ ],
664
+ key: provider.provider
665
+ }, [
666
+ h2("header", [
667
+ h2("strong", provider.label),
668
+ h2("span", provider.status)
669
+ ]),
670
+ h2("p", provider.detail),
671
+ h2("dl", provider.rows.map((row) => h2("div", { key: row.label }, [
672
+ h2("dt", row.label),
673
+ h2("dd", row.value)
674
+ ])))
675
+ ]))) : h2("p", { class: "absolute-voice-provider-status__empty" }, "Run voice traffic to see provider health."),
676
+ model.value.error ? h2("p", { class: "absolute-voice-provider-status__error" }, model.value.error) : null
677
+ ]);
678
+ }
679
+ });
680
+ // src/vue/VoiceRoutingStatus.ts
681
+ import { computed as computed2, defineComponent as defineComponent3, h as h3 } from "vue";
682
+
683
+ // src/client/routingStatus.ts
684
+ var fetchVoiceRoutingStatus = async (path = "/api/routing/latest", options = {}) => {
685
+ const fetchImpl = options.fetch ?? globalThis.fetch;
686
+ const response = await fetchImpl(path);
687
+ if (!response.ok) {
688
+ throw new Error(`Voice routing status failed: HTTP ${response.status}`);
689
+ }
690
+ return await response.json();
691
+ };
692
+ var createVoiceRoutingStatusStore = (path = "/api/routing/latest", options = {}) => {
693
+ const listeners = new Set;
694
+ let closed = false;
695
+ let timer;
696
+ let snapshot = {
697
+ decision: null,
698
+ error: null,
699
+ isLoading: false
700
+ };
701
+ const emit = () => {
702
+ for (const listener of listeners) {
703
+ listener();
704
+ }
705
+ };
706
+ const refresh = async () => {
707
+ if (closed) {
708
+ return snapshot.decision;
709
+ }
710
+ snapshot = {
711
+ ...snapshot,
712
+ error: null,
713
+ isLoading: true
714
+ };
715
+ emit();
716
+ try {
717
+ const decision = await fetchVoiceRoutingStatus(path, options);
718
+ snapshot = {
719
+ decision,
720
+ error: null,
721
+ isLoading: false,
722
+ updatedAt: Date.now()
723
+ };
724
+ emit();
725
+ return decision;
726
+ } catch (error) {
727
+ snapshot = {
728
+ ...snapshot,
729
+ error: error instanceof Error ? error.message : String(error),
730
+ isLoading: false
731
+ };
732
+ emit();
733
+ throw error;
734
+ }
735
+ };
736
+ const close = () => {
737
+ closed = true;
738
+ if (timer) {
739
+ clearInterval(timer);
740
+ timer = undefined;
741
+ }
742
+ listeners.clear();
743
+ };
744
+ if (options.intervalMs && options.intervalMs > 0) {
745
+ timer = setInterval(() => {
746
+ refresh().catch(() => {});
747
+ }, options.intervalMs);
748
+ }
749
+ return {
750
+ close,
751
+ getServerSnapshot: () => snapshot,
752
+ getSnapshot: () => snapshot,
753
+ refresh,
754
+ subscribe: (listener) => {
755
+ listeners.add(listener);
756
+ return () => {
757
+ listeners.delete(listener);
758
+ };
759
+ }
760
+ };
761
+ };
762
+
763
+ // src/client/routingStatusWidget.ts
764
+ var DEFAULT_TITLE3 = "Voice Routing";
765
+ var DEFAULT_DESCRIPTION3 = "Latest provider routing decision from the self-hosted trace store.";
766
+ var escapeHtml3 = (value) => value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
767
+ var formatValue = (value, fallback = "None") => typeof value === "string" && value.trim() ? value : typeof value === "number" && Number.isFinite(value) ? String(value) : fallback;
768
+ var createVoiceRoutingStatusViewModel = (snapshot, options = {}) => {
769
+ const decision = snapshot.decision;
770
+ const rows = decision ? [
771
+ { label: "Kind", value: decision.kind.toUpperCase() },
772
+ { label: "Policy", value: formatValue(decision.routing, "Unknown") },
773
+ { label: "Provider", value: formatValue(decision.provider, "Unknown") },
774
+ {
775
+ label: "Selected",
776
+ value: formatValue(decision.selectedProvider, "Unknown")
777
+ },
778
+ {
779
+ label: "Fallback",
780
+ value: formatValue(decision.fallbackProvider)
781
+ },
782
+ { label: "Status", value: formatValue(decision.status, "unknown") },
783
+ {
784
+ label: "Latency budget",
785
+ value: typeof decision.latencyBudgetMs === "number" ? `${decision.latencyBudgetMs}ms` : "None"
786
+ }
787
+ ] : [];
788
+ return {
789
+ decision,
790
+ description: options.description ?? DEFAULT_DESCRIPTION3,
791
+ error: snapshot.error,
792
+ isLoading: snapshot.isLoading,
793
+ label: snapshot.error ? "Unavailable" : decision ? `${formatValue(decision.kind).toUpperCase()} ${formatValue(decision.status, "unknown")}` : snapshot.isLoading ? "Checking" : "No routing yet",
794
+ rows,
795
+ status: snapshot.error ? "error" : decision ? "ready" : snapshot.isLoading ? "loading" : "empty",
796
+ title: options.title ?? DEFAULT_TITLE3,
797
+ updatedAt: snapshot.updatedAt
798
+ };
799
+ };
800
+ var renderVoiceRoutingStatusHTML = (snapshot, options = {}) => {
801
+ const model = createVoiceRoutingStatusViewModel(snapshot, options);
802
+ const rows = model.rows.length ? `<div class="absolute-voice-routing-status__grid">${model.rows.map((row) => `<div>
803
+ <span>${escapeHtml3(row.label)}</span>
804
+ <strong>${escapeHtml3(row.value)}</strong>
805
+ </div>`).join("")}</div>` : '<p class="absolute-voice-routing-status__empty">Start a voice session to see the selected provider.</p>';
806
+ return `<section class="absolute-voice-routing-status absolute-voice-routing-status--${escapeHtml3(model.status)}">
807
+ <header class="absolute-voice-routing-status__header">
808
+ <span class="absolute-voice-routing-status__eyebrow">${escapeHtml3(model.title)}</span>
809
+ <strong class="absolute-voice-routing-status__label">${escapeHtml3(model.label)}</strong>
810
+ </header>
811
+ <p class="absolute-voice-routing-status__description">${escapeHtml3(model.description)}</p>
812
+ ${rows}
813
+ ${model.error ? `<p class="absolute-voice-routing-status__error">${escapeHtml3(model.error)}</p>` : ""}
814
+ </section>`;
815
+ };
816
+ var getVoiceRoutingStatusCSS = () => `.absolute-voice-routing-status{border:1px solid #d8d2c4;border-radius:20px;background:#fffaf0;color:#16130d;padding:18px;box-shadow:0 18px 40px rgba(47,37,18,.12);font-family:inherit}.absolute-voice-routing-status--error{border-color:#f2a7a7;background:#fff5f3}.absolute-voice-routing-status__header{align-items:start;display:flex;gap:12px;justify-content:space-between}.absolute-voice-routing-status__eyebrow{color:#73664f;font-size:12px;font-weight:800;letter-spacing:.08em;text-transform:uppercase}.absolute-voice-routing-status__label{font-size:24px;line-height:1}.absolute-voice-routing-status__description{color:#514733;margin:12px 0 0}.absolute-voice-routing-status__grid{display:grid;gap:8px;grid-template-columns:repeat(2,minmax(0,1fr));margin-top:14px}.absolute-voice-routing-status__grid div{background:#fff;border:1px solid #eee4d2;border-radius:14px;padding:10px 12px}.absolute-voice-routing-status__grid span{color:#655944;display:block;font-size:12px;margin-bottom:4px}.absolute-voice-routing-status__grid strong{overflow-wrap:anywhere}.absolute-voice-routing-status__empty{color:#655944;margin:14px 0 0}.absolute-voice-routing-status__error{color:#9f1239;font-weight:700}`;
817
+ var mountVoiceRoutingStatus = (element, path = "/api/routing/latest", options = {}) => {
818
+ const store = createVoiceRoutingStatusStore(path, options);
819
+ const render = () => {
820
+ element.innerHTML = renderVoiceRoutingStatusHTML(store.getSnapshot(), options);
821
+ };
822
+ const unsubscribe = store.subscribe(render);
823
+ render();
824
+ store.refresh().catch(() => {});
825
+ return {
826
+ close: () => {
827
+ unsubscribe();
828
+ store.close();
829
+ },
830
+ refresh: store.refresh
831
+ };
832
+ };
833
+ var defineVoiceRoutingStatusElement = (tagName = "absolute-voice-routing-status") => {
834
+ if (typeof window === "undefined" || typeof customElements === "undefined" || customElements.get(tagName)) {
835
+ return;
836
+ }
837
+ customElements.define(tagName, class AbsoluteVoiceRoutingStatusElement extends HTMLElement {
838
+ mounted;
839
+ connectedCallback() {
840
+ const intervalMs = Number(this.getAttribute("interval-ms") ?? 5000);
841
+ this.mounted = mountVoiceRoutingStatus(this, this.getAttribute("path") ?? "/api/routing/latest", {
842
+ description: this.getAttribute("description") ?? undefined,
843
+ intervalMs: Number.isFinite(intervalMs) ? intervalMs : 5000,
844
+ title: this.getAttribute("title") ?? undefined
845
+ });
846
+ }
847
+ disconnectedCallback() {
848
+ this.mounted?.close();
849
+ this.mounted = undefined;
850
+ }
851
+ });
852
+ };
853
+
854
+ // src/vue/useVoiceRoutingStatus.ts
855
+ import { onUnmounted as onUnmounted3, ref as ref3, shallowRef as shallowRef3 } from "vue";
856
+ var useVoiceRoutingStatus = (path = "/api/routing/latest", options = {}) => {
857
+ const store = createVoiceRoutingStatusStore(path, options);
858
+ const decision = shallowRef3(null);
859
+ const error = ref3(null);
860
+ const isLoading = ref3(false);
861
+ const updatedAt = ref3(undefined);
862
+ const sync = () => {
863
+ const snapshot = store.getSnapshot();
864
+ decision.value = snapshot.decision;
865
+ error.value = snapshot.error;
866
+ isLoading.value = snapshot.isLoading;
867
+ updatedAt.value = snapshot.updatedAt;
868
+ };
869
+ const unsubscribe = store.subscribe(sync);
870
+ sync();
871
+ store.refresh().catch(() => {});
872
+ onUnmounted3(() => {
873
+ unsubscribe();
874
+ store.close();
875
+ });
876
+ return {
877
+ decision,
878
+ error,
879
+ isLoading,
880
+ refresh: store.refresh,
881
+ updatedAt
882
+ };
883
+ };
884
+
885
+ // src/vue/VoiceRoutingStatus.ts
886
+ var VoiceRoutingStatus = defineComponent3({
887
+ name: "VoiceRoutingStatus",
888
+ props: {
889
+ class: {
890
+ default: "",
891
+ type: String
892
+ },
893
+ description: {
894
+ default: undefined,
895
+ type: String
896
+ },
897
+ intervalMs: {
898
+ default: 5000,
899
+ type: Number
900
+ },
901
+ path: {
902
+ default: "/api/routing/latest",
903
+ type: String
904
+ },
905
+ title: {
906
+ default: undefined,
907
+ type: String
908
+ }
909
+ },
910
+ setup(props) {
911
+ const options = {
912
+ description: props.description,
913
+ intervalMs: props.intervalMs,
914
+ title: props.title
915
+ };
916
+ const status = useVoiceRoutingStatus(props.path, options);
917
+ const model = computed2(() => createVoiceRoutingStatusViewModel({
918
+ decision: status.decision.value,
919
+ error: status.error.value,
920
+ isLoading: status.isLoading.value,
921
+ updatedAt: status.updatedAt.value
922
+ }, options));
923
+ return () => h3("section", {
924
+ class: [
925
+ "absolute-voice-routing-status",
926
+ `absolute-voice-routing-status--${model.value.status}`,
927
+ props.class
928
+ ]
929
+ }, [
930
+ h3("header", { class: "absolute-voice-routing-status__header" }, [
931
+ h3("span", { class: "absolute-voice-routing-status__eyebrow" }, model.value.title),
932
+ h3("strong", { class: "absolute-voice-routing-status__label" }, model.value.label)
933
+ ]),
934
+ h3("p", { class: "absolute-voice-routing-status__description" }, model.value.description),
935
+ model.value.rows.length ? h3("div", { class: "absolute-voice-routing-status__grid" }, model.value.rows.map((row) => h3("div", { key: row.label }, [
936
+ h3("span", row.label),
937
+ h3("strong", row.value)
938
+ ]))) : h3("p", { class: "absolute-voice-routing-status__empty" }, "Start a voice session to see the selected provider."),
939
+ model.value.error ? h3("p", { class: "absolute-voice-routing-status__error" }, model.value.error) : null
940
+ ]);
941
+ }
942
+ });
943
+ // src/vue/useVoiceStream.ts
944
+ import { onUnmounted as onUnmounted4, ref as ref4, shallowRef as shallowRef4 } from "vue";
74
945
 
75
946
  // src/client/actions.ts
76
947
  var normalizeErrorMessage = (value) => {
@@ -120,6 +991,12 @@ var serverMessageToAction = (message) => {
120
991
  sessionId: message.sessionId,
121
992
  type: "complete"
122
993
  };
994
+ case "call_lifecycle":
995
+ return {
996
+ event: message.event,
997
+ sessionId: message.sessionId,
998
+ type: "call_lifecycle"
999
+ };
123
1000
  case "error":
124
1001
  return {
125
1002
  message: normalizeErrorMessage(message.message),
@@ -163,7 +1040,7 @@ var DEFAULT_SCENARIO_QUERY_PARAM = "scenarioId";
163
1040
  var noop = () => {};
164
1041
  var noopUnsubscribe = () => noop;
165
1042
  var NOOP_CONNECTION = {
166
- start: () => {},
1043
+ callControl: noop,
167
1044
  close: noop,
168
1045
  endTurn: noop,
169
1046
  getReadyState: () => WS_CLOSED,
@@ -171,6 +1048,7 @@ var NOOP_CONNECTION = {
171
1048
  getSessionId: () => "",
172
1049
  send: noop,
173
1050
  sendAudio: noop,
1051
+ start: () => {},
174
1052
  subscribe: noopUnsubscribe
175
1053
  };
176
1054
  var createSessionId = () => crypto.randomUUID();
@@ -192,6 +1070,7 @@ var isVoiceServerMessage = (value) => {
192
1070
  switch (value.type) {
193
1071
  case "audio":
194
1072
  case "assistant":
1073
+ case "call_lifecycle":
195
1074
  case "complete":
196
1075
  case "error":
197
1076
  case "final":
@@ -332,6 +1211,12 @@ var createVoiceConnection = (path, options = {}) => {
332
1211
  const endTurn = () => {
333
1212
  send({ type: "end_turn" });
334
1213
  };
1214
+ const callControl = (message) => {
1215
+ send({
1216
+ ...message,
1217
+ type: "call_control"
1218
+ });
1219
+ };
335
1220
  const close = () => {
336
1221
  clearTimers();
337
1222
  if (state.ws) {
@@ -349,7 +1234,7 @@ var createVoiceConnection = (path, options = {}) => {
349
1234
  };
350
1235
  connect();
351
1236
  return {
352
- start,
1237
+ callControl,
353
1238
  close,
354
1239
  endTurn,
355
1240
  getReadyState: () => state.ws?.readyState ?? WS_CLOSED,
@@ -357,6 +1242,7 @@ var createVoiceConnection = (path, options = {}) => {
357
1242
  getSessionId: () => state.sessionId,
358
1243
  send,
359
1244
  sendAudio,
1245
+ start,
360
1246
  subscribe
361
1247
  };
362
1248
  };
@@ -365,6 +1251,7 @@ var createVoiceConnection = (path, options = {}) => {
365
1251
  var createInitialState = () => ({
366
1252
  assistantAudio: [],
367
1253
  assistantTexts: [],
1254
+ call: null,
368
1255
  error: null,
369
1256
  isConnected: false,
370
1257
  scenarioId: null,
@@ -408,6 +1295,20 @@ var createVoiceStreamStore = () => {
408
1295
  status: "completed"
409
1296
  };
410
1297
  break;
1298
+ case "call_lifecycle":
1299
+ state = {
1300
+ ...state,
1301
+ call: {
1302
+ ...state.call,
1303
+ disposition: action.event.type === "end" ? action.event.disposition : state.call?.disposition,
1304
+ endedAt: action.event.type === "end" ? action.event.at : state.call?.endedAt,
1305
+ events: [...state.call?.events ?? [], action.event],
1306
+ lastEventAt: action.event.at,
1307
+ startedAt: state.call?.startedAt ?? action.event.at
1308
+ },
1309
+ sessionId: action.sessionId
1310
+ };
1311
+ break;
411
1312
  case "connected":
412
1313
  state = {
413
1314
  ...state,
@@ -494,6 +1395,9 @@ var createVoiceStream = (path, options = {}) => {
494
1395
  }
495
1396
  });
496
1397
  return {
1398
+ callControl(message) {
1399
+ connection.callControl(message);
1400
+ },
497
1401
  close() {
498
1402
  unsubscribeConnection();
499
1403
  connection.close();
@@ -537,6 +1441,9 @@ var createVoiceStream = (path, options = {}) => {
537
1441
  get assistantAudio() {
538
1442
  return store.getSnapshot().assistantAudio;
539
1443
  },
1444
+ get call() {
1445
+ return store.getSnapshot().call;
1446
+ },
540
1447
  sendAudio(audio) {
541
1448
  connection.sendAudio(audio);
542
1449
  },
@@ -552,17 +1459,19 @@ var createVoiceStream = (path, options = {}) => {
552
1459
  // src/vue/useVoiceStream.ts
553
1460
  var useVoiceStream = (path, options = {}) => {
554
1461
  const stream = createVoiceStream(path, options);
555
- const assistantAudio = shallowRef([]);
556
- const assistantTexts = shallowRef([]);
557
- const error = ref(null);
558
- const isConnected = ref(false);
559
- const partial = ref("");
560
- const sessionId = ref(stream.sessionId);
561
- const status = ref(stream.status);
562
- const turns = shallowRef([]);
1462
+ const assistantAudio = shallowRef4([]);
1463
+ const assistantTexts = shallowRef4([]);
1464
+ const call = shallowRef4(null);
1465
+ const error = ref4(null);
1466
+ const isConnected = ref4(false);
1467
+ const partial = ref4("");
1468
+ const sessionId = ref4(stream.sessionId);
1469
+ const status = ref4(stream.status);
1470
+ const turns = shallowRef4([]);
563
1471
  const sync = () => {
564
1472
  assistantAudio.value = [...stream.assistantAudio];
565
1473
  assistantTexts.value = [...stream.assistantTexts];
1474
+ call.value = stream.call;
566
1475
  error.value = stream.error;
567
1476
  isConnected.value = stream.isConnected;
568
1477
  partial.value = stream.partial;
@@ -576,10 +1485,12 @@ var useVoiceStream = (path, options = {}) => {
576
1485
  unsubscribe();
577
1486
  stream.close();
578
1487
  };
579
- onUnmounted(destroy);
1488
+ onUnmounted4(destroy);
580
1489
  return {
581
1490
  assistantAudio,
582
1491
  assistantTexts,
1492
+ call,
1493
+ callControl: (message) => stream.callControl(message),
583
1494
  close: () => destroy(),
584
1495
  endTurn: () => stream.endTurn(),
585
1496
  error,
@@ -592,7 +1503,7 @@ var useVoiceStream = (path, options = {}) => {
592
1503
  };
593
1504
  };
594
1505
  // src/vue/useVoiceController.ts
595
- import { onUnmounted as onUnmounted2, ref as ref2, shallowRef as shallowRef2 } from "vue";
1506
+ import { onUnmounted as onUnmounted5, ref as ref5, shallowRef as shallowRef5 } from "vue";
596
1507
 
597
1508
  // src/client/htmx.ts
598
1509
  var DEFAULT_EVENT_NAME = "voice-refresh";
@@ -1056,6 +1967,7 @@ var resolveVoiceRuntimePreset = (name = "default") => {
1056
1967
  var createInitialState2 = (stream) => ({
1057
1968
  assistantAudio: [...stream.assistantAudio],
1058
1969
  assistantTexts: [...stream.assistantTexts],
1970
+ call: stream.call,
1059
1971
  error: stream.error,
1060
1972
  isConnected: stream.isConnected,
1061
1973
  isRecording: false,
@@ -1085,6 +1997,7 @@ var createVoiceController = (path, options = {}) => {
1085
1997
  ...state,
1086
1998
  assistantAudio: [...stream.assistantAudio],
1087
1999
  assistantTexts: [...stream.assistantTexts],
2000
+ call: stream.call,
1088
2001
  error: stream.error,
1089
2002
  isConnected: stream.isConnected,
1090
2003
  partial: stream.partial,
@@ -1162,6 +2075,7 @@ var createVoiceController = (path, options = {}) => {
1162
2075
  bindHTMX(bindingOptions) {
1163
2076
  return bindVoiceHTMX(stream, bindingOptions);
1164
2077
  },
2078
+ callControl: (message) => stream.callControl(message),
1165
2079
  close,
1166
2080
  endTurn: () => stream.endTurn(),
1167
2081
  get error() {
@@ -1214,6 +2128,9 @@ var createVoiceController = (path, options = {}) => {
1214
2128
  },
1215
2129
  get assistantAudio() {
1216
2130
  return state.assistantAudio;
2131
+ },
2132
+ get call() {
2133
+ return state.call;
1217
2134
  }
1218
2135
  };
1219
2136
  };
@@ -1221,16 +2138,16 @@ var createVoiceController = (path, options = {}) => {
1221
2138
  // src/vue/useVoiceController.ts
1222
2139
  var useVoiceController = (path, options = {}) => {
1223
2140
  const controller = createVoiceController(path, options);
1224
- const assistantAudio = shallowRef2([]);
1225
- const assistantTexts = shallowRef2([]);
1226
- const error = ref2(null);
1227
- const isConnected = ref2(false);
1228
- const isRecording = ref2(false);
1229
- const partial = ref2("");
1230
- const recordingError = ref2(null);
1231
- const sessionId = ref2(controller.sessionId);
1232
- const status = ref2(controller.status);
1233
- const turns = shallowRef2([]);
2141
+ const assistantAudio = shallowRef5([]);
2142
+ const assistantTexts = shallowRef5([]);
2143
+ const error = ref5(null);
2144
+ const isConnected = ref5(false);
2145
+ const isRecording = ref5(false);
2146
+ const partial = ref5("");
2147
+ const recordingError = ref5(null);
2148
+ const sessionId = ref5(controller.sessionId);
2149
+ const status = ref5(controller.status);
2150
+ const turns = shallowRef5([]);
1234
2151
  const sync = () => {
1235
2152
  assistantAudio.value = [...controller.assistantAudio];
1236
2153
  assistantTexts.value = [...controller.assistantTexts];
@@ -1249,7 +2166,7 @@ var useVoiceController = (path, options = {}) => {
1249
2166
  unsubscribe();
1250
2167
  controller.close();
1251
2168
  };
1252
- onUnmounted2(destroy);
2169
+ onUnmounted5(destroy);
1253
2170
  return {
1254
2171
  assistantAudio,
1255
2172
  assistantTexts,
@@ -1270,7 +2187,127 @@ var useVoiceController = (path, options = {}) => {
1270
2187
  turns
1271
2188
  };
1272
2189
  };
2190
+ // src/vue/useVoiceWorkflowStatus.ts
2191
+ import { onUnmounted as onUnmounted6, ref as ref6, shallowRef as shallowRef6 } from "vue";
2192
+
2193
+ // src/client/workflowStatus.ts
2194
+ var fetchVoiceWorkflowStatus = async (path = "/evals/scenarios/json", options = {}) => {
2195
+ const fetchImpl = options.fetch ?? globalThis.fetch;
2196
+ const response = await fetchImpl(path);
2197
+ if (!response.ok) {
2198
+ throw new Error(`Voice workflow status failed: HTTP ${response.status}`);
2199
+ }
2200
+ return await response.json();
2201
+ };
2202
+ var createVoiceWorkflowStatusStore = (path = "/evals/scenarios/json", options = {}) => {
2203
+ const listeners = new Set;
2204
+ let closed = false;
2205
+ let timer;
2206
+ let snapshot = {
2207
+ error: null,
2208
+ isLoading: false
2209
+ };
2210
+ const emit = () => {
2211
+ for (const listener of listeners) {
2212
+ listener();
2213
+ }
2214
+ };
2215
+ const refresh = async () => {
2216
+ if (closed) {
2217
+ return snapshot.report;
2218
+ }
2219
+ snapshot = {
2220
+ ...snapshot,
2221
+ error: null,
2222
+ isLoading: true
2223
+ };
2224
+ emit();
2225
+ try {
2226
+ const report = await fetchVoiceWorkflowStatus(path, options);
2227
+ snapshot = {
2228
+ error: null,
2229
+ isLoading: false,
2230
+ report,
2231
+ updatedAt: Date.now()
2232
+ };
2233
+ emit();
2234
+ return report;
2235
+ } catch (error) {
2236
+ snapshot = {
2237
+ ...snapshot,
2238
+ error: error instanceof Error ? error.message : String(error),
2239
+ isLoading: false
2240
+ };
2241
+ emit();
2242
+ throw error;
2243
+ }
2244
+ };
2245
+ const close = () => {
2246
+ closed = true;
2247
+ if (timer) {
2248
+ clearInterval(timer);
2249
+ timer = undefined;
2250
+ }
2251
+ listeners.clear();
2252
+ };
2253
+ if (typeof window !== "undefined" && options.intervalMs && options.intervalMs > 0) {
2254
+ timer = setInterval(() => {
2255
+ refresh().catch(() => {});
2256
+ }, options.intervalMs);
2257
+ }
2258
+ return {
2259
+ close,
2260
+ getServerSnapshot: () => snapshot,
2261
+ getSnapshot: () => snapshot,
2262
+ refresh,
2263
+ subscribe: (listener) => {
2264
+ listeners.add(listener);
2265
+ return () => {
2266
+ listeners.delete(listener);
2267
+ };
2268
+ }
2269
+ };
2270
+ };
2271
+
2272
+ // src/vue/useVoiceWorkflowStatus.ts
2273
+ var useVoiceWorkflowStatus = (path = "/evals/scenarios/json", options = {}) => {
2274
+ const store = createVoiceWorkflowStatusStore(path, options);
2275
+ const error = ref6(null);
2276
+ const isLoading = ref6(false);
2277
+ const report = shallowRef6(undefined);
2278
+ const updatedAt = ref6(undefined);
2279
+ const sync = () => {
2280
+ const snapshot = store.getSnapshot();
2281
+ error.value = snapshot.error;
2282
+ isLoading.value = snapshot.isLoading;
2283
+ report.value = snapshot.report;
2284
+ updatedAt.value = snapshot.updatedAt;
2285
+ };
2286
+ const unsubscribe = store.subscribe(sync);
2287
+ sync();
2288
+ if (typeof window !== "undefined") {
2289
+ store.refresh().catch(() => {});
2290
+ }
2291
+ onUnmounted6(() => {
2292
+ unsubscribe();
2293
+ store.close();
2294
+ });
2295
+ return {
2296
+ error,
2297
+ isLoading,
2298
+ refresh: store.refresh,
2299
+ report,
2300
+ updatedAt
2301
+ };
2302
+ };
1273
2303
  export {
2304
+ useVoiceWorkflowStatus,
1274
2305
  useVoiceStream,
1275
- useVoiceController
2306
+ useVoiceRoutingStatus,
2307
+ useVoiceProviderStatus,
2308
+ useVoiceController,
2309
+ useVoiceAppKitStatus,
2310
+ VoiceRoutingStatus,
2311
+ VoiceProviderStatus,
2312
+ VoiceOpsStatus
1276
2313
  };