realtime-voice-agents 2.0.1 → 2.2.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 (38) hide show
  1. package/README.md +58 -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 +16 -6
  15. package/dist/gemini.d.cts +8 -3
  16. package/dist/gemini.d.mts +8 -3
  17. package/dist/gemini.mjs +16 -6
  18. package/dist/index.cjs +356 -16
  19. package/dist/index.d.cts +211 -2
  20. package/dist/index.d.mts +211 -2
  21. package/dist/index.mjs +355 -16
  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 +22 -8
  25. package/dist/openai.d.cts +31 -3
  26. package/dist/openai.d.mts +31 -3
  27. package/dist/openai.mjs +22 -8
  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.cjs +29 -2
  31. package/dist/testing.d.cts +31 -1
  32. package/dist/testing.d.mts +31 -1
  33. package/dist/testing.mjs +29 -2
  34. package/dist/xai.cjs +23 -9
  35. package/dist/xai.d.cts +7 -2
  36. package/dist/xai.d.mts +7 -2
  37. package/dist/xai.mjs +23 -9
  38. package/package.json +1 -1
package/dist/index.cjs CHANGED
@@ -1,12 +1,12 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  const require_rolldown_runtime = require("./rolldown-runtime-VH7oDXx4.cjs");
3
3
  const require_events = require("./events-BxDTIKKq.cjs");
4
- const require_BaseRealtimeProvider = require("./BaseRealtimeProvider-DI4pKtOb.cjs");
4
+ const require_BaseRealtimeProvider = require("./BaseRealtimeProvider-C2mRn1V_.cjs");
5
5
  const require_InMemorySessionStore = require("./InMemorySessionStore-B0_i-DOU.cjs");
6
6
  const require_rest = require("./rest-BvUKut_k.cjs");
7
7
  const require_mulaw = require("./mulaw-DLUObjdP.cjs");
8
- const require_BackgroundAudioPlayer = require("./BackgroundAudioPlayer-jfRULWKC.cjs");
9
- const require_session_config = require("./session-config-c8sOw1XL.cjs");
8
+ const require_noise = require("./noise-D9jKottW.cjs");
9
+ const require_session_config = require("./session-config-CqJm2Kxz.cjs");
10
10
  let node_fs = require("node:fs");
11
11
  let zod = require("zod");
12
12
  zod = require_rolldown_runtime.__toESM(zod, 1);
@@ -499,6 +499,190 @@ var PlaybackTracker = class {
499
499
  }
500
500
  };
501
501
  //#endregion
