Skip to main content

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}