realtime-voice-agents 2.0.0 → 2.1.0

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 (36) hide show
  1. package/README.md +38 -2
  2. package/dist/{BaseRealtimeProvider-BehPNT1r.d.cts → BaseRealtimeProvider-BL75_HHh.d.cts} +53 -3
  3. package/dist/{BaseRealtimeProvider-DI4pKtOb.cjs → BaseRealtimeProvider-C2mRn1V_.cjs} +10 -0
  4. package/dist/{BaseRealtimeProvider-BQigr5mB.mjs → BaseRealtimeProvider-C9C3s8jx.mjs} +10 -0
  5. package/dist/{BaseRealtimeProvider-ClP8Wx1X.d.mts → BaseRealtimeProvider-CWJ81HIt.d.mts} +53 -3
  6. package/dist/{GeminiLiveProvider-DvkTgzjG.d.cts → GeminiLiveProvider-Cc1dkxW6.d.cts} +1 -1
  7. package/dist/{GeminiLiveProvider-x2nyx5aO.d.mts → GeminiLiveProvider-TvY_cQQZ.d.mts} +1 -1
  8. package/dist/{OpenAICompatibleProvider-Bdtl-UXH.mjs → OpenAICompatibleProvider-D-2OOVBU.mjs} +98 -11
  9. package/dist/{OpenAICompatibleProvider-NS4cKVQj.cjs → OpenAICompatibleProvider-Mp0Mefbh.cjs} +98 -11
  10. package/dist/audio.cjs +9 -6
  11. package/dist/audio.d.cts +51 -1
  12. package/dist/audio.d.mts +51 -1
  13. package/dist/audio.mjs +2 -2
  14. package/dist/gemini.cjs +2 -1
  15. package/dist/gemini.d.cts +2 -2
  16. package/dist/gemini.d.mts +2 -2
  17. package/dist/gemini.mjs +2 -1
  18. package/dist/index.cjs +299 -5
  19. package/dist/index.d.cts +170 -2
  20. package/dist/index.d.mts +170 -2
  21. package/dist/index.mjs +298 -5
  22. package/dist/{BackgroundAudioPlayer-iMcivjis.mjs → noise-CJ789zzj.mjs} +94 -1
  23. package/dist/{BackgroundAudioPlayer-jfRULWKC.cjs → noise-D9jKottW.cjs} +111 -0
  24. package/dist/openai.cjs +8 -3
  25. package/dist/openai.d.cts +23 -2
  26. package/dist/openai.d.mts +23 -2
  27. package/dist/openai.mjs +8 -3
  28. package/dist/{session-config-BVLl7-ha.mjs → session-config-CbifLlkV.mjs} +1 -1
  29. package/dist/{session-config-c8sOw1XL.cjs → session-config-CqJm2Kxz.cjs} +1 -1
  30. package/dist/testing.d.cts +1 -1
  31. package/dist/testing.d.mts +1 -1
  32. package/dist/xai.cjs +9 -4
  33. package/dist/xai.d.cts +1 -1
  34. package/dist/xai.d.mts +1 -1
  35. package/dist/xai.mjs +9 -4
  36. package/package.json +3 -3
package/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # realtime-voice-agents
2
2
 
