Skip to main content

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