@absolutejs/voice 0.0.22-beta.91 → 0.0.22-beta.93

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.
@@ -1287,7 +1287,13 @@ var createVoiceController = (path, options = {}) => {
1287
1287
  capture = createMicrophoneCapture({
1288
1288
  channelCount: options.capture?.channelCount ?? preset.capture.channelCount,
1289
1289
  onLevel: options.capture?.onLevel,
1290
- onAudio: (audio) => stream.sendAudio(audio),
1290
+ onAudio: (audio) => {
1291
+ if (options.capture?.onAudio) {
1292
+ options.capture.onAudio(audio, stream.sendAudio);
1293
+ return;
1294
+ }
1295
+ stream.sendAudio(audio);
1296
+ },
1291
1297
  sampleRateHz: options.capture?.sampleRateHz ?? preset.capture.sampleRateHz
1292
1298
  });
1293
1299
  return capture;
package/dist/appKit.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { Elysia } from 'elysia';
2
2
  import { type VoiceAssistantHealthRoutesOptions } from './assistantHealth';
3
+ import { type VoiceBargeInRoutesOptions } from './bargeInRoutes';
3
4
  import { type VoiceDiagnosticsRoutesOptions } from './diagnosticsRoutes';
4
5
  import { type VoiceEvalRoutesOptions, type VoiceEvalLink } from './evalRoutes';
5
6
  import { type VoiceHandoffHealthRoutesOptions } from './handoffHealth';
@@ -12,7 +13,7 @@ import { type VoiceResilienceRoutesOptions } from './resilienceRoutes';
12
13
  import { type VoiceSessionListRoutesOptions, type VoiceSessionReplayRoutesOptions } from './sessionReplay';
13
14
  import { type VoiceTraceEventStore } from './trace';
14
15
  import { type VoiceTraceTimelineRoutesOptions } from './traceTimeline';
15
- export type VoiceAppKitSurface = 'assistantHealth' | 'diagnostics' | 'evals' | 'handoffs' | 'opsConsole' | 'providerCapabilities' | 'providerHealth' | 'productionReadiness' | 'quality' | 'resilience' | 'sessionReplay' | 'sessions' | 'traceTimeline';
16
+ export type VoiceAppKitSurface = 'assistantHealth' | 'bargeIn' | 'diagnostics' | 'evals' | 'handoffs' | 'opsConsole' | 'providerCapabilities' | 'providerHealth' | 'productionReadiness' | 'quality' | 'resilience' | 'sessionReplay' | 'sessions' | 'traceTimeline';
16
17
  export type VoiceAppKitLink = VoiceEvalLink & {
17
18
  description?: string;
18
19
  statusHref?: string;
@@ -20,6 +21,7 @@ export type VoiceAppKitLink = VoiceEvalLink & {
20
21
  export type VoiceAppKitRoutesOptions<TProvider extends string = string> = {
21
22
  appStatus?: false | VoiceAppKitStatusOptions;
22
23
  assistantHealth?: false | Partial<VoiceAssistantHealthRoutesOptions<TProvider>>;
24
+ bargeIn?: false | Partial<VoiceBargeInRoutesOptions>;
23
25
  diagnostics?: false | Partial<Omit<VoiceDiagnosticsRoutesOptions, 'store'>>;
24
26
  evals?: false | Partial<VoiceEvalRoutesOptions>;
25
27
  handoffs?: false | Partial<VoiceHandoffHealthRoutesOptions>;
@@ -0,0 +1,56 @@
1
+ import { Elysia } from 'elysia';
2
+ import type { StoredVoiceTraceEvent, VoiceTraceEventStore } from './trace';
3
+ import type { VoiceBargeInMonitorSnapshot } from './types';
4
+ export type VoiceBargeInRoutesOptions = {
5
+ headers?: HeadersInit;
6
+ htmlPath?: string;
7
+ name?: string;
8
+ path?: string;
9
+ store: VoiceTraceEventStore;
10
+ thresholdMs?: number;
11
+ title?: string;
12
+ };
13
+ export type VoiceBargeInReport = VoiceBargeInMonitorSnapshot & {
14
+ checkedAt: number;
15
+ sessions: Array<{
16
+ averageLatencyMs?: number;
17
+ failed: number;
18
+ passed: number;
19
+ sessionId: string;
20
+ total: number;
21
+ }>;
22
+ };
23
+ export declare const summarizeVoiceBargeIn: (events: StoredVoiceTraceEvent[], options?: {
24
+ thresholdMs?: number;
25
+ }) => VoiceBargeInReport;
26
+ export declare const renderVoiceBargeInHTML: (report: VoiceBargeInReport, options?: {
27
+ title?: string;
28
+ }) => string;
29
+ export declare const createVoiceBargeInRoutes: (options: VoiceBargeInRoutesOptions) => Elysia<"", {
30
+ decorator: {};
31
+ store: {};
32
+ derive: {};
33
+ resolve: {};
34
+ }, {
35
+ typebox: {};
36
+ error: {};
37
+ }, {
38
+ schema: {};
39
+ standaloneSchema: {};
40
+ macro: {};
41
+ macroFn: {};
42
+ parser: {};
43
+ response: {};
44
+ }, {}, {
45
+ derive: {};
46
+ resolve: {};
47
+ schema: {};
48
+ standaloneSchema: {};
49
+ response: {};
50
+ }, {
51
+ derive: {};
52
+ resolve: {};
53
+ schema: {};
54
+ standaloneSchema: {};
55
+ response: {};
56
+ }>;
@@ -0,0 +1,7 @@
1
+ import type { VoiceBargeInMonitor } from '../types';
2
+ export type VoiceBargeInMonitorOptions = {
3
+ fetch?: typeof fetch;
4
+ path?: string;
5
+ thresholdMs?: number;
6
+ };
7
+ export declare const createVoiceBargeInMonitor: (options?: VoiceBargeInMonitorOptions) => VoiceBargeInMonitor;
@@ -1,3 +1,3 @@
1
1
  import type { VoiceAudioPlayer, VoiceBargeInBinding, VoiceBargeInOptions, VoiceController, VoiceDuplexController, VoiceDuplexControllerOptions } from '../types';
2
- export declare const bindVoiceBargeIn: <TResult = unknown>(controller: Pick<VoiceController<TResult>, "partial" | "sendAudio" | "subscribe">, player: Pick<VoiceAudioPlayer, "interrupt" | "isPlaying">, options?: VoiceBargeInOptions) => VoiceBargeInBinding;
2
+ export declare const bindVoiceBargeIn: <TResult = unknown>(controller: Pick<VoiceController<TResult>, "partial" | "sendAudio" | "sessionId" | "subscribe">, player: Pick<VoiceAudioPlayer, "interrupt" | "isPlaying" | "lastInterruptLatencyMs" | "lastPlaybackStopLatencyMs">, options?: VoiceBargeInOptions) => VoiceBargeInBinding;
3
3
  export declare const createVoiceDuplexController: <TResult = unknown>(path: string, options?: VoiceDuplexControllerOptions) => VoiceDuplexController<TResult>;
@@ -994,7 +994,13 @@ var createVoiceController = (path, options = {}) => {
994
994
  capture = createMicrophoneCapture({
995
995
  channelCount: options.capture?.channelCount ?? preset.capture.channelCount,
996
996
  onLevel: options.capture?.onLevel,
997
- onAudio: (audio) => stream.sendAudio(audio),
997
+ onAudio: (audio) => {
998
+ if (options.capture?.onAudio) {
999
+ options.capture.onAudio(audio, stream.sendAudio);
1000
+ return;
1001
+ }
1002
+ stream.sendAudio(audio);
1003
+ },
998
1004
  sampleRateHz: options.capture?.sampleRateHz ?? preset.capture.sampleRateHz
999
1005
  });
1000
1006
  return capture;
@@ -1104,6 +1110,77 @@ var createVoiceController = (path, options = {}) => {
1104
1110
  };
1105
1111
  };
1106
1112
 
1113
+ // src/client/bargeInMonitor.ts
1114
+ var DEFAULT_THRESHOLD_MS = 250;
1115
+ var createEventId = () => `barge-in:${Date.now()}:${crypto.randomUUID?.() ?? Math.random().toString(36).slice(2)}`;
1116
+ var summarize = (events, thresholdMs) => {
1117
+ const stopped = events.filter((event) => event.status === "stopped");
1118
+ const latencies = stopped.map((event) => event.latencyMs).filter((value) => typeof value === "number");
1119
+ const failed = stopped.filter((event) => typeof event.latencyMs === "number" && event.latencyMs > thresholdMs).length;
1120
+ const passed = stopped.length - failed;
1121
+ return {
1122
+ averageLatencyMs: latencies.length > 0 ? Math.round(latencies.reduce((total, value) => total + value, 0) / latencies.length) : undefined,
1123
+ events: [...events],
1124
+ failed,
1125
+ lastEvent: events.at(-1),
1126
+ passed,
1127
+ status: events.length === 0 ? "empty" : failed > 0 ? "fail" : stopped.length === 0 ? "warn" : "pass",
1128
+ thresholdMs,
1129
+ total: stopped.length
1130
+ };
1131
+ };
1132
+ var createVoiceBargeInMonitor = (options = {}) => {
1133
+ const listeners = new Set;
1134
+ const thresholdMs = options.thresholdMs ?? DEFAULT_THRESHOLD_MS;
1135
+ const fetchImpl = options.fetch ?? globalThis.fetch;
1136
+ const events = [];
1137
+ const emit = () => {
1138
+ for (const listener of listeners) {
1139
+ listener();
1140
+ }
1141
+ };
1142
+ const postEvent = (event) => {
1143
+ if (!options.path || typeof fetchImpl !== "function") {
1144
+ return;
1145
+ }
1146
+ fetchImpl(options.path, {
1147
+ body: JSON.stringify(event),
1148
+ headers: {
1149
+ "Content-Type": "application/json"
1150
+ },
1151
+ method: "POST"
1152
+ }).catch(() => {});
1153
+ };
1154
+ const record = (status, input) => {
1155
+ const event = {
1156
+ at: Date.now(),
1157
+ id: createEventId(),
1158
+ latencyMs: input.latencyMs,
1159
+ playbackStopLatencyMs: input.playbackStopLatencyMs,
1160
+ reason: input.reason,
1161
+ sessionId: input.sessionId,
1162
+ status,
1163
+ thresholdMs
1164
+ };
1165
+ events.push(event);
1166
+ postEvent(event);
1167
+ emit();
1168
+ return event;
1169
+ };
1170
+ return {
1171
+ getSnapshot: () => summarize(events, thresholdMs),
1172
+ recordRequested: (input) => record("requested", input),
1173
+ recordSkipped: (input) => record("skipped", input),
1174
+ recordStopped: (input) => record("stopped", input),
1175
+ subscribe: (subscriber) => {
1176
+ listeners.add(subscriber);
1177
+ return () => {
1178
+ listeners.delete(subscriber);
1179
+ };
1180
+ }
1181
+ };
1182
+ };
1183
+
1107
1184
  // src/client/htmxBootstrap.ts
1108
1185
  var VOICE_WAVE_POINTS = 48;
1109
1186
  var VOICE_WAVE_WIDTH = 320;
@@ -1216,6 +1293,13 @@ var parsePromptList = (value) => {
1216
1293
  } catch {}
1217
1294
  return DEFAULT_GUIDED_PROMPTS;
1218
1295
  };
1296
+ var parseOptionalNumber = (value) => {
1297
+ if (!value) {
1298
+ return;
1299
+ }
1300
+ const parsed = Number(value);
1301
+ return Number.isFinite(parsed) ? parsed : undefined;
1302
+ };
1219
1303
  var requireElement = (root, selector, ctor, name) => {
1220
1304
  const value = selector ? document.querySelector(selector) : null;
1221
1305
  if (value instanceof ctor) {
@@ -1266,6 +1350,13 @@ var initVoiceHTMXRoot = (root) => {
1266
1350
  const guidedPrompts = parsePromptList(root.dataset.voiceGuidedPrompts);
1267
1351
  const guidedLabel = root.dataset.voiceGuidedLabel ?? DEFAULT_GUIDED_LABEL;
1268
1352
  const generalLabel = root.dataset.voiceGeneralLabel ?? DEFAULT_GENERAL_LABEL;
1353
+ const bargeInPath = root.dataset.voiceBargeInPath;
1354
+ const bargeInMonitor = bargeInPath ? createVoiceBargeInMonitor({
1355
+ path: bargeInPath,
1356
+ thresholdMs: parseOptionalNumber(root.dataset.voiceBargeInThresholdMs)
1357
+ }) : null;
1358
+ const bargeInRecentWindowMs = parseOptionalNumber(root.dataset.voiceBargeInRecentWindowMs) ?? 4000;
1359
+ const bargeInSpeechThreshold = parseOptionalNumber(root.dataset.voiceBargeInSpeechThreshold) ?? 0.04;
1269
1360
  const syncElement = requireElement(document, root.dataset.voiceSync, HTMLElement, "voice-htmx-sync");
1270
1361
  const connectionMetric = requireElement(root, root.dataset.voiceConnection, HTMLElement, "metric-connection");
1271
1362
  const errorStatus = requireElement(root, root.dataset.voiceError, HTMLElement, "status-error");
@@ -1279,9 +1370,52 @@ var initVoiceHTMXRoot = (root) => {
1279
1370
  const voiceMonitorCopy = requireElement(root, root.dataset.voiceMonitorCopy, HTMLElement, "voice-monitor-copy");
1280
1371
  const voiceWaveGlow = requireElement(root, root.dataset.voiceWaveGlow, SVGPathElement, "voice-wave-glow");
1281
1372
  const voiceWavePath = requireElement(root, root.dataset.voiceWavePath, SVGPathElement, "voice-wave-path");
1373
+ let activeMode = null;
1374
+ let hasStartedModes = {
1375
+ general: false,
1376
+ guided: false
1377
+ };
1378
+ let isCapturing = false;
1379
+ let micError = null;
1380
+ let waveLevels = createInitialVoiceWaveLevels();
1381
+ let lastInputLevel = 0;
1382
+ let lastAssistantAt = 0;
1383
+ let lastAssistantAudioCount = 0;
1384
+ let lastAssistantTextCount = 0;
1385
+ const syncBargeInOutput = () => {
1386
+ if (!bargeInMonitor) {
1387
+ return;
1388
+ }
1389
+ const voice = currentVoice();
1390
+ const audioCount = voice.assistantAudio.length;
1391
+ const textCount = voice.assistantTexts.length;
1392
+ if (audioCount > lastAssistantAudioCount || textCount > lastAssistantTextCount) {
1393
+ lastAssistantAt = Date.now();
1394
+ }
1395
+ lastAssistantAudioCount = audioCount;
1396
+ lastAssistantTextCount = textCount;
1397
+ };
1398
+ const sendAudioWithBargeInEvidence = (audio, sendAudio) => {
1399
+ syncBargeInOutput();
1400
+ if (bargeInMonitor && Date.now() - lastAssistantAt <= bargeInRecentWindowMs && lastInputLevel >= bargeInSpeechThreshold) {
1401
+ bargeInMonitor.recordRequested({
1402
+ reason: "manual-audio",
1403
+ sessionId: currentVoice().sessionId
1404
+ });
1405
+ bargeInMonitor.recordStopped({
1406
+ latencyMs: 0,
1407
+ playbackStopLatencyMs: 0,
1408
+ reason: "manual-audio",
1409
+ sessionId: currentVoice().sessionId
1410
+ });
1411
+ }
1412
+ sendAudio(audio);
1413
+ };
1282
1414
  const guidedVoice = createVoiceController(guidedPath, {
1283
1415
  capture: {
1416
+ onAudio: sendAudioWithBargeInEvidence,
1284
1417
  onLevel: (level) => {
1418
+ lastInputLevel = level;
1285
1419
  waveLevels = pushVoiceWaveLevel(waveLevels, level);
1286
1420
  renderWave();
1287
1421
  }
@@ -1290,7 +1424,9 @@ var initVoiceHTMXRoot = (root) => {
1290
1424
  });
1291
1425
  const generalVoice = createVoiceController(generalPath, {
1292
1426
  capture: {
1427
+ onAudio: sendAudioWithBargeInEvidence,
1293
1428
  onLevel: (level) => {
1429
+ lastInputLevel = level;
1294
1430
  waveLevels = pushVoiceWaveLevel(waveLevels, level);
1295
1431
  renderWave();
1296
1432
  }
@@ -1299,14 +1435,6 @@ var initVoiceHTMXRoot = (root) => {
1299
1435
  });
1300
1436
  const stopGuidedBinding = guidedVoice.bindHTMX({ element: syncElement });
1301
1437
  const stopGeneralBinding = generalVoice.bindHTMX({ element: syncElement });
1302
- let activeMode = null;
1303
- let hasStartedModes = {
1304
- general: false,
1305
- guided: false
1306
- };
1307
- let isCapturing = false;
1308
- let micError = null;
1309
- let waveLevels = createInitialVoiceWaveLevels();
1310
1438
  const currentVoice = () => activeMode === "general" ? generalVoice : guidedVoice;
1311
1439
  const renderWave = () => {
1312
1440
  const path = createVoiceWavePath(waveLevels);
@@ -1385,8 +1513,14 @@ var initVoiceHTMXRoot = (root) => {
1385
1513
  render();
1386
1514
  }
1387
1515
  };
1388
- guidedVoice.subscribe(render);
1389
- generalVoice.subscribe(render);
1516
+ guidedVoice.subscribe(() => {
1517
+ syncBargeInOutput();
1518
+ render();
1519
+ });
1520
+ generalVoice.subscribe(() => {
1521
+ syncBargeInOutput();
1522
+ render();
1523
+ });
1390
1524
  startGuidedButton.addEventListener("click", () => {
1391
1525
  startMode("guided");
1392
1526
  });
@@ -5,6 +5,7 @@ export { createVoiceController } from './controller';
5
5
  export { bindVoiceBargeIn, createVoiceDuplexController } from './duplex';
6
6
  export { bindVoiceHTMX } from './htmx';
7
7
  export { createMicrophoneCapture } from './microphone';
8
+ export { createVoiceBargeInMonitor } from './bargeInMonitor';
8
9
  export { createVoiceAppKitStatusStore, fetchVoiceAppKitStatus } from './appKitStatus';
9
10
  export { createVoiceOpsStatusViewModel, defineVoiceOpsStatusElement, getVoiceOpsStatusCSS, getVoiceOpsStatusLabel, mountVoiceOpsStatus, renderVoiceOpsStatusHTML } from './opsStatusWidget';
10
11
  export { createVoiceRoutingStatusStore, fetchVoiceRoutingStatus } from './routingStatus';
@@ -21,6 +22,7 @@ export { createVoiceTurnQualityViewModel, defineVoiceTurnQualityElement, getVoic
21
22
  export { createVoiceTraceTimelineViewModel, defineVoiceTraceTimelineElement, getVoiceTraceTimelineCSS, mountVoiceTraceTimeline, renderVoiceTraceTimelineWidgetHTML } from './traceTimelineWidget';
22
23
  export { createVoiceWorkflowStatusStore, fetchVoiceWorkflowStatus } from './workflowStatus';
23
24
  export type { VoiceAppKitStatusClientOptions, VoiceAppKitStatusSnapshot } from './appKitStatus';
25
+ export type { VoiceBargeInMonitorOptions } from './bargeInMonitor';
24
26
  export type { VoiceOpsStatusSurfaceView, VoiceOpsStatusViewModel, VoiceOpsStatusWidgetOptions } from './opsStatusWidget';
25
27
  export type { VoiceRoutingStatusClientOptions, VoiceRoutingStatusSnapshot } from './routingStatus';
26
28
  export type { VoiceRoutingStatusViewModel, VoiceRoutingStatusWidgetOptions } from './routingStatusWidget';
@@ -1446,7 +1446,13 @@ var createVoiceController = (path, options = {}) => {
1446
1446
  capture = createMicrophoneCapture({
1447
1447
  channelCount: options.capture?.channelCount ?? preset.capture.channelCount,
1448
1448
  onLevel: options.capture?.onLevel,
1449
- onAudio: (audio) => stream.sendAudio(audio),
1449
+ onAudio: (audio) => {
1450
+ if (options.capture?.onAudio) {
1451
+ options.capture.onAudio(audio, stream.sendAudio);
1452
+ return;
1453
+ }
1454
+ stream.sendAudio(audio);
1455
+ },
1450
1456
  sampleRateHz: options.capture?.sampleRateHz ?? preset.capture.sampleRateHz
1451
1457
  });
1452
1458
  return capture;
@@ -1560,11 +1566,26 @@ var DEFAULT_INTERRUPT_THRESHOLD = 0.08;
1560
1566
  var shouldInterruptForLevel = (level, options = {}) => (options.enabled ?? true) && level >= (options.interruptThreshold ?? DEFAULT_INTERRUPT_THRESHOLD);
1561
1567
  var bindVoiceBargeIn = (controller, player, options = {}) => {
1562
1568
  let lastPartial = controller.partial;
1563
- const interruptIfPlaying = () => {
1569
+ const interruptIfPlaying = (reason) => {
1564
1570
  if (!player.isPlaying || options.enabled === false) {
1571
+ options.monitor?.recordSkipped({
1572
+ reason,
1573
+ sessionId: controller.sessionId
1574
+ });
1565
1575
  return;
1566
1576
  }
1567
- player.interrupt();
1577
+ options.monitor?.recordRequested({
1578
+ reason,
1579
+ sessionId: controller.sessionId
1580
+ });
1581
+ player.interrupt().then(() => {
1582
+ options.monitor?.recordStopped({
1583
+ latencyMs: player.lastInterruptLatencyMs,
1584
+ playbackStopLatencyMs: player.lastPlaybackStopLatencyMs,
1585
+ reason,
1586
+ sessionId: controller.sessionId
1587
+ });
1588
+ });
1568
1589
  };
1569
1590
  const unsubscribe = controller.subscribe(() => {
1570
1591
  if (options.interruptOnPartial === false) {
@@ -1572,7 +1593,7 @@ var bindVoiceBargeIn = (controller, player, options = {}) => {
1572
1593
  return;
1573
1594
  }
1574
1595
  if (!lastPartial && controller.partial) {
1575
- interruptIfPlaying();
1596
+ interruptIfPlaying("partial-transcript");
1576
1597
  }
1577
1598
  lastPartial = controller.partial;
1578
1599
  });
@@ -1582,11 +1603,11 @@ var bindVoiceBargeIn = (controller, player, options = {}) => {
1582
1603
  },
1583
1604
  handleLevel: (level) => {
1584
1605
  if (shouldInterruptForLevel(level, options)) {
1585
- interruptIfPlaying();
1606
+ interruptIfPlaying("input-level");
1586
1607
  }
1587
1608
  },
1588
1609
  sendAudio: (audio) => {
1589
- interruptIfPlaying();
1610
+ interruptIfPlaying("manual-audio");
1590
1611
  controller.sendAudio(audio);
1591
1612
  }
1592
1613
  };
@@ -1616,13 +1637,93 @@ var createVoiceDuplexController = (path, options = {}) => {
1616
1637
  audioPlayer,
1617
1638
  close,
1618
1639
  interruptAssistant: async () => {
1640
+ options.bargeIn?.monitor?.recordRequested({
1641
+ reason: "manual-interrupt",
1642
+ sessionId: controller.sessionId
1643
+ });
1619
1644
  await audioPlayer.interrupt();
1645
+ options.bargeIn?.monitor?.recordStopped({
1646
+ latencyMs: audioPlayer.lastInterruptLatencyMs,
1647
+ playbackStopLatencyMs: audioPlayer.lastPlaybackStopLatencyMs,
1648
+ reason: "manual-interrupt",
1649
+ sessionId: controller.sessionId
1650
+ });
1620
1651
  },
1621
1652
  sendAudio: (audio) => {
1622
1653
  bargeInBinding?.sendAudio(audio);
1623
1654
  }
1624
1655
  };
1625
1656
  };
1657
+ // src/client/bargeInMonitor.ts
1658
+ var DEFAULT_THRESHOLD_MS = 250;
1659
+ var createEventId = () => `barge-in:${Date.now()}:${crypto.randomUUID?.() ?? Math.random().toString(36).slice(2)}`;
1660
+ var summarize = (events, thresholdMs) => {
1661
+ const stopped = events.filter((event) => event.status === "stopped");
1662
+ const latencies = stopped.map((event) => event.latencyMs).filter((value) => typeof value === "number");
1663
+ const failed = stopped.filter((event) => typeof event.latencyMs === "number" && event.latencyMs > thresholdMs).length;
1664
+ const passed = stopped.length - failed;
1665
+ return {
1666
+ averageLatencyMs: latencies.length > 0 ? Math.round(latencies.reduce((total, value) => total + value, 0) / latencies.length) : undefined,
1667
+ events: [...events],
1668
+ failed,
1669
+ lastEvent: events.at(-1),
1670
+ passed,
1671
+ status: events.length === 0 ? "empty" : failed > 0 ? "fail" : stopped.length === 0 ? "warn" : "pass",
1672
+ thresholdMs,
1673
+ total: stopped.length
1674
+ };
1675
+ };
1676
+ var createVoiceBargeInMonitor = (options = {}) => {
1677
+ const listeners = new Set;
1678
+ const thresholdMs = options.thresholdMs ?? DEFAULT_THRESHOLD_MS;
1679
+ const fetchImpl = options.fetch ?? globalThis.fetch;
1680
+ const events = [];
1681
+ const emit = () => {
1682
+ for (const listener of listeners) {
1683
+ listener();
1684
+ }
1685
+ };
1686
+ const postEvent = (event) => {
1687
+ if (!options.path || typeof fetchImpl !== "function") {
1688
+ return;
1689
+ }
1690
+ fetchImpl(options.path, {
1691
+ body: JSON.stringify(event),
1692
+ headers: {
1693
+ "Content-Type": "application/json"
1694
+ },
1695
+ method: "POST"
1696
+ }).catch(() => {});
1697
+ };
1698
+ const record = (status, input) => {
1699
+ const event = {
1700
+ at: Date.now(),
1701
+ id: createEventId(),
1702
+ latencyMs: input.latencyMs,
1703
+ playbackStopLatencyMs: input.playbackStopLatencyMs,
1704
+ reason: input.reason,
1705
+ sessionId: input.sessionId,
1706
+ status,
1707
+ thresholdMs
1708
+ };
1709
+ events.push(event);
1710
+ postEvent(event);
1711
+ emit();
1712
+ return event;
1713
+ };
1714
+ return {
1715
+ getSnapshot: () => summarize(events, thresholdMs),
1716
+ recordRequested: (input) => record("requested", input),
1717
+ recordSkipped: (input) => record("skipped", input),
1718
+ recordStopped: (input) => record("stopped", input),
1719
+ subscribe: (subscriber) => {
1720
+ listeners.add(subscriber);
1721
+ return () => {
1722
+ listeners.delete(subscriber);
1723
+ };
1724
+ }
1725
+ };
1726
+ };
1626
1727
  // src/client/appKitStatus.ts
1627
1728
  var fetchVoiceAppKitStatus = async (path = "/app-kit/status", options = {}) => {
1628
1729
  const fetchImpl = options.fetch ?? globalThis.fetch;
@@ -3046,6 +3147,7 @@ export {
3046
3147
  createVoiceDuplexController,
3047
3148
  createVoiceController,
3048
3149
  createVoiceConnection,
3150
+ createVoiceBargeInMonitor,
3049
3151
  createVoiceAudioPlayer,
3050
3152
  createVoiceAppKitStatusStore,
3051
3153
  createMicrophoneCapture,
package/dist/index.d.ts CHANGED
@@ -2,6 +2,7 @@ export { voice } from './plugin';
2
2
  export { createVoiceAppKit, createVoiceAppKitRoutes, summarizeVoiceAppKitStatus } from './appKit';
3
3
  export { createVoiceAssistant, createVoiceExperiment, summarizeVoiceAssistantRuns } from './assistant';
4
4
  export { createVoiceAssistantHealthHTMLHandler, createVoiceAssistantHealthJSONHandler, createVoiceAssistantHealthRoutes, renderVoiceAssistantHealthHTML, summarizeVoiceAssistantHealth } from './assistantHealth';
5
+ export { createVoiceBargeInRoutes, renderVoiceBargeInHTML, summarizeVoiceBargeIn } from './bargeInRoutes';
5
6
  export { buildVoiceDiagnosticsMarkdown, createVoiceDiagnosticsRoutes, resolveVoiceDiagnosticsTraceFilter } from './diagnosticsRoutes';
6
7
  export { compareVoiceEvalBaseline, createVoiceFileEvalBaselineStore, createVoiceFileScenarioFixtureStore, createVoiceEvalRoutes, renderVoiceEvalBaselineHTML, renderVoiceEvalHTML, renderVoiceScenarioEvalHTML, renderVoiceScenarioFixtureEvalHTML, runVoiceScenarioEvals, runVoiceScenarioFixtureEvals, runVoiceSessionEvals } from './evalRoutes';
7
8
  export { createVoiceWorkflowContract, createVoiceWorkflowContractHandler, createVoiceWorkflowContractPreset, createVoiceWorkflowScenario, recordVoiceWorkflowContractTrace, validateVoiceWorkflowRouteResult } from './workflowContract';
@@ -48,6 +49,7 @@ export { resolveVoiceRuntimePreset } from './presets';
48
49
  export { resolveTurnDetectionConfig, TURN_PROFILE_DEFAULTS } from './turnProfiles';
49
50
  export { createVoiceCallReviewFromLiveTelephonyReport, createVoiceCallReviewRecorder, renderVoiceCallReviewHTML, renderVoiceCallReviewMarkdown } from './testing/review';
50
51
  export type { VoiceAppKitLink, VoiceAppKitRoutes, VoiceAppKitRoutesOptions, VoiceAppKitStatus, VoiceAppKitStatusOptions, VoiceAppKitStatusReport, VoiceAppKitSurface } from './appKit';
52
+ export type { VoiceBargeInReport, VoiceBargeInRoutesOptions } from './bargeInRoutes';
51
53
  export type { VoiceAssistant, VoiceAssistantArtifactPlan, VoiceAssistantExperiment, VoiceAssistantExperimentOptions, VoiceAssistantGuardrailInput, VoiceAssistantGuardrails, VoiceAssistantMemoryLifecycle, VoiceAssistantMemoryLifecycleInput, VoiceAssistantOptions, VoiceAssistantOutputGuardrailInput, VoiceAssistantPreset, VoiceAssistantRunsSummary, VoiceAssistantRunSummary, VoiceAssistantVariant } from './assistant';
52
54
  export type { VoiceAssistantHealthFailure, VoiceAssistantHealthHTMLHandlerOptions, VoiceAssistantHealthRoutesOptions, VoiceAssistantHealthSummary, VoiceAssistantHealthSummaryOptions } from './assistantHealth';
53
55
  export type { VoiceAssistantMemoryBinding, VoiceAssistantMemoryHandle, VoiceAssistantMemoryOptions, VoiceAssistantMemoryRecord, VoiceAssistantMemoryStore } from './assistantMemory';