502
+ //#region src/vad/NoiseAdaptiveVadController.ts
503
+ /**
504
+ * Noise-adaptive VAD policy: watches inbound caller audio through a
505
+ * NoiseFloorEstimator and, on sustained pervasive noise, proposes a bounded
506
+ * turn-detection escalation (server VAD threshold up stepwise; semantic VAD
507
+ * and sensitivity-only providers get suggestion events instead).
508
+ *
509
+ * Modeled on InterruptionController: pure decision objects, zero timers —
510
+ * every clock is measured in milliseconds of inbound audio, so behavior is
511
+ * deterministic in tests and matches wall clock on a live call.
512
+ *
513
+ * Lifecycle: onInboundFrame() PROPOSES without mutating ladder state — the
514
+ * identical proposal recurs until the caller settles it:
515
+ * - commitApplied(): the provider ACKNOWLEDGED the change (auto mode). Only
516
+ * this and rebase() move the effective config — an emitted-but-unapplied
517
+ * suggestion is never treated as reality.
518
+ * - markSuggested(): the suggestion was surfaced (suggest mode, or a rung
519
+ * that is never auto-applied). Latched until rebase() so it fires once.
520
+ * - defer(): the apply failed or timed out — retry after cooldownMs.
521
+ * - rebase(): the app changed VAD via session.updateVad() — new effective
522
+ * base, fresh ladder. rebase(null) disables the controller entirely (turn
523
+ * detection off — noise adaptation must never re-enable it on its own).
524
+ */
525
+ function round2(value) {
526
+ return Math.round(value * 100) / 100;
527
+ }
528
+ var NoiseAdaptiveVadController = class {
529
+ windowMs;
530
+ noiseFloorDb;
531
+ sustainMs;
532
+ cooldownMs;
533
+ maxSteps;
534
+ thresholdStep;
535
+ maxThreshold;
536
+ profile;
537
+ estimator;
538
+ /** Current EFFECTIVE config — moved only by commitApplied()/rebase(). */
539
+ effective;
540
+ disabled;
541
+ stepsTaken = 0;
542
+ suggestionLatched = false;
543
+ sustainedMs = 0;
544
+ /** Infinity = no step yet, so the first proposal is never cooldown-blocked. */
545
+ msSinceLastStep = Number.POSITIVE_INFINITY;
546
+ elapsedMs = 0;
547
+ analyzedMs = 0;
548
+ /** Memoized next rung; `undefined` = dirty, `null` = ladder exhausted. */
549
+ cachedNext = void 0;
550
+ constructor(options, effectiveVad, profile) {
551
+ this.windowMs = options.windowMs ?? 5e3;
552
+ this.noiseFloorDb = options.noiseFloorDb ?? -45;
553
+ this.sustainMs = options.sustainMs ?? 3e3;
554
+ this.cooldownMs = options.cooldownMs ?? 15e3;
555
+ this.maxSteps = options.maxSteps ?? 1;
556
+ this.thresholdStep = options.thresholdStep ?? .1;
557
+ this.maxThreshold = options.maxThreshold ?? .9;
558
+ this.profile = profile;
559
+ this.estimator = new require_noise.NoiseFloorEstimator({ windowMs: this.windowMs });
560
+ this.disabled = effectiveVad === null;
561
+ this.effective = effectiveVad ?? { type: "server" };
562
+ }
563
+ /** No escalation room at all (disabled, or the ladder has no next rung). */
564
+ get exhausted() {
565
+ return this.disabled || this.nextRung() === null;
566
+ }
567
+ /**
568
+ * Feed one inbound Twilio frame (base64 μ-law). `excluded` frames (user
569
+ * speech, agent playback, pregreeting) advance the cooldown clock but are
570
+ * kept out of the floor estimate — they freeze, not reset, the sustain
571
+ * counter. Pure w.r.t. ladder state: the same proposal recurs until
572
+ * commitApplied/markSuggested/defer/rebase.
573
+ */
574
+ onInboundFrame(base64Mulaw, excluded) {
575
+ if (this.disabled || this.suggestionLatched) return null;
576
+ const next = this.nextRung();
577
+ if (!next) return null;
578
+ const bytes = Buffer.from(base64Mulaw, "base64");
579
+ if (bytes.length === 0) return null;
580
+ const frameMs = bytes.length / 8;
581
+ this.elapsedMs += frameMs;
582
+ this.msSinceLastStep += frameMs;
583
+ if (excluded) return null;
584
+ this.estimator.addAudio(bytes);
585
+ this.analyzedMs += frameMs;
586
+ if (!this.estimator.isWarm) return null;
587
+ const floor = this.estimator.floorDb();
588
+ if (floor >= this.noiseFloorDb) this.sustainedMs += frameMs;
589
+ else this.sustainedMs = 0;
590
+ if (this.sustainedMs < this.sustainMs) return null;
591
+ if (this.msSinceLastStep < this.cooldownMs) return null;
592
+ return {
593
+ previous: { ...this.effective },
594
+ suggested: next.suggested,
595
+ step: this.stepsTaken + 1,
596
+ maxSteps: this.maxSteps,
597
+ autoApplicable: next.autoApplicable,
598
+ metrics: {
599
+ noiseFloorDb: floor,
600
+ windowMs: this.windowMs,
601
+ sustainedMs: this.sustainedMs,
602
+ analyzedMs: this.analyzedMs,
603
+ elapsedMs: this.elapsedMs
604
+ }
605
+ };
606
+ }
607
+ /** The proposal was applied AND acknowledged: it is the new effective truth. */
608
+ commitApplied(adjustment) {
609
+ this.effective = { ...adjustment.suggested };
610
+ this.stepsTaken++;
611
+ this.sustainedMs = 0;
612
+ this.msSinceLastStep = 0;
613
+ this.cachedNext = void 0;
614
+ }
615
+ /**
616
+ * The suggestion was emitted without being applied: latch so it fires once
617
+ * per effective state. The effective config is deliberately NOT moved — an
618
+ * unapplied suggestion is not reality.
619
+ */
620
+ markSuggested(_adjustment) {
621
+ this.suggestionLatched = true;
622
+ }
623
+ /** The apply failed/timed out/was superseded: retry after cooldownMs. */
624
+ defer() {
625
+ this.msSinceLastStep = 0;
626
+ }
627
+ /**
628
+ * App-driven updateVad(): the declared config is the new effective base —
629
+ * a fresh ladder starts from it (never below it). `null` disables the
630
+ * controller; a later non-null rebase re-enables it. A full cooldown
631
+ * applies before the next adaptive step.
632
+ */
633
+ rebase(vad) {
634
+ this.disabled = vad === null;
635
+ this.effective = vad ? { ...vad } : { type: "server" };
636
+ this.stepsTaken = 0;
637
+ this.suggestionLatched = false;
638
+ this.sustainedMs = 0;
639
+ this.msSinceLastStep = 0;
640
+ this.cachedNext = void 0;
641
+ }
642
+ nextRung() {
643
+ if (this.cachedNext === void 0) this.cachedNext = this.computeNextRung();
644
+ return this.cachedNext;
645
+ }
646
+ computeNextRung() {
647
+ const effective = this.effective;
648
+ if (effective.type === "semantic") {
649
+ if (effective.eagerness === "low") return null;
650
+ return {
651
+ suggested: {
652
+ ...effective,
653
+ eagerness: "low"
654
+ },
655
+ autoApplicable: false
656
+ };
657
+ }
658
+ if (this.stepsTaken < this.maxSteps) {
659
+ const baseline = effective.threshold ?? this.profile?.defaultServerThreshold;
660
+ if (baseline !== void 0) {
661
+ const ceiling = Math.min(this.maxThreshold, this.profile?.maxServerThreshold ?? 1);
662
+ const bottom = this.profile?.minServerThreshold ?? 0;
663
+ const target = round2(Math.min(Math.max(baseline + this.thresholdStep, bottom), ceiling));
664
+ if (target > baseline + 1e-9) return {
665
+ suggested: {
666
+ ...effective,
667
+ type: "server",
668
+ threshold: target,
669
+ startSensitivity: "low"
670
+ },
671
+ autoApplicable: true
672
+ };
673
+ }
674
+ }
675
+ if (effective.startSensitivity !== "low") return {
676
+ suggested: {
677
+ ...effective,
678
+ startSensitivity: "low"
679
+ },
680
+ autoApplicable: false
681
+ };
682
+ return null;
683
+ }
684
+ };
685
+ //#endregion
502
686
  //#region src/providers/base/reconnect.ts
