gemini_adk_fluent_rs/live/config.rs
1//! Model, session, and tool configuration methods for `Live`.
2//!
3//! # Boolean-setter rule
4//!
5//! Every L2 fluent builder follows one rule for on/off capabilities:
6//!
7//! - A capability that is **off by default** is enabled by a no-argument verb:
8//! `.transcription()`, `.session_resume()`, `.affective_dialog()`,
9//! `.proactive_audio()`, `.include_thoughts()`, `.prompt_on_enter()`.
10//! - A capability that is **on by default** is disabled by `no_<x>()`:
11//! `.no_tool_advisory()`.
12//! - A `bool` parameter appears only where both values are routinely passed
13//! from data — a `SessionSpec` mapping a field onto the builder — never as
14//! the way an application spells "on".
15//!
16//! There is no `.x(true)` / `.x(false)` pair to remember: the method name says
17//! which way it flips, and the default is the absence of the call.
18
19use std::sync::Arc;
20use std::time::Duration;
21
22use gemini_adk_rs::live::needs::RepairConfig;
23use gemini_adk_rs::live::persistence::SessionPersistence;
24use gemini_adk_rs::live::steering::{ContextDelivery, SteeringMode};
25use gemini_adk_rs::live::{Delivery, DeliveryConfig, ResultFormatter, ToolExecutionMode};
26use gemini_adk_rs::tool::{ToolDispatcher, ToolFunction};
27use gemini_genai_rs::prelude::*;
28
29use super::{DeferredAgentTool, Live};
30
31/// Token count at which the default context window compression kicks in.
32///
33/// Every Live model both platforms serve today has a 128k-token window, and
34/// audio runs at roughly 25 tokens a second in each direction, so this fires
35/// only in a call that is already about an hour old — never in a short or
36/// medium session — while leaving headroom before the hard limit.
37pub const DEFAULT_COMPRESSION_TRIGGER_TOKENS: u32 = 100_000;
38
39/// Token count the default sliding window compresses the history down to.
40///
41/// Half the trigger: enough to keep the last half hour or so of a call in
42/// full, so the model still has the conversation it is in.
43pub const DEFAULT_COMPRESSION_TARGET_TOKENS: u32 = 50_000;
44
45impl Live {
46 // -- Model & Voice --
47
48 /// Set the Gemini model.
49 ///
50 /// Without this, connect resolves a default the target platform actually
51 /// serves (overridable via the `GEMINI_MODEL` environment variable) —
52 /// see the `connect_from_env` docs on [`Live`].
53 pub fn model(mut self, model: ModelId) -> Self {
54 self.config = self.config.model(model);
55 self
56 }
57
58 /// Set the output voice.
59 pub fn voice(mut self, voice: Voice) -> Self {
60 self.config = self.config.voice(voice);
61 self
62 }
63
64 /// Set the system instruction.
65 pub fn instruction(mut self, instruction: impl Into<String>) -> Self {
66 self.config = self.config.system_instruction(instruction);
67 self
68 }
69
70 /// Switch to text-only mode (no audio output).
71 ///
72 /// Sets response modality to `Text` and disables speech config.
73 /// Use with `ModelId::LIVE_2_5_FLASH_NATIVE_AUDIO` for text-only conversations.
74 pub fn text_only(mut self) -> Self {
75 self.config = self.config.text_only();
76 self
77 }
78
79 /// Add a raw `Tool` declaration to the session configuration.
80 ///
81 /// Use this for tools that aren't registered through the `ToolDispatcher`
82 /// (e.g., raw `FunctionDeclaration` lists, Google Search, code execution).
83 pub fn add_tool(mut self, tool: Tool) -> Self {
84 self.config = self.config.add_tool(tool);
85 self
86 }
87
88 /// Set a greeting prompt to trigger the model to initiate the conversation.
89 ///
90 /// When set, this text is sent immediately after the session connects,
91 /// causing the model to respond first (e.g. with a greeting or introduction).
92 ///
93 /// ```no_run
94 /// # use gemini_adk_fluent_rs::prelude::*;
95 /// # async fn run() -> Result<(), AgentError> {
96 /// let handle = Live::builder()
97 /// .instruction("You are a friendly assistant")
98 /// .greeting("Greet the user warmly and introduce yourself.")
99 /// .connect_from_env()
100 /// .await?;
101 /// // Model will speak first without any user input
102 /// # let _ = handle; Ok(())
103 /// # }
104 /// ```
105 pub fn greeting(mut self, prompt: impl Into<String>) -> Self {
106 self.greeting = Some(prompt.into());
107 self
108 }
109
110 /// Set the temperature.
111 pub fn temperature(mut self, temp: f32) -> Self {
112 self.config = self.config.temperature(temp);
113 self
114 }
115
116 // -- Wire recording --
117
118 /// Record every wire byte (both directions) to a JSONL log at `path`.
119 ///
120 /// The log is written by a
121 /// [`FileWireRecorder`](gemini_genai_rs::transport::FileWireRecorder) created at connect time (a connect error is returned if the file cannot
122 /// be created). Replay it offline with `adk session replay <path>` or
123 /// [`gemini_adk_rs::live::replay::replay_session`].
124 ///
125 /// ```no_run
126 /// # use gemini_adk_fluent_rs::prelude::*;
127 /// # async fn run() -> Result<(), AgentError> {
128 /// let handle = Live::builder()
129 /// .record_wire("/tmp/session.wire.jsonl")
130 /// .connect_from_env()
131 /// .await?;
132 /// # let _ = handle; Ok(())
133 /// # }
134 /// ```
135 pub fn record_wire(mut self, path: impl Into<std::path::PathBuf>) -> Self {
136 self.record_wire_path = Some(path.into());
137 self
138 }
139
140 /// Record every wire byte to a custom
141 /// [`WireRecorder`](gemini_genai_rs::transport::WireRecorder) implementation.
142 ///
143 /// Overrides (and is overridden by) the most recent of this and
144 /// [`record_wire`](Self::record_wire).
145 pub fn wire_recorder(
146 mut self,
147 recorder: Arc<dyn gemini_genai_rs::transport::WireRecorder>,
148 ) -> Self {
149 self.record_wire_path = None;
150 self.config = self.config.record_wire(recorder);
151 self
152 }
153
154 // -- Tools --
155
156 /// Register tools: a `+`-combined [`ToolComposite`] from the `T`
157 /// namespace, or a single [`ToolFunction`] (a `SimpleTool`/`TypedTool`,
158 /// the value a `#[tool]` function returns, an `Arc<dyn ToolFunction>`).
159 ///
160 /// Runtime tools go into the session's dispatcher (created on demand),
161 /// built-ins into the session config, agent tools share the session
162 /// `State`, and `T::mcp(..)` toolsets are connected at `connect`.
163 ///
164 /// ```no_run
165 /// # use gemini_adk_fluent_rs::prelude::*;
166 /// #[derive(serde::Deserialize, schemars::JsonSchema)]
167 /// struct City {
168 /// /// The city to report on.
169 /// city: String,
170 /// }
171 ///
172 /// Live::builder()
173 /// .tools(
174 /// T::typed("get_weather", "Get weather", |args: City| async move {
175 /// Ok(serde_json::json!({ "city": args.city, "temp": 22 }))
176 /// })
177 /// + T::google_search()
178 /// );
179 /// ```
180 ///
181 /// [`ToolComposite`]: crate::compose::tools::ToolComposite
182 pub fn tools(mut self, tools: impl Into<crate::compose::tools::ToolComposite>) -> Self {
183 use crate::compose::tools::ToolResolution;
184 for entry in tools.into().entries {
185 match entry.classify() {
186 ToolResolution::Runtime(f) => {
187 self.dispatcher
188 .get_or_insert_with(ToolDispatcher::new)
189 .register_function(f);
190 }
191 ToolResolution::BuiltIn(tool) => {
192 self.config = self.config.add_tool(tool);
193 }
194 ToolResolution::Agent {
195 name,
196 description,
197 agent,
198 } => {
199 // Reuse the deferred agent-tool path so the sub-agent shares
200 // the session State created at connect time.
201 self.deferred_agent_tools.push(DeferredAgentTool {
202 name,
203 description,
204 agent,
205 });
206 }
207 ToolResolution::Deferred(deferred) => {
208 // MCP toolsets need an async connection; resolve them at
209 // connect time (see build_and_connect).
210 self.deferred_tools.push(deferred);
211 }
212 }
213 }
214 self
215 }
216
217 /// Give the model a code tool, `run_code`: it writes a short Python
218 /// program whose functions are the tools named here, and reads only the
219 /// program's final value. Each call the program makes is governed like a
220 /// direct call (phase, decisions, flow gate, middleware), and a refused
221 /// call raises `RuntimeError` in the program.
222 ///
223 /// Use it for reads the model would otherwise add up, sort or join in its
224 /// head, and for chains of dependent reads; leave commits out. Measured
225 /// on Gemini 3.8 Live over 120 payments: right in 5 of 5 runs against 2
226 /// of 5 with function calls, with 77% fewer prompt tokens.
227 ///
228 /// ```no_run
229 /// # use gemini_adk_fluent_rs::prelude::*;
230 /// use gemini_adk_fluent_rs::code_mode::CodeMode;
231 /// Live::builder().code_mode(
232 /// CodeMode::new()
233 /// .function_returning(
234 /// "lookup_payment_history",
235 /// r#"{"payments": [{"paid": str, "due": str, "amount": float}]}"#,
236 /// )
237 /// .function("check_availability"),
238 /// );
239 /// ```
240 pub fn code_mode(mut self, code: gemini_adk_rs::code_mode::CodeMode) -> Self {
241 self.code_mode = Some(code);
242 self
243 }
244
245 /// Register one tool — anything that implements [`ToolFunction`].
246 ///
247 /// ```no_run
248 /// # use gemini_adk_fluent_rs::prelude::*;
249 /// #[tool("Get the weather for a city")]
250 /// async fn get_weather(city: String) -> Result<serde_json::Value, ToolError> {
251 /// Ok(serde_json::json!({"city": city, "temp": 22}))
252 /// }
253 /// Live::builder().tool(get_weather());
254 /// ```
255 pub fn tool(self, f: impl ToolFunction + 'static) -> Self {
256 self.tools(crate::compose::tools::ToolComposite::from_function(
257 Arc::new(f),
258 ))
259 }
260
261 /// Use a [`ToolDispatcher`] you built yourself as the session's dispatcher
262 /// — the escape hatch for streaming tools, input-streaming tools, or a
263 /// dispatcher shared with other components.
264 ///
265 /// Tools already registered through [`tools`](Self::tools) or
266 /// [`tool`](Self::tool) are kept: they are merged into `dispatcher`, whose
267 /// own tools, timeout and confirmation provider win on a name clash.
268 /// Tools registered after this call are added to it.
269 pub fn dispatcher(mut self, mut dispatcher: ToolDispatcher) -> Self {
270 if let Some(registered) = self.dispatcher.take() {
271 dispatcher.merge(registered);
272 }
273 self.dispatcher = Some(dispatcher);
274 self
275 }
276
277 /// Register a text agent as a tool the live model can call.
278 ///
279 /// The agent shares the session's `State`, so it can read live-extracted
280 /// values and its mutations are visible to watchers and phase transitions.
281 ///
282 /// ```no_run
283 /// # use gemini_adk_fluent_rs::prelude::*;
284 /// # let verifier_agent = FnTextAgent::new("verifier", |_| Ok("verified".to_string()));
285 /// # let calc_pipeline = FnTextAgent::new("calc", |_| Ok("plan".to_string()));
286 /// Live::builder()
287 /// .agent_tool("verify_identity", "Verify caller identity", verifier_agent)
288 /// .agent_tool("calc_payment", "Calculate payment plans", calc_pipeline);
289 /// ```
290 pub fn agent_tool(
291 mut self,
292 name: impl Into<String>,
293 description: impl Into<String>,
294 agent: impl gemini_adk_rs::text::TextAgent + 'static,
295 ) -> Self {
296 self.deferred_agent_tools.push(DeferredAgentTool {
297 name: name.into(),
298 description: description.into(),
299 agent: Arc::new(agent),
300 });
301 self
302 }
303
304 /// Register a text agent (already `Arc`'d) as a tool.
305 #[deprecated(
306 since = "2.1.0",
307 note = "`agent_tool` accepts an `Arc<dyn TextAgent>` directly; use `agent_tool`"
308 )]
309 pub fn agent_tool_arc(
310 mut self,
311 name: impl Into<String>,
312 description: impl Into<String>,
313 agent: Arc<dyn gemini_adk_rs::text::TextAgent>,
314 ) -> Self {
315 self.deferred_agent_tools.push(DeferredAgentTool {
316 name: name.into(),
317 description: description.into(),
318 agent,
319 });
320 self
321 }
322
323 /// Enable Google Search built-in tool.
324 pub fn google_search(mut self) -> Self {
325 self.config = self.config.with_google_search();
326 self
327 }
328
329 /// Enable code execution built-in tool.
330 pub fn code_execution(mut self) -> Self {
331 self.config = self.config.with_code_execution();
332 self
333 }
334
335 /// Enable URL context built-in tool.
336 pub fn url_context(mut self) -> Self {
337 self.config = self.config.with_url_context();
338 self
339 }
340
341 /// Mark a tool for background execution (zero dead-air).
342 ///
343 /// When the model calls this tool, an immediate "running" acknowledgment
344 /// is sent back while the tool executes in a background task. The final
345 /// result is delivered asynchronously when complete.
346 pub fn tool_background(mut self, tool_name: impl Into<String>) -> Self {
347 self.tool_execution_modes.insert(
348 tool_name.into(),
349 ToolExecutionMode::Background {
350 formatter: None,
351 scheduling: None,
352 },
353 );
354 self
355 }
356
357 /// Mark a tool for background execution with a custom result formatter.
358 ///
359 /// The formatter controls the shape of the acknowledgment ("running"),
360 /// completion, and cancellation messages sent to the model.
361 pub fn tool_background_with_formatter(
362 mut self,
363 tool_name: impl Into<String>,
364 formatter: Arc<dyn ResultFormatter>,
365 ) -> Self {
366 self.tool_execution_modes.insert(
367 tool_name.into(),
368 ToolExecutionMode::Background {
369 formatter: Some(formatter),
370 scheduling: None,
371 },
372 );
373 self
374 }
375
376 /// Mark a tool for background execution with a specific scheduling mode.
377 ///
378 /// The scheduling mode controls how the model handles async results:
379 /// - `Interrupt`: halts current output, immediately reports the result
380 /// - `WhenIdle`: waits until current output finishes before handling
381 /// - `Silent`: integrates the result without notifying the user
382 pub fn tool_background_with_scheduling(
383 mut self,
384 tool_name: impl Into<String>,
385 scheduling: gemini_genai_rs::prelude::FunctionResponseScheduling,
386 ) -> Self {
387 self.tool_execution_modes.insert(
388 tool_name.into(),
389 ToolExecutionMode::Background {
390 formatter: None,
391 scheduling: Some(scheduling),
392 },
393 );
394 self
395 }
396
397 // -- Audio/Video Config --
398
399 /// Enable transcription of both the user's speech and the model's audio
400 /// (delivered to `on_input_transcript` / `on_output_transcript`).
401 pub fn transcription(self) -> Self {
402 self.input_transcription().output_transcription()
403 }
404
405 /// Enable transcription of the user's speech only.
406 pub fn input_transcription(mut self) -> Self {
407 self.config = self.config.input_transcription(true);
408 self
409 }
410
411 /// Enable transcription of the model's audio output only.
412 pub fn output_transcription(mut self) -> Self {
413 self.config = self.config.output_transcription(true);
414 self
415 }
416
417 /// Enable thinking/reasoning with a token budget (Gemini 2.5+).
418 ///
419 /// Sets the thinking budget for the Live session. Use with
420 /// `.include_thoughts()` and `.on_thought()` to receive thought summaries.
421 ///
422 /// ```no_run
423 /// # use gemini_adk_fluent_rs::prelude::*;
424 /// Live::builder()
425 /// .thinking(1024)
426 /// .include_thoughts()
427 /// .on_thought(|text| println!("[Thought] {text}"));
428 /// ```
429 ///
430 /// **Platform support:** Google AI only. On Vertex AI, `thinkingConfig`
431 /// is automatically stripped from the setup message.
432 pub fn thinking(mut self, budget: u32) -> Self {
433 self.config = self.config.thinking(budget);
434 self
435 }
436
437 /// Set the thinking level, for models that take one instead of a budget.
438 /// Gemini 3.8 Live Extended Thinking
439 /// ([`ModelId::LIVE_3_8_EXTENDED_THINKING`]) refuses a session without it.
440 pub fn thinking_level(mut self, level: ThinkingLevel) -> Self {
441 self.config = self.config.thinking_level(level);
442 self
443 }
444
445 /// Include the model's thought summaries in responses.
446 ///
447 /// When enabled, the model emits `SessionEvent::Thought` events containing
448 /// its reasoning process. Register an `.on_thought()` callback to receive them.
449 ///
450 /// **Platform support:** Google AI only. Stripped on Vertex AI.
451 pub fn include_thoughts(mut self) -> Self {
452 self.config = self.config.include_thoughts(true);
453 self
454 }
455
456 /// Enable affective dialog (emotionally expressive responses).
457 pub fn affective_dialog(mut self) -> Self {
458 self.config = self.config.affective_dialog(true);
459 self
460 }
461
462 /// Enable proactive audio: the model may decide not to respond to input
463 /// it judges not addressed to it. Pair with
464 /// [`soft_turn_timeout`](Self::soft_turn_timeout).
465 ///
466 /// Vertex AI only: Google AI refuses a setup carrying `proactivity`, so
467 /// it is left off there, as it is for Gemini 3.8 Live (always on).
468 pub fn proactive_audio(mut self) -> Self {
469 self.config = self.config.proactive_audio(true);
470 self
471 }
472
473 /// Set media resolution for video/image input.
474 pub fn media_resolution(mut self, res: MediaResolution) -> Self {
475 self.config = self.config.media_resolution(res);
476 self
477 }
478
479 /// Answer with Gemini 3.8 Live Avatar video (Vertex AI): synchronized
480 /// 24 FPS video of a prebuilt or custom avatar, delivered to
481 /// [`on_media`](Self::on_media). Sets the response modality to `VIDEO`,
482 /// as the API requires. Google AI's `gemini-3.8-live` refuses `VIDEO`.
483 ///
484 /// ```no_run
485 /// # use gemini_adk_fluent_rs::prelude::*;
486 /// Live::builder()
487 /// .model(ModelId::LIVE_3_8)
488 /// .avatar(AvatarConfig::prebuilt("Ben"))
489 /// .on_media(|chunk| println!("{} bytes of {}", chunk.data.len(), chunk.mime_type));
490 /// ```
491 pub fn avatar(mut self, avatar: AvatarConfig) -> Self {
492 self.config = self.config.avatar(avatar);
493 self
494 }
495
496 /// Speak in a voice replicated from a recorded sample (Gemini 3.8 Live
497 /// on Vertex AI; allow-listed customers).
498 pub fn replicated_voice(mut self, voice: ReplicatedVoiceConfig) -> Self {
499 self.config = self.config.replicated_voice(voice);
500 self
501 }
502
503 /// Bias transcription of the user's speech toward domain terms —
504 /// product names, SKUs, proper nouns (Gemini 3.8 Live). Enables input
505 /// transcription.
506 pub fn custom_vocabulary<S: Into<String>>(
507 mut self,
508 terms: impl IntoIterator<Item = S>,
509 ) -> Self {
510 self.config = self.config.custom_vocabulary(terms);
511 self
512 }
513
514 /// Transcribe the user's speech with these settings (language hints,
515 /// custom vocabulary).
516 pub fn input_transcription_config(mut self, config: AudioTranscriptionConfig) -> Self {
517 self.config = self.config.input_transcription_config(config);
518 self
519 }
520
521 /// Transcribe the model's speech with these settings.
522 pub fn output_transcription_config(mut self, config: AudioTranscriptionConfig) -> Self {
523 self.config = self.config.output_transcription_config(config);
524 self
525 }
526
527 // -- VAD & Activity --
528
529 /// Run the RNNoise speech enhancer over outgoing mic audio inside
530 /// `send_audio` *(feature `denoise`)* — the same stage as
531 /// [`voice::Denoiser`](crate::voice::Denoiser), applied server-side to
532 /// hosted surfaces (web bridge, API server) that do not run a local
533 /// pump. See the hardening chapter for the measured benchmark.
534 #[cfg(feature = "denoise")]
535 pub fn mic_denoise(mut self) -> Self {
536 self.input_audio.stages.push(InputStage::Denoise);
537 self
538 }
539
540 /// Chain a [`NoiseGate`](crate::voice::NoiseGate) over outgoing mic
541 /// audio inside `send_audio`, after any denoiser — frames whose RMS
542 /// falls below `threshold_rms` are silenced, with `hold_frames` of
543 /// hangover. Calibrate the threshold between the caller's level and the
544 /// background (measured sweet spot 400–700 behind the denoiser).
545 pub fn mic_noise_gate(mut self, threshold_rms: f64, hold_frames: u32) -> Self {
546 self.input_audio.stages.push(InputStage::NoiseGate {
547 threshold_rms,
548 hold_frames,
549 });
550 self
551 }
552
553 /// Chain any [`InputAudioProcessor`](gemini_adk_rs::live::InputAudioProcessor)
554 /// over outgoing mic audio inside `send_audio` — the open slot for
555 /// application-side stages (a DeepFilterNet enhancer, AGC, a custom
556 /// filter). Stages run in the order configured.
557 pub fn mic_processor(
558 mut self,
559 processor: impl gemini_adk_rs::live::InputAudioProcessor + 'static,
560 ) -> Self {
561 self.input_audio
562 .stages
563 .push(InputStage::Custom(Box::new(processor)));
564 self
565 }
566
567 /// Replace the input VAD's configuration (the detector that runs inside
568 /// `send_audio` for client-side speech edges). Use
569 /// [`VadConfig::noisy_street()`](gemini_genai_rs::vad::VadConfig::noisy_street)
570 /// behind `mic_denoise` (the `denoise` feature) for noisy environments.
571 pub fn input_vad(mut self, config: gemini_genai_rs::vad::VadConfig) -> Self {
572 self.input_audio.vad = Some(config);
573 self
574 }
575
576 /// Give this client's input VAD interruption authority: the session is
577 /// configured with the server's automatic activity detection disabled,
578 /// and `send_audio` emits `activityStart`/`activityEnd` on the input
579 /// VAD's speech edges. Measured ~2× faster barge-in than server
580 /// authority; pair with `mic_denoise` (the `denoise` feature) and
581 /// [`mic_noise_gate`](Self::mic_noise_gate) or noise will drive the
582 /// marks.
583 pub fn client_interruption_authority(mut self) -> Self {
584 self.input_audio.client_authority = true;
585 self
586 }
587
588 /// Install a turn-commit policy between the input VAD's speech edges and
589 /// the activity marks sent under
590 /// [`client_interruption_authority`](Self::client_interruption_authority).
591 ///
592 /// Raw edges make two measured mistakes as turn signals (TurnBench dev
593 /// set, 38 real dyadic conversations): committing end-of-turn during
594 /// mid-turn pauses (fp 0.206 raw -> 0.087 with an 800 ms hold), and
595 /// treating backchannels ("mm-hm") over model speech as barge-ins
596 /// (fp 0.702 raw -> 0.062 with a 1400 ms sustain). See
597 /// [`TurnCommitConfig`](gemini_adk_rs::live::TurnCommitConfig) for the
598 /// presets carrying those operating points.
599 pub fn turn_commit(mut self, config: gemini_adk_rs::live::TurnCommitConfig) -> Self {
600 self.input_audio.turn_commit = Some(config);
601 self
602 }
603
604 /// [`turn_commit`](Self::turn_commit) with millisecond knobs.
605 pub fn turn_commit_ms(self, eot_hold_ms: u64, min_interruption_ms: u64) -> Self {
606 self.turn_commit(gemini_adk_rs::live::TurnCommitConfig {
607 eot_hold: std::time::Duration::from_millis(eot_hold_ms),
608 min_interruption: std::time::Duration::from_millis(min_interruption_ms),
609 })
610 }
611
612 /// Set only the end-of-turn hold, keeping (or defaulting) the sustain —
613 /// one of the two granular forms Flow Studio codegen emits.
614 pub fn turn_commit_eot_hold_ms(mut self, eot_hold_ms: u64) -> Self {
615 let mut config = self.input_audio.turn_commit.unwrap_or_default();
616 config.eot_hold = std::time::Duration::from_millis(eot_hold_ms);
617 self.input_audio.turn_commit = Some(config);
618 self
619 }
620
621 /// Set only the interruption sustain, keeping (or defaulting) the hold.
622 pub fn turn_commit_min_interruption_ms(mut self, min_interruption_ms: u64) -> Self {
623 let mut config = self.input_audio.turn_commit.unwrap_or_default();
624 config.min_interruption = std::time::Duration::from_millis(min_interruption_ms);
625 self.input_audio.turn_commit = Some(config);
626 self
627 }
628
629 /// Configure server-side VAD.
630 pub fn vad(mut self, detection: AutomaticActivityDetection) -> Self {
631 self.config = self.config.server_vad(detection);
632 self
633 }
634
635 /// Set activity handling mode (interrupts vs no-interruption).
636 pub fn activity_handling(mut self, handling: ActivityHandling) -> Self {
637 self.config = self.config.activity_handling(handling);
638 self
639 }
640
641 /// Set turn coverage mode.
642 pub fn turn_coverage(mut self, coverage: TurnCoverage) -> Self {
643 self.config = self.config.turn_coverage(coverage);
644 self
645 }
646
647 // -- Session Lifecycle --
648
649 /// Enable session resumption: the server issues resumption handles that
650 /// [`session_resume_from`](Self::session_resume_from) accepts on the next
651 /// connect.
652 pub fn session_resume(mut self) -> Self {
653 self.config = self.config.session_resumption();
654 self
655 }
656
657 /// Enable session resumption in transparent mode (Vertex AI only; left off
658 /// the wire on Google AI): resumption updates
659 /// also name the last client message the server consumed, so a resumed
660 /// session knows what to send again.
661 pub fn transparent_resumption(mut self) -> Self {
662 self.config = self.config.transparent_resumption();
663 self
664 }
665
666 /// Accept conversation history sent as client content before the first
667 /// turn — required by Gemini 3.8 Live to seed history.
668 pub fn history_in_client_content(mut self) -> Self {
669 self.config = self.config.initial_history_in_client_content(true);
670 self
671 }
672
673 /// Ask the server for explicit voice-activity events at the edges of
674 /// user speech (Vertex AI; left off the wire on Google AI).
675 pub fn explicit_vad_signal(mut self) -> Self {
676 self.config = self.config.explicit_vad_signal(true);
677 self
678 }
679
680 /// Resume a previous session from a server-issued resumption handle.
681 ///
682 /// Capture the handle from the old session before it ends — via
683 /// [`LiveHandle::resume_handle`](gemini_adk_rs::live::LiveHandle::resume_handle)
684 /// (e.g. inside the `on_go_away` callback) or from a persisted
685 /// [`SessionSnapshot`](gemini_adk_rs::live::SessionSnapshot) — and pass it
686 /// here on the next connect. Resumption stays enabled for the new session,
687 /// so fresh handles keep arriving. No automatic reconnect is performed.
688 pub fn session_resume_from(mut self, handle: impl Into<String>) -> Self {
689 self.config = self.config.resume_from(handle);
690 self
691 }
692
693 /// Set the context window compression thresholds.
694 ///
695 /// Compression is **on by default** at
696 /// [`DEFAULT_COMPRESSION_TRIGGER_TOKENS`] / [`DEFAULT_COMPRESSION_TARGET_TOKENS`],
697 /// so a long call keeps going instead of ending when the model's context
698 /// fills. Use this to tune the thresholds;
699 /// [`no_context_compression`](Self::no_context_compression) turns it off.
700 pub fn context_compression(mut self, trigger_tokens: u32, target_tokens: u32) -> Self {
701 self.config = self
702 .config
703 .context_window_compression(target_tokens)
704 .context_window_trigger_tokens(trigger_tokens);
705 self
706 }
707
708 /// Disable context window compression.
709 ///
710 /// Without it the server ends the session once the model's context window
711 /// is full. Turn it off only when a session is known to be short, or when
712 /// the whole history must stay verbatim for its full length.
713 pub fn no_context_compression(mut self) -> Self {
714 self.config.context_window_compression = None;
715 self
716 }
717
718 // -- Control Plane --
719
720 /// Enable soft turn detection for proactive silence awareness.
721 ///
722 /// When `proactiveAudio` is enabled, the model may choose not to respond.
723 /// After VAD end, if the model stays silent for `timeout`, a lightweight
724 /// "soft turn" updates state and fires watchers without forcing a response.
725 pub fn soft_turn_timeout(mut self, timeout: Duration) -> Self {
726 self.soft_turn_timeout = Some(timeout);
727 self
728 }
729
730 /// Set the steering mode for how the phase machine delivers instructions.
731 ///
732 /// - `InstructionUpdate` (default): Replace system instruction on transition.
733 /// - `ContextInjection`: Inject steering via `send_client_content`.
734 /// - `Hybrid`: Instruction on transition, context injection per turn.
735 /// - `ContextUpdate`: Gemini 3.8 Live only. On each phase or flow-step
736 /// change, a `contextUpdate` replaces the instruction (base plus phase)
737 /// and the declared tools, so the model is offered only the current
738 /// phase's and step's tools. Falls back to `Hybrid` on other models.
739 pub fn steering_mode(mut self, mode: SteeringMode) -> Self {
740 self.steering_mode = Some(mode);
741 self
742 }
743
744 /// Set when model-role context turns are delivered to the wire.
745 ///
746 /// - `Immediate` (default): Send as a single batched frame during
747 /// TurnComplete processing.
748 /// - `Deferred`: Queue context and flush before the next user send
749 /// (`send_audio`/`send_text`/`send_video`). Eliminates isolated
750 /// WebSocket frames during silence that can confuse the model.
751 ///
752 /// ```no_run
753 /// # use gemini_adk_fluent_rs::prelude::*;
754 /// Live::builder()
755 /// .steering_mode(SteeringMode::ContextInjection)
756 /// .context_delivery(ContextDelivery::Deferred)
757 /// .phase("greeting")
758 /// .instruction("Welcome the guest")
759 /// .done()
760 /// .initial_phase("greeting");
761 /// ```
762 pub fn context_delivery(mut self, mode: ContextDelivery) -> Self {
763 self.context_delivery = mode;
764 self
765 }
766
767 /// Set the fast-lane delivery (backpressure) policy for every event class.
768 ///
769 /// The event router forwards fast-lane frames (audio, text, transcripts,
770 /// thoughts, VAD, phase) to the fast-lane consumer over a bounded channel.
771 /// By default every class is [`Delivery::Lossless`] — the router awaits
772 /// (`send().await`) when the channel is full, which preserves the historical
773 /// behavior. Opt classes into [`Delivery::LossyDropNewest`] to drop the
774 /// newest frame on overflow instead of stalling the router (and thereby
775 /// stalling control-lane routing too).
776 ///
777 /// ```no_run
778 /// # use gemini_adk_fluent_rs::prelude::*;
779 /// use gemini_adk_fluent_rs::live::{Delivery, DeliveryConfig};
780 /// Live::builder()
781 /// .delivery(DeliveryConfig::default()
782 /// .audio(Delivery::LossyDropNewest)
783 /// .transcript(Delivery::LossyDropNewest));
784 /// ```
785 pub fn delivery(mut self, delivery: DeliveryConfig) -> Self {
786 self.delivery = delivery;
787 self
788 }
789
790 /// Convenience: set the audio class to [`Delivery::LossyDropNewest`] so the
791 /// router never blocks on a slow audio consumer, dropping the newest PCM
792 /// frame on overflow. Other classes keep their current policy.
793 pub fn lossy_audio(mut self) -> Self {
794 self.delivery.audio = Delivery::LossyDropNewest;
795 self
796 }
797
798 /// Convenience: set the transcript class to [`Delivery::LossyDropNewest`].
799 /// Other classes keep their current policy.
800 pub fn lossy_transcript(mut self) -> Self {
801 self.delivery.transcript = Delivery::LossyDropNewest;
802 self
803 }
804
805 /// Install transcript redaction: sensitive strings (card numbers,
806 /// one-time codes, custom patterns) are removed at the event router,
807 /// before callbacks, the transcript buffer, extraction, or persistence
808 /// see the text.
809 ///
810 /// ```no_run
811 /// # use gemini_adk_fluent_rs::prelude::*;
812 /// use gemini_adk_rs::live::redaction::TranscriptRedactor;
813 /// Live::builder()
814 /// .redaction(TranscriptRedactor::new().card_numbers().long_digits(6));
815 /// ```
816 pub fn redaction(
817 mut self,
818 redactor: gemini_adk_rs::live::redaction::TranscriptRedactor,
819 ) -> Self {
820 self.redactor = Some(redactor);
821 self
822 }
823
824 /// Enable the conversation repair protocol.
825 ///
826 /// Tracks unfulfilled `needs` per phase. After `nudge_after` stalled turns,
827 /// injects a gentle nudge. After `escalate_after` turns, sets
828 /// `repair:escalation` in state for phase guards to handle.
829 pub fn repair(mut self, config: RepairConfig) -> Self {
830 self.repair_config = Some(config);
831 self
832 }
833
834 /// Set a session persistence backend for surviving process restarts.
835 pub fn persistence(mut self, backend: Arc<dyn SessionPersistence>) -> Self {
836 self.persistence = Some(backend);
837 self
838 }
839
840 /// Set the session ID for persistence.
841 pub fn session_id(mut self, id: impl Into<String>) -> Self {
842 self.session_id = Some(id.into());
843 self
844 }
845
846 /// Disable the tool availability advisory on phase transitions.
847 ///
848 /// By default the SDK injects a model-role context turn telling the model
849 /// which tools are available in the new phase.
850 pub fn no_tool_advisory(mut self) -> Self {
851 self.tool_advisory = false;
852 self
853 }
854}
855
856/// Input-audio hardening configuration accumulated by the builder and
857/// applied to the [`LiveHandle`](gemini_adk_rs::live::LiveHandle) right
858/// after connect. Stages run over each outgoing frame **in the order they
859/// were configured** — chain the denoiser before the gate so the gate
860/// calibrates on clean levels (see `Live::mic_denoise`,
861/// `Live::mic_noise_gate`, `Live::mic_processor`, `Live::input_vad`,
862/// `Live::client_interruption_authority`).
863#[derive(Default)]
864pub struct InputAudioConfig {
865 /// Ordered mic-chain stages.
866 pub stages: Vec<InputStage>,
867 /// Replacement input-VAD configuration.
868 pub vad: Option<gemini_genai_rs::vad::VadConfig>,
869 /// Client VAD sends activity marks; server auto-detection is disabled.
870 pub client_authority: bool,
871 /// Turn-commit policy between VAD edges and activity marks.
872 pub turn_commit: Option<gemini_adk_rs::live::TurnCommitConfig>,
873}
874
875/// One stage of the input mic chain — the named stages the SDK ships plus
876/// an open [`Custom`](Self::Custom) slot for any
877/// [`InputAudioProcessor`](gemini_adk_rs::live::InputAudioProcessor).
878pub enum InputStage {
879 /// RNNoise speech enhancement (feature `denoise`).
880 #[cfg(feature = "denoise")]
881 Denoise,
882 /// Level gate: silences frames whose RMS falls below the threshold.
883 NoiseGate {
884 /// RMS threshold in sample units.
885 threshold_rms: f64,
886 /// Quiet frames the gate stays open after the last loud one.
887 hold_frames: u32,
888 },
889 /// Any caller-supplied processor (denoisers, AGC, custom filters).
890 Custom(Box<dyn gemini_adk_rs::live::InputAudioProcessor>),
891}
892
893/// Everything [`InputAudioConfig::build_processors`] hands the handle:
894/// materialized mic-chain stages, optional VAD replacement, client
895/// activity authority, and the optional turn-commit policy.
896pub(crate) type BuiltInputAudio = (
897 Vec<Box<dyn gemini_adk_rs::live::InputAudioProcessor>>,
898 Option<gemini_genai_rs::vad::VadConfig>,
899 bool,
900 Option<gemini_adk_rs::live::TurnCommitConfig>,
901);
902
903impl InputAudioConfig {
904 /// Whether any part of the input path is configured.
905 pub fn is_configured(&self) -> bool {
906 !self.stages.is_empty()
907 || self.vad.is_some()
908 || self.client_authority
909 || self.turn_commit.is_some()
910 }
911
912 /// Materialize the configured stages into runnable processors.
913 pub(crate) fn build_processors(self) -> BuiltInputAudio {
914 let processors = self
915 .stages
916 .into_iter()
917 .map(
918 |stage| -> Box<dyn gemini_adk_rs::live::InputAudioProcessor> {
919 match stage {
920 #[cfg(feature = "denoise")]
921 InputStage::Denoise => Box::new(crate::voice::Denoiser::new(16_000)),
922 InputStage::NoiseGate {
923 threshold_rms,
924 hold_frames,
925 } => Box::new(crate::voice::NoiseGate::new(threshold_rms, hold_frames)),
926 InputStage::Custom(processor) => processor,
927 }
928 },
929 )
930 .collect();
931 (
932 processors,
933 self.vad,
934 self.client_authority,
935 self.turn_commit,
936 )
937 }
938}
939
940#[cfg(test)]
941mod input_audio_tests {
942 use super::*;
943
944 #[test]
945 fn turn_commit_flows_through_build() {
946 let config = InputAudioConfig {
947 turn_commit: Some(gemini_adk_rs::live::TurnCommitConfig::conversational()),
948 ..Default::default()
949 };
950 assert!(config.is_configured());
951 let (_, _, _, turn_commit) = config.build_processors();
952 assert_eq!(
953 turn_commit,
954 Some(gemini_adk_rs::live::TurnCommitConfig::conversational())
955 );
956 }
957
958 struct Doubler;
959 impl gemini_adk_rs::live::InputAudioProcessor for Doubler {
960 fn process_frame(&mut self, frame: &mut Vec<i16>) {
961 for s in frame.iter_mut() {
962 *s = s.saturating_mul(2);
963 }
964 }
965 }
966
967 #[test]
968 fn stages_run_in_configured_order() {
969 let live = crate::live::Live::builder()
970 .mic_processor(Doubler)
971 .mic_noise_gate(400.0, 3)
972 .input_vad(gemini_genai_rs::vad::VadConfig::noisy_street())
973 .client_interruption_authority();
974 assert!(live.input_audio.is_configured());
975 assert_eq!(live.input_audio.stages.len(), 2);
976 assert!(matches!(live.input_audio.stages[0], InputStage::Custom(_)));
977 assert!(matches!(
978 live.input_audio.stages[1],
979 InputStage::NoiseGate { .. }
980 ));
981 let (mut processors, vad, client, _) = live.input_audio.build_processors();
982 assert_eq!(processors.len(), 2);
983 assert_eq!(vad.unwrap().start_threshold_db, 21.0);
984 assert!(client);
985 // The custom stage actually runs: 100 doubles to 200, then the gate
986 // (RMS 200 < 400) silences the frame — order is observable.
987 let mut frame = vec![100i16; 480];
988 for p in processors.iter_mut() {
989 p.process_frame(&mut frame);
990 }
991 assert!(
992 frame.iter().all(|&s| s == 0),
993 "gate should silence the doubled but still-quiet frame"
994 );
995 }
996}
997
998#[cfg(test)]
999mod compression_default_tests {
1000 use super::*;
1001 use gemini_genai_rs::prelude::{ContextWindowCompressionConfig, SlidingWindow};
1002
1003 #[test]
1004 fn compression_is_on_by_default_at_the_documented_thresholds() {
1005 let live = Live::builder();
1006 assert_eq!(
1007 live.config.context_window_compression,
1008 Some(ContextWindowCompressionConfig {
1009 sliding_window: Some(SlidingWindow {
1010 target_tokens: Some(DEFAULT_COMPRESSION_TARGET_TOKENS),
1011 }),
1012 trigger_tokens: Some(DEFAULT_COMPRESSION_TRIGGER_TOKENS),
1013 })
1014 );
1015 }
1016
1017 #[test]
1018 fn explicit_thresholds_replace_the_default() {
1019 let live = Live::builder().context_compression(4096, 2048);
1020 let cwc = live
1021 .config
1022 .context_window_compression
1023 .expect("compression set");
1024 assert_eq!(cwc.trigger_tokens, Some(4096));
1025 assert_eq!(cwc.sliding_window.and_then(|w| w.target_tokens), Some(2048));
1026 }
1027
1028 #[test]
1029 fn no_context_compression_turns_it_off() {
1030 let live = Live::builder().no_context_compression();
1031 assert_eq!(live.config.context_window_compression, None);
1032 }
1033}