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