gemini_adk_fluent_rs/live/
mod.rs

1//! `Live` — Fluent builder for callback-driven Gemini Live sessions.
2//!
3//! Wraps L1's `LiveSessionBuilder` with ergonomic callback registration
4//! and integration with composition modules (M, T, P).
5//!
6//! # Callback Modes
7//!
8//! Control-lane callbacks support two execution modes via [`gemini_adk_rs::live::CallbackMode`]:
9//!
10//! - **Default methods** (e.g., `.on_turn_complete()`) → [`gemini_adk_rs::live::CallbackMode::Blocking`]
11//! - **`_concurrent` methods** (e.g., `.on_turn_complete_concurrent()`) → [`gemini_adk_rs::live::CallbackMode::Concurrent`]
12//!
13//! Use concurrent mode for fire-and-forget work (logging, analytics, webhook dispatch).
14//!
15//! # Background Tool Execution
16//!
17//! Mark tools for background execution to eliminate dead air in voice sessions:
18//!
19//! ```rust,ignore
20//! Live::builder()
21//!     .tools(dispatcher)
22//!     .tool_background("search_kb")
23//!     .connect_vertex(project, location, token)
24//!     .await?;
25//! ```
26
27mod callbacks;
28mod config;
29mod connect;
30mod contract;
31mod extraction;
32mod phases;
33
34use std::collections::HashMap;
35use std::sync::Arc;
36use std::time::Duration;
37
38pub use gemini_adk_rs::live::extractor::TurnExtractor;
39pub use gemini_adk_rs::live::needs::RepairConfig;
40pub use gemini_adk_rs::live::persistence::SessionPersistence;
41pub use gemini_adk_rs::live::steering::{ContextDelivery, SteeringMode};
42pub use gemini_adk_rs::live::{
43    ComputedRegistry, EventCallbacks, InstructionModifier, Phase, TemporalRegistry,
44    ToolExecutionMode, WatcherRegistry,
45};
46use gemini_adk_rs::llm::BaseLlm;
47use gemini_adk_rs::tool::ToolDispatcher;
48use gemini_genai_rs::prelude::*;
49
50// Carve (gap #9): `gemini_adk_fluent_rs::live` is the curated home for the full
51// Live control plane. The kernel `prelude` keeps only `Live` + the headline types;
52// everything else (persistence, steering, repair, transcripts, extraction triggers,
53// soft-turn, runtime contract, …) is re-exported here. (Explicit, rather than a
54// glob, to avoid shadowing the L1/L2 private `callbacks`/`contract` modules.)
55pub use gemini_adk_rs::live::{
56    BackendInputVad, BackendVadSnapshot, BackgroundAgentDispatcher, BackgroundToolTracker,
57    CallbackMode, ComputedContract, ComputedVar, ConsecutiveFailureDetector, ContextBuilder,
58    ControlContract, DefaultResultFormatter, DeferredWriter, EffectMode, EffectPolicy,
59    ExtractionTrigger, ExtractorContract, FieldPromotion, FsPersistence, LiveEffect,
60    LiveEffectExecutor, LiveEvent, LiveEventStream, LiveHandle, LiveReactor, LiveSessionBuilder,
61    LlmExtractor, MemoryPersistence, MergePolicy, NeedsFulfillment, PatternDetector,
62    PendingContext, PhaseContract, PhaseInstruction, PhaseMachine, PhasePreparation,
63    PhaseTransition, PredicateFn, PreparationContract, PromotionContract, RateDetector, Reaction,
64    ReactorEvent, ReactorRule, RepairAction, ResultFormatter, RuntimeContract, SessionSignals,
65    SessionSnapshot, SessionTelemetry, SessionType, SoftTurnDetector, SustainedDetector,
66    ToolCallSummary, ToolContract, TranscriptBuffer, TranscriptTurn, TranscriptWindow, Transition,
67    TransitionContract, TransitionEvaluation, TransitionResult, TransitionTrigger,
68    TurnCountDetector, VoiceRuntimeState, WatchPredicate, Watcher, WatcherContract,
69};
70// Offline record/replay harness (Milestone 7 determinism spine).
71pub use gemini_adk_rs::live::replay::{
72    attach_session, collect_events_until_idle, replay_session, ReplaySession,
73};
74
75/// A deferred agent tool registration (resolved at connect time when State is available).
76pub(crate) struct DeferredAgentTool {
77    pub(crate) name: String,
78    pub(crate) description: String,
79    pub(crate) agent: Arc<dyn gemini_adk_rs::text::TextAgent>,
80}
81
82/// Fluent builder for constructing and connecting Gemini Live sessions.
83///
84/// Accumulates model configuration, callbacks, extractors, phases, watchers,
85/// temporal patterns, and tool execution modes, then connects via one of
86/// the `connect_*` methods.
87///
88/// Control-lane callbacks can be registered with `_concurrent` suffixed
89/// methods for fire-and-forget execution. Tools can be marked for background
90/// execution via [`tool_background()`](Self::tool_background).
91///
92/// # Example
93/// ```ignore
94/// let session = Live::builder()
95///     .model(GeminiModel::Gemini2_0FlashLive)
96///     .voice(Voice::Kore)
97///     .instruction("You are a weather assistant")
98///     .tools(dispatcher)
99///     .on_audio(|data| playback_tx.send(data.clone()).ok())
100///     .on_text(|t| print!("{t}"))
101///     .on_interrupted(|| async { playback.flush().await; })
102///     .connect_vertex("project", "us-central1", token)
103///     .await?;
104/// ```
105///
106/// # Extraction Pipeline
107/// ```ignore
108/// let handle = Live::builder()
109///     .model(GeminiModel::Gemini2_0FlashLive)
110///     .instruction("You are a restaurant order assistant")
111///     .extract_turns::<OrderState>(
112///         flash_llm,
113///         "Extract: items ordered, quantities, modifications, order_phase",
114///     )
115///     .on_extracted(|name, value| async move {
116///         println!("Extracted {name}: {value}");
117///     })
118///     .connect_vertex(project, location, token)
119///     .await?;
120///
121/// // Read latest extraction from shared State at any time:
122/// let order: Option<OrderState> = handle.extracted("OrderState");
123/// ```
124pub struct Live {
125    pub(crate) config: SessionConfig,
126    pub(crate) callbacks: EventCallbacks,
127    pub(crate) dispatcher: Option<ToolDispatcher>,
128    pub(crate) extractors: Vec<Arc<dyn TurnExtractor>>,
129    // L1 registries
130    pub(crate) computed: ComputedRegistry,
131    pub(crate) phases: Vec<Phase>,
132    pub(crate) initial_phase: Option<String>,
133    pub(crate) watchers: WatcherRegistry,
134    pub(crate) temporal: TemporalRegistry,
135    pub(crate) greeting: Option<String>,
136    // Phase defaults: modifiers + prompt_on_enter inherited by all phases.
137    pub(crate) phase_default_modifiers: Vec<InstructionModifier>,
138    pub(crate) phase_default_prompt_on_enter: bool,
139    // Per-tool execution modes (standard vs background).
140    pub(crate) tool_execution_modes: HashMap<String, ToolExecutionMode>,
141    // Deferred agent tools (resolved at connect time).
142    pub(crate) deferred_agent_tools: Vec<DeferredAgentTool>,
143    // Tools requiring async I/O to resolve (MCP/A2A/OpenAPI/Search),
144    // resolved at connect time.
145    pub(crate) deferred_tools: Vec<crate::compose::tools::DeferredTool>,
146    // LLMs to warm up at connect time.
147    pub(crate) warm_up_llms: Vec<Arc<dyn BaseLlm>>,
148    // Control plane configuration.
149    pub(crate) soft_turn_timeout: Option<Duration>,
150    pub(crate) steering_mode: SteeringMode,
151    pub(crate) context_delivery: ContextDelivery,
152    pub(crate) delivery: gemini_adk_rs::live::DeliveryConfig,
153    pub(crate) repair_config: Option<RepairConfig>,
154    pub(crate) persistence: Option<Arc<dyn SessionPersistence>>,
155    pub(crate) session_id: Option<String>,
156    pub(crate) tool_advisory: bool,
157    pub(crate) telemetry_interval: Option<Duration>,
158    // Middleware layers run around tool dispatch in the control lane.
159    pub(crate) middleware_layers: Vec<Arc<dyn gemini_adk_rs::middleware::Middleware>>,
160    // Confirmation provider consulted before running `T::confirm(..)` tools.
161    pub(crate) confirmation_provider:
162        Option<Arc<dyn gemini_adk_rs::confirmation::ConfirmationProvider>>,
163    // Governed flow (DAG) + its enforcement mode.
164    pub(crate) flow: Option<gemini_adk_rs::flow::Flow>,
165    pub(crate) flow_mode: gemini_adk_rs::flow::Enforcement,
166    // Per-step on_enter actions: run an agent in a mode when a step activates.
167    pub(crate) flow_actions: Vec<(
168        String,
169        Arc<dyn gemini_adk_rs::text::TextAgent>,
170        gemini_adk_rs::orchestration::Mode,
171    )>,
172    // Wire-log path: a FileWireRecorder is created here at connect time.
173    pub(crate) record_wire_path: Option<std::path::PathBuf>,
174}
175
176impl Live {
177    /// Start building a Live session.
178    ///
179    /// # Examples
180    ///
181    /// Minimal live session setup:
182    ///
183    /// ```rust,ignore
184    /// use gemini_adk_fluent_rs::prelude::*;
185    ///
186    /// let handle = Live::builder()
187    ///     .model(GeminiModel::Gemini2_0FlashLive)
188    ///     .voice(Voice::Kore)
189    ///     .instruction("You are a helpful assistant")
190    ///     .greeting("Hello! How can I help?")
191    ///     .on_audio(|data| { /* send to speaker */ })
192    ///     .on_text(|t| print!("{t}"))
193    ///     .connect_google_ai("API_KEY")
194    ///     .await?;
195    ///
196    /// handle.send_text("What is the weather?").await?;
197    /// handle.disconnect().await?;
198    /// ```
199    ///
200    /// With phases and state-based transitions:
201    ///
202    /// ```rust,ignore
203    /// let handle = Live::builder()
204    ///     .model(GeminiModel::Gemini2_0FlashLive)
205    ///     .phase("greeting")
206    ///         .instruction("Welcome the user")
207    ///         .transition("main", S::is_true("greeted"))
208    ///         .done()
209    ///     .phase("main")
210    ///         .instruction("Help the user")
211    ///         .terminal()
212    ///         .done()
213    ///     .initial_phase("greeting")
214    ///     .connect_google_ai("API_KEY")
215    ///     .await?;
216    /// ```
217    pub fn builder() -> Self {
218        Self {
219            config: SessionConfig::from_endpoint(ApiEndpoint::google_ai("")),
220            callbacks: EventCallbacks::default(),
221            dispatcher: None,
222            extractors: Vec::new(),
223            computed: ComputedRegistry::new(),
224            phases: Vec::new(),
225            initial_phase: None,
226            watchers: WatcherRegistry::new(),
227            temporal: TemporalRegistry::new(),
228            greeting: None,
229            phase_default_modifiers: Vec::new(),
230            phase_default_prompt_on_enter: false,
231            tool_execution_modes: HashMap::new(),
232            deferred_agent_tools: Vec::new(),
233            deferred_tools: Vec::new(),
234            warm_up_llms: Vec::new(),
235            soft_turn_timeout: None,
236            steering_mode: SteeringMode::default(),
237            context_delivery: ContextDelivery::default(),
238            delivery: gemini_adk_rs::live::DeliveryConfig::default(),
239            repair_config: None,
240            persistence: None,
241            session_id: None,
242            tool_advisory: true,
243            telemetry_interval: None,
244            middleware_layers: Vec::new(),
245            confirmation_provider: None,
246            flow: None,
247            flow_mode: gemini_adk_rs::flow::Enforcement::Enforce,
248            flow_actions: Vec::new(),
249            record_wire_path: None,
250        }
251    }
252
253    /// Govern the session with a [`Flow`](gemini_adk_rs::flow::Flow) DAG and
254    /// **enforce** it: inadmissible tool calls are blocked and active-step
255    /// postures steer the model at each turn boundary.
256    pub fn govern(mut self, flow: gemini_adk_rs::flow::Flow) -> Self {
257        self.flow = Some(flow);
258        self.flow_mode = gemini_adk_rs::flow::Enforcement::Enforce;
259        self
260    }
261
262    /// Attach a [`Flow`](gemini_adk_rs::flow::Flow) in **observe** mode: nothing
263    /// is blocked, but deviations are recorded for audit/analytics.
264    pub fn observe(mut self, flow: gemini_adk_rs::flow::Flow) -> Self {
265        self.flow = Some(flow);
266        self.flow_mode = gemini_adk_rs::flow::Enforcement::Observe;
267        self
268    }
269
270    /// Govern the session with a pre-compiled
271    /// [`CompiledFlow`](gemini_adk_rs::flow::CompiledFlow) and **enforce** it.
272    ///
273    /// A `CompiledFlow` carries proof that
274    /// [`Flow::compile`](gemini_adk_rs::flow::Flow::compile) (or
275    /// [`Flow::compile_with_tools`](gemini_adk_rs::flow::Flow::compile_with_tools))
276    /// already surfaced its diagnostics, so connect does **not** re-validate or
277    /// re-compile it — compile once at load time, govern many sessions.
278    pub fn govern_compiled(self, flow: gemini_adk_rs::flow::CompiledFlow) -> Self {
279        self.govern(flow.into_flow())
280    }
281
282    /// Attach a pre-compiled
283    /// [`CompiledFlow`](gemini_adk_rs::flow::CompiledFlow) in **observe** mode:
284    /// nothing is blocked, but deviations are recorded for audit/analytics.
285    /// Like [`govern_compiled`](Self::govern_compiled), the flow is not
286    /// re-validated or re-compiled at connect.
287    pub fn observe_compiled(self, flow: gemini_adk_rs::flow::CompiledFlow) -> Self {
288        self.observe(flow.into_flow())
289    }
290
291    /// Run an agent the first time the named flow step becomes active.
292    ///
293    /// The agent reads its inputs from `State` and its result lands in
294    /// `{step}:result` ([`AgentMode::Call`] resolves inline at the turn boundary;
295    /// [`AgentMode::Dispatch`]/[`AgentMode::Background`] run detached). A
296    /// downstream step can then complete on it via `Guard::resolved(step)`. This
297    /// is how a governed flow drives in-session orchestration. Requires a flow
298    /// (`govern`/`observe`).
299    ///
300    /// [`AgentMode::Call`]: gemini_adk_rs::orchestration::Mode::Call
301    /// [`AgentMode::Dispatch`]: gemini_adk_rs::orchestration::Mode::Dispatch
302    /// [`AgentMode::Background`]: gemini_adk_rs::orchestration::Mode::Background
303    pub fn on_enter(
304        mut self,
305        step: impl Into<String>,
306        agent: Arc<dyn gemini_adk_rs::text::TextAgent>,
307        mode: gemini_adk_rs::orchestration::Mode,
308    ) -> Self {
309        self.flow_actions.push((step.into(), agent, mode));
310        self
311    }
312
313    /// Gate `T::confirm(..)` tools behind a confirmation provider.
314    ///
315    /// When set, any confirmation-gated tool is checked against `provider`
316    /// before it runs; a denied decision returns an error to the model instead
317    /// of executing the tool. Accepts any [`ConfirmationProvider`] — including a
318    /// plain async closure of `Fn(ConfirmationRequest) -> impl Future<Output = ToolConfirmation>`.
319    ///
320    /// [`ConfirmationProvider`]: gemini_adk_rs::confirmation::ConfirmationProvider
321    /// [`ConfirmationRequest`]: gemini_adk_rs::confirmation::ConfirmationRequest
322    /// [`ToolConfirmation`]: gemini_adk_rs::confirmation::ToolConfirmation
323    pub fn confirmation_provider(
324        mut self,
325        provider: Arc<dyn gemini_adk_rs::confirmation::ConfirmationProvider>,
326    ) -> Self {
327        self.confirmation_provider = Some(provider);
328        self
329    }
330
331    /// Attach a [`MiddlewareComposite`](crate::compose::middleware::MiddlewareComposite)
332    /// — every layer runs around tool
333    /// dispatch in the control lane (`before_tool` can veto a call,
334    /// `after_tool` and `on_tool_error` observe results).
335    ///
336    /// Compose layers with `|`, e.g. `M::log() | M::latency()`.
337    ///
338    /// Note: model-level hooks (`before_model`/`after_model`) are TextAgent
339    /// pipeline concepts and do not apply to a streaming Live session.
340    pub fn middleware(
341        mut self,
342        composite: crate::compose::middleware::MiddlewareComposite,
343    ) -> Self {
344        self.middleware_layers.extend(composite.layers);
345        self
346    }
347
348    /// Set the periodic telemetry emission interval.
349    ///
350    /// When set, the processor emits `LiveEvent::Telemetry` snapshots
351    /// and `LiveEvent::TurnMetrics` at this rate.
352    pub fn telemetry_interval(mut self, interval: Duration) -> Self {
353        self.telemetry_interval = Some(interval);
354        self
355    }
356}
357
358#[cfg(test)]
359mod tests {
360    use super::*;
361    use std::sync::Arc;
362    use std::time::Duration;
363
364    #[test]
365    fn builder_chain_compiles() {
366        let _live = Live::builder()
367            .model(GeminiModel::Gemini2_0FlashLive)
368            .voice(Voice::Kore)
369            .instruction("Test")
370            .temperature(0.7)
371            .google_search()
372            .transcription(true, true)
373            .affective_dialog(true)
374            .session_resume(true)
375            .context_compression(4000, 2000)
376            .on_audio(|_data| {})
377            .on_text(|_t| {})
378            .on_vad_start(|| {})
379            .on_interrupted(|| async {})
380            .on_turn_complete(|| async {})
381            .on_go_away(|_d| async {})
382            .on_connected(|_writer| async {})
383            .on_disconnected(|_r| async {})
384            .on_error(|_e| async {});
385        // Just verify the builder chain compiles
386    }
387
388    #[test]
389    fn govern_compiled_attaches_precompiled_flow_without_recompiling() {
390        use gemini_adk_rs::flow::{Enforcement, Flow, Guard};
391
392        let compiled = Flow::new()
393            .step("greet")
394            .done(Guard::is_true("greeted"))
395            .step("end")
396            .after("greet")
397            .terminal()
398            .build()
399            .expect("valid flow")
400            .compile()
401            .expect("flow compiles");
402
403        // Enforce mode.
404        let live = Live::builder().govern_compiled(compiled.clone());
405        assert!(live.flow.is_some(), "compiled flow attached");
406        assert_eq!(live.flow_mode, Enforcement::Enforce);
407
408        // Observe mode.
409        let live = Live::builder().observe_compiled(compiled);
410        assert!(live.flow.is_some(), "compiled flow attached");
411        assert_eq!(live.flow_mode, Enforcement::Observe);
412    }
413
414    #[test]
415    fn builder_with_extraction_compiles() {
416        use gemini_adk_rs::llm::{BaseLlm, LlmError, LlmRequest, LlmResponse};
417        use schemars::JsonSchema;
418
419        #[derive(serde::Deserialize, serde::Serialize, JsonSchema)]
420        struct OrderState {
421            phase: String,
422            items: Vec<String>,
423        }
424
425        struct FakeLlm;
426
427        #[async_trait::async_trait]
428        impl BaseLlm for FakeLlm {
429            fn model_id(&self) -> &str {
430                "fake"
431            }
432            async fn generate(&self, _req: LlmRequest) -> Result<LlmResponse, LlmError> {
433                unimplemented!()
434            }
435        }
436
437        let _live = Live::builder()
438            .model(GeminiModel::Gemini2_0FlashLive)
439            .instruction("Restaurant order assistant")
440            .extract_turns::<OrderState>(
441                Arc::new(FakeLlm),
442                "Extract order state: items, quantities, phase",
443            )
444            .on_extracted(|name, value| async move {
445                let _ = (name, value);
446            })
447            // Outbound interceptors
448            .before_tool_response(|responses, _state| async move {
449                responses // pass through
450            })
451            .on_turn_boundary(|_state, _writer| async move {
452                // inject context
453            })
454            .instruction_template(|state| {
455                let phase: String = state.get("phase").unwrap_or_default();
456                match phase.as_str() {
457                    "ordering" => Some("Take orders accurately.".into()),
458                    _ => None,
459                }
460            });
461        // Just verify the builder chain with all features compiles
462    }
463
464    #[test]
465    fn builder_with_computed_state_compiles() {
466        let _live = Live::builder()
467            .model(GeminiModel::Gemini2_0FlashLive)
468            .instruction("Test computed state")
469            .computed("doubled", &["app:count"], |state| {
470                let count: i64 = state.get("app:count")?;
471                Some(serde_json::json!(count * 2))
472            })
473            .computed("level", &["app:score"], |state| {
474                let score: f64 = state.get("app:score")?;
475                if score > 0.5 {
476                    Some(serde_json::json!("high"))
477                } else {
478                    Some(serde_json::json!("low"))
479                }
480            });
481    }
482
483    #[test]
484    fn builder_with_phases_compiles() {
485        let _live = Live::builder()
486            .model(GeminiModel::Gemini2_0FlashLive)
487            .phase("greeting")
488            .instruction("Welcome the user warmly")
489            .transition("main", |s| s.get::<bool>("greeted").unwrap_or(false))
490            .on_enter(|state, _writer| async move {
491                let _ = state.set("entered_greeting", true);
492            })
493            .done()
494            .phase("main")
495            .dynamic_instruction(|s| {
496                let topic: String = s.get("topic").unwrap_or_default();
497                format!("Discuss {topic}")
498            })
499            .tools(vec!["search".into(), "lookup".into()])
500            .transition("farewell", |s| s.get::<bool>("done").unwrap_or(false))
501            .done()
502            .phase("farewell")
503            .instruction("Say goodbye")
504            .terminal()
505            .done()
506            .initial_phase("greeting");
507    }
508
509    #[test]
510    fn builder_with_phase_guard_compiles() {
511        let _live = Live::builder()
512            .model(GeminiModel::Gemini2_0FlashLive)
513            .phase("start")
514            .instruction("Begin")
515            .transition("secure", |_| true)
516            .done()
517            .phase("secure")
518            .instruction("Secure area")
519            .guard(|s| s.get::<bool>("verified").unwrap_or(false))
520            .on_exit(|state, _writer| async move {
521                let _ = state.set("left_secure", true);
522            })
523            .terminal()
524            .done()
525            .initial_phase("start");
526    }
527
528    #[test]
529    fn builder_with_watchers_compiles() {
530        let _live = Live::builder()
531            .model(GeminiModel::Gemini2_0FlashLive)
532            .watch("app:score")
533            .crossed_above(0.9)
534            .then(|_old, _new, state| async move {
535                let _ = state.set("high_score_alert", true);
536            })
537            .watch("app:status")
538            .changed_to(serde_json::json!("complete"))
539            .blocking()
540            .then(|_old, _new, _state| async move {
541                // blocking action
542            })
543            .watch("app:flag")
544            .became_true()
545            .then(|_old, _new, _state| async move {
546                // flag became true
547            });
548    }
549
550    #[test]
551    fn builder_with_temporal_patterns_compiles() {
552        let _live = Live::builder()
553            .model(GeminiModel::Gemini2_0FlashLive)
554            .when_sustained(
555                "user_confused",
556                |s| s.get::<bool>("confused").unwrap_or(false),
557                Duration::from_secs(30),
558                |_state, _writer| async move {
559                    // offer help
560                },
561            )
562            .when_rate(
563                "rapid_errors",
564                |evt| matches!(evt, SessionEvent::TextDelta(_)),
565                5,
566                Duration::from_secs(10),
567                |_state, _writer| async move {
568                    // throttle
569                },
570            )
571            .when_turns(
572                "stuck_in_loop",
573                |s| s.get::<bool>("repeating").unwrap_or(false),
574                3,
575                |_state, _writer| async move {
576                    // break loop
577                },
578            );
579    }
580
581    #[test]
582    fn builder_full_l1_chain_compiles() {
583        // Full chain combining all L1 features in a single builder
584        let _live = Live::builder()
585            .model(GeminiModel::Gemini2_0FlashLive)
586            .voice(Voice::Kore)
587            .instruction("Full featured agent")
588            // Computed state
589            .computed("sentiment_level", &["app:sentiment_score"], |state| {
590                let score: f64 = state.get("app:sentiment_score")?;
591                if score > 0.7 {
592                    Some(serde_json::json!("positive"))
593                } else if score < 0.3 {
594                    Some(serde_json::json!("negative"))
595                } else {
596                    Some(serde_json::json!("neutral"))
597                }
598            })
599            // Phases
600            .phase("greeting")
601            .instruction("Greet the user")
602            .transition("help", |s| s.get::<bool>("needs_help").unwrap_or(false))
603            .done()
604            .phase("help")
605            .instruction("Help the user")
606            .terminal()
607            .done()
608            .initial_phase("greeting")
609            // Watchers
610            .watch("app:sentiment_score")
611            .crossed_below(0.2)
612            .then(|_old, _new, state| async move {
613                let _ = state.set("alert:low_sentiment", true);
614            })
615            // Temporal
616            .when_turns(
617                "repeated_confusion",
618                |s| s.get::<bool>("confused").unwrap_or(false),
619                3,
620                |_state, _writer| async move {},
621            )
622            // Standard callbacks
623            .on_audio(|_data| {})
624            .on_text(|_t| {})
625            .on_turn_complete(|| async {});
626    }
627
628    #[test]
629    fn builder_with_callback_modes_compiles() {
630        let _live = Live::builder()
631            .model(GeminiModel::Gemini2_0FlashLive)
632            .on_turn_complete_concurrent(|| async {})
633            .on_error_concurrent(|_e| async {})
634            .on_extracted_concurrent(|_name, _val| async {})
635            .on_extraction_error_concurrent(|_name, _err| async {})
636            .on_connected_concurrent(|_w| async {})
637            .on_disconnected_concurrent(|_r| async {})
638            .on_go_away_concurrent(|_d| async {});
639    }
640
641    #[test]
642    fn builder_with_background_tools_compiles() {
643        use gemini_adk_rs::live::DefaultResultFormatter;
644
645        let _live = Live::builder()
646            .model(GeminiModel::Gemini2_0FlashLive)
647            .tool_background("search_kb")
648            .tool_background_with_formatter("analyze_document", Arc::new(DefaultResultFormatter));
649    }
650
651    #[test]
652    fn builder_mixed_callback_modes_and_bg_tools() {
653        use gemini_adk_rs::live::DefaultResultFormatter;
654
655        let _live = Live::builder()
656            .model(GeminiModel::Gemini2_0FlashLive)
657            .voice(Voice::Kore)
658            .instruction("Full featured agent")
659            .tool_background("slow_tool")
660            .tool_background_with_formatter("kb_search", Arc::new(DefaultResultFormatter))
661            .on_turn_complete_concurrent(|| async {})
662            .on_extracted_concurrent(|_name, _val| async {})
663            .on_audio(|_data| {})
664            .on_text(|_t| {})
665            .on_interrupted(|| async {});
666    }
667}