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