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::{ControlPlaneConfig, spawn_event_processor, spawn_telemetry_lane};
26use super::session_signals::SessionSignals;
27use super::soft_turn::SoftTurnDetector;
28use super::steering::{ContextDelivery, SteeringMode};
29use super::telemetry::SessionTelemetry;
30use super::temporal::TemporalRegistry;
31use super::watcher::WatcherRegistry;
32
33pub struct LiveSessionBuilder {
43 config: SessionConfig,
44 callbacks: EventCallbacks,
45 dispatcher: Option<Arc<ToolDispatcher>>,
46 extractors: Vec<Arc<dyn TurnExtractor>>,
47 computed: Option<ComputedRegistry>,
48 phase_machine: Option<PhaseMachine>,
49 watchers: Option<WatcherRegistry>,
50 temporal: Option<TemporalRegistry>,
51 greeting: Option<String>,
52 state: Option<State>,
53 execution_modes: HashMap<String, ToolExecutionMode>,
54 soft_turn_timeout: Option<std::time::Duration>,
56 steering_mode: SteeringMode,
57 context_delivery: ContextDelivery,
58 delivery: super::processor::DeliveryConfig,
59 repair_config: Option<RepairConfig>,
60 persistence: Option<Arc<dyn SessionPersistence>>,
61 session_id: Option<String>,
62 tool_advisory: bool,
63 telemetry_interval: Option<std::time::Duration>,
64 middleware: Vec<Arc<dyn crate::middleware::Middleware>>,
65 flow: Option<crate::flow::FlowStack>,
66 redactor: Option<Arc<super::redaction::TranscriptRedactor>>,
67 clock: Option<crate::clock::SharedClock>,
68 lockstep: Option<Arc<super::processor::Lockstep>>,
69}
70
71impl LiveSessionBuilder {
72 pub fn new(config: SessionConfig) -> Self {
74 Self {
75 config,
76 callbacks: EventCallbacks::default(),
77 dispatcher: None,
78 extractors: Vec::new(),
79 computed: None,
80 phase_machine: None,
81 watchers: None,
82 temporal: None,
83 greeting: None,
84 state: None,
85 execution_modes: HashMap::new(),
86 soft_turn_timeout: None,
87 steering_mode: SteeringMode::default(),
88 context_delivery: ContextDelivery::default(),
89 delivery: super::processor::DeliveryConfig::default(),
90 repair_config: None,
91 persistence: None,
92 session_id: None,
93 tool_advisory: true,
94 telemetry_interval: None,
95 middleware: Vec::new(),
96 flow: None,
97 redactor: None,
98 clock: None,
99 lockstep: None,
100 }
101 }
102
103 pub(crate) fn lockstep(mut self, lockstep: Arc<super::processor::Lockstep>) -> Self {
106 self.lockstep = Some(lockstep);
107 self
108 }
109
110 pub fn redaction(mut self, redactor: super::redaction::TranscriptRedactor) -> Self {
117 self.redactor = redactor.is_active().then(|| Arc::new(redactor));
118 self
119 }
120
121 pub fn clock(mut self, clock: crate::clock::SharedClock) -> Self {
130 self.clock = Some(clock);
131 self
132 }
133
134 pub fn middleware(mut self, layer: Arc<dyn crate::middleware::Middleware>) -> Self {
140 self.middleware.push(layer);
141 self
142 }
143
144 pub fn flow_monitor(self, monitor: crate::flow::FlowMonitor) -> Self {
148 self.flow_stack(monitor.into_stack())
149 }
150
151 pub fn flow_stack(mut self, stack: crate::flow::FlowStack) -> Self {
155 self.flow = Some(stack);
156 self
157 }
158
159 pub fn state(mut self, state: State) -> Self {
165 self.state = Some(state);
166 self
167 }
168
169 pub fn greeting(mut self, prompt: impl Into<String>) -> Self {
171 self.greeting = Some(prompt.into());
172 self
173 }
174
175 pub fn dispatcher(mut self, dispatcher: ToolDispatcher) -> Self {
177 for tool in dispatcher.to_tool_declarations() {
179 self.config = self.config.add_tool(tool);
180 }
181 self.dispatcher = Some(Arc::new(dispatcher));
182 self
183 }
184
185 pub fn callbacks(mut self, callbacks: EventCallbacks) -> Self {
187 self.callbacks = callbacks;
188 self
189 }
190
191 pub fn extractor(mut self, extractor: Arc<dyn TurnExtractor>) -> Self {
193 self.extractors.push(extractor);
194 self
195 }
196
197 pub fn computed(mut self, registry: ComputedRegistry) -> Self {
199 self.computed = Some(registry);
200 self
201 }
202
203 pub fn phase_machine(mut self, machine: PhaseMachine) -> Self {
205 self.phase_machine = Some(machine);
206 self
207 }
208
209 pub fn watchers(mut self, registry: WatcherRegistry) -> Self {
211 self.watchers = Some(registry);
212 self
213 }
214
215 pub fn temporal(mut self, registry: TemporalRegistry) -> Self {
217 self.temporal = Some(registry);
218 self
219 }
220
221 pub fn tool_execution_mode(
226 mut self,
227 tool_name: impl Into<String>,
228 mode: ToolExecutionMode,
229 ) -> Self {
230 self.execution_modes.insert(tool_name.into(), mode);
231 self
232 }
233
234 pub fn soft_turn_timeout(mut self, timeout: std::time::Duration) -> Self {
241 self.soft_turn_timeout = Some(timeout);
242 self
243 }
244
245 pub fn steering_mode(mut self, mode: SteeringMode) -> Self {
247 self.steering_mode = mode;
248 self
249 }
250
251 pub fn context_delivery(mut self, mode: ContextDelivery) -> Self {
256 self.context_delivery = mode;
257 self
258 }
259
260 pub fn delivery(mut self, delivery: super::processor::DeliveryConfig) -> Self {
267 self.delivery = delivery;
268 self
269 }
270
271 pub fn repair(mut self, config: RepairConfig) -> Self {
276 self.repair_config = Some(config);
277 self
278 }
279
280 pub fn persistence(mut self, backend: Arc<dyn SessionPersistence>) -> Self {
282 self.persistence = Some(backend);
283 self
284 }
285
286 pub fn session_id(mut self, id: impl Into<String>) -> Self {
288 self.session_id = Some(id.into());
289 self
290 }
291
292 pub fn tool_advisory(mut self, enabled: bool) -> Self {
294 self.tool_advisory = enabled;
295 self
296 }
297
298 pub fn telemetry_interval(mut self, interval: std::time::Duration) -> Self {
303 self.telemetry_interval = Some(interval);
304 self
305 }
306
307 pub async fn connect(self) -> Result<LiveHandle, AgentError> {
320 let mut plan = self.into_plan()?;
321
322 let config = plan.config.take().expect("plan always carries a config");
325 let session = ConnectBuilder::new(config)
326 .connect()
327 .await
328 .map_err(AgentError::Session)?;
329 finish_connect(plan, session).await
330 }
331
332 pub async fn connect_with_transport<T: Transport>(
342 self,
343 transport: T,
344 ) -> Result<LiveHandle, AgentError> {
345 let mut plan = self.into_plan()?;
346 let config = plan.config.take().expect("plan always carries a config");
347 let session = ConnectBuilder::new(config)
348 .transport_config(TransportConfig {
349 max_reconnect_attempts: 0,
350 ..TransportConfig::default()
351 })
352 .transport(transport)
353 .connect()
354 .await
355 .map_err(AgentError::Session)?;
356 finish_connect(plan, session).await
357 }
358}
359
360async fn finish_connect(
363 plan: SessionPlan,
364 session: SessionHandle,
365) -> Result<LiveHandle, AgentError> {
366 let mut events = session.subscribe();
376 tokio::select! {
377 () = session.wait_for_phase(SessionPhase::Active) => {}
378 failure = wait_for_connect_failure(&mut events) => {
379 return Err(AgentError::Session(SessionError::SetupFailed(
380 gemini_genai_rs::session::SetupError::ServerRejected {
381 code: None,
382 message: failure,
383 },
384 )));
385 }
386 }
387 let runtime = build_runtime(plan, session);
388 spawn_lanes(runtime).await
389}
390
391impl LiveSessionBuilder {
392 pub(crate) fn into_plan(self) -> Result<SessionPlan, AgentError> {
399 if let Some(ref pm) = self.phase_machine {
401 pm.validate()?;
402 }
403 if let Some(ref computed) = self.computed {
404 computed.validate()?;
405 }
406
407 let mut config = self.config;
409 for (tool_name, mode) in &self.execution_modes {
410 if matches!(
411 mode,
412 super::background_tool::ToolExecutionMode::Background { .. }
413 ) {
414 for tool in &mut config.tools {
415 if let Some(ref mut decls) = tool.function_declarations {
416 for decl in decls {
417 if decl.name == *tool_name {
418 decl.behavior = Some(
419 gemini_genai_rs::prelude::FunctionCallingBehavior::NonBlocking,
420 );
421 }
422 }
423 }
424 }
425 }
426 }
427
428 let base_instruction = config.system_instruction.as_ref().map(|content| {
433 content
434 .parts
435 .iter()
436 .filter_map(|part| match part {
437 gemini_genai_rs::prelude::Part::Text { text } => Some(text.as_str()),
438 _ => None,
439 })
440 .collect::<Vec<_>>()
441 .join("\n")
442 });
443
444 Ok(SessionPlan {
445 config: Some(config),
446 base_instruction,
447 callbacks: self.callbacks,
448 dispatcher: self.dispatcher,
449 extractors: self.extractors,
450 computed: self.computed,
451 phase_machine: self.phase_machine,
452 watchers: self.watchers,
453 temporal: self.temporal,
454 greeting: self.greeting,
455 state: self.state,
456 execution_modes: self.execution_modes,
457 soft_turn_timeout: self.soft_turn_timeout,
458 steering_mode: self.steering_mode,
459 context_delivery: self.context_delivery,
460 delivery: self.delivery,
461 repair_config: self.repair_config,
462 persistence: self.persistence,
463 session_id: self.session_id,
464 tool_advisory: self.tool_advisory,
465 telemetry_interval: self.telemetry_interval,
466 middleware: self.middleware,
467 flow: self.flow,
468 redactor: self.redactor,
469 clock: self.clock,
470 lockstep: self.lockstep,
471 })
472 }
473}
474
475pub(crate) struct SessionPlan {
483 config: Option<SessionConfig>,
486 base_instruction: Option<String>,
489 callbacks: EventCallbacks,
490 dispatcher: Option<Arc<ToolDispatcher>>,
491 extractors: Vec<Arc<dyn TurnExtractor>>,
492 computed: Option<ComputedRegistry>,
493 phase_machine: Option<PhaseMachine>,
494 watchers: Option<WatcherRegistry>,
495 temporal: Option<TemporalRegistry>,
496 greeting: Option<String>,
497 state: Option<State>,
498 execution_modes: HashMap<String, ToolExecutionMode>,
499 soft_turn_timeout: Option<std::time::Duration>,
500 steering_mode: SteeringMode,
501 context_delivery: ContextDelivery,
502 delivery: super::processor::DeliveryConfig,
503 repair_config: Option<RepairConfig>,
504 persistence: Option<Arc<dyn SessionPersistence>>,
505 session_id: Option<String>,
506 tool_advisory: bool,
507 telemetry_interval: Option<std::time::Duration>,
508 middleware: Vec<Arc<dyn crate::middleware::Middleware>>,
509 flow: Option<crate::flow::FlowStack>,
510 redactor: Option<Arc<super::redaction::TranscriptRedactor>>,
511 clock: Option<crate::clock::SharedClock>,
512 lockstep: Option<Arc<super::processor::Lockstep>>,
513}
514
515pub(crate) struct SessionRuntime {
523 session: SessionHandle,
524 callbacks: Arc<EventCallbacks>,
525 dispatcher: Option<Arc<ToolDispatcher>>,
526 extractors: Vec<Arc<dyn TurnExtractor>>,
527 computed: Option<ComputedRegistry>,
528 phase_machine: Option<tokio::sync::Mutex<PhaseMachine>>,
529 watchers: Option<WatcherRegistry>,
530 temporal: Option<Arc<TemporalRegistry>>,
531 greeting: Option<String>,
532 state: State,
533 execution_modes: HashMap<String, ToolExecutionMode>,
534 background_tracker: Arc<BackgroundToolTracker>,
535 telemetry: Arc<SessionTelemetry>,
536 telemetry_interval: Option<std::time::Duration>,
537 control_plane: ControlPlaneConfig,
538 pending_context: Option<Arc<PendingContext>>,
539 writer: Arc<dyn SessionWriter>,
541 user_writer: Arc<dyn SessionWriter>,
543 event_rx: tokio::sync::broadcast::Receiver<gemini_genai_rs::prelude::SessionEvent>,
544 telem_rx: tokio::sync::broadcast::Receiver<gemini_genai_rs::prelude::SessionEvent>,
545 on_usage_cb: Option<super::callbacks::UsageCallback>,
546 live_event_tx: tokio::sync::broadcast::Sender<super::events::LiveEvent>,
547 telem_cancel: CancellationToken,
548 flow_monitor: Option<crate::flow::SharedFlowStack>,
549}
550
551pub(crate) fn build_runtime(plan: SessionPlan, session: SessionHandle) -> SessionRuntime {
556 let flow_monitor = plan.flow.map(crate::flow::FlowStack::into_shared);
559 let mut callbacks = plan.callbacks;
560 let on_usage_cb = callbacks.on_usage.take();
561 let callbacks = Arc::new(callbacks);
562 let raw_writer: Arc<dyn SessionWriter> = Arc::new(session.clone());
563 let state = plan.state.unwrap_or_default();
564 if let Some(clock) = plan.clock {
565 state.set_clock(clock);
566 }
567 if let Some(stack) = &flow_monitor {
568 stack.lock().publish_timing(&state);
569 }
570
571 let event_rx = session.subscribe();
573 let telem_rx = session.subscribe();
574
575 if let Some(ref pm) = plan.phase_machine {
577 let _ = state.session().set("phase", pm.current());
578 if let Some(phase) = pm.current_phase()
579 && !phase.needs.is_empty()
580 {
581 let _ = state.set("session:phase_needs", phase.needs.clone());
582 }
583 }
584
585 let phase_machine_mutex = plan.phase_machine.map(|mut machine| {
586 machine.set_clock(state.clock());
587 tokio::sync::Mutex::new(machine)
588 });
589 let temporal_arc = plan.temporal.map(Arc::new);
590 let background_tracker = Arc::new(BackgroundToolTracker::new());
591
592 let telemetry = Arc::new(SessionTelemetry::new());
594 let telem_cancel = CancellationToken::new();
595
596 let mut control_plane = ControlPlaneConfig {
598 soft_turn: plan.soft_turn_timeout.map(SoftTurnDetector::new),
599 steering_mode: plan.steering_mode,
600 context_delivery: plan.context_delivery,
601 delivery: plan.delivery,
602 needs_fulfillment: plan.repair_config.map(NeedsFulfillment::new),
603 persistence: plan.persistence,
604 session_id: plan.session_id,
605 tool_advisory: plan.tool_advisory,
606 base_instruction: plan.base_instruction,
607 pending_context: None, middleware: {
609 let mut chain = crate::middleware::MiddlewareChain::new();
610 for layer in plan.middleware {
611 chain.add(layer);
612 }
613 Arc::new(chain)
614 },
615 flow: flow_monitor.clone(),
616 redactor: plan.redactor,
617 lockstep: plan.lockstep,
618 playback: super::playback::PlaybackClock::new(state.clock()),
619 };
620
621 let stage_defers = flow_monitor.as_ref().is_some_and(|stack| {
628 stack
629 .lock()
630 .timing_policies()
631 .values()
632 .any(|t| t.context_delivery == Some(ContextDelivery::Deferred))
633 });
634 let pending_context = if plan.context_delivery == ContextDelivery::Deferred || stage_defers {
635 Some(Arc::new(PendingContext::new()))
636 } else {
637 None
638 };
639
640 let (writer, user_writer) = if let Some(ref pending) = pending_context {
642 let deferred: Arc<dyn SessionWriter> =
643 Arc::new(DeferredWriter::new(raw_writer.clone(), pending.clone()));
644 (raw_writer, deferred)
648 } else {
649 (raw_writer.clone(), raw_writer)
650 };
651
652 control_plane.pending_context = pending_context.clone();
654
655 use super::events::LiveEvent;
657 use tokio::sync::broadcast;
658 let (live_event_tx, _) = broadcast::channel::<LiveEvent>(4096);
659
660 SessionRuntime {
661 session,
662 callbacks,
663 dispatcher: plan.dispatcher,
664 extractors: plan.extractors,
665 computed: plan.computed,
666 phase_machine: phase_machine_mutex,
667 watchers: plan.watchers,
668 temporal: temporal_arc,
669 greeting: plan.greeting,
670 state,
671 execution_modes: plan.execution_modes,
672 background_tracker,
673 telemetry,
674 telemetry_interval: plan.telemetry_interval,
675 control_plane,
676 pending_context,
677 writer,
678 user_writer,
679 event_rx,
680 telem_rx,
681 on_usage_cb,
682 live_event_tx,
683 telem_cancel,
684 flow_monitor,
685 }
686}
687
688async fn wait_for_connect_failure(
702 events: &mut tokio::sync::broadcast::Receiver<SessionEvent>,
703) -> String {
704 let mut last_error: Option<String> = None;
705 loop {
706 match events.recv().await {
707 Ok(SessionEvent::Error(err)) => last_error = Some(err.to_string()),
708 Ok(SessionEvent::Disconnected(reason)) => {
709 let reason = reason.unwrap_or_else(|| "connection closed".to_string());
710 return match last_error {
711 Some(err) => format!("{reason} ({err})"),
712 None => reason,
713 };
714 }
715 Ok(_) => {}
716 Err(tokio::sync::broadcast::error::RecvError::Closed) => {
718 return last_error.unwrap_or_else(|| "session ended during setup".to_string());
719 }
720 Err(tokio::sync::broadcast::error::RecvError::Lagged(_)) => {}
721 }
722 }
723}
724
725pub(crate) async fn spawn_lanes(rt: SessionRuntime) -> Result<LiveHandle, AgentError> {
726 use super::events::LiveEvent;
727
728 let conversation_id = rt
733 .control_plane
734 .session_id
735 .clone()
736 .unwrap_or_else(|| rt.session.session_id().to_string());
737 let session_span = tracing::info_span!(
738 "live_session",
739 "gen_ai.operation.name" = "live",
740 "gen_ai.system" = "gemini",
741 "gen_ai.conversation.id" = %conversation_id,
742 );
743 let entered = session_span.enter();
744
745 let session_signals = SessionSignals::new(rt.state.clone());
747 let _telem_handle = spawn_telemetry_lane(
748 rt.telem_rx,
749 session_signals,
750 rt.telemetry.clone(),
751 rt.telem_cancel.clone(),
752 rt.on_usage_cb,
753 );
754
755 let greeting_writer = rt.user_writer.clone();
757 let playback = rt.control_plane.playback.clone();
758 let (fast_handle, ctrl_handle, ctrl_tx) = spawn_event_processor(
759 rt.event_rx,
760 rt.callbacks,
761 rt.dispatcher,
762 rt.writer,
763 rt.extractors,
764 rt.state.clone(),
765 rt.computed,
766 rt.phase_machine,
767 rt.watchers,
768 rt.temporal,
769 Some(rt.background_tracker.clone()),
770 rt.execution_modes,
771 rt.control_plane,
772 rt.live_event_tx.clone(),
773 );
774 drop(entered);
775
776 if let Some(interval) = rt.telemetry_interval {
778 let telem_tx = rt.live_event_tx.clone();
779 let telem_ref = rt.telemetry.clone();
780 tokio::spawn(async move {
781 let mut tick = tokio::time::interval(interval);
782 let mut prev_turns = 0u64;
783 loop {
784 tick.tick().await;
785 let snap = telem_ref.snapshot();
786 if let Some(obj) = snap.as_object() {
787 let tc = obj
788 .get("turn_count")
789 .or_else(|| obj.get("response_count"))
790 .and_then(serde_json::Value::as_u64)
791 .unwrap_or(0);
792 if tc > prev_turns {
793 let latency = obj
794 .get("last_response_latency_ms")
795 .and_then(serde_json::Value::as_u64)
796 .unwrap_or(0) as u32;
797 let prompt = obj
798 .get("prompt_token_count")
799 .and_then(serde_json::Value::as_u64)
800 .unwrap_or(0) as u32;
801 let response = obj
802 .get("response_token_count")
803 .and_then(serde_json::Value::as_u64)
804 .unwrap_or(0) as u32;
805 let _ = telem_tx.send(LiveEvent::TurnMetrics {
806 turn: tc as u32,
807 latency_ms: latency,
808 prompt_tokens: prompt,
809 response_tokens: response,
810 });
811 prev_turns = tc;
812 }
813 }
814 if telem_tx.send(LiveEvent::Telemetry(snap)).is_err() {
815 break;
816 }
817 }
818 });
819 }
820
821 if let Some(greeting) = rt.greeting {
823 greeting_writer
824 .send_text(greeting)
825 .await
826 .map_err(AgentError::Session)?;
827 }
828
829 Ok(LiveHandle::new(
830 rt.session,
831 rt.user_writer,
832 fast_handle,
833 ctrl_handle,
834 rt.state,
835 rt.telemetry,
836 rt.live_event_tx,
837 rt.pending_context,
838 rt.flow_monitor,
839 rt.background_tracker,
840 rt.telem_cancel,
841 )
842 .with_control_sender(ctrl_tx)
843 .with_playback(playback))
844}
845
846#[cfg(test)]
847mod tests {
848 use super::*;
849
850 #[test]
851 fn builder_creates_with_defaults() {
852 let config = SessionConfig::new("test-key");
853 let builder = LiveSessionBuilder::new(config);
854 assert!(builder.dispatcher.is_none());
855 assert!(builder.computed.is_none());
856 assert!(builder.phase_machine.is_none());
857 assert!(builder.watchers.is_none());
858 assert!(builder.temporal.is_none());
859 }
860
861 #[test]
862 fn into_plan_derives_defaults() {
863 let config = SessionConfig::new("test-key");
864 let plan = LiveSessionBuilder::new(config)
865 .into_plan()
866 .expect("default builder should produce a plan");
867
868 assert!(plan.config.is_some());
870 assert!(plan.dispatcher.is_none());
872 assert!(plan.phase_machine.is_none());
873 assert!(plan.persistence.is_none());
874 assert!(plan.session_id.is_none());
875 assert!(plan.greeting.is_none());
876 assert!(plan.soft_turn_timeout.is_none());
877 assert!(plan.telemetry_interval.is_none());
878 assert!(plan.repair_config.is_none());
879 assert!(plan.flow.is_none());
880 assert!(plan.execution_modes.is_empty());
881 assert!(plan.middleware.is_empty());
882 assert_eq!(plan.steering_mode, SteeringMode::default());
883 assert_eq!(plan.context_delivery, ContextDelivery::default());
884 assert!(plan.tool_advisory);
886 }
887
888 #[test]
889 fn into_plan_carries_persistence_and_session_id() {
890 let config = SessionConfig::new("test-key");
891 let plan = LiveSessionBuilder::new(config)
892 .session_id("user-123-session-456")
893 .into_plan()
894 .expect("plan derivation should succeed");
895
896 assert_eq!(plan.session_id.as_deref(), Some("user-123-session-456"));
897 }
898
899 #[test]
900 fn into_plan_carries_steering_and_context_delivery() {
901 let config = SessionConfig::new("test-key");
902 let plan = LiveSessionBuilder::new(config)
903 .steering_mode(SteeringMode::ContextInjection)
904 .context_delivery(ContextDelivery::Deferred)
905 .tool_advisory(false)
906 .into_plan()
907 .expect("plan derivation should succeed");
908
909 assert_eq!(plan.steering_mode, SteeringMode::ContextInjection);
910 assert_eq!(plan.context_delivery, ContextDelivery::Deferred);
911 assert!(!plan.tool_advisory);
912 }
913
914 #[test]
915 fn into_plan_carries_greeting_and_telemetry_interval() {
916 let config = SessionConfig::new("test-key");
917 let plan = LiveSessionBuilder::new(config)
918 .greeting("Hello there")
919 .telemetry_interval(std::time::Duration::from_secs(5))
920 .soft_turn_timeout(std::time::Duration::from_secs(2))
921 .into_plan()
922 .expect("plan derivation should succeed");
923
924 assert_eq!(plan.greeting.as_deref(), Some("Hello there"));
925 assert_eq!(
926 plan.telemetry_interval,
927 Some(std::time::Duration::from_secs(5))
928 );
929 assert_eq!(
930 plan.soft_turn_timeout,
931 Some(std::time::Duration::from_secs(2))
932 );
933 }
934
935 #[test]
936 fn into_plan_validates_phase_machine() {
937 let config = SessionConfig::new("test-key");
940 let pm = PhaseMachine::new("nonexistent");
941 let result = LiveSessionBuilder::new(config)
942 .phase_machine(pm)
943 .into_plan();
944 assert!(result.is_err(), "invalid phase machine should fail to plan");
945 }
946
947 #[test]
948 fn into_plan_carries_valid_phase_machine_and_seeds_nothing() {
949 let config = SessionConfig::new("test-key");
952 let mut pm = PhaseMachine::new("start");
953 pm.add_phase(crate::live::phase::Phase::new("start", "Start phase"));
954 let plan = LiveSessionBuilder::new(config)
955 .phase_machine(pm)
956 .into_plan()
957 .expect("valid phase machine should plan");
958
959 assert!(plan.phase_machine.is_some());
960 }
961
962 #[test]
963 fn into_plan_applies_non_blocking_to_background_tools() {
964 use gemini_genai_rs::prelude::{FunctionCallingBehavior, FunctionDeclaration, Tool};
965
966 let decl = FunctionDeclaration {
967 name: "search_kb".into(),
968 description: "Search".into(),
969 parameters: None,
970 behavior: None,
971 };
972 let config = SessionConfig::new("test-key").add_tool(Tool::functions(vec![decl]));
973
974 let plan = LiveSessionBuilder::new(config)
975 .tool_execution_mode(
976 "search_kb",
977 ToolExecutionMode::Background {
978 formatter: None,
979 scheduling: None,
980 },
981 )
982 .into_plan()
983 .expect("plan derivation should succeed");
984
985 let cfg = plan.config.expect("config carried");
986 let decl = cfg.tools[0]
987 .function_declarations
988 .as_ref()
989 .unwrap()
990 .iter()
991 .find(|d| d.name == "search_kb")
992 .unwrap();
993 assert_eq!(decl.behavior, Some(FunctionCallingBehavior::NonBlocking));
994 }
995}