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}