1use std::collections::HashMap;
4use std::sync::Arc;
5
6use tokio_util::sync::CancellationToken;
7
8use gemini_genai_rs::prelude::{ConnectBuilder, SessionConfig, SessionEvent, SessionPhase};
9use gemini_genai_rs::session::{SessionError, SessionHandle, SessionWriter};
10use gemini_genai_rs::transport::{Transport, TransportConfig};
11
12use crate::error::AgentError;
13use crate::state::State;
14use crate::tool::ToolDispatcher;
15
16use super::background_tool::{BackgroundToolTracker, ToolExecutionMode};
17use super::callbacks::EventCallbacks;
18use super::computed::ComputedRegistry;
19use super::context_writer::{DeferredWriter, PendingContext};
20use super::extractor::TurnExtractor;
21use super::handle::LiveHandle;
22use super::needs::{NeedsFulfillment, RepairConfig};
23use super::persistence::SessionPersistence;
24use super::phase::PhaseMachine;
25use super::processor::{
26 ControlPlaneConfig, TelemetryTap, spawn_event_processor, spawn_telemetry_lane,
27};
28use super::session_signals::SessionSignals;
29use super::soft_turn::SoftTurnDetector;
30use super::steering::{ContextDelivery, SteeringMode};
31use super::telemetry::SessionTelemetry;
32use super::temporal::TemporalRegistry;
33use super::tool_scope::{self, ToolScope};
34use super::watcher::WatcherRegistry;
35
36pub struct LiveSessionBuilder {
46 config: SessionConfig,
47 callbacks: EventCallbacks,
48 dispatcher: Option<Arc<ToolDispatcher>>,
49 extractors: Vec<Arc<dyn TurnExtractor>>,
50 computed: Option<ComputedRegistry>,
51 phase_machine: Option<PhaseMachine>,
52 watchers: Option<WatcherRegistry>,
53 temporal: Option<TemporalRegistry>,
54 greeting: Option<String>,
55 state: Option<State>,
56 execution_modes: HashMap<String, ToolExecutionMode>,
57 soft_turn_timeout: Option<std::time::Duration>,
59 steering_mode: SteeringMode,
60 steering_set: bool,
62 context_delivery: ContextDelivery,
63 delivery: super::processor::DeliveryConfig,
64 repair_config: Option<RepairConfig>,
65 persistence: Option<Arc<dyn SessionPersistence>>,
66 session_id: Option<String>,
67 tool_advisory: bool,
68 telemetry_interval: Option<std::time::Duration>,
69 middleware: Vec<Arc<dyn crate::middleware::Middleware>>,
70 flow: Option<crate::flow::FlowStack>,
71 decisions: Option<Arc<crate::decision::Decisions>>,
72 code_mode: Option<Arc<crate::code_mode::CodeMode>>,
73 tasks: Option<crate::tasks::TaskRuntime>,
74 redactor: Option<Arc<super::redaction::TranscriptRedactor>>,
75 clock: Option<crate::clock::SharedClock>,
76 lockstep: Option<Arc<super::processor::Lockstep>>,
77 event_capacity: usize,
78}
79
80impl LiveSessionBuilder {
81 pub fn new(config: SessionConfig) -> Self {
83 Self {
84 config,
85 callbacks: EventCallbacks::default(),
86 dispatcher: None,
87 extractors: Vec::new(),
88 computed: None,
89 phase_machine: None,
90 watchers: None,
91 temporal: None,
92 greeting: None,
93 state: None,
94 execution_modes: HashMap::new(),
95 soft_turn_timeout: None,
96 steering_mode: SteeringMode::default(),
97 steering_set: false,
98 context_delivery: ContextDelivery::default(),
99 delivery: super::processor::DeliveryConfig::default(),
100 repair_config: None,
101 persistence: None,
102 session_id: None,
103 tool_advisory: true,
104 telemetry_interval: None,
105 middleware: Vec::new(),
106 flow: None,
107 decisions: None,
108 code_mode: None,
109 tasks: None,
110 redactor: None,
111 clock: None,
112 lockstep: None,
113 event_capacity: super::events::DEFAULT_EVENT_CAPACITY,
114 }
115 }
116
117 pub fn tasks(mut self, tasks: crate::tasks::TaskRuntime) -> Self {
122 self.tasks = Some(tasks);
123 self
124 }
125
126 pub(crate) fn lockstep(mut self, lockstep: Arc<super::processor::Lockstep>) -> Self {
129 self.lockstep = Some(lockstep);
130 self
131 }
132
133 pub fn redaction(mut self, redactor: super::redaction::TranscriptRedactor) -> Self {
140 self.redactor = redactor.is_active().then(|| Arc::new(redactor));
141 self
142 }
143
144 pub fn clock(mut self, clock: crate::clock::SharedClock) -> Self {
153 self.clock = Some(clock);
154 self
155 }
156
157 pub fn middleware(mut self, layer: Arc<dyn crate::middleware::Middleware>) -> Self {
163 self.middleware.push(layer);
164 self
165 }
166
167 pub fn flow_monitor(self, monitor: crate::flow::FlowMonitor) -> Self {
171 self.flow_stack(monitor.into_stack())
172 }
173
174 pub fn flow_stack(mut self, stack: crate::flow::FlowStack) -> Self {
178 self.flow = Some(stack);
179 self
180 }
181
182 pub fn decisions(mut self, decisions: Arc<crate::decision::Decisions>) -> Self {
192 self.decisions = Some(decisions);
193 self
194 }
195
196 pub fn state(mut self, state: State) -> Self {
202 self.state = Some(state);
203 self
204 }
205
206 pub fn greeting(mut self, prompt: impl Into<String>) -> Self {
208 self.greeting = Some(prompt.into());
209 self
210 }
211
212 pub fn code_mode(mut self, code: crate::code_mode::CodeMode) -> Self {
216 self.code_mode = Some(Arc::new(code));
217 self
218 }
219
220 pub fn dispatcher(mut self, dispatcher: ToolDispatcher) -> Self {
222 for tool in dispatcher.to_tool_declarations() {
224 self.config = self.config.add_tool(tool);
225 }
226 self.dispatcher = Some(Arc::new(dispatcher));
227 self
228 }
229
230 pub fn callbacks(mut self, callbacks: EventCallbacks) -> Self {
232 self.callbacks = callbacks;
233 self
234 }
235
236 pub fn extractor(mut self, extractor: Arc<dyn TurnExtractor>) -> Self {
238 self.extractors.push(extractor);
239 self
240 }
241
242 pub fn computed(mut self, registry: ComputedRegistry) -> Self {
244 self.computed = Some(registry);
245 self
246 }
247
248 pub fn phase_machine(mut self, machine: PhaseMachine) -> Self {
250 self.phase_machine = Some(machine);
251 self
252 }
253
254 pub fn watchers(mut self, registry: WatcherRegistry) -> Self {
256 self.watchers = Some(registry);
257 self
258 }
259
260 pub fn temporal(mut self, registry: TemporalRegistry) -> Self {
262 self.temporal = Some(registry);
263 self
264 }
265
266 pub fn tool_execution_mode(
271 mut self,
272 tool_name: impl Into<String>,
273 mode: ToolExecutionMode,
274 ) -> Self {
275 self.execution_modes.insert(tool_name.into(), mode);
276 self
277 }
278
279 pub fn soft_turn_timeout(mut self, timeout: std::time::Duration) -> Self {
286 self.soft_turn_timeout = Some(timeout);
287 self
288 }
289
290 pub fn steering_mode(mut self, mode: SteeringMode) -> Self {
296 self.steering_mode = mode;
297 self.steering_set = true;
298 self
299 }
300
301 pub fn context_delivery(mut self, mode: ContextDelivery) -> Self {
306 self.context_delivery = mode;
307 self
308 }
309
310 pub fn delivery(mut self, delivery: super::processor::DeliveryConfig) -> Self {
317 self.delivery = delivery;
318 self
319 }
320
321 pub fn repair(mut self, config: RepairConfig) -> Self {
326 self.repair_config = Some(config);
327 self
328 }
329
330 pub fn persistence(mut self, backend: Arc<dyn SessionPersistence>) -> Self {
332 self.persistence = Some(backend);
333 self
334 }
335
336 pub fn session_id(mut self, id: impl Into<String>) -> Self {
338 self.session_id = Some(id.into());
339 self
340 }
341
342 pub fn tool_advisory(mut self, enabled: bool) -> Self {
344 self.tool_advisory = enabled;
345 self
346 }
347
348 pub fn telemetry_interval(mut self, interval: std::time::Duration) -> Self {
353 self.telemetry_interval = Some(interval);
354 self
355 }
356
357 pub fn event_capacity(mut self, capacity: usize) -> Self {
369 self.event_capacity = capacity.max(1);
370 self
371 }
372
373 pub async fn connect(self) -> Result<LiveHandle, AgentError> {
386 let mut plan = self.into_plan()?;
387
388 let config = plan.config.take().expect("plan always carries a config");
391 let session = ConnectBuilder::new(config)
392 .connect()
393 .await
394 .map_err(AgentError::Session)?;
395 finish_connect(plan, session).await
396 }
397
398 pub async fn connect_with_transport<T: Transport>(
408 self,
409 transport: T,
410 ) -> Result<LiveHandle, AgentError> {
411 let mut plan = self.into_plan()?;
412 let config = plan.config.take().expect("plan always carries a config");
413 let session = ConnectBuilder::new(config)
414 .transport_config(TransportConfig {
415 max_reconnect_attempts: 0,
416 ..TransportConfig::default()
417 })
418 .transport(transport)
419 .connect()
420 .await
421 .map_err(AgentError::Session)?;
422 finish_connect(plan, session).await
423 }
424}
425
426async fn finish_connect(
429 plan: SessionPlan,
430 session: SessionHandle,
431) -> Result<LiveHandle, AgentError> {
432 let mut events = session.subscribe();
442 tokio::select! {
443 () = session.wait_for_phase(SessionPhase::Active) => {}
444 failure = wait_for_connect_failure(&mut events) => {
445 return Err(AgentError::Session(SessionError::SetupFailed(
446 gemini_genai_rs::session::SetupError::ServerRejected {
447 code: None,
448 message: failure,
449 },
450 )));
451 }
452 }
453 let runtime = build_runtime(plan, session);
454 spawn_lanes(runtime).await
455}
456
457impl LiveSessionBuilder {
458 pub(crate) fn into_plan(self) -> Result<SessionPlan, AgentError> {
465 if self.tasks.is_some()
467 && (self.flow.is_some()
468 || self.phase_machine.is_some()
469 || self.dispatcher.is_some()
470 || !self.config.tools.is_empty()
471 || !self.extractors.is_empty()
472 || self.computed.is_some()
473 || self.watchers.is_some()
474 || self
475 .temporal
476 .as_ref()
477 .is_some_and(|temporal| !temporal.is_empty())
478 || self.callbacks.on_tool_call.is_some()
479 || !self.middleware.is_empty())
480 {
481 return Err(AgentError::Config(
482 "task sessions own their tools, state and governance; configure these inside skills".into(),
483 ));
484 }
485 if let Some(ref pm) = self.phase_machine {
486 pm.validate()?;
487 }
488 if let Some(code) = &self.code_mode {
489 if self.tasks.is_some() {
490 return Err(AgentError::Config(
491 "a task session cannot have a session-wide code tool".into(),
492 ));
493 }
494 let known: Vec<&str> = self
495 .dispatcher
496 .as_ref()
497 .map(|d| d.names().collect())
498 .unwrap_or_default();
499 let missing: Vec<&str> = code
500 .function_list()
501 .iter()
502 .map(|f| f.name.as_str())
503 .filter(|name| !known.contains(name))
504 .collect();
505 if !missing.is_empty() {
506 return Err(AgentError::Config(format!(
507 "the code tool exposes {} but the session has no tool of that name",
508 missing.join(", ")
509 )));
510 }
511 if known.contains(&code.tool()) {
512 return Err(AgentError::Config(format!(
513 "the code tool's name '{}' is also a tool of the session",
514 code.tool()
515 )));
516 }
517 }
518 if let Some(ref computed) = self.computed {
519 computed.validate()?;
520 }
521
522 let mut config = self.config;
524 if let Some(tasks) = &self.tasks {
525 config.tools.extend(super::task_tools::declarations(tasks));
526 let catalog = super::task_tools::catalog_instruction(tasks);
527 let instruction = config.system_instruction.get_or_insert_with(|| {
528 gemini_genai_rs::prelude::Content {
529 role: None,
530 parts: Vec::new(),
531 }
532 });
533 instruction
534 .parts
535 .push(gemini_genai_rs::prelude::Part::Text { text: catalog });
536 }
537 for (tool_name, mode) in &self.execution_modes {
538 if matches!(
539 mode,
540 super::background_tool::ToolExecutionMode::Background { .. }
541 ) {
542 for tool in &mut config.tools {
543 if let Some(ref mut decls) = tool.function_declarations {
544 for decl in decls {
545 if decl.name == *tool_name {
546 decl.behavior = Some(
547 gemini_genai_rs::prelude::FunctionCallingBehavior::NonBlocking,
548 );
549 }
550 }
551 }
552 }
553 }
554 }
555
556 let base_instruction = config.system_instruction.as_ref().map(|content| {
561 content
562 .parts
563 .iter()
564 .filter_map(|part| match part {
565 gemini_genai_rs::prelude::Part::Text { text } => Some(text.as_str()),
566 _ => None,
567 })
568 .collect::<Vec<_>>()
569 .join("\n")
570 });
571
572 let steering_mode = match self.steering_mode {
575 SteeringMode::ContextUpdate if !config.supports_context_update() => {
576 tracing::warn!(
577 model = %config.resolved_model(),
578 "SteeringMode::ContextUpdate needs a model that accepts contextUpdate; steering as Hybrid"
579 );
580 SteeringMode::Hybrid
581 }
582 mode => mode,
583 };
584
585 let tool_scope = if steering_mode == SteeringMode::ContextUpdate {
590 let state = self.state.clone().unwrap_or_default();
591 let phase = self
592 .phase_machine
593 .as_ref()
594 .and_then(PhaseMachine::current_phase);
595 let admitted = tool_scope::offered_names(
596 &config.tools,
597 phase.and_then(|p| p.tools_enabled.as_deref()),
598 self.flow.as_ref(),
599 &state,
600 );
601 let scope = ToolScope::new(
602 std::mem::take(&mut config.tools),
603 base_instruction.clone(),
604 &admitted,
605 )
606 .with_code(self.code_mode.clone());
607 config.tools = scope.tools_for(&admitted);
608 if let Some(phase) = phase {
609 let instruction = scope.instruction_for(
610 &phase
611 .instruction
612 .resolve_with_modifiers(&state, &phase.modifiers),
613 );
614 if !instruction.is_empty() {
615 config = config.system_instruction(instruction);
616 }
617 }
618 Some(Arc::new(scope))
619 } else {
620 if let Some(code) = &self.code_mode {
623 config.tools = tool_scope::with_code(std::mem::take(&mut config.tools), code);
624 }
625 None
626 };
627
628 let task_scope = match &self.tasks {
634 Some(tasks)
635 if config.supports_context_update()
636 && (!self.steering_set
637 || self.steering_mode == SteeringMode::ContextUpdate) =>
638 {
639 let declared = super::task_tools::entry_names(tasks);
640 let scope = ToolScope::new(
641 std::mem::take(&mut config.tools),
642 base_instruction.clone(),
643 &declared,
644 );
645 config.tools = scope.tools_for(&declared);
646 Some(Arc::new(scope))
647 }
648 _ => None,
649 };
650
651 Ok(SessionPlan {
652 config: Some(config),
653 base_instruction,
654 tool_scope,
655 task_scope,
656 code_mode: self.code_mode,
657 callbacks: self.callbacks,
658 dispatcher: self.dispatcher,
659 extractors: self.extractors,
660 computed: self.computed,
661 phase_machine: self.phase_machine,
662 watchers: self.watchers,
663 temporal: self.temporal,
664 greeting: self.greeting,
665 state: self.state,
666 execution_modes: self.execution_modes,
667 soft_turn_timeout: self.soft_turn_timeout,
668 steering_mode,
669 context_delivery: self.context_delivery,
670 delivery: self.delivery,
671 repair_config: self.repair_config,
672 persistence: self.persistence,
673 session_id: self.session_id,
674 tool_advisory: self.tool_advisory,
675 telemetry_interval: self.telemetry_interval,
676 middleware: self.middleware,
677 decisions: {
678 warn_unanswerable(self.flow.as_ref(), self.decisions.as_deref());
679 self.decisions
680 },
681 flow: self.flow,
682 tasks: self.tasks,
683 redactor: self.redactor,
684 clock: self.clock,
685 lockstep: self.lockstep,
686 event_capacity: self.event_capacity,
687 })
688 }
689}
690
691fn warn_unanswerable(
694 flow: Option<&crate::flow::FlowStack>,
695 decisions: Option<&crate::decision::Decisions>,
696) {
697 let Some(flow) = flow else {
698 return;
699 };
700 let mut named = flow.main().flow().decisions();
701 for ov in flow.overlays() {
702 named.extend(ov.trigger().decisions());
703 named.extend(ov.flow().decisions());
704 }
705 let missing: Vec<&String> = named
706 .iter()
707 .filter(|q| decisions.is_none_or(|d| d.get(q).is_none()))
708 .collect();
709 if !missing.is_empty() {
710 tracing::warn!(
711 ?missing,
712 "the flow's guards name decisions no decision service declares; those guards never hold"
713 );
714 }
715}
716
717pub(crate) struct SessionPlan {
725 config: Option<SessionConfig>,
728 base_instruction: Option<String>,
731 tool_scope: Option<Arc<ToolScope>>,
733 task_scope: Option<Arc<ToolScope>>,
736 code_mode: Option<Arc<crate::code_mode::CodeMode>>,
738 callbacks: EventCallbacks,
739 dispatcher: Option<Arc<ToolDispatcher>>,
740 extractors: Vec<Arc<dyn TurnExtractor>>,
741 computed: Option<ComputedRegistry>,
742 phase_machine: Option<PhaseMachine>,
743 watchers: Option<WatcherRegistry>,
744 temporal: Option<TemporalRegistry>,
745 greeting: Option<String>,
746 state: Option<State>,
747 execution_modes: HashMap<String, ToolExecutionMode>,
748 soft_turn_timeout: Option<std::time::Duration>,
749 steering_mode: SteeringMode,
750 context_delivery: ContextDelivery,
751 delivery: super::processor::DeliveryConfig,
752 repair_config: Option<RepairConfig>,
753 persistence: Option<Arc<dyn SessionPersistence>>,
754 session_id: Option<String>,
755 tool_advisory: bool,
756 telemetry_interval: Option<std::time::Duration>,
757 middleware: Vec<Arc<dyn crate::middleware::Middleware>>,
758 flow: Option<crate::flow::FlowStack>,
759 decisions: Option<Arc<crate::decision::Decisions>>,
760 tasks: Option<crate::tasks::TaskRuntime>,
761 redactor: Option<Arc<super::redaction::TranscriptRedactor>>,
762 clock: Option<crate::clock::SharedClock>,
763 lockstep: Option<Arc<super::processor::Lockstep>>,
764 event_capacity: usize,
765}
766
767pub(crate) struct SessionRuntime {
775 session: SessionHandle,
776 callbacks: Arc<EventCallbacks>,
777 dispatcher: Option<Arc<ToolDispatcher>>,
778 extractors: Vec<Arc<dyn TurnExtractor>>,
779 computed: Option<ComputedRegistry>,
780 phase_machine: Option<tokio::sync::Mutex<PhaseMachine>>,
781 watchers: Option<WatcherRegistry>,
782 temporal: Option<Arc<TemporalRegistry>>,
783 greeting: Option<String>,
784 state: State,
785 execution_modes: HashMap<String, ToolExecutionMode>,
786 background_tracker: Arc<BackgroundToolTracker>,
787 telemetry: Arc<SessionTelemetry>,
788 telemetry_interval: Option<std::time::Duration>,
789 control_plane: ControlPlaneConfig,
790 pending_context: Option<Arc<PendingContext>>,
791 writer: Arc<dyn SessionWriter>,
793 user_writer: Arc<dyn SessionWriter>,
795 event_rx: tokio::sync::broadcast::Receiver<gemini_genai_rs::prelude::SessionEvent>,
796 on_usage_cb: Option<super::callbacks::UsageCallback>,
797 live_event_tx: tokio::sync::broadcast::Sender<super::events::LiveEvent>,
798 telem_cancel: CancellationToken,
799 flow_monitor: Option<crate::flow::SharedFlowStack>,
800}
801
802pub(crate) fn build_runtime(plan: SessionPlan, session: SessionHandle) -> SessionRuntime {
807 let flow_monitor = plan.flow.map(crate::flow::FlowStack::into_shared);
810 let task_status = plan
811 .tasks
812 .as_ref()
813 .map(|tasks| Arc::new(parking_lot::Mutex::new(tasks.snapshot())));
814 let mut callbacks = plan.callbacks;
815 let on_usage_cb = callbacks.on_usage.take();
816 let callbacks = Arc::new(callbacks);
817 let raw_writer: Arc<dyn SessionWriter> = Arc::new(session.clone());
818 let state = plan.state.unwrap_or_default();
819 if let Some(clock) = plan.clock {
820 state.set_clock(clock);
821 }
822 if let Some(stack) = &flow_monitor {
823 stack.lock().publish_timing(&state);
824 }
825
826 let event_rx = session.subscribe();
828
829 if let Some(ref pm) = plan.phase_machine {
831 let _ = state.session().set("phase", pm.current());
832 if let Some(phase) = pm.current_phase()
833 && !phase.needs.is_empty()
834 {
835 let _ = state.set("session:phase_needs", phase.needs.clone());
836 }
837 }
838
839 if let Some(scope) = plan.tool_scope.as_ref().or(plan.task_scope.as_ref()) {
841 let _ = state
842 .session()
843 .set(super::tool_scope::DECLARED_TOOLS_KEY, scope.declared());
844 }
845
846 let phase_machine_mutex = plan.phase_machine.map(|mut machine| {
847 machine.set_clock(state.clock());
848 tokio::sync::Mutex::new(machine)
849 });
850 let temporal_arc = plan.temporal.map(Arc::new);
851 let background_tracker = Arc::new(BackgroundToolTracker::new());
852
853 let telemetry = Arc::new(SessionTelemetry::new());
855 let telem_cancel = CancellationToken::new();
856
857 let mut control_plane = ControlPlaneConfig {
859 soft_turn: plan.soft_turn_timeout.map(SoftTurnDetector::new),
860 steering_mode: plan.steering_mode,
861 context_delivery: plan.context_delivery,
862 delivery: plan.delivery,
863 needs_fulfillment: plan.repair_config.map(NeedsFulfillment::new),
864 persistence: plan.persistence,
865 session_id: plan.session_id,
866 tool_advisory: plan.tool_advisory,
867 tool_scope: plan.tool_scope,
868 task_scope: plan.task_scope,
869 code_mode: plan.code_mode,
870 base_instruction: plan.base_instruction,
871 pending_context: None, middleware: {
873 let mut chain = crate::middleware::MiddlewareChain::new();
874 for layer in plan.middleware {
875 chain.add(layer);
876 }
877 Arc::new(chain)
878 },
879 flow: flow_monitor.clone(),
880 decisions: plan.decisions,
881 tasks: plan.tasks,
882 task_status,
883 redactor: plan.redactor,
884 lockstep: plan.lockstep,
885 playback: super::playback::PlaybackClock::new(state.clock()),
886 };
887
888 let stage_defers = flow_monitor.as_ref().is_some_and(|stack| {
895 stack
896 .lock()
897 .timing_policies()
898 .values()
899 .any(|t| t.context_delivery == Some(ContextDelivery::Deferred))
900 });
901 let pending_context = if plan.context_delivery == ContextDelivery::Deferred || stage_defers {
902 Some(Arc::new(PendingContext::new()))
903 } else {
904 None
905 };
906
907 let (writer, user_writer) = if let Some(ref pending) = pending_context {
909 let deferred: Arc<dyn SessionWriter> =
910 Arc::new(DeferredWriter::new(raw_writer.clone(), pending.clone()));
911 (raw_writer, deferred)
915 } else {
916 (raw_writer.clone(), raw_writer)
917 };
918
919 control_plane.pending_context = pending_context.clone();
921
922 use super::events::LiveEvent;
924 use tokio::sync::broadcast;
925 let (live_event_tx, _) = broadcast::channel::<LiveEvent>(plan.event_capacity);
926
927 SessionRuntime {
928 session,
929 callbacks,
930 dispatcher: plan.dispatcher,
931 extractors: plan.extractors,
932 computed: plan.computed,
933 phase_machine: phase_machine_mutex,
934 watchers: plan.watchers,
935 temporal: temporal_arc,
936 greeting: plan.greeting,
937 state,
938 execution_modes: plan.execution_modes,
939 background_tracker,
940 telemetry,
941 telemetry_interval: plan.telemetry_interval,
942 control_plane,
943 pending_context,
944 writer,
945 user_writer,
946 event_rx,
947 on_usage_cb,
948 live_event_tx,
949 telem_cancel,
950 flow_monitor,
951 }
952}
953
954async fn wait_for_connect_failure(
968 events: &mut tokio::sync::broadcast::Receiver<SessionEvent>,
969) -> String {
970 let mut last_error: Option<String> = None;
971 loop {
972 match events.recv().await {
973 Ok(SessionEvent::Error(err)) => last_error = Some(err.to_string()),
974 Ok(SessionEvent::Disconnected(reason)) => {
975 let reason = reason.unwrap_or_else(|| "connection closed".to_string());
976 return match last_error {
977 Some(err) => format!("{reason} ({err})"),
978 None => reason,
979 };
980 }
981 Ok(_) => {}
982 Err(tokio::sync::broadcast::error::RecvError::Closed) => {
984 return last_error.unwrap_or_else(|| "session ended during setup".to_string());
985 }
986 Err(tokio::sync::broadcast::error::RecvError::Lagged(_)) => {}
987 }
988 }
989}
990
991pub(crate) async fn spawn_lanes(rt: SessionRuntime) -> Result<LiveHandle, AgentError> {
992 use super::events::LiveEvent;
993
994 let conversation_id = rt
999 .control_plane
1000 .session_id
1001 .clone()
1002 .unwrap_or_else(|| rt.session.session_id().to_string());
1003 let session_span = tracing::info_span!(
1004 "live_session",
1005 "gen_ai.operation.name" = "live",
1006 "gen_ai.system" = "gemini",
1007 "gen_ai.conversation.id" = %conversation_id,
1008 );
1009 let entered = session_span.enter();
1010
1011 let session_signals = Arc::new(SessionSignals::new(rt.state.clone()));
1014 let (telemetry_tap, telem_rx) =
1015 TelemetryTap::new(rt.telemetry.clone(), session_signals.clone());
1016 let _telem_handle = spawn_telemetry_lane(
1017 telem_rx,
1018 session_signals,
1019 rt.telem_cancel.clone(),
1020 rt.on_usage_cb,
1021 );
1022
1023 let opening = super::control_plane::opening_steering(&rt.control_plane.flow, &rt.state);
1027 if !opening.is_empty() {
1028 let contents: Vec<_> = opening
1029 .into_iter()
1030 .map(gemini_genai_rs::prelude::Content::model)
1031 .collect();
1032 let delivery = rt
1033 .state
1034 .get::<crate::flow::VoiceTiming>(crate::flow::VOICE_TIMING_KEY)
1035 .and_then(|t| t.context_delivery)
1036 .unwrap_or(rt.control_plane.context_delivery);
1037 match (&delivery, &rt.pending_context) {
1038 (ContextDelivery::Deferred, Some(pending)) => pending.extend(contents),
1039 _ => rt
1040 .writer
1041 .send_client_content(contents, false)
1042 .await
1043 .map_err(AgentError::Session)?,
1044 }
1045 }
1046
1047 let greeting_writer = rt.user_writer.clone();
1049 let playback = rt.control_plane.playback.clone();
1050 let task_status = rt.control_plane.task_status.clone();
1051 let (fast_handle, ctrl_handle, ctrl_tx) = spawn_event_processor(
1052 rt.event_rx,
1053 rt.callbacks,
1054 rt.dispatcher,
1055 rt.writer,
1056 rt.extractors,
1057 rt.state.clone(),
1058 rt.computed,
1059 rt.phase_machine,
1060 rt.watchers,
1061 rt.temporal,
1062 Some(rt.background_tracker.clone()),
1063 rt.execution_modes,
1064 rt.control_plane,
1065 rt.live_event_tx.clone(),
1066 Some(telemetry_tap),
1067 );
1068 drop(entered);
1069
1070 if let Some(interval) = rt.telemetry_interval {
1072 let telem_tx = rt.live_event_tx.clone();
1073 let telem_ref = rt.telemetry.clone();
1074 tokio::spawn(async move {
1075 let mut tick = tokio::time::interval(interval);
1076 let mut prev_turns = 0u64;
1077 loop {
1078 tick.tick().await;
1079 let snap = telem_ref.snapshot();
1080 if let Some(obj) = snap.as_object() {
1081 let tc = obj
1082 .get("turn_count")
1083 .or_else(|| obj.get("response_count"))
1084 .and_then(serde_json::Value::as_u64)
1085 .unwrap_or(0);
1086 if tc > prev_turns {
1087 let latency = obj
1088 .get("last_response_latency_ms")
1089 .and_then(serde_json::Value::as_u64)
1090 .unwrap_or(0) as u32;
1091 let prompt = obj
1092 .get("prompt_token_count")
1093 .and_then(serde_json::Value::as_u64)
1094 .unwrap_or(0) as u32;
1095 let response = obj
1096 .get("response_token_count")
1097 .and_then(serde_json::Value::as_u64)
1098 .unwrap_or(0) as u32;
1099 let _ = telem_tx.send(LiveEvent::TurnMetrics {
1100 turn: tc as u32,
1101 latency_ms: latency,
1102 prompt_tokens: prompt,
1103 response_tokens: response,
1104 });
1105 prev_turns = tc;
1106 }
1107 }
1108 if telem_tx.send(LiveEvent::Telemetry(snap)).is_err() {
1109 break;
1110 }
1111 }
1112 });
1113 }
1114
1115 if let Some(greeting) = rt.greeting {
1117 greeting_writer
1118 .send_text(greeting)
1119 .await
1120 .map_err(AgentError::Session)?;
1121 }
1122
1123 Ok(LiveHandle::new(
1124 rt.session,
1125 rt.user_writer,
1126 fast_handle,
1127 ctrl_handle,
1128 rt.state,
1129 rt.telemetry,
1130 rt.live_event_tx,
1131 rt.pending_context,
1132 rt.flow_monitor,
1133 rt.background_tracker,
1134 rt.telem_cancel,
1135 )
1136 .with_control_sender(ctrl_tx)
1137 .with_playback(playback)
1138 .with_task_status(task_status))
1139}
1140
1141#[cfg(test)]
1142mod tests {
1143 use super::*;
1144
1145 #[test]
1146 fn builder_creates_with_defaults() {
1147 let config = SessionConfig::new("test-key");
1148 let builder = LiveSessionBuilder::new(config);
1149 assert!(builder.dispatcher.is_none());
1150 assert!(builder.computed.is_none());
1151 assert!(builder.phase_machine.is_none());
1152 assert!(builder.watchers.is_none());
1153 assert!(builder.temporal.is_none());
1154 }
1155
1156 #[test]
1157 fn into_plan_derives_defaults() {
1158 let config = SessionConfig::new("test-key");
1159 let plan = LiveSessionBuilder::new(config)
1160 .into_plan()
1161 .expect("default builder should produce a plan");
1162
1163 assert!(plan.config.is_some());
1165 assert!(plan.dispatcher.is_none());
1167 assert!(plan.phase_machine.is_none());
1168 assert!(plan.persistence.is_none());
1169 assert!(plan.session_id.is_none());
1170 assert!(plan.greeting.is_none());
1171 assert!(plan.soft_turn_timeout.is_none());
1172 assert!(plan.telemetry_interval.is_none());
1173 assert!(plan.repair_config.is_none());
1174 assert!(plan.flow.is_none());
1175 assert!(plan.execution_modes.is_empty());
1176 assert!(plan.middleware.is_empty());
1177 assert_eq!(plan.steering_mode, SteeringMode::default());
1178 assert_eq!(plan.context_delivery, ContextDelivery::default());
1179 assert!(plan.tool_advisory);
1181 }
1182
1183 #[test]
1184 fn into_plan_carries_persistence_and_session_id() {
1185 let config = SessionConfig::new("test-key");
1186 let plan = LiveSessionBuilder::new(config)
1187 .session_id("user-123-session-456")
1188 .into_plan()
1189 .expect("plan derivation should succeed");
1190
1191 assert_eq!(plan.session_id.as_deref(), Some("user-123-session-456"));
1192 }
1193
1194 #[test]
1195 fn into_plan_carries_steering_and_context_delivery() {
1196 let config = SessionConfig::new("test-key");
1197 let plan = LiveSessionBuilder::new(config)
1198 .steering_mode(SteeringMode::ContextInjection)
1199 .context_delivery(ContextDelivery::Deferred)
1200 .tool_advisory(false)
1201 .into_plan()
1202 .expect("plan derivation should succeed");
1203
1204 assert_eq!(plan.steering_mode, SteeringMode::ContextInjection);
1205 assert_eq!(plan.context_delivery, ContextDelivery::Deferred);
1206 assert!(!plan.tool_advisory);
1207 }
1208
1209 fn two_phase_machine() -> PhaseMachine {
1210 use super::super::phase::Phase;
1211 let mut verify = Phase::new("verify", "Verify the caller.");
1212 verify.tools_enabled = Some(vec!["verify_identity".into()]);
1213 let mut serve = Phase::new("serve", "Serve the caller.");
1214 serve.tools_enabled = Some(vec!["get_balance".into()]);
1215 let mut machine = PhaseMachine::new("verify");
1216 machine.add_phase(verify);
1217 machine.add_phase(serve);
1218 machine
1219 }
1220
1221 fn banking_config(model: gemini_genai_rs::prelude::ModelId) -> SessionConfig {
1222 use gemini_genai_rs::prelude::{FunctionDeclaration, Tool};
1223 let decl = |name: &str| FunctionDeclaration {
1224 name: name.into(),
1225 description: name.into(),
1226 parameters: None,
1227 behavior: None,
1228 };
1229 SessionConfig::new("test-key")
1230 .model(model)
1231 .system_instruction("You are a bank agent.")
1232 .add_tool(Tool::functions(vec![
1233 decl("verify_identity"),
1234 decl("get_balance"),
1235 ]))
1236 }
1237
1238 fn declared(config: &SessionConfig) -> Vec<String> {
1239 config
1240 .tools
1241 .iter()
1242 .filter_map(|t| t.function_declarations.as_ref())
1243 .flatten()
1244 .map(|d| d.name.clone())
1245 .collect()
1246 }
1247
1248 fn instruction_text(config: &SessionConfig) -> String {
1249 config
1250 .system_instruction
1251 .iter()
1252 .flat_map(|c| &c.parts)
1253 .filter_map(|p| match p {
1254 gemini_genai_rs::prelude::Part::Text { text } => Some(text.as_str()),
1255 _ => None,
1256 })
1257 .collect()
1258 }
1259
1260 fn lookup_dispatcher() -> ToolDispatcher {
1261 let mut dispatcher = ToolDispatcher::new();
1262 for name in ["lookup", "book"] {
1263 dispatcher.register_function(Arc::new(crate::tool::SimpleTool::new(
1264 name,
1265 format!("{name} tool"),
1266 None,
1267 |_| async { Ok(serde_json::json!({})) },
1268 )));
1269 }
1270 dispatcher
1271 }
1272
1273 #[test]
1274 fn a_code_tool_is_declared_at_setup_beside_the_tools() {
1275 let plan = LiveSessionBuilder::new(SessionConfig::new("test-key"))
1276 .dispatcher(lookup_dispatcher())
1277 .code_mode(crate::code_mode::CodeMode::new().function("lookup"))
1278 .into_plan()
1279 .expect("plan");
1280 let mut names = declared(plan.config.as_ref().unwrap());
1281 names.sort();
1282 assert_eq!(names, vec!["book", "lookup", "run_code"]);
1283 assert!(plan.code_mode.is_some());
1284 }
1285
1286 #[test]
1287 fn a_code_tool_must_expose_tools_the_session_has() {
1288 let err = LiveSessionBuilder::new(SessionConfig::new("test-key"))
1289 .dispatcher(lookup_dispatcher())
1290 .code_mode(crate::code_mode::CodeMode::new().function("refund"))
1291 .into_plan()
1292 .err()
1293 .expect("an unknown function is refused");
1294 assert!(err.to_string().contains("refund"), "{err}");
1295
1296 let err = LiveSessionBuilder::new(SessionConfig::new("test-key"))
1297 .dispatcher(lookup_dispatcher())
1298 .code_mode(
1299 crate::code_mode::CodeMode::new()
1300 .function("lookup")
1301 .tool_name("book"),
1302 )
1303 .into_plan()
1304 .err()
1305 .expect("a name clash is refused");
1306 assert!(err.to_string().contains("'book'"), "{err}");
1307 }
1308
1309 #[test]
1310 fn context_update_steering_declares_only_the_initial_phase_at_setup() {
1311 use gemini_genai_rs::prelude::ModelId;
1312 let plan = LiveSessionBuilder::new(banking_config(ModelId::LIVE_3_8))
1313 .phase_machine(two_phase_machine())
1314 .steering_mode(SteeringMode::ContextUpdate)
1315 .into_plan()
1316 .expect("plan derivation should succeed");
1317
1318 assert_eq!(plan.steering_mode, SteeringMode::ContextUpdate);
1319 let config = plan.config.as_ref().unwrap();
1320 assert_eq!(declared(config), vec!["verify_identity"]);
1321 assert_eq!(
1322 instruction_text(config),
1323 "You are a bank agent.\n\nVerify the caller."
1324 );
1325 let scope = plan.tool_scope.as_ref().expect("scope under ContextUpdate");
1327 assert_eq!(scope.declared(), vec!["verify_identity"]);
1328 assert_eq!(
1329 plan.base_instruction.as_deref(),
1330 Some("You are a bank agent.")
1331 );
1332 }
1333
1334 #[test]
1335 fn a_task_session_declares_only_task_control_where_context_update_is_accepted() {
1336 use gemini_genai_rs::prelude::ModelId;
1337 let session = |model, steering: Option<SteeringMode>| {
1338 let mut builder = LiveSessionBuilder::new(
1339 SessionConfig::new("test-key")
1340 .model(model)
1341 .system_instruction("You are a bank agent."),
1342 )
1343 .tasks(crate::tasks::two_skill_runtime());
1344 if let Some(mode) = steering {
1345 builder = builder.steering_mode(mode);
1346 }
1347 builder.into_plan().expect("plan derivation should succeed")
1348 };
1349
1350 let plan = session(ModelId::LIVE_3_8, None);
1351 let config = plan.config.as_ref().unwrap();
1352 assert_eq!(
1354 declared(config),
1355 vec!["task_control", "start_billing", "start_faq"]
1356 );
1357 let scope = plan.task_scope.as_ref().expect("a task scope on 3.8");
1358 assert_eq!(
1359 scope.declared(),
1360 vec!["start_billing", "start_faq", "task_control"]
1361 );
1362 assert!(
1363 instruction_text(config).contains("Installed skills"),
1364 "the catalog stays in the instruction"
1365 );
1366 assert!(
1367 plan.tool_scope.is_none(),
1368 "phases and flows are not involved"
1369 );
1370
1371 let plan = session(ModelId::LIVE_3_8, Some(SteeringMode::ContextUpdate));
1372 assert!(plan.task_scope.is_some());
1373
1374 for plan in [
1377 session(ModelId::FLASH_2_5_NATIVE_AUDIO_LATEST, None),
1378 session(ModelId::LIVE_3_8, Some(SteeringMode::Hybrid)),
1379 ] {
1380 assert!(plan.task_scope.is_none());
1381 assert_eq!(
1382 declared(plan.config.as_ref().unwrap()),
1383 vec![
1384 "task_control",
1385 "start_billing",
1386 "start_faq",
1387 "billing__verify",
1388 "billing__pay",
1389 "faq__answer"
1390 ]
1391 );
1392 }
1393 }
1394
1395 #[test]
1396 fn context_update_steering_falls_back_to_hybrid_without_model_support() {
1397 use gemini_genai_rs::prelude::ModelId;
1398 let plan = LiveSessionBuilder::new(banking_config(ModelId::FLASH_2_5_NATIVE_AUDIO_LATEST))
1399 .phase_machine(two_phase_machine())
1400 .steering_mode(SteeringMode::ContextUpdate)
1401 .into_plan()
1402 .expect("plan derivation should succeed");
1403
1404 assert_eq!(plan.steering_mode, SteeringMode::Hybrid);
1405 assert!(plan.tool_scope.is_none());
1406 let config = plan.config.as_ref().unwrap();
1407 assert_eq!(
1408 declared(config),
1409 vec!["verify_identity", "get_balance"],
1410 "every tool is declared, as under the other modes"
1411 );
1412 assert_eq!(instruction_text(config), "You are a bank agent.");
1413 }
1414
1415 #[test]
1416 fn other_steering_modes_declare_every_tool_at_setup() {
1417 use gemini_genai_rs::prelude::ModelId;
1418 let plan = LiveSessionBuilder::new(banking_config(ModelId::LIVE_3_8))
1419 .phase_machine(two_phase_machine())
1420 .steering_mode(SteeringMode::Hybrid)
1421 .into_plan()
1422 .expect("plan derivation should succeed");
1423 assert!(plan.tool_scope.is_none());
1424 assert_eq!(
1425 declared(plan.config.as_ref().unwrap()),
1426 vec!["verify_identity", "get_balance"]
1427 );
1428 }
1429
1430 #[test]
1431 fn into_plan_carries_greeting_and_telemetry_interval() {
1432 let config = SessionConfig::new("test-key");
1433 let plan = LiveSessionBuilder::new(config)
1434 .greeting("Hello there")
1435 .telemetry_interval(std::time::Duration::from_secs(5))
1436 .soft_turn_timeout(std::time::Duration::from_secs(2))
1437 .into_plan()
1438 .expect("plan derivation should succeed");
1439
1440 assert_eq!(plan.greeting.as_deref(), Some("Hello there"));
1441 assert_eq!(
1442 plan.telemetry_interval,
1443 Some(std::time::Duration::from_secs(5))
1444 );
1445 assert_eq!(
1446 plan.soft_turn_timeout,
1447 Some(std::time::Duration::from_secs(2))
1448 );
1449 }
1450
1451 #[test]
1452 fn into_plan_carries_event_capacity() {
1453 let default = LiveSessionBuilder::new(SessionConfig::new("test-key"))
1454 .into_plan()
1455 .expect("plan derivation should succeed");
1456 assert_eq!(
1457 default.event_capacity,
1458 super::super::events::DEFAULT_EVENT_CAPACITY
1459 );
1460
1461 let custom = LiveSessionBuilder::new(SessionConfig::new("test-key"))
1462 .event_capacity(64)
1463 .into_plan()
1464 .expect("plan derivation should succeed");
1465 assert_eq!(custom.event_capacity, 64);
1466
1467 let zero = LiveSessionBuilder::new(SessionConfig::new("test-key"))
1469 .event_capacity(0)
1470 .into_plan()
1471 .expect("plan derivation should succeed");
1472 assert_eq!(zero.event_capacity, 1);
1473 }
1474
1475 #[tokio::test]
1476 async fn event_capacity_bounds_how_far_a_subscriber_can_lag() {
1477 use super::super::events::LiveEvent;
1478 use gemini_genai_rs::session::SessionState;
1479 use tokio::sync::broadcast::error::RecvError;
1480
1481 let (command_tx, _command_rx) = tokio::sync::mpsc::channel(8);
1482 let (event_tx, _) = tokio::sync::broadcast::channel(16);
1483 let (phase_tx, phase_rx) = tokio::sync::watch::channel(SessionPhase::Active);
1484 let state = Arc::new(SessionState::with_events(phase_tx, event_tx.clone()));
1485 let session = SessionHandle::new(command_tx, event_tx, state, phase_rx);
1486
1487 let plan = LiveSessionBuilder::new(SessionConfig::new("test-key"))
1488 .event_capacity(4)
1489 .into_plan()
1490 .expect("plan derivation should succeed");
1491 let runtime = build_runtime(plan, session);
1492 let mut rx = runtime.live_event_tx.subscribe();
1493 for _ in 0..5 {
1494 let _ = runtime.live_event_tx.send(LiveEvent::TurnComplete);
1495 }
1496 assert!(matches!(rx.recv().await, Err(RecvError::Lagged(1))));
1498 for _ in 0..4 {
1499 assert!(matches!(rx.recv().await, Ok(LiveEvent::TurnComplete)));
1500 }
1501 }
1502
1503 #[test]
1504 fn into_plan_validates_phase_machine() {
1505 let config = SessionConfig::new("test-key");
1508 let pm = PhaseMachine::new("nonexistent");
1509 let result = LiveSessionBuilder::new(config)
1510 .phase_machine(pm)
1511 .into_plan();
1512 assert!(result.is_err(), "invalid phase machine should fail to plan");
1513 }
1514
1515 #[test]
1516 fn into_plan_carries_valid_phase_machine_and_seeds_nothing() {
1517 let config = SessionConfig::new("test-key");
1520 let mut pm = PhaseMachine::new("start");
1521 pm.add_phase(crate::live::phase::Phase::new("start", "Start phase"));
1522 let plan = LiveSessionBuilder::new(config)
1523 .phase_machine(pm)
1524 .into_plan()
1525 .expect("valid phase machine should plan");
1526
1527 assert!(plan.phase_machine.is_some());
1528 }
1529
1530 #[test]
1531 fn into_plan_applies_non_blocking_to_background_tools() {
1532 use gemini_genai_rs::prelude::{FunctionCallingBehavior, FunctionDeclaration, Tool};
1533
1534 let decl = FunctionDeclaration {
1535 name: "search_kb".into(),
1536 description: "Search".into(),
1537 parameters: None,
1538 behavior: None,
1539 };
1540 let config = SessionConfig::new("test-key").add_tool(Tool::functions(vec![decl]));
1541
1542 let plan = LiveSessionBuilder::new(config)
1543 .tool_execution_mode(
1544 "search_kb",
1545 ToolExecutionMode::Background {
1546 formatter: None,
1547 scheduling: None,
1548 },
1549 )
1550 .into_plan()
1551 .expect("plan derivation should succeed");
1552
1553 let cfg = plan.config.expect("config carried");
1554 let decl = cfg.tools[0]
1555 .function_declarations
1556 .as_ref()
1557 .unwrap()
1558 .iter()
1559 .find(|d| d.name == "search_kb")
1560 .unwrap();
1561 assert_eq!(decl.behavior, Some(FunctionCallingBehavior::NonBlocking));
1562 }
1563}