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