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}