3
3
  [![npm version](https://img.shields.io/npm/v/realtime-voice-agents)](https://www.npmjs.com/package/realtime-voice-agents)
4
- [![CI](https://github.com/yakovsinwani/twilio-realtime-agents/actions/workflows/ci.yml/badge.svg)](https://github.com/yakovsinwani/twilio-realtime-agents/actions/workflows/ci.yml)
4
+ [![CI](https://github.com/yakovsinwani/realtime-voice-agents/actions/workflows/ci.yml/badge.svg)](https://github.com/yakovsinwani/realtime-voice-agents/actions/workflows/ci.yml)
5
5
  [![node](https://img.shields.io/node/v/realtime-voice-agents)](https://www.npmjs.com/package/realtime-voice-agents)
6
6
  [![license](https://img.shields.io/npm/l/realtime-voice-agents)](LICENSE)
7
7
 
@@ -193,6 +193,41 @@ session: {
193
193
 
194
194
  Blocked attempts emit `interruption.blocked` with a cause (`guard` | `rate_limit` | `tool_running` | …). Honored ones flush Twilio, truncate the model's context to the heard milliseconds, and emit `playback.interrupted` with exactly how much the caller heard.
195
195
 
196
+ ## Noise-adaptive VAD (opt-in)
197
+
198
+ Server VAD tuned for quiet rooms misfires on noisy lines — street, car, speakerphone — as phantom barge-ins and chopped replies. `noiseAdaptiveVad` measures the line itself: a per-frame μ-law meter estimates the caller's noise floor (a low percentile over a sliding window, so speech doesn't read as noise), and when it stays high, the bridge escalates turn detection mid-call.
199
+
200
+ ```ts
201
+ session: {
202
+ vad: { type: 'server', threshold: 0.5 },
203
+ noiseAdaptiveVad: {}, // {} = defaults below
204
+ // mode: 'auto', // 'suggest' = events only, you apply
205
+ // noiseFloorDb: -45, // trigger floor, dBFS
206
+ // windowMs: 5000, sustainMs: 3000, // how much/how long analyzed audio
207
+ // cooldownMs: 15_000, maxSteps: 1, // escalation pacing (per call)
208
+ // thresholdStep: 0.1, maxThreshold: 0.9,
209
+ }
210
+ ```
211
+
212
+ How a step lands, per provider:
213
+
214
+ | Provider | Escalation |
215
+ | --- | --- |
216
+ | OpenAI | `threshold` +0.1/step, auto-applied via ack-gated `session.update`; baseline = your `vad.threshold`, else OpenAI's documented 0.5 |
217
+ | xAI | same, from xAI's documented **0.85** default, clamped to its 0.1–0.9 range |
218
+ | Gemini | `vad.suggestion` event only (`startSensitivity: 'low'` analog) — the Live API has no mid-session config updates |
219
+ | semantic VAD | `vad.suggestion` event only: `eagerness: 'low'` trades **end-of-turn latency** (waits up to ~8s) for stability, so that call is yours |
220
+
221
+ The baseline comes from the provider's ACKnowledged effective config plus its declared `capabilities.vadTuning` profile — never guessed (assuming 0.5 on xAI would *lower* its 0.85 default). Escalation is one-way per call: steps up, never back down; `maxSteps` and `cooldownMs` bound the blast radius.
222
+
223
+ Every decision emits `vad.suggestion` (recommended config + `noiseFloorDb`/`analyzedMs`/`elapsedMs` metrics); an applied-and-acknowledged step also emits `vad.adjusted`. In `mode: 'suggest'` nothing is applied automatically — accept with `session.updateVad(info.suggested)`, which persists across reconnects and rebases future escalation on top of it. `updateVad(null)` disables turn detection AND suspends adaptation (it never re-enables VAD by itself).
224
+
225
+ Fine print:
226
+
227
+ - Caller speech, agent playback (speakerphone bleed), and the pre-synthesized greeting are excluded from the floor estimate — a long monologue in a quiet room never escalates. `windowMs` counts **analyzed** idle-line audio, so warmup can span 30–60s of real conversation; the metrics exist to tune this from field data.
228
+ - Enabling the feature also serializes ALL mid-call session updates for that session: one in flight, acknowledged before the next; an ack timeout reconnects into known-good state. Disabled = the legacy fire-and-forget behavior, untouched.
229
+ - Complementary knobs: OpenAI's native `audio.input.noise_reduction` (reachable via the provider's `sessionOptions`) runs before VAD and may fix much of the problem upstream; `interruptions.rateLimit` reacts to barge-in churn after the fact, while this reacts to the audio itself. All three coexist.
230
+
196
231
  ## Pre-synthesized greeting (~1.5s to first word)
197
232
 
198
233
  The slowest part of answering is the provider handshake. Pre-record the greeting once, and the bridge burst-writes it onto the call **while the session is still connecting** — then keeps the model from greeting twice (instruction reinforcement + assistant-turn seeding + suppressed auto-greet) and gates caller audio until Twilio's mark confirms playout.
@@ -217,7 +252,7 @@ Bundled presets (all synthesized, license-free, seamless loops): `elevator-jazz`
217
252
 
218
253
  ## Events (session)
219
254
 
220
- `call.started/ended/failed` · `provider.connected/reconnecting/reconnected/closed` · `agent.speech.started/ended` (generation) · **`playback.started/finished/interrupted`** (what the caller heard, mark-confirmed) · `user.speech.started/ended` · `transcript.user/agent` · `tool.started/completed/failed` · `tool.approval.required` · `agent.handoff` · `interruption` / `interruption.blocked` · `background_audio.started/stopped` · `dtmf` · `usage.updated` · `error`.
255
+ `call.started/ended/failed` · `provider.connected/reconnecting/reconnected/closed` · `agent.speech.started/ended` (generation) · **`playback.started/finished/interrupted`** (what the caller heard, mark-confirmed) · `user.speech.started/ended` · `transcript.user/agent` · `tool.started/completed/failed` · `tool.approval.required` · `agent.handoff` · `interruption` / `interruption.blocked` · `vad.suggestion` / `vad.adjusted` (noise-adaptive VAD) · `background_audio.started/stopped` · `dtmf` · `usage.updated` · `error`.
221
256
 
222
257
  ```ts
223
258
  bridge.on('session.started', (session) => {
@@ -244,6 +279,7 @@ session: {
244
279
  reconnect: { maxAttempts: 5, initialDelayMs: 250, maxDelayMs: 8000, jitter: true },
245
280
  hangup: { markTimeoutMs: 7000 }, // goodbye watchdog
246
281
  vad: undefined, // normalized VAD, mapped per provider
282
+ noiseAdaptiveVad: undefined, // opt-in noise → VAD escalation ({} enables; see its section)
247
283
  toolResultDelivery: 'afterPlayback', // or 'immediate'
248
284
  toolBackgroundAudio: undefined, // default hold audio for tools
249
285
  handoffVoicePolicy: 'keep', // or 'reconnect' to switch voices
@@ -17,6 +17,21 @@ declare const noopLogger: Logger;
17
17
  declare function consoleLogger(context?: Record<string, unknown>): Logger;
18
18
  //#endregion
19
19
  //#region src/providers/base/capabilities.d.ts
20
+ /**
21
+ * The provider's server-VAD tuning envelope. Declared by provider factories
22
+ * (never by the generic OpenAI-compatible base — an arbitrary compatible
23
+ * service must not silently inherit OpenAI's defaults). Noise-adaptive VAD
24
+ * refuses to invent a threshold baseline: absent both this profile and an
25
+ * explicit configured threshold, the numeric ladder stays unavailable.
26
+ */
27
+ interface VadTuningProfile {
28
+ /** The provider's effective server-VAD threshold when none is configured. */
29
+ defaultServerThreshold?: number;
30
+ /** Lowest threshold the provider accepts (clamp target). */
31
+ minServerThreshold?: number;
32
+ /** Highest threshold the provider accepts (clamp target). */
33
+ maxServerThreshold?: number;
34
+ }
20
35
  /**
21
36
  * Capability flags per provider. The engine branches on these — never on a
22
37
  * provider's name — so new providers slot in without touching session logic.
@@ -43,6 +58,11 @@ interface ProviderCapabilities {
43
58
  * Optional so existing third-party providers keep compiling (absent = false).
44
59
  */
45
60
  vadInterruptControl?: boolean;
61
+ /**
62
+ * Server-VAD threshold envelope for noise-adaptive tuning. Optional; absent
63
+ * means the provider declared none (see VadTuningProfile).
64
+ */
65
+ vadTuning?: VadTuningProfile;
46
66
  }
47
67
  //#endregion
48
68
  //#region src/providers/base/events.d.ts
@@ -182,6 +202,13 @@ interface ProviderSessionInit {
182
202
  * (documented fallback: guard protects buffered audio only).
183
203
  */
184
204
  bridgeOwnsInterruptions?: boolean;
205
+ /**
206
+ * Serialize mid-session updates: one session.update in flight at a time,
207
+ * each acknowledged (or timed out) before the next is sent. Set by the
208
+ * bridge when noise-adaptive VAD is configured; absent/false keeps the
209
+ * legacy fire-and-forget update behavior exactly as before.
210
+ */
211
+ serializedSessionUpdates?: boolean;
185
212
  /** Provider-native session options, deep-merged last (escape hatch). */
186
213
  providerOptions?: Record<string, unknown>;
187
214
  }
@@ -194,6 +221,15 @@ interface SendTextOptions {
194
221
  interface SendToolResultOptions {
195
222
  triggerResponse?: boolean;
196
223
  }
224
+ interface SessionUpdateOptions {
225
+ /**
226
+ * Resolve with the provider's acknowledgement of THIS update (`true`) rather
227
+ * than at send time. Meaningful only with serialized session updates (see
228
+ * `ProviderSessionInit.serializedSessionUpdates`); providers without ack
229
+ * support resolve `false`/`void`, which callers treat as not-acknowledged.
230
+ */
231
+ awaitAck?: boolean;
232
+ }
197
233
  /**
198
234
  * The provider contract: μ-law in, μ-law out, normalized events.
199
235
  * Transcoding (when the provider speaks PCM) lives inside the provider, so
@@ -217,8 +253,22 @@ declare abstract class BaseRealtimeProvider extends TypedEmitter<ProviderEvents>
217
253
  abstract createResponse(options?: {
218
254
  instructions?: string;
219
255
  }): void;
220
- /** Mid-session config change (instructions/tools/voice) where supported. */
221
- abstract updateSession(patch: Partial<ProviderSessionInit>): Promise<void>;
256
+ /**
257
+ * Mid-session config change (instructions/tools/voice/vad) where supported.
258
+ * Return `true` to signal the provider ACKNOWLEDGED the update — the
259
+ * noise-adaptive VAD auto-commit requires it. The `boolean | void` return
260
+ * keeps existing third-party `Promise<void>` implementations compiling;
261
+ * anything other than `true` is treated as not-acknowledged.
262
+ */
263
+ abstract updateSession(patch: Partial<ProviderSessionInit>, options?: SessionUpdateOptions): Promise<boolean | void>;
264
+ /**
265
+ * The last ACKNOWLEDGED turn-detection config on the live session — what was
266
+ * actually sent and confirmed, never desired/pending state. Undefined until
267
+ * the provider reports one (third-party providers may never set it).
268
+ */
269
+ getEffectiveVad(): VadConfig | null | undefined;
270
+ /** Set by subclasses when a config carrying `vad` is acknowledged. */
271
+ protected effectiveVadValue: VadConfig | null | undefined;
222
272
  /** Interrupt in-flight generation (where the wire protocol supports it). */
223
273
  cancelResponse(): void;
224
274
  /** Trim the last assistant item to what the caller actually heard. */
@@ -236,4 +286,4 @@ interface ProviderFactoryContext {
236
286
  /** A fresh provider per call; connection config is captured in the factory closure. */
237
287
  type ProviderFactory = (context: ProviderFactoryContext) => BaseRealtimeProvider;
238
288
  //#endregion
239
- export { noopLogger as _, ProviderToolSchema as a, VadConfig as c, ProviderEvents as d, ProviderToolCall as f, consoleLogger as g, Logger as h, ProviderSessionInit as i, ProviderAudioDelta as l, ProviderCapabilities as m, ProviderFactory as n, SendTextOptions as o, ProviderUsage as p, ProviderFactoryContext as r, SendToolResultOptions as s, BaseRealtimeProvider as t, ProviderCloseInfo as u };
289
+ export { Logger as _, ProviderToolSchema as a, SessionUpdateOptions as c, ProviderCloseInfo as d, ProviderEvents as f, VadTuningProfile as g, ProviderCapabilities as h, ProviderSessionInit as i, VadConfig as l, ProviderUsage as m, ProviderFactory as n, SendTextOptions as o, ProviderToolCall as p, ProviderFactoryContext as r, SendToolResultOptions as s, BaseRealtimeProvider as t, ProviderAudioDelta as u, consoleLogger as v, noopLogger as y };
@@ -46,6 +46,16 @@ function deepMerge(base, patch) {
46
46
  * instance — the session layer owns reconnect policy and context re-injection.
47
47
  */
48
48
  var BaseRealtimeProvider = class extends require_events.TypedEmitter {
49
+ /**
50
+ * The last ACKNOWLEDGED turn-detection config on the live session — what was
51
+ * actually sent and confirmed, never desired/pending state. Undefined until
52
+ * the provider reports one (third-party providers may never set it).
53
+ */
54
+ getEffectiveVad() {
55
+ return this.effectiveVadValue;
56
+ }
57
+ /** Set by subclasses when a config carrying `vad` is acknowledged. */
58
+ effectiveVadValue = void 0;
49
59
  /** Interrupt in-flight generation (where the wire protocol supports it). */
50
60
  cancelResponse() {}
51
61
  /** Trim the last assistant item to what the caller actually heard. */
@@ -46,6 +46,16 @@ function deepMerge(base, patch) {
46
46
  * instance — the session layer owns reconnect policy and context re-injection.
47
47
  */
48
48
  var BaseRealtimeProvider = class extends TypedEmitter {
49
+ /**
50
+ * The last ACKNOWLEDGED turn-detection config on the live session — what was
51
+ * actually sent and confirmed, never desired/pending state. Undefined until
52
+ * the provider reports one (third-party providers may never set it).
53
+ */
54
+ getEffectiveVad() {
55
+ return this.effectiveVadValue;
56
+ }
57
+ /** Set by subclasses when a config carrying `vad` is acknowledged. */
58
+ effectiveVadValue = void 0;
49
59
  /** Interrupt in-flight generation (where the wire protocol supports it). */
50
60
  cancelResponse() {}
51
61
  /** Trim the last assistant item to what the caller actually heard. */
@@ -17,6 +17,21 @@ declare const noopLogger: Logger;
17
17
  declare function consoleLogger(context?: Record<string, unknown>): Logger;
18
18
  //#endregion
19
19
  //#region src/providers/base/capabilities.d.ts
20
+ /**
21
+ * The provider's server-VAD tuning envelope. Declared by provider factories
22
+ * (never by the generic OpenAI-compatible base — an arbitrary compatible
23
+ * service must not silently inherit OpenAI's defaults). Noise-adaptive VAD
24
+ * refuses to invent a threshold baseline: absent both this profile and an
25
+ * explicit configured threshold, the numeric ladder stays unavailable.
26
+ */
27
+ interface VadTuningProfile {
28
+ /** The provider's effective server-VAD threshold when none is configured. */
29
+ defaultServerThreshold?: number;
30
+ /** Lowest threshold the provider accepts (clamp target). */
31
+ minServerThreshold?: number;
32
+ /** Highest threshold the provider accepts (clamp target). */
33
+ maxServerThreshold?: number;
34
+ }
20
35
  /**
21
36
  * Capability flags per provider. The engine branches on these — never on a
22
37
  * provider's name — so new providers slot in without touching session logic.
@@ -43,6 +58,11 @@ interface ProviderCapabilities {
43
58
  * Optional so existing third-party providers keep compiling (absent = false).
44
59
  */
45
60
  vadInterruptControl?: boolean;
61
+ /**
62
+ * Server-VAD threshold envelope for noise-adaptive tuning. Optional; absent
63
+ * means the provider declared none (see VadTuningProfile).
64
+ */
65
+ vadTuning?: VadTuningProfile;
46
66
  }
47
67
  //#endregion
48
68
  //#region src/providers/base/events.d.ts
@@ -182,6 +202,13 @@ interface ProviderSessionInit {
182
202
  * (documented fallback: guard protects buffered audio only).
183
203
  */
184
204
  bridgeOwnsInterruptions?: boolean;
205
+ /**
206
+ * Serialize mid-session updates: one session.update in flight at a time,
207
+ * each acknowledged (or timed out) before the next is sent. Set by the
208
+ * bridge when noise-adaptive VAD is configured; absent/false keeps the
209
+ * legacy fire-and-forget update behavior exactly as before.
210
+ */
211
+ serializedSessionUpdates?: boolean;
185
212
  /** Provider-native session options, deep-merged last (escape hatch). */
186
213
  providerOptions?: Record<string, unknown>;
187
214
  }
@@ -194,6 +221,15 @@ interface SendTextOptions {
194
221
  interface SendToolResultOptions {
195
222
  triggerResponse?: boolean;
196
223
  }
224
+ interface SessionUpdateOptions {
225
+ /**
226
+ * Resolve with the provider's acknowledgement of THIS update (`true`) rather
227
+ * than at send time. Meaningful only with serialized session updates (see
228
+ * `ProviderSessionInit.serializedSessionUpdates`); providers without ack
229
+ * support resolve `false`/`void`, which callers treat as not-acknowledged.
230
+ */
231
+ awaitAck?: boolean;
232
+ }
197
233
  /**
198
234
  * The provider contract: μ-law in, μ-law out, normalized events.
199
235
  * Transcoding (when the provider speaks PCM) lives inside the provider, so
@@ -217,8 +253,22 @@ declare abstract class BaseRealtimeProvider extends TypedEmitter<ProviderEvents>
217
253
  abstract createResponse(options?: {
218
254
  instructions?: string;
219
255
  }): void;
220
- /** Mid-session config change (instructions/tools/voice) where supported. */
221
- abstract updateSession(patch: Partial<ProviderSessionInit>): Promise<void>;
256
+ /**
257
+ * Mid-session config change (instructions/tools/voice/vad) where supported.
258
+ * Return `true` to signal the provider ACKNOWLEDGED the update — the
259
+ * noise-adaptive VAD auto-commit requires it. The `boolean | void` return
260
+ * keeps existing third-party `Promise<void>` implementations compiling;
261
+ * anything other than `true` is treated as not-acknowledged.
262
+ */
263
+ abstract updateSession(patch: Partial<ProviderSessionInit>, options?: SessionUpdateOptions): Promise<boolean | void>;
264
+ /**
265
+ * The last ACKNOWLEDGED turn-detection config on the live session — what was
266
+ * actually sent and confirmed, never desired/pending state. Undefined until
267
+ * the provider reports one (third-party providers may never set it).
268
+ */
269
+ getEffectiveVad(): VadConfig | null | undefined;
270
+ /** Set by subclasses when a config carrying `vad` is acknowledged. */
271
+ protected effectiveVadValue: VadConfig | null | undefined;
222
272
  /** Interrupt in-flight generation (where the wire protocol supports it). */
223
273
  cancelResponse(): void;
224
274
  /** Trim the last assistant item to what the caller actually heard. */
@@ -236,4 +286,4 @@ interface ProviderFactoryContext {
236
286
  /** A fresh provider per call; connection config is captured in the factory closure. */
237
287
  type ProviderFactory = (context: ProviderFactoryContext) => BaseRealtimeProvider;
238
288
  //#endregion
239
- export { noopLogger as _, ProviderToolSchema as a, VadConfig as c, ProviderEvents as d, ProviderToolCall as f, consoleLogger as g, Logger as h, ProviderSessionInit as i, ProviderAudioDelta as l, ProviderCapabilities as m, ProviderFactory as n, SendTextOptions as o, ProviderUsage as p, ProviderFactoryContext as r, SendToolResultOptions as s, BaseRealtimeProvider as t, ProviderCloseInfo as u };
289
+ export { Logger as _, ProviderToolSchema as a, SessionUpdateOptions as c, ProviderCloseInfo as d, ProviderEvents as f, VadTuningProfile as g, ProviderCapabilities as h, ProviderSessionInit as i, VadConfig as l, ProviderUsage as m, ProviderFactory as n, SendTextOptions as o, ProviderToolCall as p, ProviderFactoryContext as r, SendToolResultOptions as s, BaseRealtimeProvider as t, ProviderAudioDelta as u, consoleLogger as v, noopLogger as y };
@@ -1,4 +1,4 @@
1
- import { c as VadConfig, h as Logger, i as ProviderSessionInit, m as ProviderCapabilities, o as SendTextOptions, s as SendToolResultOptions, t as BaseRealtimeProvider } from "./BaseRealtimeProvider-BehPNT1r.cjs";
1
+ import { _ as Logger, h as ProviderCapabilities, i as ProviderSessionInit, l as VadConfig, o as SendTextOptions, s as SendToolResultOptions, t as BaseRealtimeProvider } from "./BaseRealtimeProvider-BL75_HHh.cjs";
2
2
  //#region src/providers/gemini/GeminiLiveProvider.d.ts
3
3
  /** Structural slice of @google/genai's live session (also what fakes implement). */
4
4
  interface GeminiLiveSessionLike {
@@ -1,4 +1,4 @@
1
- import { c as VadConfig, h as Logger, i as ProviderSessionInit, m as ProviderCapabilities, o as SendTextOptions, s as SendToolResultOptions, t as BaseRealtimeProvider } from "./BaseRealtimeProvider-ClP8Wx1X.mjs";
1
+ import { _ as Logger, h as ProviderCapabilities, i as ProviderSessionInit, l as VadConfig, o as SendTextOptions, s as SendToolResultOptions, t as BaseRealtimeProvider } from "./BaseRealtimeProvider-CWJ81HIt.mjs";
2
2
  //#region src/providers/gemini/GeminiLiveProvider.d.ts
3
3
  /** Structural slice of @google/genai's live session (also what fakes implement). */
4
4
  interface GeminiLiveSessionLike {
@@ -1,5 +1,5 @@
1
- import { a as noopLogger, t as BaseRealtimeProvider } from "./BaseRealtimeProvider-BQigr5mB.mjs";
2
- import { t as buildSessionUpdate } from "./session-config-BVLl7-ha.mjs";
1
+ import { a as noopLogger, t as BaseRealtimeProvider } from "./BaseRealtimeProvider-C9C3s8jx.mjs";
2
+ import { t as buildSessionUpdate } from "./session-config-CbifLlkV.mjs";
3
3
  import WebSocket$1 from "ws";
4
4
  //#region src/providers/openai-compatible/OpenAICompatibleProvider.ts
5
5
  /**
@@ -41,6 +41,10 @@ var OpenAICompatibleProvider = class extends BaseRealtimeProvider {
41
41
  responseActive = false;
42
42
  pendingCreate = null;
43
43
  lastCreateSent = null;
44
+ /** Serialized-update pipeline (enabled via init.serializedSessionUpdates). */
45
+ serializedUpdates = false;
46
+ updateQueue = [];
47
+ updateInFlight = null;
44
48
  constructor(config, logger = noopLogger) {
45
49
  super();
46
50
  this.config = config;
@@ -62,14 +66,11 @@ var OpenAICompatibleProvider = class extends BaseRealtimeProvider {
62
66
  }
63
67
  async connect(init) {
64
68
  if (this.ws) await this.close();
65
- let vad = init.vad !== void 0 ? init.vad : this.config.defaultVad;
66
- if (init.bridgeOwnsInterruptions && this.capabilities.vadInterruptControl && vad !== null) vad = {
67
- interruptResponse: false,
68
- ...vad ?? { type: "server" }
69
- };
69
+ this.failPendingUpdates();
70
+ this.serializedUpdates = init.serializedSessionUpdates === true;
70
71
  this.sessionInit = {
71
72
  ...init,
72
- vad,
73
+ vad: this.resolveVad(init.vad, init.bridgeOwnsInterruptions),
73
74
  transcription: init.transcription !== void 0 ? init.transcription : this.config.defaultTranscription
74
75
  };
75
76
  this.intentionalClose = false;
@@ -148,6 +149,7 @@ var OpenAICompatibleProvider = class extends BaseRealtimeProvider {
148
149
  ws.on("close", (code, reasonBuf) => {
149
150
  const reason = reasonBuf?.toString();
150
151
  this.ready = false;
152
+ this.failPendingUpdates();
151
153
  if (!settled) {
152
154
  fail(/* @__PURE__ */ new Error(`${this.name} socket closed during setup (${code} ${reason ?? ""})`));
153
155
  return;
@@ -159,6 +161,7 @@ var OpenAICompatibleProvider = class extends BaseRealtimeProvider {
159
161
  });
160
162
  });
161
163
  });
164
+ this.effectiveVadValue = this.sessionInit.vad;
162
165
  }
163
166
  async close() {
164
167
  this.intentionalClose = true;
@@ -239,13 +242,87 @@ var OpenAICompatibleProvider = class extends BaseRealtimeProvider {
239
242
  ...create.instructions ? { response: { instructions: create.instructions } } : {}
240
243
  });
241
244
  }
242
- async updateSession(patch) {
245
+ async updateSession(patch, options = {}) {
243
246
  if (!this.sessionInit) throw new Error("updateSession before connect");
247
+ let resolved = patch;
248
+ if ("vad" in patch) resolved = {
249
+ ...patch,
250
+ vad: this.resolveVad(patch.vad, this.sessionInit.bridgeOwnsInterruptions)
251
+ };
244
252
  this.sessionInit = {
245
253
  ...this.sessionInit,
246
- ...patch
254
+ ...resolved
255
+ };
256
+ if (!this.serializedUpdates) {
257
+ const open = this.ws?.readyState === WebSocket$1.OPEN;
258
+ this.send(this.buildSessionPayload());
259
+ return open;
260
+ }
261
+ let resolveSent;
262
+ let resolveAck;
263
+ const sent = new Promise((resolve) => resolveSent = resolve);
264
+ const acked = new Promise((resolve) => resolveAck = resolve);
265
+ this.updateQueue.push({
266
+ payload: this.buildSessionPayload(),
267
+ touchesVad: "vad" in resolved,
268
+ resolvedVad: resolved.vad,
269
+ resolveSent,
270
+ resolveAck,
271
+ ackTimer: null
272
+ });
273
+ this.pumpUpdateQueue();
274
+ return options.awaitAck ? acked : sent;
275
+ }
276
+ /**
277
+ * Bridge-owned barge-in: the server must NOT auto-cancel the active
278
+ * response on speech onset, or a guard-blocked interruption still kills
279
+ * the sentence mid-air. An explicit vad.interruptResponse wins (spread
280
+ * order). Applied on connect AND on every vad patch.
281
+ */
282
+ resolveVad(vad, bridgeOwnsInterruptions) {
283
+ let resolved = vad !== void 0 ? vad : this.config.defaultVad;
284
+ if (bridgeOwnsInterruptions && this.capabilities.vadInterruptControl && resolved !== null) resolved = {
285
+ interruptResponse: false,
286
+ ...resolved ?? { type: "server" }
247
287
  };
248
- this.send(this.buildSessionPayload());
288
+ return resolved;
289
+ }
290
+ /** Send the next queued update once nothing is awaiting its ack. */
291
+ pumpUpdateQueue() {
292
+ if (this.updateInFlight || this.updateQueue.length === 0) return;
293
+ if (this.ws?.readyState !== WebSocket$1.OPEN) {
294
+ this.failPendingUpdates();
295
+ return;
296
+ }
297
+ const entry = this.updateQueue.shift();
298
+ this.updateInFlight = entry;
299
+ this.send(entry.payload);
300
+ entry.resolveSent(true);
301
+ const timeoutMs = this.config.sessionUpdateAckTimeoutMs ?? 3e3;
302
+ entry.ackTimer = setTimeout(() => {
303
+ entry.ackTimer = null;
304
+ if (this.updateInFlight !== entry) return;
305
+ this.updateInFlight = null;
306
+ entry.resolveAck(false);
307
+ this.logger.warn("session.update not acknowledged in time — desyncing the connection", { timeoutMs });
308
+ this.ws?.terminate();
309
+ }, timeoutMs);
310
+ entry.ackTimer.unref?.();
311
+ }
312
+ /** Settle every queued/in-flight update as not-acked (socket gone/replaced). */
313
+ failPendingUpdates() {
314
+ const inFlight = this.updateInFlight;
315
+ this.updateInFlight = null;
316
+ if (inFlight) {
317
+ if (inFlight.ackTimer) clearTimeout(inFlight.ackTimer);
318
+ inFlight.resolveAck(false);
319
+ }
320
+ const queued = this.updateQueue;
321
+ this.updateQueue = [];
322
+ for (const entry of queued) {
323
+ entry.resolveSent(false);
324
+ entry.resolveAck(false);
325
+ }
249
326
  }
250
327
  buildSessionPayload() {
251
328
  return (this.config.buildSession ?? buildSessionUpdate)(this.sessionInit, {
@@ -267,6 +344,16 @@ var OpenAICompatibleProvider = class extends BaseRealtimeProvider {
267
344
  }
268
345
  handleEvent(event) {
269
346
  switch (event.type) {
347
+ case "session.updated": {
348
+ const entry = this.updateInFlight;
349
+ if (!entry) break;
350
+ this.updateInFlight = null;
351
+ if (entry.ackTimer) clearTimeout(entry.ackTimer);
352
+ if (entry.touchesVad) this.effectiveVadValue = entry.resolvedVad;
353
+ entry.resolveAck(true);
354
+ this.pumpUpdateQueue();
355
+ break;
356
+ }
270
357
  case "response.created": {
271
358
  const responseId = event.response?.id ?? `resp_${Date.now()}`;
272
359
  this.currentResponseId = responseId;
@@ -1,6 +1,6 @@
1
1
  const require_rolldown_runtime = require("./rolldown-runtime-VH7oDXx4.cjs");
2
- const require_BaseRealtimeProvider = require("./BaseRealtimeProvider-DI4pKtOb.cjs");
3
- const require_session_config = require("./session-config-c8sOw1XL.cjs");
2
+ const require_BaseRealtimeProvider = require("./BaseRealtimeProvider-C2mRn1V_.cjs");
3
+ const require_session_config = require("./session-config-CqJm2Kxz.cjs");
4
4
  let ws = require("ws");
5
5
  ws = require_rolldown_runtime.__toESM(ws, 1);
6
6
  //#region src/providers/openai-compatible/OpenAICompatibleProvider.ts
@@ -43,6 +43,10 @@ var OpenAICompatibleProvider = class extends require_BaseRealtimeProvider.BaseRe
43
43
  responseActive = false;
44
44
  pendingCreate = null;
45
45
  lastCreateSent = null;
46
+ /** Serialized-update pipeline (enabled via init.serializedSessionUpdates). */
47
+ serializedUpdates = false;
48
+ updateQueue = [];
49
+ updateInFlight = null;
46
50
  constructor(config, logger = require_BaseRealtimeProvider.noopLogger) {
47
51
  super();
48
52
  this.config = config;
@@ -64,14 +68,11 @@ var OpenAICompatibleProvider = class extends require_BaseRealtimeProvider.BaseRe
64
68
  }
65
69
  async connect(init) {
66
70
  if (this.ws) await this.close();
67
- let vad = init.vad !== void 0 ? init.vad : this.config.defaultVad;
68
- if (init.bridgeOwnsInterruptions && this.capabilities.vadInterruptControl && vad !== null) vad = {
69
- interruptResponse: false,
70
- ...vad ?? { type: "server" }
71
- };
71
+ this.failPendingUpdates();
72
+ this.serializedUpdates = init.serializedSessionUpdates === true;
72
73
  this.sessionInit = {
73
74
  ...init,
74
- vad,
75
+ vad: this.resolveVad(init.vad, init.bridgeOwnsInterruptions),
75
76
  transcription: init.transcription !== void 0 ? init.transcription : this.config.defaultTranscription
76
77
  };
77
78
  this.intentionalClose = false;
@@ -150,6 +151,7 @@ var OpenAICompatibleProvider = class extends require_BaseRealtimeProvider.BaseRe
150
151
  ws$1.on("close", (code, reasonBuf) => {
151
152
  const reason = reasonBuf?.toString();
152
153
  this.ready = false;
154
+ this.failPendingUpdates();
153
155
  if (!settled) {
154
156
  fail(/* @__PURE__ */ new Error(`${this.name} socket closed during setup (${code} ${reason ?? ""})`));
155
157
  return;
@@ -161,6 +163,7 @@ var OpenAICompatibleProvider = class extends require_BaseRealtimeProvider.BaseRe
161
163
  });
162
164
  });
163
165
  });
166
+ this.effectiveVadValue = this.sessionInit.vad;
164
167
  }
165
168
  async close() {
166
169
  this.intentionalClose = true;
@@ -241,13 +244,87 @@ var OpenAICompatibleProvider = class extends require_BaseRealtimeProvider.BaseRe
241
244
  ...create.instructions ? { response: { instructions: create.instructions } } : {}
242
245
  });
243
246
  }
244
- async updateSession(patch) {
247
+ async updateSession(patch, options = {}) {
245
248
  if (!this.sessionInit) throw new Error("updateSession before connect");
249
+ let resolved = patch;
250
+ if ("vad" in patch) resolved = {
251
+ ...patch,
252
+ vad: this.resolveVad(patch.vad, this.sessionInit.bridgeOwnsInterruptions)
253
+ };
246
254
  this.sessionInit = {
247
255
  ...this.sessionInit,
248
- ...patch
256
+ ...resolved
257
+ };
258
+ if (!this.serializedUpdates) {
259
+ const open = this.ws?.readyState === ws.default.OPEN;
260
+ this.send(this.buildSessionPayload());
261
+ return open;
262
+ }
263
+ let resolveSent;
264
+ let resolveAck;
265
+ const sent = new Promise((resolve) => resolveSent = resolve);
266
+ const acked = new Promise((resolve) => resolveAck = resolve);
267
+ this.updateQueue.push({
268
+ payload: this.buildSessionPayload(),
269
+ touchesVad: "vad" in resolved,
270
+ resolvedVad: resolved.vad,
271
+ resolveSent,
272
+ resolveAck,
273
+ ackTimer: null
274
+ });
275
+ this.pumpUpdateQueue();
276
+ return options.awaitAck ? acked : sent;
277
+ }
278
+ /**
279
+ * Bridge-owned barge-in: the server must NOT auto-cancel the active
280
+ * response on speech onset, or a guard-blocked interruption still kills
281
+ * the sentence mid-air. An explicit vad.interruptResponse wins (spread
282
+ * order). Applied on connect AND on every vad patch.
283
+ */
284
+ resolveVad(vad, bridgeOwnsInterruptions) {
285
+ let resolved = vad !== void 0 ? vad : this.config.defaultVad;
286
+ if (bridgeOwnsInterruptions && this.capabilities.vadInterruptControl && resolved !== null) resolved = {
287
+ interruptResponse: false,
288
+ ...resolved ?? { type: "server" }
249
289
  };
250
- this.send(this.buildSessionPayload());
290
+ return resolved;
291
+ }
292
+ /** Send the next queued update once nothing is awaiting its ack. */
293
+ pumpUpdateQueue() {
294
+ if (this.updateInFlight || this.updateQueue.length === 0) return;
295
+ if (this.ws?.readyState !== ws.default.OPEN) {
296
+ this.failPendingUpdates();
297
+ return;
298
+ }
299
+ const entry = this.updateQueue.shift();
300
+ this.updateInFlight = entry;
301
+ this.send(entry.payload);
302
+ entry.resolveSent(true);
303
+ const timeoutMs = this.config.sessionUpdateAckTimeoutMs ?? 3e3;
304
+ entry.ackTimer = setTimeout(() => {
305
+ entry.ackTimer = null;
306
+ if (this.updateInFlight !== entry) return;
307
+ this.updateInFlight = null;
308
+ entry.resolveAck(false);
309
+ this.logger.warn("session.update not acknowledged in time — desyncing the connection", { timeoutMs });
310
+ this.ws?.terminate();
311
+ }, timeoutMs);
312
+ entry.ackTimer.unref?.();
313
+ }
314
+ /** Settle every queued/in-flight update as not-acked (socket gone/replaced). */
315
+ failPendingUpdates() {
316
+ const inFlight = this.updateInFlight;
317
+ this.updateInFlight = null;
318
+ if (inFlight) {
319
+ if (inFlight.ackTimer) clearTimeout(inFlight.ackTimer);
320
+ inFlight.resolveAck(false);
321
+ }
322
+ const queued = this.updateQueue;
323
+ this.updateQueue = [];
324
+ for (const entry of queued) {
325
+ entry.resolveSent(false);
326
+ entry.resolveAck(false);
327
+ }
251
328
  }
252
329
  buildSessionPayload() {
253
330
  return (this.config.buildSession ?? require_session_config.buildSessionUpdate)(this.sessionInit, {
@@ -269,6 +346,16 @@ var OpenAICompatibleProvider = class extends require_BaseRealtimeProvider.BaseRe
269
346
  }
270
347
  handleEvent(event) {
271
348
  switch (event.type) {
349
+ case "session.updated": {
350
+ const entry = this.updateInFlight;
351
+ if (!entry) break;
352
+ this.updateInFlight = null;
353
+ if (entry.ackTimer) clearTimeout(entry.ackTimer);
354
+ if (entry.touchesVad) this.effectiveVadValue = entry.resolvedVad;
355
+ entry.resolveAck(true);
356
+ this.pumpUpdateQueue();
357
+ break;
358
+ }
272
359
  case "response.created": {
273
360
  const responseId = event.response?.id ?? `resp_${Date.now()}`;
274
361
  this.currentResponseId = responseId;
package/dist/audio.cjs CHANGED
@@ -1,21 +1,24 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  const require_mulaw = require("./mulaw-DLUObjdP.cjs");
3
- const require_BackgroundAudioPlayer = require("./BackgroundAudioPlayer-jfRULWKC.cjs");
3
+ const require_noise = require("./noise-D9jKottW.cjs");
4
4
  const require_transcode = require("./transcode-C9aJG7_W.cjs");
5
- exports.BackgroundAudioPlayer = require_BackgroundAudioPlayer.BackgroundAudioPlayer;
5
+ exports.BackgroundAudioPlayer = require_noise.BackgroundAudioPlayer;
6
6
  exports.InboundTranscoder = require_transcode.InboundTranscoder;
7
7
  exports.MULAW_FRAME_BYTES_20MS = require_mulaw.MULAW_FRAME_BYTES_20MS;
8
8
  exports.MULAW_SAMPLE_RATE = require_mulaw.MULAW_SAMPLE_RATE;
9
9
  exports.MULAW_SILENCE_BYTE = require_mulaw.MULAW_SILENCE_BYTE;
10
+ exports.NOISE_DBFS_FLOOR = require_noise.NOISE_DBFS_FLOOR;
11
+ exports.NoiseFloorEstimator = require_noise.NoiseFloorEstimator;
10
12
  exports.OutboundTranscoder = require_transcode.OutboundTranscoder;
11
13
  exports.Resampler = require_transcode.Resampler;
12
14
  exports.base64ByteLength = require_mulaw.base64ByteLength;
13
- exports.fadeMulaw = require_BackgroundAudioPlayer.fadeMulaw;
14
- exports.isPresetName = require_BackgroundAudioPlayer.isPresetName;
15
- exports.loadBackgroundAudio = require_BackgroundAudioPlayer.loadBackgroundAudio;
15
+ exports.fadeMulaw = require_noise.fadeMulaw;
16
+ exports.isPresetName = require_noise.isPresetName;
17
+ exports.loadBackgroundAudio = require_noise.loadBackgroundAudio;
16
18
  exports.mulawBytesToMs = require_mulaw.mulawBytesToMs;
17
19
  exports.mulawDecodeSample = require_mulaw.mulawDecodeSample;
18
20
  exports.mulawEncodeSample = require_mulaw.mulawEncodeSample;
21
+ exports.mulawFrameDbfs = require_noise.mulawFrameDbfs;
19
22
  exports.mulawToPcm16 = require_mulaw.mulawToPcm16;
20
23
  exports.pcm16ToMulaw = require_mulaw.pcm16ToMulaw;
21
- exports.scaleMulaw = require_BackgroundAudioPlayer.scaleMulaw;
24
+ exports.scaleMulaw = require_noise.scaleMulaw;