1use std::sync::Arc;
12use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
13use std::time::Duration;
14
15use bytes::Bytes;
16use tokio::sync::{broadcast, mpsc};
17use tokio_util::sync::CancellationToken;
18use tracing::Instrument;
19
20use gemini_genai_rs::prelude::{SessionEvent, SessionPhase};
21use gemini_genai_rs::session::SessionWriter;
22
23use crate::state::State;
24use crate::tool::ToolDispatcher;
25
26use super::background_tool::BackgroundToolTracker;
27use super::callbacks::EventCallbacks;
28use super::computed::ComputedRegistry;
29use super::context_writer::PendingContext;
30use super::control_plane::run_control_lane;
31use super::events::LiveEvent;
32use super::extractor::TurnExtractor;
33use super::needs::NeedsFulfillment;
34use super::persistence::SessionPersistence;
35use super::phase::PhaseMachine;
36use super::session_signals::SessionSignals;
37use super::soft_turn::SoftTurnDetector;
38use super::steering::{ContextDelivery, SteeringMode};
39use super::telemetry::SessionTelemetry;
40use super::temporal::TemporalRegistry;
41use super::watcher::WatcherRegistry;
42
43#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
54pub enum Delivery {
55 #[default]
61 Lossless,
62 LossyDropNewest,
75}
76
77#[derive(Debug, Clone, Copy, PartialEq, Eq)]
85pub struct DeliveryConfig {
86 pub audio: Delivery,
88 pub media: Delivery,
90 pub text: Delivery,
92 pub transcript: Delivery,
95 pub thought: Delivery,
97 pub vad: Delivery,
99 pub phase: Delivery,
101}
102
103impl Default for DeliveryConfig {
104 fn default() -> Self {
105 Self {
106 audio: Delivery::Lossless,
107 media: Delivery::Lossless,
108 text: Delivery::Lossless,
109 transcript: Delivery::Lossless,
110 thought: Delivery::Lossless,
111 vad: Delivery::Lossless,
112 phase: Delivery::Lossless,
113 }
114 }
115}
116
117impl DeliveryConfig {
118 pub fn lossless() -> Self {
121 Self::default()
122 }
123
124 pub fn audio(mut self, d: Delivery) -> Self {
126 self.audio = d;
127 self
128 }
129
130 pub fn media(mut self, d: Delivery) -> Self {
132 self.media = d;
133 self
134 }
135
136 pub fn text(mut self, d: Delivery) -> Self {
138 self.text = d;
139 self
140 }
141
142 pub fn transcript(mut self, d: Delivery) -> Self {
144 self.transcript = d;
145 self
146 }
147
148 pub fn thought(mut self, d: Delivery) -> Self {
150 self.thought = d;
151 self
152 }
153
154 pub fn vad(mut self, d: Delivery) -> Self {
156 self.vad = d;
157 self
158 }
159
160 pub fn phase(mut self, d: Delivery) -> Self {
162 self.phase = d;
163 self
164 }
165}
166
167#[derive(Debug, Default)]
173pub(crate) struct DroppedFrames {
174 pub audio: AtomicU64,
175 pub media: AtomicU64,
176 pub text: AtomicU64,
177 pub transcript: AtomicU64,
178 pub thought: AtomicU64,
179 pub vad: AtomicU64,
180 pub phase: AtomicU64,
181}
182
183impl DroppedFrames {
184 #[cfg(test)]
189 pub(crate) fn total(&self) -> u64 {
190 self.audio.load(Ordering::Relaxed)
191 + self.media.load(Ordering::Relaxed)
192 + self.text.load(Ordering::Relaxed)
193 + self.transcript.load(Ordering::Relaxed)
194 + self.thought.load(Ordering::Relaxed)
195 + self.vad.load(Ordering::Relaxed)
196 + self.phase.load(Ordering::Relaxed)
197 }
198}
199
200async fn deliver_fast(
209 tx: &mpsc::Sender<FastEvent>,
210 ev: FastEvent,
211 policy: Delivery,
212 dropped: &AtomicU64,
213) {
214 match policy {
215 Delivery::Lossless => {
216 let _ = tx.send(ev).await;
217 }
218 Delivery::LossyDropNewest => {
219 if let Err(mpsc::error::TrySendError::Full(_)) = tx.try_send(ev) {
220 dropped.fetch_add(1, Ordering::Relaxed);
221 }
222 }
225 }
226}
227
228pub(crate) enum FastEvent {
230 Audio(Bytes),
231 Media(gemini_genai_rs::session::InlineMedia),
232 Text(String),
233 TextComplete(String),
234 InputTranscript(String),
235 OutputTranscript(String),
236 Thought(String),
237 VadStart,
238 VadEnd,
239 Phase(SessionPhase),
240 Interrupted,
242 Barrier(tokio::sync::oneshot::Sender<()>),
244}
245
246pub(crate) enum ControlEvent {
248 Barrier(tokio::sync::oneshot::Sender<()>),
250 ToolCall(Vec<gemini_genai_rs::prelude::FunctionCall>),
251 TaskCommand {
252 command: crate::tasks::TaskCommand,
253 reply: tokio::sync::oneshot::Sender<Result<crate::tasks::TaskSessionSnapshot, String>>,
254 },
255 TaskCompleted(crate::tasks::InvocationCompletion),
256 TaskServicesCompleted(crate::tasks::TaskServiceCompletion),
257 TaskTimer,
258 ShutdownTasks(tokio::sync::oneshot::Sender<()>),
259 ToolCallCancelled(Vec<String>),
260 ToolCompleted {
264 call_id: String,
266 name: String,
268 ok: bool,
270 },
271 Interrupted {
274 heard_chars: Option<usize>,
275 },
276 TurnComplete,
277 GenerationComplete,
279 GoAway(Option<std::time::Duration>),
280 Connected,
281 Disconnected(Option<String>),
282 SessionResumeUpdate(gemini_genai_rs::session::ResumeInfo),
283 Error(String),
284 InputTranscript(String),
286 OutputTranscript(String),
287}
288
289pub(crate) struct SharedState {
291 pub interrupted: AtomicBool,
293 pub barge_in: parking_lot::Mutex<CancellationToken>,
301 pub resume_handle: parking_lot::Mutex<Option<String>>,
303 pub last_instruction: parking_lot::Mutex<Option<String>>,
305 pub last_context: parking_lot::Mutex<Vec<String>>,
314 pub pending_context: Option<Arc<PendingContext>>,
316 pub delivery: DeliveryConfig,
318 pub dropped: DroppedFrames,
320 pub redactor: Option<Arc<crate::live::redaction::TranscriptRedactor>>,
323 pub playback: super::playback::PlaybackClock,
325 pub speech: parking_lot::Mutex<super::playback::TurnSpeech>,
328}
329
330pub(crate) struct ControlPlaneConfig {
337 pub base_instruction: Option<String>,
347 pub soft_turn: Option<SoftTurnDetector>,
349 pub steering_mode: SteeringMode,
351 pub context_delivery: ContextDelivery,
355 pub needs_fulfillment: Option<NeedsFulfillment>,
357 pub persistence: Option<Arc<dyn SessionPersistence>>,
359 pub session_id: Option<String>,
361 pub tool_advisory: bool,
363 pub tool_scope: Option<Arc<super::tool_scope::ToolScope>>,
367 pub task_scope: Option<Arc<super::tool_scope::ToolScope>>,
371 pub code_mode: Option<Arc<crate::code_mode::CodeMode>>,
374 pub pending_context: Option<Arc<PendingContext>>,
378 pub middleware: Arc<crate::middleware::MiddlewareChain>,
381 pub flow: Option<crate::flow::SharedFlowStack>,
389 pub decisions: Option<Arc<crate::decision::Decisions>>,
392 pub tasks: Option<crate::tasks::TaskRuntime>,
394 pub task_status: Option<Arc<parking_lot::Mutex<crate::tasks::TaskSessionSnapshot>>>,
396 pub delivery: DeliveryConfig,
399 pub redactor: Option<Arc<crate::live::redaction::TranscriptRedactor>>,
402 pub lockstep: Option<Arc<Lockstep>>,
405 pub playback: super::playback::PlaybackClock,
408}
409
410#[derive(Debug, Default)]
415pub(crate) struct Lockstep {
416 settled: AtomicU64,
417 notify: tokio::sync::Notify,
418}
419
420impl Lockstep {
421 pub(crate) fn settled(&self) -> u64 {
423 self.settled.load(Ordering::Acquire)
424 }
425
426 fn settle(&self, n: u64) {
427 self.settled.fetch_add(n, Ordering::AcqRel);
428 self.notify.notify_waiters();
429 }
430
431 pub(crate) async fn wait_for(&self, count: u64, timeout: std::time::Duration) {
433 let deadline = tokio::time::Instant::now() + timeout;
434 loop {
435 let notified = self.notify.notified();
436 if self.settled() >= count {
437 return;
438 }
439 if tokio::time::timeout_at(deadline, notified).await.is_err() {
440 return;
441 }
442 }
443 }
444}
445
446impl Default for ControlPlaneConfig {
447 fn default() -> Self {
448 Self {
449 base_instruction: None,
450 soft_turn: None,
451 steering_mode: SteeringMode::default(),
452 context_delivery: ContextDelivery::default(),
453 needs_fulfillment: None,
454 persistence: None,
455 session_id: None,
456 tool_advisory: true,
457 tool_scope: None,
458 task_scope: None,
459 code_mode: None,
460 pending_context: None,
461 middleware: Arc::new(crate::middleware::MiddlewareChain::new()),
462 flow: None,
463 decisions: None,
464 tasks: None,
465 task_status: None,
466 delivery: DeliveryConfig::default(),
467 redactor: None,
468 lockstep: None,
469 playback: super::playback::PlaybackClock::new(crate::clock::system_clock()),
470 }
471 }
472}
473
474#[allow(
475 clippy::too_many_arguments,
476 reason = "lane spawn site: parameters are the owned subsystem handles split between the fast and control lanes"
477)]
478pub(crate) fn spawn_event_processor(
479 mut event_rx: broadcast::Receiver<SessionEvent>,
480 callbacks: Arc<EventCallbacks>,
481 dispatcher: Option<Arc<ToolDispatcher>>,
482 writer: Arc<dyn SessionWriter>,
483 extractors: Vec<Arc<dyn TurnExtractor>>,
484 state: State,
485 computed: Option<ComputedRegistry>,
486 phase_machine: Option<tokio::sync::Mutex<PhaseMachine>>,
487 watchers: Option<WatcherRegistry>,
488 temporal: Option<Arc<TemporalRegistry>>,
489 background_tracker: Option<Arc<BackgroundToolTracker>>,
490 execution_modes: std::collections::HashMap<String, super::background_tool::ToolExecutionMode>,
491 control_plane: ControlPlaneConfig,
492 live_event_tx: broadcast::Sender<LiveEvent>,
493 telemetry: Option<TelemetryTap>,
494) -> (
495 tokio::task::JoinHandle<()>,
496 tokio::task::JoinHandle<()>,
497 mpsc::WeakSender<ControlEvent>,
498) {
499 let shared = Arc::new(SharedState {
500 interrupted: AtomicBool::new(false),
501 barge_in: parking_lot::Mutex::new(CancellationToken::new()),
502 resume_handle: parking_lot::Mutex::new(None),
503 last_instruction: parking_lot::Mutex::new(None),
504 last_context: parking_lot::Mutex::new(super::control_plane::opening_steering(
507 &control_plane.flow,
508 &state,
509 )),
510 pending_context: control_plane.pending_context.clone(),
511 delivery: control_plane.delivery,
512 dropped: DroppedFrames::default(),
513 redactor: control_plane.redactor.clone(),
514 playback: control_plane.playback.clone(),
515 speech: parking_lot::Mutex::default(),
516 });
517
518 let timer_cancel = CancellationToken::new();
519
520 let (fast_tx, fast_rx) = mpsc::channel::<FastEvent>(512);
531 let (ctrl_tx, ctrl_rx) = mpsc::channel::<ControlEvent>(512);
532
533 let mut telemetry = telemetry;
536 let fast_tx_clone = fast_tx.clone();
537 let ctrl_tx_clone = ctrl_tx.clone();
538 let shared_clone = shared.clone();
539 let lockstep = control_plane.lockstep.clone();
540 tokio::spawn(
541 async move {
542 let mut turn = super::turn_trace::TurnTrace::new();
544 loop {
545 match event_rx.recv().await {
546 Ok(event) => {
547 let terminal = matches!(event, SessionEvent::Disconnected(_));
555 let boundary = matches!(event, SessionEvent::TurnComplete);
556 if let Some(tap) = telemetry.as_mut() {
557 turn.span().in_scope(|| tap.observe(&event));
558 }
559 route_event(event, &fast_tx_clone, &ctrl_tx_clone, &shared_clone)
560 .instrument(turn.span())
561 .await;
562 if let Some(lockstep) = &lockstep {
563 let (fast_done, fast_rx) = tokio::sync::oneshot::channel();
564 let (ctrl_done, ctrl_rx) = tokio::sync::oneshot::channel();
565 let _ = fast_tx_clone.send(FastEvent::Barrier(fast_done)).await;
566 let _ = ctrl_tx_clone.send(ControlEvent::Barrier(ctrl_done)).await;
567 let _ = fast_rx.await;
568 let _ = ctrl_rx.await;
569 lockstep.settle(1);
570 }
571 if boundary {
572 turn.advance();
573 tracing::debug!(parent: &turn.span(), "turn started");
574 }
575 if terminal {
576 break;
577 }
578 }
579 Err(broadcast::error::RecvError::Lagged(n)) => {
580 tracing::warn!(skipped = n, "Event processor lagged, skipped events");
581 if let Some(lockstep) = &lockstep {
582 lockstep.settle(n);
583 }
584 }
585 Err(broadcast::error::RecvError::Closed) => break,
586 }
587 }
588 }
589 .in_current_span(),
590 );
591
592 let fast_callbacks = callbacks.clone();
594 let fast_shared = shared.clone();
595 let fast_event_tx = live_event_tx.clone();
596 let fast_handle = tokio::spawn(
597 async move {
598 run_fast_lane(fast_rx, fast_callbacks, fast_shared, fast_event_tx).await;
599 }
600 .in_current_span(),
601 );
602
603 let timer_temporal = temporal.clone();
605 let task_timer = control_plane.tasks.is_some();
606 let reprompt = control_plane.flow.as_ref().is_some_and(|stack| {
608 stack
609 .lock()
610 .timing_policies()
611 .values()
612 .any(|t| t.reprompt_after_ms.is_some())
613 });
614 let reprompt_inputs = reprompt.then(|| (state.clone(), writer.clone(), live_event_tx.clone()));
615 let timer_state = state.clone();
616 let timer_writer = writer.clone();
617
618 let ctrl_callbacks = callbacks;
620 let ctrl_shared = shared;
621 let ctrl_timer_cancel = timer_cancel.clone();
622 let ctrl_tx_weak = ctrl_tx.downgrade();
627 let ctrl_handle = tokio::spawn(
628 async move {
629 run_control_lane(
630 ctrl_rx,
631 ctrl_tx_weak,
632 ctrl_callbacks,
633 dispatcher,
634 writer,
635 ctrl_shared,
636 extractors,
637 state,
638 computed,
639 phase_machine,
640 watchers,
641 temporal,
642 background_tracker,
643 execution_modes,
644 control_plane,
645 live_event_tx,
646 )
647 .await;
648 ctrl_timer_cancel.cancel();
649 }
650 .in_current_span(),
651 );
652
653 if task_timer {
655 let sender = ctrl_tx.downgrade();
656 let cancel = timer_cancel.clone();
657 tokio::spawn(async move {
658 let mut interval = tokio::time::interval(Duration::from_millis(500));
659 interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
660 interval.tick().await;
661 loop {
662 tokio::select! {
663 _ = cancel.cancelled() => break,
664 _ = interval.tick() => {
665 let Some(sender) = sender.upgrade() else { break };
666 if sender.send(ControlEvent::TaskTimer).await.is_err() {
667 break;
668 }
669 }
670 }
671 }
672 });
673 }
674
675 if let Some(ref temporal_ref) = timer_temporal
676 && temporal_ref.needs_timer()
677 {
678 let t = temporal_ref.clone();
679 let cancel = timer_cancel.clone();
680 tokio::spawn(async move {
681 let mut interval = tokio::time::interval(Duration::from_millis(500));
682 loop {
683 tokio::select! {
684 _ = cancel.cancelled() => break,
685 _ = interval.tick() => {
686 for action in t.check_all(&timer_state, None, &timer_writer) {
687 tokio::spawn(action);
688 }
689 }
690 }
691 }
692 });
693 }
694
695 if let Some((state, writer, events)) = reprompt_inputs {
696 tokio::spawn(super::reprompt::run_reprompt_timer(
697 state,
698 writer,
699 events,
700 timer_cancel.clone(),
701 ));
702 }
703
704 let ctrl_tx_handle = ctrl_tx.downgrade();
709
710 (fast_handle, ctrl_handle, ctrl_tx_handle)
711}
712
713pub(crate) enum TelemetryMsg {
715 Event(SessionEvent),
717 ResponseLatency(Duration),
720}
721
722const TELEMETRY_LANE_CAPACITY: usize = 256;
725
726pub(crate) struct TelemetryTap {
735 telemetry: Arc<SessionTelemetry>,
736 signals: Arc<SessionSignals>,
737 lane: mpsc::Sender<TelemetryMsg>,
738 turn_usage: Option<gemini_genai_rs::prelude::UsageMetadata>,
740}
741
742impl TelemetryTap {
743 pub(crate) fn new(
746 telemetry: Arc<SessionTelemetry>,
747 signals: Arc<SessionSignals>,
748 ) -> (Self, mpsc::Receiver<TelemetryMsg>) {
749 let (lane, rx) = mpsc::channel(TELEMETRY_LANE_CAPACITY);
750 let tap = Self {
751 telemetry,
752 signals,
753 lane,
754 turn_usage: None,
755 };
756 (tap, rx)
757 }
758
759 pub(crate) fn observe(&mut self, event: &SessionEvent) {
762 match event {
763 SessionEvent::AudioData(data) => {
764 if let Some(latency) = self.telemetry.record_audio_out(data.len()) {
765 tracing::info!(
766 latency_ms = latency.as_millis() as u64,
767 "first model audio after the user's turn"
768 );
769 self.forward(TelemetryMsg::ResponseLatency(latency));
770 }
771 self.signals.touch_activity();
772 return;
773 }
774 SessionEvent::TextDelta(_) => {
775 if let Some(latency) = self.telemetry.record_text_out() {
776 tracing::info!(
777 latency_ms = latency.as_millis() as u64,
778 "first model text after the user's turn"
779 );
780 self.forward(TelemetryMsg::ResponseLatency(latency));
781 }
782 self.signals.touch_activity();
783 return;
784 }
785 SessionEvent::TextComplete(_) => {
786 self.signals.touch_activity();
787 return;
788 }
789 SessionEvent::VoiceActivityEnd => self.telemetry.record_vad_end(),
790 SessionEvent::Interrupted => self.telemetry.record_interruption(),
791 SessionEvent::TurnComplete => {
792 self.telemetry.record_turn_complete();
793 if let Some(usage) = self.turn_usage.take() {
794 self.telemetry.record_turn_usage(&usage);
795 }
796 }
797 SessionEvent::VoiceActivityStart => self.telemetry.mark_turn_start(),
798 SessionEvent::Usage(usage) => {
799 self.telemetry.record_usage(
800 usage.total_token_count,
801 usage.prompt_token_count,
802 usage.response_token_count,
803 usage.cached_content_token_count,
804 usage.thoughts_token_count,
805 );
806 self.turn_usage = Some(usage.clone());
807 }
808 _ => {}
809 }
810 self.forward(TelemetryMsg::Event(event.clone()));
811 }
812
813 fn forward(&self, msg: TelemetryMsg) {
814 if let Err(mpsc::error::TrySendError::Full(_)) = self.lane.try_send(msg) {
816 tracing::warn!("Telemetry lane lagged, dropped a session signal");
817 }
818 }
819}
820
821pub(crate) fn spawn_telemetry_lane(
826 mut rx: mpsc::Receiver<TelemetryMsg>,
827 signals: Arc<SessionSignals>,
828 cancel: CancellationToken,
829 on_usage: Option<super::callbacks::UsageCallback>,
830) -> tokio::task::JoinHandle<()> {
831 tokio::spawn(async move {
832 let mut debounce = tokio::time::interval(Duration::from_millis(100));
833 debounce.tick().await;
835 let mut turn = super::turn_trace::TurnTrace::new();
837 loop {
838 tokio::select! {
839 biased;
840 msg = rx.recv() => match msg {
841 Some(TelemetryMsg::ResponseLatency(latency)) => {
842 signals.record_response_latency(latency);
843 }
844 Some(TelemetryMsg::Event(event)) => {
845 turn.span().in_scope(|| {
846 if let (SessionEvent::Usage(usage), Some(cb)) = (&event, &on_usage) {
847 cb(usage);
848 }
849 signals.on_event(&event);
850 });
851 if matches!(event, SessionEvent::TurnComplete) {
852 turn.advance();
853 }
854 }
855 None => break,
857 },
858 _ = debounce.tick() => {
859 signals.flush_timing();
861 }
862 _ = cancel.cancelled() => break,
863 }
864 }
865 }.in_current_span())
866}
867
868async fn route_event(
870 event: SessionEvent,
871 fast_tx: &mpsc::Sender<FastEvent>,
872 ctrl_tx: &mpsc::Sender<ControlEvent>,
873 shared: &SharedState,
874) {
875 let delivery = &shared.delivery;
876 let dropped = &shared.dropped;
877 match event {
878 SessionEvent::AudioData(data) => {
880 shared.speech.lock().on_audio(data.len(), &shared.playback);
881 deliver_fast(
882 fast_tx,
883 FastEvent::Audio(data),
884 delivery.audio,
885 &dropped.audio,
886 )
887 .await;
888 }
889 SessionEvent::Media(media) => {
890 deliver_fast(
891 fast_tx,
892 FastEvent::Media(media),
893 delivery.media,
894 &dropped.media,
895 )
896 .await;
897 }
898 SessionEvent::TextDelta(text) => {
899 deliver_fast(fast_tx, FastEvent::Text(text), delivery.text, &dropped.text).await;
900 }
901 SessionEvent::TextComplete(text) => {
902 let text = match &shared.redactor {
903 Some(redactor) => redactor.redact(text),
904 None => text,
905 };
906 deliver_fast(
907 fast_tx,
908 FastEvent::TextComplete(text),
909 delivery.text,
910 &dropped.text,
911 )
912 .await;
913 }
914 SessionEvent::InputTranscription(text) => {
917 let text = match &shared.redactor {
918 Some(redactor) => redactor.redact(text),
919 None => text,
920 };
921 deliver_fast(
922 fast_tx,
923 FastEvent::InputTranscript(text.clone()),
924 delivery.transcript,
925 &dropped.transcript,
926 )
927 .await;
928 let _ = ctrl_tx.send(ControlEvent::InputTranscript(text)).await;
929 }
930 SessionEvent::OutputTranscription(text) => {
931 let text = match &shared.redactor {
932 Some(redactor) => redactor.redact(text),
933 None => text,
934 };
935 shared.speech.lock().on_text(&text);
936 deliver_fast(
937 fast_tx,
938 FastEvent::OutputTranscript(text.clone()),
939 delivery.transcript,
940 &dropped.transcript,
941 )
942 .await;
943 let _ = ctrl_tx.send(ControlEvent::OutputTranscript(text)).await;
944 }
945 SessionEvent::Thought(text) => {
946 deliver_fast(
947 fast_tx,
948 FastEvent::Thought(text),
949 delivery.thought,
950 &dropped.thought,
951 )
952 .await;
953 }
954 SessionEvent::VoiceActivityStart => {
955 tracing::debug!("user started speaking");
956 deliver_fast(fast_tx, FastEvent::VadStart, delivery.vad, &dropped.vad).await;
957 }
958 SessionEvent::VoiceActivityEnd => {
959 tracing::debug!("user stopped speaking");
960 deliver_fast(fast_tx, FastEvent::VadEnd, delivery.vad, &dropped.vad).await;
961 }
962 SessionEvent::PhaseChanged(phase) => {
963 deliver_fast(
964 fast_tx,
965 FastEvent::Phase(phase),
966 delivery.phase,
967 &dropped.phase,
968 )
969 .await;
970 }
971 SessionEvent::SessionResumeUpdate(info) => {
972 *shared.resume_handle.lock() = Some(info.handle.clone());
973 let _ = ctrl_tx.send(ControlEvent::SessionResumeUpdate(info)).await;
974 }
975 SessionEvent::GenerationComplete => {
976 let _ = ctrl_tx.send(ControlEvent::GenerationComplete).await;
977 }
978
979 SessionEvent::ToolCall(calls) => {
981 let _ = ctrl_tx.send(ControlEvent::ToolCall(calls)).await;
982 }
983 SessionEvent::ToolCallCancelled(ids) => {
984 let _ = ctrl_tx.send(ControlEvent::ToolCallCancelled(ids)).await;
985 }
986 SessionEvent::Interrupted => {
987 tracing::debug!("user interrupted the model");
988 shared.interrupted.store(true, Ordering::Release);
990 shared.barge_in.lock().cancel();
994 let heard_chars = shared.speech.lock().on_interrupted(&shared.playback);
997 let _ = fast_tx.send(FastEvent::Interrupted).await;
998 let _ = ctrl_tx
999 .send(ControlEvent::Interrupted { heard_chars })
1000 .await;
1001 }
1002 SessionEvent::TurnComplete => {
1003 shared.speech.lock().on_turn_complete();
1004 let _ = ctrl_tx.send(ControlEvent::TurnComplete).await;
1005 }
1006 SessionEvent::Usage(_) => {}
1008 SessionEvent::GoAway(time_left) => {
1009 let _ = ctrl_tx.send(ControlEvent::GoAway(time_left)).await;
1010 }
1011 SessionEvent::Connected => {
1012 let _ = ctrl_tx.send(ControlEvent::Connected).await;
1013 }
1014 SessionEvent::Disconnected(reason) => {
1015 let _ = ctrl_tx.send(ControlEvent::Disconnected(reason)).await;
1016 }
1017 SessionEvent::Error(err) => {
1018 let _ = ctrl_tx.send(ControlEvent::Error(err.to_string())).await;
1019 }
1020 other => {
1024 tracing::debug!(?other, "unhandled SessionEvent variant (newer wire event?)");
1025 }
1026 }
1027}
1028
1029async fn run_fast_lane(
1032 mut rx: mpsc::Receiver<FastEvent>,
1033 callbacks: Arc<EventCallbacks>,
1034 shared: Arc<SharedState>,
1035 event_tx: broadcast::Sender<LiveEvent>,
1036) {
1037 while let Some(event) = rx.recv().await {
1038 match event {
1039 FastEvent::Barrier(done) => {
1040 let _ = done.send(());
1041 }
1042 FastEvent::Audio(data) => {
1043 if !shared.interrupted.load(Ordering::Acquire) {
1045 if let Some(cb) = &callbacks.on_audio {
1046 cb(&data);
1047 }
1048 let _ = event_tx.send(LiveEvent::Audio(data));
1049 }
1050 }
1051 FastEvent::Media(media) => {
1052 if !shared.interrupted.load(Ordering::Acquire) {
1054 if let Some(cb) = &callbacks.on_media {
1055 cb(&media);
1056 }
1057 let _ = event_tx.send(LiveEvent::Media(media));
1058 }
1059 }
1060 FastEvent::Text(delta) => {
1061 if let Some(cb) = &callbacks.on_text {
1062 cb(&delta);
1063 }
1064 let _ = event_tx.send(LiveEvent::TextDelta(delta));
1065 }
1066 FastEvent::TextComplete(text) => {
1067 if let Some(cb) = &callbacks.on_text_complete {
1068 cb(&text);
1069 }
1070 let _ = event_tx.send(LiveEvent::TextComplete(text));
1071 }
1072 FastEvent::InputTranscript(text) => {
1073 if let Some(cb) = &callbacks.on_input_transcript {
1075 cb(&text, false);
1076 }
1077 let _ = event_tx.send(LiveEvent::InputTranscript {
1078 text,
1079 is_final: false,
1080 });
1081 }
1082 FastEvent::OutputTranscript(text) => {
1083 if let Some(cb) = &callbacks.on_output_transcript {
1085 cb(&text, false);
1086 }
1087 let _ = event_tx.send(LiveEvent::OutputTranscript {
1088 text,
1089 is_final: false,
1090 });
1091 }
1092 FastEvent::Thought(text) => {
1093 if let Some(cb) = &callbacks.on_thought {
1094 cb(&text);
1095 }
1096 let _ = event_tx.send(LiveEvent::Thought(text));
1097 }
1098 FastEvent::VadStart => {
1099 if let Some(cb) = &callbacks.on_vad_start {
1100 cb();
1101 }
1102 let _ = event_tx.send(LiveEvent::VadStart);
1103 }
1104 FastEvent::VadEnd => {
1105 if let Some(cb) = &callbacks.on_vad_end {
1106 cb();
1107 }
1108 let _ = event_tx.send(LiveEvent::VadEnd);
1109 }
1110 FastEvent::Phase(phase) => {
1111 if let Some(cb) = &callbacks.on_session_phase {
1112 cb(phase);
1113 }
1114 }
1116 FastEvent::Interrupted => {
1117 }
1120 }
1121 }
1122}
1123
1124#[cfg(test)]
1125mod tests {
1126 use super::*;
1127 use std::sync::atomic::AtomicUsize;
1128
1129 use crate::live::events::LiveEvent;
1130 use gemini_genai_rs::prelude::FunctionResponse;
1131
1132 fn dummy_event_tx() -> broadcast::Sender<LiveEvent> {
1133 broadcast::channel::<LiveEvent>(16).0
1134 }
1135
1136 #[test]
1137 fn delivery_config_default_is_all_lossless() {
1138 let cfg = DeliveryConfig::default();
1139 assert_eq!(cfg.audio, Delivery::Lossless);
1140 assert_eq!(cfg.text, Delivery::Lossless);
1141 assert_eq!(cfg.transcript, Delivery::Lossless);
1142 assert_eq!(cfg.thought, Delivery::Lossless);
1143 assert_eq!(cfg.vad, Delivery::Lossless);
1144 assert_eq!(cfg.phase, Delivery::Lossless);
1145 assert_eq!(Delivery::default(), Delivery::Lossless);
1147 }
1148
1149 #[tokio::test]
1150 async fn lossy_drop_newest_does_not_block_and_counts_drops() {
1151 let (tx, _rx) = mpsc::channel::<FastEvent>(1);
1154 tx.send(FastEvent::VadStart).await.unwrap(); let dropped = AtomicU64::new(0);
1156
1157 let res = tokio::time::timeout(
1160 Duration::from_millis(100),
1161 deliver_fast(&tx, FastEvent::VadEnd, Delivery::LossyDropNewest, &dropped),
1162 )
1163 .await;
1164 assert!(res.is_ok(), "deliver_fast blocked under LossyDropNewest");
1165 assert_eq!(dropped.load(Ordering::Relaxed), 1);
1166 }
1167
1168 #[tokio::test]
1169 async fn lossless_delivers_on_non_full_channel() {
1170 let (tx, mut rx) = mpsc::channel::<FastEvent>(4);
1171 let dropped = AtomicU64::new(0);
1172
1173 deliver_fast(
1174 &tx,
1175 FastEvent::Text("hello".into()),
1176 Delivery::Lossless,
1177 &dropped,
1178 )
1179 .await;
1180
1181 assert_eq!(dropped.load(Ordering::Relaxed), 0);
1183 match rx.recv().await {
1184 Some(FastEvent::Text(s)) => assert_eq!(s, "hello"),
1185 other => panic!("expected Text frame, got {:?}", other.is_some()),
1186 }
1187 }
1188
1189 #[test]
1190 fn dropped_frames_total_sums_classes() {
1191 let d = DroppedFrames::default();
1192 d.audio.fetch_add(2, Ordering::Relaxed);
1193 d.transcript.fetch_add(3, Ordering::Relaxed);
1194 assert_eq!(d.total(), 5);
1195 }
1196
1197 #[tokio::test]
1198 async fn fast_lane_routes_audio() {
1199 let count = Arc::new(AtomicUsize::new(0));
1200 let count_clone = count.clone();
1201
1202 let callbacks = EventCallbacks {
1203 on_audio: Some(Box::new(move |_| {
1204 count_clone.fetch_add(1, Ordering::SeqCst);
1205 })),
1206 ..Default::default()
1207 };
1208 let callbacks = Arc::new(callbacks);
1209
1210 let (event_tx, _) = broadcast::channel(16);
1211 let event_rx = event_tx.subscribe();
1212
1213 let writer: Arc<dyn SessionWriter> = Arc::new(crate::agent_session::NoOpSessionWriter);
1214
1215 let (fast_handle, ctrl_handle, _ctrl_tx) = spawn_event_processor(
1216 event_rx,
1217 callbacks,
1218 None,
1219 writer,
1220 vec![],
1221 State::new(),
1222 None,
1223 None,
1224 None,
1225 None,
1226 None,
1227 std::collections::HashMap::new(),
1228 ControlPlaneConfig::default(),
1229 dummy_event_tx(),
1230 None,
1231 );
1232
1233 let _ = event_tx.send(SessionEvent::AudioData(Bytes::from_static(b"audio1")));
1235 let _ = event_tx.send(SessionEvent::AudioData(Bytes::from_static(b"audio2")));
1236
1237 tokio::time::sleep(Duration::from_millis(50)).await;
1239
1240 assert_eq!(count.load(Ordering::SeqCst), 2);
1241
1242 drop(event_tx);
1244 let _ = fast_handle.await;
1245 let _ = ctrl_handle.await;
1246 }
1247
1248 #[tokio::test]
1249 async fn media_chunks_reach_on_media_not_on_audio() {
1250 let count = Arc::new(AtomicUsize::new(0));
1251 let count_clone = count.clone();
1252
1253 let callbacks = EventCallbacks {
1254 on_media: Some(Box::new(
1255 move |media: &gemini_genai_rs::session::InlineMedia| {
1256 assert!(media.is_video());
1257 count_clone.fetch_add(1, Ordering::SeqCst);
1258 },
1259 )),
1260 ..Default::default()
1261 };
1262 let callbacks = Arc::new(callbacks);
1263
1264 let (event_tx, _) = broadcast::channel(16);
1265 let event_rx = event_tx.subscribe();
1266
1267 let writer: Arc<dyn SessionWriter> = Arc::new(crate::agent_session::NoOpSessionWriter);
1268
1269 let (fast_handle, ctrl_handle, _ctrl_tx) = spawn_event_processor(
1270 event_rx,
1271 callbacks,
1272 None,
1273 writer,
1274 vec![],
1275 State::new(),
1276 None,
1277 None,
1278 None,
1279 None,
1280 None,
1281 std::collections::HashMap::new(),
1282 ControlPlaneConfig::default(),
1283 dummy_event_tx(),
1284 None,
1285 );
1286
1287 for _ in 0..2 {
1289 let _ = event_tx.send(SessionEvent::Media(gemini_genai_rs::session::InlineMedia {
1290 mime_type: "video/mp4".into(),
1291 data: Bytes::from_static(b"mp4"),
1292 }));
1293 }
1294
1295 tokio::time::sleep(Duration::from_millis(50)).await;
1297
1298 assert_eq!(count.load(Ordering::SeqCst), 2);
1299
1300 drop(event_tx);
1302 let _ = fast_handle.await;
1303 let _ = ctrl_handle.await;
1304 }
1305
1306 #[tokio::test]
1307 async fn interrupt_suppresses_audio() {
1308 let count = Arc::new(AtomicUsize::new(0));
1309 let count_clone = count.clone();
1310
1311 let callbacks = EventCallbacks {
1312 on_audio: Some(Box::new(move |_| {
1313 count_clone.fetch_add(1, Ordering::SeqCst);
1314 })),
1315 ..Default::default()
1316 };
1317 let callbacks = Arc::new(callbacks);
1318
1319 let (event_tx, _) = broadcast::channel(16);
1320 let event_rx = event_tx.subscribe();
1321
1322 let writer: Arc<dyn SessionWriter> = Arc::new(crate::agent_session::NoOpSessionWriter);
1323
1324 let (fast_handle, ctrl_handle, _ctrl_tx) = spawn_event_processor(
1325 event_rx,
1326 callbacks,
1327 None,
1328 writer,
1329 vec![],
1330 State::new(),
1331 None,
1332 None,
1333 None,
1334 None,
1335 None,
1336 std::collections::HashMap::new(),
1337 ControlPlaneConfig::default(),
1338 dummy_event_tx(),
1339 None,
1340 );
1341
1342 let _ = event_tx.send(SessionEvent::AudioData(Bytes::from_static(b"before")));
1344 tokio::time::sleep(Duration::from_millis(20)).await;
1345 let _ = event_tx.send(SessionEvent::Interrupted);
1346 tokio::time::sleep(Duration::from_millis(20)).await;
1347 let _ = event_tx.send(SessionEvent::AudioData(Bytes::from_static(b"during")));
1348 tokio::time::sleep(Duration::from_millis(50)).await;
1349
1350 assert!(count.load(Ordering::SeqCst) >= 1);
1352
1353 drop(event_tx);
1354 let _ = fast_handle.await;
1355 let _ = ctrl_handle.await;
1356 }
1357
1358 #[tokio::test]
1359 async fn control_lane_routes_turn_complete() {
1360 let called = Arc::new(AtomicBool::new(false));
1361 let called_clone = called.clone();
1362
1363 let callbacks = EventCallbacks {
1364 on_turn_complete: Some(Arc::new(move || {
1365 let c = called_clone.clone();
1366 Box::pin(async move {
1367 c.store(true, Ordering::SeqCst);
1368 })
1369 })),
1370 ..Default::default()
1371 };
1372 let callbacks = Arc::new(callbacks);
1373
1374 let (event_tx, _) = broadcast::channel(16);
1375 let event_rx = event_tx.subscribe();
1376
1377 let writer: Arc<dyn SessionWriter> = Arc::new(crate::agent_session::NoOpSessionWriter);
1378
1379 let (fast_handle, ctrl_handle, _ctrl_tx) = spawn_event_processor(
1380 event_rx,
1381 callbacks,
1382 None,
1383 writer,
1384 vec![],
1385 State::new(),
1386 None,
1387 None,
1388 None,
1389 None,
1390 None,
1391 std::collections::HashMap::new(),
1392 ControlPlaneConfig::default(),
1393 dummy_event_tx(),
1394 None,
1395 );
1396
1397 let _ = event_tx.send(SessionEvent::TurnComplete);
1398 tokio::time::sleep(Duration::from_millis(50)).await;
1399
1400 assert!(called.load(Ordering::SeqCst));
1401
1402 drop(event_tx);
1403 let _ = fast_handle.await;
1404 let _ = ctrl_handle.await;
1405 }
1406
1407 #[tokio::test]
1408 async fn transcript_accumulates_in_control_lane() {
1409 let callbacks = Arc::new(EventCallbacks::default());
1410
1411 let (event_tx, _) = broadcast::channel(16);
1412 let event_rx = event_tx.subscribe();
1413
1414 let writer: Arc<dyn SessionWriter> = Arc::new(crate::agent_session::NoOpSessionWriter);
1415
1416 let state = State::new();
1417 let (fast_handle, ctrl_handle, _ctrl_tx) = spawn_event_processor(
1418 event_rx,
1419 callbacks,
1420 None,
1421 writer,
1422 vec![],
1423 state.clone(),
1424 None,
1425 None,
1426 None,
1427 None,
1428 None,
1429 std::collections::HashMap::new(),
1430 ControlPlaneConfig::default(),
1431 dummy_event_tx(),
1432 None,
1433 );
1434
1435 let _ = event_tx.send(SessionEvent::InputTranscription("Hello ".to_string()));
1437 let _ = event_tx.send(SessionEvent::InputTranscription("world".to_string()));
1438 let _ = event_tx.send(SessionEvent::OutputTranscription("Hi there!".to_string()));
1439 tokio::time::sleep(Duration::from_millis(50)).await;
1440
1441 let _ = event_tx.send(SessionEvent::TurnComplete);
1443 tokio::time::sleep(Duration::from_millis(50)).await;
1444
1445 let tc: u32 = state.session().get("turn_count").unwrap_or(0);
1447 assert_eq!(tc, 1);
1448
1449 drop(event_tx);
1450 let _ = fast_handle.await;
1451 let _ = ctrl_handle.await;
1452 }
1453
1454 #[tokio::test]
1455 async fn an_interrupted_turn_keeps_only_what_was_heard() {
1456 let finals = Arc::new(parking_lot::Mutex::new(Vec::<String>::new()));
1457 let sink = finals.clone();
1458 let callbacks = Arc::new(EventCallbacks {
1459 on_output_transcript: Some(Box::new(move |text: &str, is_final: bool| {
1460 if is_final {
1461 sink.lock().push(text.to_string());
1462 }
1463 })),
1464 ..EventCallbacks::default()
1465 });
1466 let (event_tx, _) = broadcast::channel(64);
1467 let event_rx = event_tx.subscribe();
1468 let writer: Arc<dyn SessionWriter> = Arc::new(crate::agent_session::NoOpSessionWriter);
1469 let clock = Arc::new(crate::clock::ManualClock::new());
1470 let control_plane = ControlPlaneConfig {
1471 playback: crate::live::playback::PlaybackClock::new(clock.clone()),
1472 ..ControlPlaneConfig::default()
1473 };
1474 let playback = control_plane.playback.clone();
1475 let (fast_handle, ctrl_handle, _ctrl_tx) = spawn_event_processor(
1476 event_rx,
1477 callbacks,
1478 None,
1479 writer,
1480 vec![],
1481 State::new(),
1482 None,
1483 None,
1484 None,
1485 None,
1486 None,
1487 std::collections::HashMap::new(),
1488 control_plane,
1489 dummy_event_tx(),
1490 None,
1491 );
1492
1493 let _ = event_tx.send(SessionEvent::AudioData(Bytes::from(vec![0u8; 4 * 48_000])));
1496 let _ = event_tx.send(SessionEvent::OutputTranscription(
1497 "Your table for four is booked for Friday at eight, and I have noted the allergy."
1498 .to_string(),
1499 ));
1500 tokio::time::sleep(Duration::from_millis(50)).await;
1501 playback.queued(Duration::from_secs(4));
1502 clock.advance(Duration::from_millis(1_500));
1504 let _ = event_tx.send(SessionEvent::Interrupted);
1505 let _ = event_tx.send(SessionEvent::TurnComplete);
1506 tokio::time::sleep(Duration::from_millis(50)).await;
1507
1508 assert_eq!(*finals.lock(), ["Your table for four is"]);
1509
1510 drop(event_tx);
1511 let _ = fast_handle.await;
1512 let _ = ctrl_handle.await;
1513 }
1514
1515 #[tokio::test]
1516 async fn extractor_runs_on_turn_complete() {
1517 use crate::live::extractor::TurnExtractor;
1518 use crate::live::transcript::TranscriptTurn;
1519 use crate::llm::LlmError;
1520
1521 struct FixedExtractor;
1522
1523 #[async_trait::async_trait]
1524 impl TurnExtractor for FixedExtractor {
1525 fn name(&self) -> &str {
1526 "TestExtractor"
1527 }
1528 fn window_size(&self) -> usize {
1529 3
1530 }
1531 async fn extract(
1532 &self,
1533 _turns: &[TranscriptTurn],
1534 ) -> Result<serde_json::Value, LlmError> {
1535 Ok(serde_json::json!({"score": 0.9, "mood": "happy"}))
1536 }
1537 }
1538
1539 let callbacks = Arc::new(EventCallbacks::default());
1540
1541 let (event_tx, _) = broadcast::channel(16);
1542 let event_rx = event_tx.subscribe();
1543
1544 let writer: Arc<dyn SessionWriter> = Arc::new(crate::agent_session::NoOpSessionWriter);
1545
1546 let state = State::new();
1547
1548 let extractors: Vec<Arc<dyn TurnExtractor>> = vec![Arc::new(FixedExtractor)];
1549
1550 let (fast_handle, ctrl_handle, _ctrl_tx) = spawn_event_processor(
1551 event_rx,
1552 callbacks,
1553 None,
1554 writer,
1555 extractors,
1556 state.clone(),
1557 None,
1558 None,
1559 None,
1560 None,
1561 None,
1562 std::collections::HashMap::new(),
1563 ControlPlaneConfig::default(),
1564 dummy_event_tx(),
1565 None,
1566 );
1567
1568 let _ = event_tx.send(SessionEvent::InputTranscription("hi".to_string()));
1570 tokio::time::sleep(Duration::from_millis(20)).await;
1571 let _ = event_tx.send(SessionEvent::TurnComplete);
1572 tokio::time::sleep(Duration::from_millis(100)).await;
1573
1574 let score: Option<f64> = state.get("score");
1576 assert_eq!(score, Some(0.9));
1577 let mood: Option<String> = state.get("mood");
1578 assert_eq!(mood, Some("happy".to_string()));
1579
1580 drop(event_tx);
1581 let _ = fast_handle.await;
1582 let _ = ctrl_handle.await;
1583 }
1584
1585 #[tokio::test]
1594 async fn control_sender_records_text_on_transcript() {
1595 use crate::live::extractor::TurnExtractor;
1596 use crate::live::transcript::TranscriptTurn;
1597 use crate::llm::LlmError;
1598
1599 struct WindowRecorder(Arc<parking_lot::Mutex<Vec<String>>>);
1601
1602 #[async_trait::async_trait]
1603 impl TurnExtractor for WindowRecorder {
1604 fn name(&self) -> &str {
1605 "WindowRecorder"
1606 }
1607 fn window_size(&self) -> usize {
1608 3
1609 }
1610 async fn extract(
1611 &self,
1612 turns: &[TranscriptTurn],
1613 ) -> Result<serde_json::Value, LlmError> {
1614 self.0.lock().extend(turns.iter().map(|t| t.user.clone()));
1615 Ok(serde_json::json!({}))
1616 }
1617 }
1618
1619 let seen = Arc::new(parking_lot::Mutex::new(Vec::new()));
1620
1621 let (event_tx, _) = broadcast::channel(16);
1622 let event_rx = event_tx.subscribe();
1623 let writer: Arc<dyn SessionWriter> = Arc::new(crate::agent_session::NoOpSessionWriter);
1624 let extractors: Vec<Arc<dyn TurnExtractor>> = vec![Arc::new(WindowRecorder(seen.clone()))];
1625
1626 let (fast_handle, ctrl_handle, ctrl_tx) = spawn_event_processor(
1627 event_rx,
1628 Arc::new(EventCallbacks::default()),
1629 None,
1630 writer,
1631 extractors,
1632 State::new(),
1633 None,
1634 None,
1635 None,
1636 None,
1637 None,
1638 std::collections::HashMap::new(),
1639 ControlPlaneConfig::default(),
1640 dummy_event_tx(),
1641 None,
1642 );
1643
1644 let tx = ctrl_tx.upgrade().expect("control lane is alive");
1646 tx.send(ControlEvent::InputTranscript("I am pescatarian".into()))
1647 .await
1648 .expect("control lane accepts the typed turn");
1649 drop(tx);
1650
1651 tokio::time::sleep(Duration::from_millis(20)).await;
1652 let _ = event_tx.send(SessionEvent::TurnComplete);
1653 tokio::time::sleep(Duration::from_millis(100)).await;
1654
1655 assert_eq!(
1656 seen.lock().as_slice(),
1657 ["I am pescatarian"],
1658 "typed turn must reach the extractor's transcript window"
1659 );
1660
1661 drop(event_tx);
1662 let _ = fast_handle.await;
1663 let _ = ctrl_handle.await;
1664 }
1665
1666 #[tokio::test]
1670 async fn control_sender_does_not_keep_lane_alive() {
1671 let (event_tx, _) = broadcast::channel(16);
1672 let event_rx = event_tx.subscribe();
1673 let writer: Arc<dyn SessionWriter> = Arc::new(crate::agent_session::NoOpSessionWriter);
1674
1675 let (fast_handle, ctrl_handle, ctrl_tx) = spawn_event_processor(
1676 event_rx,
1677 Arc::new(EventCallbacks::default()),
1678 None,
1679 writer,
1680 vec![],
1681 State::new(),
1682 None,
1683 None,
1684 None,
1685 None,
1686 None,
1687 std::collections::HashMap::new(),
1688 ControlPlaneConfig::default(),
1689 dummy_event_tx(),
1690 None,
1691 );
1692
1693 drop(event_tx);
1695
1696 tokio::time::timeout(Duration::from_secs(2), ctrl_handle)
1697 .await
1698 .expect("control lane must shut down while the handle's sender is held")
1699 .expect("control lane joined cleanly");
1700
1701 assert!(
1702 ctrl_tx.upgrade().is_none(),
1703 "control channel must be closed once the router is gone"
1704 );
1705
1706 let _ = fast_handle.await;
1707 }
1708
1709 #[tokio::test]
1710 async fn telemetry_tap_counts_inline_and_forwards_only_state_events() {
1711 let telemetry = Arc::new(SessionTelemetry::new());
1712 let signals = Arc::new(SessionSignals::new(State::new()));
1713 let (mut tap, mut lane) = TelemetryTap::new(telemetry.clone(), signals);
1714
1715 tap.observe(&SessionEvent::AudioData(Bytes::from_static(b"chunk1")));
1716 tap.observe(&SessionEvent::AudioData(Bytes::from_static(b"chunk2")));
1717 tap.observe(&SessionEvent::TextDelta("hi".into()));
1718 tap.observe(&SessionEvent::VoiceActivityEnd);
1719 tokio::time::sleep(Duration::from_millis(5)).await;
1720 tap.observe(&SessionEvent::AudioData(Bytes::from_static(b"response")));
1721
1722 let snap = telemetry.snapshot();
1724 assert_eq!(snap["audio_chunks_out"], 3);
1725 assert_eq!(snap["response_count"], 1);
1726
1727 assert!(matches!(
1730 lane.try_recv(),
1731 Ok(TelemetryMsg::Event(SessionEvent::VoiceActivityEnd))
1732 ));
1733 assert!(matches!(
1734 lane.try_recv(),
1735 Ok(TelemetryMsg::ResponseLatency(_))
1736 ));
1737 assert!(lane.try_recv().is_err());
1738 }
1739
1740 #[tokio::test]
1741 async fn telemetry_lane_writes_signals_and_reports_usage() {
1742 let state = State::new();
1743 let telemetry = Arc::new(SessionTelemetry::new());
1744 let signals = Arc::new(SessionSignals::new(state.clone()));
1745 let (mut tap, lane) = TelemetryTap::new(telemetry.clone(), signals.clone());
1746 let usage_seen = Arc::new(AtomicU64::new(0));
1747 let seen = usage_seen.clone();
1748 let on_usage: super::super::callbacks::UsageCallback = Box::new(move |usage| {
1749 seen.store(
1750 usage.total_token_count.unwrap_or(0) as u64,
1751 Ordering::SeqCst,
1752 );
1753 });
1754 let cancel = CancellationToken::new();
1755 let handle = spawn_telemetry_lane(lane, signals, cancel.clone(), Some(on_usage));
1756
1757 let usage: gemini_genai_rs::prelude::UsageMetadata =
1758 serde_json::from_value(serde_json::json!({ "totalTokenCount": 42 })).expect("usage");
1759 tap.observe(&SessionEvent::VoiceActivityStart);
1760 tap.observe(&SessionEvent::Usage(usage));
1761 tap.observe(&SessionEvent::TurnComplete);
1762 tokio::time::sleep(Duration::from_millis(50)).await;
1763
1764 assert_eq!(state.session().get::<bool>("is_user_speaking"), Some(true));
1765 assert_eq!(state.session().get::<u64>("total_token_count"), Some(42));
1766 assert_eq!(usage_seen.load(Ordering::SeqCst), 42);
1767 assert_eq!(telemetry.snapshot()["total_token_count"], 42);
1768
1769 drop(tap);
1771 tokio::time::timeout(Duration::from_secs(1), handle)
1772 .await
1773 .expect("lane exits once the router is gone")
1774 .expect("lane task");
1775 }
1776
1777 #[tokio::test]
1778 async fn background_tool_sends_ack_immediately() {
1779 use crate::live::background_tool::{BackgroundToolTracker, ToolExecutionMode};
1780 use crate::tool::{SimpleTool, ToolDispatcher};
1781
1782 let tool = SimpleTool::new(
1784 "slow_search",
1785 "A slow search tool",
1786 Some(serde_json::json!({"type": "object", "properties": {"q": {"type": "string"}}})),
1787 |_args| async move {
1788 tokio::time::sleep(Duration::from_millis(200)).await;
1789 Ok(serde_json::json!({"results": ["found"]}))
1790 },
1791 );
1792
1793 let mut dispatcher = ToolDispatcher::new();
1794 dispatcher.register(tool);
1795
1796 let mut execution_modes = std::collections::HashMap::new();
1797 execution_modes.insert(
1798 "slow_search".to_string(),
1799 ToolExecutionMode::Background {
1800 formatter: None,
1801 scheduling: None,
1802 },
1803 );
1804
1805 let sent = Arc::new(parking_lot::Mutex::new(Vec::<Vec<FunctionResponse>>::new()));
1806 let sent_clone = sent.clone();
1807
1808 struct RecordingWriter {
1810 sent: Arc<parking_lot::Mutex<Vec<Vec<FunctionResponse>>>>,
1811 }
1812
1813 #[async_trait::async_trait]
1814 impl SessionWriter for RecordingWriter {
1815 async fn send_audio(
1816 &self,
1817 _data: bytes::Bytes,
1818 ) -> Result<(), gemini_genai_rs::session::SessionError> {
1819 Ok(())
1820 }
1821 async fn send_text(
1822 &self,
1823 _text: String,
1824 ) -> Result<(), gemini_genai_rs::session::SessionError> {
1825 Ok(())
1826 }
1827 async fn send_video(
1828 &self,
1829 _data: bytes::Bytes,
1830 ) -> Result<(), gemini_genai_rs::session::SessionError> {
1831 Ok(())
1832 }
1833 async fn send_tool_response(
1834 &self,
1835 responses: Vec<FunctionResponse>,
1836 ) -> Result<(), gemini_genai_rs::session::SessionError> {
1837 self.sent.lock().push(responses);
1838 Ok(())
1839 }
1840 async fn update_instruction(
1841 &self,
1842 _instruction: String,
1843 ) -> Result<(), gemini_genai_rs::session::SessionError> {
1844 Ok(())
1845 }
1846 async fn send_client_content(
1847 &self,
1848 _content: Vec<gemini_genai_rs::prelude::Content>,
1849 _turn_complete: bool,
1850 ) -> Result<(), gemini_genai_rs::session::SessionError> {
1851 Ok(())
1852 }
1853 async fn signal_activity_start(
1854 &self,
1855 ) -> Result<(), gemini_genai_rs::session::SessionError> {
1856 Ok(())
1857 }
1858 async fn signal_activity_end(
1859 &self,
1860 ) -> Result<(), gemini_genai_rs::session::SessionError> {
1861 Ok(())
1862 }
1863 async fn disconnect(&self) -> Result<(), gemini_genai_rs::session::SessionError> {
1864 Ok(())
1865 }
1866 }
1867
1868 let writer: Arc<dyn SessionWriter> = Arc::new(RecordingWriter { sent: sent_clone });
1869 let callbacks = Arc::new(EventCallbacks::default());
1870 let tracker = Arc::new(BackgroundToolTracker::new());
1871
1872 let (event_tx, _) = broadcast::channel(16);
1873 let event_rx = event_tx.subscribe();
1874
1875 let (fast_handle, ctrl_handle, _ctrl_tx) = spawn_event_processor(
1876 event_rx,
1877 callbacks,
1878 Some(Arc::new(dispatcher)),
1879 writer,
1880 vec![],
1881 State::new(),
1882 None,
1883 None,
1884 None,
1885 None,
1886 Some(tracker.clone()),
1887 execution_modes,
1888 ControlPlaneConfig::default(),
1889 dummy_event_tx(),
1890 None,
1891 );
1892
1893 let _ = event_tx.send(SessionEvent::ToolCall(vec![
1895 gemini_genai_rs::prelude::FunctionCall {
1896 name: "slow_search".to_string(),
1897 args: serde_json::json!({"q": "test"}),
1898 id: Some("fc_1".to_string()),
1899 },
1900 ]));
1901
1902 tokio::time::sleep(Duration::from_millis(50)).await;
1904
1905 {
1907 let responses = sent.lock();
1908 assert!(!responses.is_empty(), "Should have sent ack immediately");
1910 assert_eq!(responses[0][0].response["status"], "running");
1911 }
1912
1913 tokio::time::sleep(Duration::from_millis(300)).await;
1915
1916 {
1917 let responses = sent.lock();
1918 assert!(
1920 responses.len() >= 2,
1921 "Should have sent result after completion"
1922 );
1923 assert_eq!(responses[1][0].response["status"], "completed");
1924 }
1925
1926 drop(event_tx);
1927 let _ = fast_handle.await;
1928 let _ = ctrl_handle.await;
1929 }
1930
1931 #[tokio::test]
1932 async fn callback_mode_blocking_awaits_inline() {
1933 use crate::live::ExecutionMode;
1934 use std::sync::atomic::AtomicU32;
1935
1936 let order = Arc::new(AtomicU32::new(0));
1937 let order_clone = order.clone();
1938
1939 let callbacks = EventCallbacks {
1940 on_turn_complete: Some(Arc::new(move || {
1942 let o = order_clone.clone();
1943 Box::pin(async move {
1944 tokio::time::sleep(Duration::from_millis(10)).await;
1946 o.store(1, Ordering::SeqCst);
1947 })
1948 })),
1949 on_turn_complete_mode: ExecutionMode::Blocking,
1950 ..Default::default()
1951 };
1952 let callbacks = Arc::new(callbacks);
1953
1954 let (event_tx, _) = broadcast::channel(16);
1955 let event_rx = event_tx.subscribe();
1956
1957 let writer: Arc<dyn SessionWriter> = Arc::new(crate::agent_session::NoOpSessionWriter);
1958
1959 let (fast_handle, ctrl_handle, _ctrl_tx) = spawn_event_processor(
1960 event_rx,
1961 callbacks,
1962 None,
1963 writer,
1964 vec![],
1965 State::new(),
1966 None,
1967 None,
1968 None,
1969 None,
1970 None,
1971 std::collections::HashMap::new(),
1972 ControlPlaneConfig::default(),
1973 dummy_event_tx(),
1974 None,
1975 );
1976
1977 let _ = event_tx.send(SessionEvent::TurnComplete);
1978 tokio::time::sleep(Duration::from_millis(100)).await;
1979
1980 assert_eq!(order.load(Ordering::SeqCst), 1);
1982
1983 drop(event_tx);
1984 let _ = fast_handle.await;
1985 let _ = ctrl_handle.await;
1986 }
1987
1988 #[tokio::test]
1989 async fn interruption_beats_slow_inline_tool() {
1990 use crate::tool::{SimpleTool, ToolDispatcher};
1991
1992 let mut dispatcher = ToolDispatcher::new();
1994 dispatcher.register(SimpleTool::new("slow", "slow", None, |_args| async move {
1995 tokio::time::sleep(Duration::from_secs(5)).await;
1996 Ok(serde_json::json!({"done": true}))
1997 }));
1998
1999 let interrupted_at = Arc::new(parking_lot::Mutex::new(None::<std::time::Instant>));
2000 let flag = interrupted_at.clone();
2001 let callbacks = EventCallbacks {
2002 on_interrupted: Some(Arc::new(move || {
2003 let flag = flag.clone();
2004 Box::pin(async move {
2005 *flag.lock() = Some(std::time::Instant::now());
2006 })
2007 })),
2008 ..Default::default()
2009 };
2010
2011 let (event_tx, _) = broadcast::channel(16);
2012 let event_rx = event_tx.subscribe();
2013 let writer: Arc<dyn SessionWriter> = Arc::new(crate::agent_session::NoOpSessionWriter);
2014
2015 let (fast_handle, ctrl_handle, _ctrl_tx) = spawn_event_processor(
2016 event_rx,
2017 Arc::new(callbacks),
2018 Some(Arc::new(dispatcher)),
2019 writer,
2020 vec![],
2021 State::new(),
2022 None,
2023 None,
2024 None,
2025 None,
2026 None,
2027 std::collections::HashMap::new(),
2028 ControlPlaneConfig::default(),
2029 dummy_event_tx(),
2030 None,
2031 );
2032
2033 let start = std::time::Instant::now();
2035 let _ = event_tx.send(SessionEvent::ToolCall(vec![
2036 gemini_genai_rs::prelude::FunctionCall {
2037 name: "slow".to_string(),
2038 args: serde_json::json!({}),
2039 id: Some("fc_slow".to_string()),
2040 },
2041 ]));
2042 tokio::time::sleep(Duration::from_millis(50)).await;
2043 let _ = event_tx.send(SessionEvent::Interrupted);
2044
2045 let mut waited = Duration::ZERO;
2048 while interrupted_at.lock().is_none() && waited < Duration::from_secs(2) {
2049 tokio::time::sleep(Duration::from_millis(25)).await;
2050 waited += Duration::from_millis(25);
2051 }
2052 let fired = (*interrupted_at.lock()).expect("on_interrupted must fire");
2053 assert!(
2054 fired.duration_since(start) < Duration::from_secs(2),
2055 "interruption must not wait for the slow tool"
2056 );
2057
2058 drop(event_tx);
2059 let _ = fast_handle.await;
2060 let _ = ctrl_handle.await;
2061 }
2062
2063 #[tokio::test]
2064 async fn control_lane_exit_persists_final_snapshot_synchronously() {
2065 use crate::live::persistence::{MemoryPersistence, SessionPersistence};
2066
2067 let persistence = Arc::new(MemoryPersistence::new());
2068 let control_plane = ControlPlaneConfig {
2069 persistence: Some(persistence.clone()),
2070 session_id: Some("final-drain".to_string()),
2071 ..Default::default()
2072 };
2073
2074 let (event_tx, _) = broadcast::channel(16);
2075 let event_rx = event_tx.subscribe();
2076 let writer: Arc<dyn SessionWriter> = Arc::new(crate::agent_session::NoOpSessionWriter);
2077
2078 let (fast_handle, ctrl_handle, _ctrl_tx) = spawn_event_processor(
2079 event_rx,
2080 Arc::new(EventCallbacks::default()),
2081 None,
2082 writer,
2083 vec![],
2084 State::new(),
2085 None,
2086 None,
2087 None,
2088 None,
2089 None,
2090 std::collections::HashMap::new(),
2091 control_plane,
2092 dummy_event_tx(),
2093 None,
2094 );
2095
2096 let _ = event_tx.send(SessionEvent::InputTranscription("last words".to_string()));
2099 tokio::time::sleep(Duration::from_millis(50)).await;
2100
2101 drop(event_tx);
2104 let _ = fast_handle.await;
2105 let _ = ctrl_handle.await;
2106
2107 let snap = persistence
2108 .load("final-drain")
2109 .await
2110 .unwrap()
2111 .expect("control-lane exit must persist a final snapshot");
2112 assert_eq!(snap.turn_count, 0);
2113 }
2114
2115 #[tokio::test]
2116 async fn lanes_exit_after_terminal_disconnected_event() {
2117 let callbacks = Arc::new(EventCallbacks::default());
2122 let (event_tx, _) = broadcast::channel(16);
2123 let event_rx = event_tx.subscribe();
2124 let writer: Arc<dyn SessionWriter> = Arc::new(crate::agent_session::NoOpSessionWriter);
2125
2126 let (fast_handle, ctrl_handle, _ctrl_tx) = spawn_event_processor(
2127 event_rx,
2128 callbacks,
2129 None,
2130 writer,
2131 vec![],
2132 State::new(),
2133 None,
2134 None,
2135 None,
2136 None,
2137 None,
2138 std::collections::HashMap::new(),
2139 ControlPlaneConfig::default(),
2140 dummy_event_tx(),
2141 None,
2142 );
2143
2144 let _ = event_tx.send(SessionEvent::Disconnected(None));
2145
2146 let joined = tokio::time::timeout(Duration::from_secs(2), async {
2149 let _ = fast_handle.await;
2150 let _ = ctrl_handle.await;
2151 })
2152 .await;
2153 assert!(
2154 joined.is_ok(),
2155 "lanes must exit after the terminal Disconnected event"
2156 );
2157 drop(event_tx);
2158 }
2159
2160 #[tokio::test]
2161 async fn callback_mode_concurrent_spawns_task() {
2162 use crate::live::ExecutionMode;
2163
2164 let called = Arc::new(AtomicBool::new(false));
2165 let called_clone = called.clone();
2166
2167 let callbacks = EventCallbacks {
2168 on_turn_complete: Some(Arc::new(move || {
2169 let c = called_clone.clone();
2170 Box::pin(async move {
2171 tokio::time::sleep(Duration::from_millis(10)).await;
2172 c.store(true, Ordering::SeqCst);
2173 })
2174 })),
2175 on_turn_complete_mode: ExecutionMode::Concurrent,
2176 ..Default::default()
2177 };
2178 let callbacks = Arc::new(callbacks);
2179
2180 let (event_tx, _) = broadcast::channel(16);
2181 let event_rx = event_tx.subscribe();
2182
2183 let writer: Arc<dyn SessionWriter> = Arc::new(crate::agent_session::NoOpSessionWriter);
2184
2185 let (fast_handle, ctrl_handle, _ctrl_tx) = spawn_event_processor(
2186 event_rx,
2187 callbacks,
2188 None,
2189 writer,
2190 vec![],
2191 State::new(),
2192 None,
2193 None,
2194 None,
2195 None,
2196 None,
2197 std::collections::HashMap::new(),
2198 ControlPlaneConfig::default(),
2199 dummy_event_tx(),
2200 None,
2201 );
2202
2203 let _ = event_tx.send(SessionEvent::TurnComplete);
2204 tokio::time::sleep(Duration::from_millis(100)).await;
2206
2207 assert!(called.load(Ordering::SeqCst));
2209
2210 drop(event_tx);
2211 let _ = fast_handle.await;
2212 let _ = ctrl_handle.await;
2213 }
2214}