503
687
  const DEFAULT_RECONNECT_POLICY = {
504
688
  maxAttempts: 5,
@@ -698,6 +882,12 @@ function createTransferCallTool(options = {}) {
698
882
  * socket is released exactly once.
699
883
  */
700
884
  const MAX_BUFFERED_INBOUND_FRAMES = 250;
885
+ /**
886
+ * Cap on the noise meter's caller-speech exclusion: Gemini never emits a
887
+ * speech-stopped event, and phantom speech starts on a noisy line must not
888
+ * latch the meter off forever.
889
+ */
890
+ const MAX_SPEECH_EXCLUSION_MS = 15e3;
701
891
  var CallSession = class extends require_events.TypedEmitter {
702
892
  callSid;
703
893
  streamSid;
@@ -717,6 +907,8 @@ var CallSession = class extends require_events.TypedEmitter {
717
907
  runningTools = /* @__PURE__ */ new Map();
718
908
  timers = /* @__PURE__ */ new Set();
719
909
  provider = null;
910
+ /** Primary + fallbacks, in try-order. Only walked while connecting. */
911
+ providerChain;
720
912
  activeAgentValue;
721
913
  generating = false;
722
914
  currentResponseId = null;
@@ -754,12 +946,28 @@ var CallSession = class extends require_events.TypedEmitter {
754
946
  handoffInProgress = false;
755
947
  /** Pre-synthesized greeting playout state. */
756
948
  pregreeting = null;
949
+ /** Noise-adaptive VAD (opt-in); built after connect from the provider's ACKed config. */
950
+ noiseVad = null;
951
+ /**
952
+ * DESIRED turn-detection override — drives reconnect/handoff rebuilds via
953
+ * buildProviderInit. Distinct from the provider's ACKed effective state:
954
+ * manual updateVad() promotes it immediately (user intent survives network
955
+ * failures); adaptive applies promote it only after the provider's ack.
956
+ */
957
+ vadOverride = void 0;
958
+ /** VAD mutations in flight (manual AND adaptive); proposals drop while > 0. */
959
+ vadOpsInFlight = 0;
960
+ /** Bumped by every updateVad(); a stale revision means an adaptive apply was superseded. */
961
+ vadRevision = 0;
962
+ userSpeechActive = false;
963
+ userSpeechStartedAtMs = 0;
757
964
  constructor(deps) {
758
965
  super();
759
966
  this.deps = deps;
760
967
  this.callSid = deps.start.start.callSid;
761
968
  this.streamSid = deps.start.start.streamSid ?? deps.start.streamSid;
762
969
  this.log = require_BaseRealtimeProvider.childLogger(deps.logger, { callSid: this.callSid });
970
+ this.providerChain = [deps.providerFactory, ...deps.fallbacks ?? []];
763
971
  this.activeAgentValue = deps.agent;
764
972
  this.context = new SessionContext(deps.options.context);
765
973
  this.interruptions = new InterruptionController(deps.options.interruptions);
@@ -773,7 +981,7 @@ var CallSession = class extends require_events.TypedEmitter {
773
981
  this.answered = deps.answeredEarly === true || this.callInfo.direction === "inbound";
774
982
  this.agents = collectAgentGraph(deps.agent);
775
983
  this.middlewares = deps.middlewares ?? [];
776
- this.bgAudio = new require_BackgroundAudioPlayer.BackgroundAudioPlayer({
984
+ this.bgAudio = new require_noise.BackgroundAudioPlayer({
777
985
  sendMedia: (payload) => {
778
986
  if (this.stateValue === "active" && this.deps.transport.isOpen) this.deps.transport.sendMedia(payload);
779
987
  },
@@ -798,17 +1006,7 @@ var CallSession = class extends require_events.TypedEmitter {
798
1006
  /** Connect the provider and activate the call. Called by the bridge. */
799
1007
  async begin() {
800
1008
  this.playPreGreeting();
801
- try {
802
- this.provider = this.deps.providerFactory({
803
- logger: this.log,
804
- callSid: this.callSid
805
- });
806
- this.wireProvider(this.provider);
807
- await this.provider.connect(this.buildProviderInit());
808
- } catch (error) {
809
- this.fail(error instanceof Error ? error : new Error(String(error)), "provider-failed");
810
- return;
811
- }
1009
+ if (!await this.connectInitialProvider()) return;
812
1010
  if (this.stateValue !== "connecting") return;
813
1011
  this.stateValue = "active";
814
1012
  this.emit("provider.connected");
@@ -820,6 +1018,10 @@ var CallSession = class extends require_events.TypedEmitter {
820
1018
  to: this.callInfo.to,
821
1019
  customParameters: this.callInfo.customParameters
822
1020
  });
1021
+ if (this.deps.options.noiseAdaptiveVad && this.deps.options.vad !== null) {
1022
+ this.noiseVad = new NoiseAdaptiveVadController(this.deps.options.noiseAdaptiveVad, this.provider.getEffectiveVad(), this.provider.capabilities.vadTuning);
1023
+ if (this.noiseVad.exhausted) this.log.warn("noiseAdaptiveVad enabled but the effective VAD config leaves no escalation room");
1024
+ }
823
1025
  if (this.pregreeting) this.provider.sendText(this.pregreeting.text, {
824
1026
  role: "assistant",
825
1027
  triggerResponse: false
@@ -830,6 +1032,55 @@ var CallSession = class extends require_events.TypedEmitter {
830
1032
  this.saveSnapshot();
831
1033
  this.maybeGreet();
832
1034
  }
1035
+ /**
1036
+ * Walk primary + fallbacks until one connects. A provider that fails to
1037
+ * come up is detached BEFORE the chain advances, so a half-open socket's
1038
+ * late events can never reach the session once the next provider owns the
1039
+ * call. Connect-time only by design: once a provider has answered, the
1040
+ * call stays with it. Returns false after failing the call (chain
1041
+ * exhausted) or when the call was torn down while connecting.
1042
+ */
1043
+ async connectInitialProvider() {
1044
+ let lastFrom = "provider";
1045
+ let lastError = null;
1046
+ for (const factory of this.providerChain) {
1047
+ let provider;
1048
+ try {
1049
+ provider = factory({
1050
+ logger: this.log,
1051
+ callSid: this.callSid
1052
+ });
1053
+ } catch (error) {
1054
+ lastError = toError(error);
1055
+ this.log.warn("provider factory threw — trying the next fallback", { error: String(lastError) });
1056
+ continue;
1057
+ }
1058
+ if (lastError) this.emit("provider.fallback", {
1059
+ from: lastFrom,
1060
+ to: provider.name,
1061
+ error: lastError
1062
+ });
1063
+ this.provider = provider;
1064
+ this.wireProvider(provider);
1065
+ try {
1066
+ await provider.connect(this.buildProviderInit());
1067
+ return true;
1068
+ } catch (error) {
1069
+ provider.removeAllListeners();
1070
+ provider.close().catch(() => {});
1071
+ this.provider = null;
1072
+ if (this.stateValue !== "connecting") return false;
1073
+ lastFrom = provider.name;
1074
+ lastError = toError(error);
1075
+ this.log.warn("provider failed to connect", {
1076
+ provider: provider.name,
1077
+ error: String(lastError)
1078
+ });
1079
+ }
1080
+ }
1081
+ this.fail(lastError ?? /* @__PURE__ */ new Error("provider connect failed"), "provider-failed");
1082
+ return false;
1083
+ }
833
1084
  /** The outbound leg was answered (host's Twilio status callback). */
834
1085
  notifyAnswered() {
835
1086
  if (this.answered) return;
@@ -842,6 +1093,36 @@ var CallSession = class extends require_events.TypedEmitter {
842
1093
  async updateInstructions(instructions) {
843
1094
  await this.provider?.updateSession({ instructions });
844
1095
  }
1096
+ /**
1097
+ * Replace the session's turn-detection config mid-call. The value is the
1098
+ * new DESIRED state: it survives reconnects and handoffs, and the
1099
+ * noise-adaptive ladder (when enabled) rebases onto it — future escalation
1100
+ * is relative to this config, never below it. `null` disables turn
1101
+ * detection AND suspends noise adaptation (adaptation never re-enables VAD
1102
+ * on its own); a later non-null call re-enables both.
1103
+ *
1104
+ * Returns true when the live session applied it — provider-acknowledged
1105
+ * when `noiseAdaptiveVad` is configured (serialized updates), best-effort
1106
+ * "sent" otherwise. False when it could not be applied live (no provider,
1107
+ * call ended, no sessionUpdate capability, or the ack timed out — the
1108
+ * resulting reconnect then applies the stored config).
1109
+ */
1110
+ async updateVad(vad) {
1111
+ this.vadRevision++;
1112
+ this.vadOpsInFlight++;
1113
+ try {
1114
+ this.vadOverride = vad;
1115
+ if (!this.provider || this.stateValue === "ended") return false;
1116
+ if (!this.provider.capabilities.sessionUpdate) {
1117
+ this.log.warn("updateVad: provider has no mid-session update — stored for the next reconnect");
1118
+ return false;
1119
+ }
1120
+ return await this.provider.updateSession({ vad }, { awaitAck: true }) === true;
1121
+ } finally {
1122
+ this.noiseVad?.rebase(vad);
1123
+ this.vadOpsInFlight--;
1124
+ }
1125
+ }
845
1126
  /** Manual barge-in: stop the agent mid-sentence. */
846
1127
  interrupt() {
847
1128
  if (!this.generating && !this.tracker.isPlaybackActive()) return;
@@ -935,8 +1216,9 @@ var CallSession = class extends require_events.TypedEmitter {
935
1216
  return {
936
1217
  instructions: this.activeAgentValue.resolveInstructions(this.context) + (this.pregreeting ? `\n\nYou already opened the call by saying: "${this.pregreeting.text}". Do not greet again — continue the conversation from there.` : ""),
937
1218
  voice: this.activeAgentValue.voice,
938
- vad: this.deps.options.vad,
1219
+ vad: this.vadOverride !== void 0 ? this.vadOverride : this.deps.options.vad,
939
1220
  bridgeOwnsInterruptions: true,
1221
+ serializedSessionUpdates: this.deps.options.noiseAdaptiveVad !== void 0,
940
1222
  tools: [...this.toolset.values()].map((tool) => {
941
1223
  const decorated = decorateTool(tool, this.middlewares);
942
1224
  return {
@@ -1027,12 +1309,15 @@ var CallSession = class extends require_events.TypedEmitter {
1027
1309
  this.emit("transcript.user", entry);
1028
1310
  });
1029
1311
  provider.on("userSpeechStarted", () => {
1312
+ this.userSpeechActive = true;
1313
+ this.userSpeechStartedAtMs = Date.now();
1030
1314
  this.clearIdleTimer();
1031
1315
  this.nudgeCount = 0;
1032
1316
  this.emit("user.speech.started");
1033
1317
  this.handleBargeIn();
1034
1318
  });
1035
1319
  provider.on("userSpeechStopped", () => {
1320
+ this.userSpeechActive = false;
1036
1321
  if (this.blockedUserTurn === "speaking") this.blockedUserTurn = "committed";
1037
1322
  this.emit("user.speech.ended");
1038
1323
  });
@@ -1054,6 +1339,11 @@ var CallSession = class extends require_events.TypedEmitter {
1054
1339
  }
1055
1340
  handleInboundMedia(payload) {
1056
1341
  if (this.stateValue === "ended" || this.stateValue === "ending") return;
1342
+ if (this.noiseVad) {
1343
+ const excluded = this.isUserSpeechExcluded() || this.tracker.isPlaybackActive() || this.pregreeting !== null && !this.pregreeting.played;
1344
+ const proposal = this.noiseVad.onInboundFrame(payload, excluded);
1345
+ if (proposal) this.handleVadProposal(proposal);
1346
+ }
1057
1347
  if (this.pregreeting && !this.pregreeting.played) return;
1058
1348
  if (this.deps.options.deafness.ignoreUserAudioUntilFirstTurnDone && !this.firstTurnDone) return;
1059
1349
  if (this.deps.options.deafness.muteDuringToolExecution && this.runningTools.size > 0) return;
@@ -1070,6 +1360,51 @@ var CallSession = class extends require_events.TypedEmitter {
1070
1360
  for (const payload of this.inboundBuffer) this.provider.sendAudio(payload);
1071
1361
  this.inboundBuffer = [];
1072
1362
  }
1363
+ /** Caller speech is excluded from the noise floor, capped so a missing
1364
+ * speech-stopped event (Gemini) can't latch the meter off forever. */
1365
+ isUserSpeechExcluded() {
1366
+ return this.userSpeechActive && Date.now() - this.userSpeechStartedAtMs < MAX_SPEECH_EXCLUSION_MS;
1367
+ }
1368
+ handleVadProposal(proposal) {
1369
+ if (this.vadOpsInFlight > 0) return;
1370
+ const willAutoApply = (this.deps.options.noiseAdaptiveVad?.mode ?? "auto") === "auto" && proposal.autoApplicable && this.provider?.capabilities.sessionUpdate === true;
1371
+ this.emit("vad.suggestion", {
1372
+ ...proposal,
1373
+ willAutoApply
1374
+ });
1375
+ if (!willAutoApply) {
1376
+ this.noiseVad?.markSuggested(proposal);
1377
+ return;
1378
+ }
1379
+ this.vadOpsInFlight++;
1380
+ const revisionAtPropose = this.vadRevision;
1381
+ this.applyAdaptiveVadUpdate(proposal, revisionAtPropose).catch((error) => {
1382
+ this.noiseVad?.defer();
1383
+ this.log.warn("noise-adaptive VAD apply failed", { error: String(error) });
1384
+ }).finally(() => {
1385
+ this.vadOpsInFlight--;
1386
+ });
1387
+ }
1388
+ /**
1389
+ * Adaptive applies promote NOTHING until the provider ACKs: a timed-out
1390
+ * update must not leak into vadOverride, or the desync-reconnect would
1391
+ * apply a config the controller never committed (dual truth). Contrast
1392
+ * with updateVad(), where the user's declared intent persists regardless.
1393
+ */
1394
+ async applyAdaptiveVadUpdate(proposal, revisionAtPropose) {
1395
+ const provider = this.provider;
1396
+ if (!provider || this.stateValue === "ended") {
1397
+ this.noiseVad?.defer();
1398
+ return;
1399
+ }
1400
+ const acked = await provider.updateSession({ vad: proposal.suggested }, { awaitAck: true });
1401
+ if (revisionAtPropose !== this.vadRevision) return;
1402
+ if (acked === true) {
1403
+ this.vadOverride = proposal.suggested;
1404
+ this.noiseVad?.commitApplied(proposal);
1405
+ this.emit("vad.adjusted", proposal);
1406
+ } else this.noiseVad?.defer();
1407
+ }
1073
1408
  handleMarkEcho(name) {
1074
1409
  if (name === PREGREETING_MARK) {
1075
1410
  if (this.pregreeting && !this.pregreeting.played) {
@@ -1866,6 +2201,9 @@ var CallSession = class extends require_events.TypedEmitter {
1866
2201
  const PREGREETING_MARK = "pre:greeting";
1867
2202
  /** 400ms per frame — matches production burst-write implementations. */
1868
2203
  const PREGREETING_CHUNK_BYTES = 3200;
2204
+ function toError(value) {
2205
+ return value instanceof Error ? value : new Error(String(value));
2206
+ }
1869
2207
  function safeJsonStringify(value) {
1870
2208
  try {
1871
2209
  return JSON.stringify(value) ?? "null";
@@ -2000,6 +2338,7 @@ var TwilioRealtimeBridge = class extends require_events.TypedEmitter {
2000
2338
  transport,
2001
2339
  start,
2002
2340
  providerFactory: this.config.provider,
2341
+ fallbacks: this.config.fallbacks,
2003
2342
  agent,
2004
2343
  options,
2005
2344
  store: this.store,
@@ -2121,6 +2460,7 @@ exports.DEFAULT_RECONNECT_POLICY = DEFAULT_RECONNECT_POLICY;
2121
2460
  exports.DEFAULT_SESSION_OPTIONS = DEFAULT_SESSION_OPTIONS;
2122
2461
  exports.InMemorySessionStore = require_InMemorySessionStore.InMemorySessionStore;
2123
2462
  exports.InterruptionController = InterruptionController;
2463
+ exports.NoiseAdaptiveVadController = NoiseAdaptiveVadController;
2124
2464
  exports.PlaybackTracker = PlaybackTracker;
2125
2465
  exports.SessionContext = SessionContext;
2126
2466
  exports.TwilioRealtimeBridge = TwilioRealtimeBridge;