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    /// Register one tool — anything that implements [`ToolFunction`].
218    ///
219    /// ```no_run
220    /// # use gemini_adk_fluent_rs::prelude::*;
221    /// #[tool("Get the weather for a city")]
222    /// async fn get_weather(city: String) -> Result<serde_json::Value, ToolError> {
223    ///     Ok(serde_json::json!({"city": city, "temp": 22}))
224    /// }
225    /// Live::builder().tool(get_weather());
226    /// ```
227    pub fn tool(self, f: impl ToolFunction + 'static) -> Self {
228        self.tools(crate::compose::tools::ToolComposite::from_function(
229            Arc::new(f),
230        ))
231    }
232
233    /// Use a [`ToolDispatcher`] you built yourself as the session's dispatcher
234    /// — the escape hatch for streaming tools, input-streaming tools, or a
235    /// dispatcher shared with other components.
236    ///
237    /// Tools already registered through [`tools`](Self::tools) or
238    /// [`tool`](Self::tool) are kept: they are merged into `dispatcher`, whose
239    /// own tools, timeout and confirmation provider win on a name clash.
240    /// Tools registered after this call are added to it.
241    pub fn dispatcher(mut self, mut dispatcher: ToolDispatcher) -> Self {
242        if let Some(registered) = self.dispatcher.take() {
243            dispatcher.merge(registered);
244        }
245        self.dispatcher = Some(dispatcher);
246        self
247    }
248
249    /// Register a text agent as a tool the live model can call.
250    ///
251    /// The agent shares the session's `State`, so it can read live-extracted
252    /// values and its mutations are visible to watchers and phase transitions.
253    ///
254    /// ```no_run
255    /// # use gemini_adk_fluent_rs::prelude::*;
256    /// # let verifier_agent = FnTextAgent::new("verifier", |_| Ok("verified".to_string()));
257    /// # let calc_pipeline = FnTextAgent::new("calc", |_| Ok("plan".to_string()));
258    /// Live::builder()
259    ///     .agent_tool("verify_identity", "Verify caller identity", verifier_agent)
260    ///     .agent_tool("calc_payment", "Calculate payment plans", calc_pipeline);
261    /// ```
262    pub fn agent_tool(
263        mut self,
264        name: impl Into<String>,
265        description: impl Into<String>,
266        agent: impl gemini_adk_rs::text::TextAgent + 'static,
267    ) -> Self {
268        self.deferred_agent_tools.push(DeferredAgentTool {
269            name: name.into(),
270            description: description.into(),
271            agent: Arc::new(agent),
272        });
273        self
274    }
275
276    /// Register a text agent (already `Arc`'d) as a tool.
277    #[deprecated(
278        since = "2.1.0",
279        note = "`agent_tool` accepts an `Arc<dyn TextAgent>` directly; use `agent_tool`"
280    )]
281    pub fn agent_tool_arc(
282        mut self,
283        name: impl Into<String>,
284        description: impl Into<String>,
285        agent: Arc<dyn gemini_adk_rs::text::TextAgent>,
286    ) -> Self {
287        self.deferred_agent_tools.push(DeferredAgentTool {
288            name: name.into(),
289            description: description.into(),
290            agent,
291        });
292        self
293    }
294
295    /// Enable Google Search built-in tool.
296    pub fn google_search(mut self) -> Self {
297        self.config = self.config.with_google_search();
298        self
299    }
300
301    /// Enable code execution built-in tool.
302    pub fn code_execution(mut self) -> Self {
303        self.config = self.config.with_code_execution();
304        self
305    }
306
307    /// Enable URL context built-in tool.
308    pub fn url_context(mut self) -> Self {
309        self.config = self.config.with_url_context();
310        self
311    }
312
313    /// Mark a tool for background execution (zero dead-air).
314    ///
315    /// When the model calls this tool, an immediate "running" acknowledgment
316    /// is sent back while the tool executes in a background task. The final
317    /// result is delivered asynchronously when complete.
318    pub fn tool_background(mut self, tool_name: impl Into<String>) -> Self {
319        self.tool_execution_modes.insert(
320            tool_name.into(),
321            ToolExecutionMode::Background {
322                formatter: None,
323                scheduling: None,
324            },
325        );
326        self
327    }
328
329    /// Mark a tool for background execution with a custom result formatter.
330    ///
331    /// The formatter controls the shape of the acknowledgment ("running"),
332    /// completion, and cancellation messages sent to the model.
333    pub fn tool_background_with_formatter(
334        mut self,
335        tool_name: impl Into<String>,
336        formatter: Arc<dyn ResultFormatter>,
337    ) -> Self {
338        self.tool_execution_modes.insert(
339            tool_name.into(),
340            ToolExecutionMode::Background {
341                formatter: Some(formatter),
342                scheduling: None,
343            },
344        );
345        self
346    }
347
348    /// Mark a tool for background execution with a specific scheduling mode.
349    ///
350    /// The scheduling mode controls how the model handles async results:
351    /// - `Interrupt`: halts current output, immediately reports the result
352    /// - `WhenIdle`: waits until current output finishes before handling
353    /// - `Silent`: integrates the result without notifying the user
354    pub fn tool_background_with_scheduling(
355        mut self,
356        tool_name: impl Into<String>,
357        scheduling: gemini_genai_rs::prelude::FunctionResponseScheduling,
358    ) -> Self {
359        self.tool_execution_modes.insert(
360            tool_name.into(),
361            ToolExecutionMode::Background {
362                formatter: None,
363                scheduling: Some(scheduling),
364            },
365        );
366        self
367    }
368
369    // -- Audio/Video Config --
370
371    /// Enable transcription of both the user's speech and the model's audio
372    /// (delivered to `on_input_transcript` / `on_output_transcript`).
373    pub fn transcription(self) -> Self {
374        self.input_transcription().output_transcription()
375    }
376
377    /// Enable transcription of the user's speech only.
378    pub fn input_transcription(mut self) -> Self {
379        self.config = self.config.input_transcription(true);
380        self
381    }
382
383    /// Enable transcription of the model's audio output only.
384    pub fn output_transcription(mut self) -> Self {
385        self.config = self.config.output_transcription(true);
386        self
387    }
388
389    /// Enable thinking/reasoning with a token budget (Gemini 2.5+).
390    ///
391    /// Sets the thinking budget for the Live session. Use with
392    /// `.include_thoughts()` and `.on_thought()` to receive thought summaries.
393    ///
394    /// ```no_run
395    /// # use gemini_adk_fluent_rs::prelude::*;
396    /// Live::builder()
397    ///     .thinking(1024)
398    ///     .include_thoughts()
399    ///     .on_thought(|text| println!("[Thought] {text}"));
400    /// ```
401    ///
402    /// **Platform support:** Google AI only. On Vertex AI, `thinkingConfig`
403    /// is automatically stripped from the setup message.
404    pub fn thinking(mut self, budget: u32) -> Self {
405        self.config = self.config.thinking(budget);
406        self
407    }
408
409    /// Set the thinking level, for models that take one instead of a budget.
410    /// Gemini 3.8 Live Extended Thinking
411    /// ([`ModelId::LIVE_3_8_EXTENDED_THINKING`]) refuses a session without it.
412    pub fn thinking_level(mut self, level: ThinkingLevel) -> Self {
413        self.config = self.config.thinking_level(level);
414        self
415    }
416
417    /// Include the model's thought summaries in responses.
418    ///
419    /// When enabled, the model emits `SessionEvent::Thought` events containing
420    /// its reasoning process. Register an `.on_thought()` callback to receive them.
421    ///
422    /// **Platform support:** Google AI only. Stripped on Vertex AI.
423    pub fn include_thoughts(mut self) -> Self {
424        self.config = self.config.include_thoughts(true);
425        self
426    }
427
428    /// Enable affective dialog (emotionally expressive responses).
429    pub fn affective_dialog(mut self) -> Self {
430        self.config = self.config.affective_dialog(true);
431        self
432    }
433
434    /// Enable proactive audio: the model may decide not to respond to input
435    /// it judges not addressed to it. Pair with
436    /// [`soft_turn_timeout`](Self::soft_turn_timeout).
437    ///
438    /// Vertex AI only: Google AI refuses a setup carrying `proactivity`, so
439    /// it is left off there, as it is for Gemini 3.8 Live (always on).
440    pub fn proactive_audio(mut self) -> Self {
441        self.config = self.config.proactive_audio(true);
442        self
443    }
444
445    /// Set media resolution for video/image input.
446    pub fn media_resolution(mut self, res: MediaResolution) -> Self {
447        self.config = self.config.media_resolution(res);
448        self
449    }
450
451    /// Answer with Gemini 3.8 Live Avatar video (Vertex AI): synchronized
452    /// 24 FPS video of a prebuilt or custom avatar, delivered to
453    /// [`on_media`](Self::on_media). Sets the response modality to `VIDEO`,
454    /// as the API requires. Google AI's `gemini-3.8-live` refuses `VIDEO`.
455    ///
456    /// ```no_run
457    /// # use gemini_adk_fluent_rs::prelude::*;
458    /// Live::builder()
459    ///     .model(ModelId::LIVE_3_8)
460    ///     .avatar(AvatarConfig::prebuilt("Ben"))
461    ///     .on_media(|chunk| println!("{} bytes of {}", chunk.data.len(), chunk.mime_type));
462    /// ```
463    pub fn avatar(mut self, avatar: AvatarConfig) -> Self {
464        self.config = self.config.avatar(avatar);
465        self
466    }
467
468    /// Speak in a voice replicated from a recorded sample (Gemini 3.8 Live
469    /// on Vertex AI; allow-listed customers).
470    pub fn replicated_voice(mut self, voice: ReplicatedVoiceConfig) -> Self {
471        self.config = self.config.replicated_voice(voice);
472        self
473    }
474
475    /// Bias transcription of the user's speech toward domain terms —
476    /// product names, SKUs, proper nouns (Gemini 3.8 Live). Enables input
477    /// transcription.
478    pub fn custom_vocabulary<S: Into<String>>(
479        mut self,
480        terms: impl IntoIterator<Item = S>,
481    ) -> Self {
482        self.config = self.config.custom_vocabulary(terms);
483        self
484    }
485
486    /// Transcribe the user's speech with these settings (language hints,
487    /// custom vocabulary).
488    pub fn input_transcription_config(mut self, config: AudioTranscriptionConfig) -> Self {
489        self.config = self.config.input_transcription_config(config);
490        self
491    }
492
493    /// Transcribe the model's speech with these settings.
494    pub fn output_transcription_config(mut self, config: AudioTranscriptionConfig) -> Self {
495        self.config = self.config.output_transcription_config(config);
496        self
497    }
498
499    // -- VAD & Activity --
500
501    /// Run the RNNoise speech enhancer over outgoing mic audio inside
502    /// `send_audio` *(feature `denoise`)* — the same stage as
503    /// [`voice::Denoiser`](crate::voice::Denoiser), applied server-side to
504    /// hosted surfaces (web bridge, API server) that do not run a local
505    /// pump. See the hardening chapter for the measured benchmark.
506    #[cfg(feature = "denoise")]
507    pub fn mic_denoise(mut self) -> Self {
508        self.input_audio.stages.push(InputStage::Denoise);
509        self
510    }
511
512    /// Chain a [`NoiseGate`](crate::voice::NoiseGate) over outgoing mic
513    /// audio inside `send_audio`, after any denoiser — frames whose RMS
514    /// falls below `threshold_rms` are silenced, with `hold_frames` of
515    /// hangover. Calibrate the threshold between the caller's level and the
516    /// background (measured sweet spot 400–700 behind the denoiser).
517    pub fn mic_noise_gate(mut self, threshold_rms: f64, hold_frames: u32) -> Self {
518        self.input_audio.stages.push(InputStage::NoiseGate {
519            threshold_rms,
520            hold_frames,
521        });
522        self
523    }
524
525    /// Chain any [`InputAudioProcessor`](gemini_adk_rs::live::InputAudioProcessor)
526    /// over outgoing mic audio inside `send_audio` — the open slot for
527    /// application-side stages (a DeepFilterNet enhancer, AGC, a custom
528    /// filter). Stages run in the order configured.
529    pub fn mic_processor(
530        mut self,
531        processor: impl gemini_adk_rs::live::InputAudioProcessor + 'static,
532    ) -> Self {
533        self.input_audio
534            .stages
535            .push(InputStage::Custom(Box::new(processor)));
536        self
537    }
538
539    /// Replace the input VAD's configuration (the detector that runs inside
540    /// `send_audio` for client-side speech edges). Use
541    /// [`VadConfig::noisy_street()`](gemini_genai_rs::vad::VadConfig::noisy_street)
542    /// behind [`mic_denoise`](Self::mic_denoise) for noisy environments.
543    pub fn input_vad(mut self, config: gemini_genai_rs::vad::VadConfig) -> Self {
544        self.input_audio.vad = Some(config);
545        self
546    }
547
548    /// Give this client's input VAD interruption authority: the session is
549    /// configured with the server's automatic activity detection disabled,
550    /// and `send_audio` emits `activityStart`/`activityEnd` on the input
551    /// VAD's speech edges. Measured ~2× faster barge-in than server
552    /// authority; pair with [`mic_denoise`](Self::mic_denoise) (and
553    /// [`mic_noise_gate`](Self::mic_noise_gate)) or noise will drive the
554    /// marks.
555    pub fn client_interruption_authority(mut self) -> Self {
556        self.input_audio.client_authority = true;
557        self
558    }
559
560    /// Install a turn-commit policy between the input VAD's speech edges and
561    /// the activity marks sent under
562    /// [`client_interruption_authority`](Self::client_interruption_authority).
563    ///
564    /// Raw edges make two measured mistakes as turn signals (TurnBench dev
565    /// set, 38 real dyadic conversations): committing end-of-turn during
566    /// mid-turn pauses (fp 0.206 raw -> 0.087 with an 800 ms hold), and
567    /// treating backchannels ("mm-hm") over model speech as barge-ins
568    /// (fp 0.702 raw -> 0.062 with a 1400 ms sustain). See
569    /// [`TurnCommitConfig`](gemini_adk_rs::live::TurnCommitConfig) for the
570    /// presets carrying those operating points.
571    pub fn turn_commit(mut self, config: gemini_adk_rs::live::TurnCommitConfig) -> Self {
572        self.input_audio.turn_commit = Some(config);
573        self
574    }
575
576    /// [`turn_commit`](Self::turn_commit) with millisecond knobs.
577    pub fn turn_commit_ms(self, eot_hold_ms: u64, min_interruption_ms: u64) -> Self {
578        self.turn_commit(gemini_adk_rs::live::TurnCommitConfig {
579            eot_hold: std::time::Duration::from_millis(eot_hold_ms),
580            min_interruption: std::time::Duration::from_millis(min_interruption_ms),
581        })
582    }
583
584    /// Set only the end-of-turn hold, keeping (or defaulting) the sustain —
585    /// one of the two granular forms Flow Studio codegen emits.
586    pub fn turn_commit_eot_hold_ms(mut self, eot_hold_ms: u64) -> Self {
587        let mut config = self.input_audio.turn_commit.unwrap_or_default();
588        config.eot_hold = std::time::Duration::from_millis(eot_hold_ms);
589        self.input_audio.turn_commit = Some(config);
590        self
591    }
592
593    /// Set only the interruption sustain, keeping (or defaulting) the hold.
594    pub fn turn_commit_min_interruption_ms(mut self, min_interruption_ms: u64) -> Self {
595        let mut config = self.input_audio.turn_commit.unwrap_or_default();
596        config.min_interruption = std::time::Duration::from_millis(min_interruption_ms);
597        self.input_audio.turn_commit = Some(config);
598        self
599    }
600
601    /// Configure server-side VAD.
602    pub fn vad(mut self, detection: AutomaticActivityDetection) -> Self {
603        self.config = self.config.server_vad(detection);
604        self
605    }
606
607    /// Set activity handling mode (interrupts vs no-interruption).
608    pub fn activity_handling(mut self, handling: ActivityHandling) -> Self {
609        self.config = self.config.activity_handling(handling);
610        self
611    }
612
613    /// Set turn coverage mode.
614    pub fn turn_coverage(mut self, coverage: TurnCoverage) -> Self {
615        self.config = self.config.turn_coverage(coverage);
616        self
617    }
618
619    // -- Session Lifecycle --
620
621    /// Enable session resumption: the server issues resumption handles that
622    /// [`session_resume_from`](Self::session_resume_from) accepts on the next
623    /// connect.
624    pub fn session_resume(mut self) -> Self {
625        self.config = self.config.session_resumption();
626        self
627    }
628
629    /// Enable session resumption in transparent mode (Vertex AI only; left off
630    /// the wire on Google AI): resumption updates
631    /// also name the last client message the server consumed, so a resumed
632    /// session knows what to send again.
633    pub fn transparent_resumption(mut self) -> Self {
634        self.config = self.config.transparent_resumption();
635        self
636    }
637
638    /// Accept conversation history sent as client content before the first
639    /// turn — required by Gemini 3.8 Live to seed history.
640    pub fn history_in_client_content(mut self) -> Self {
641        self.config = self.config.initial_history_in_client_content(true);
642        self
643    }
644
645    /// Ask the server for explicit voice-activity events at the edges of
646    /// user speech (Vertex AI; left off the wire on Google AI).
647    pub fn explicit_vad_signal(mut self) -> Self {
648        self.config = self.config.explicit_vad_signal(true);
649        self
650    }
651
652    /// Resume a previous session from a server-issued resumption handle.
653    ///
654    /// Capture the handle from the old session before it ends — via
655    /// [`LiveHandle::resume_handle`](gemini_adk_rs::live::LiveHandle::resume_handle)
656    /// (e.g. inside the `on_go_away` callback) or from a persisted
657    /// [`SessionSnapshot`](gemini_adk_rs::live::SessionSnapshot) — and pass it
658    /// here on the next connect. Resumption stays enabled for the new session,
659    /// so fresh handles keep arriving. No automatic reconnect is performed.
660    pub fn session_resume_from(mut self, handle: impl Into<String>) -> Self {
661        self.config = self.config.resume_from(handle);
662        self
663    }
664
665    /// Set the context window compression thresholds.
666    ///
667    /// Compression is **on by default** at
668    /// [`DEFAULT_COMPRESSION_TRIGGER_TOKENS`] / [`DEFAULT_COMPRESSION_TARGET_TOKENS`],
669    /// so a long call keeps going instead of ending when the model's context
670    /// fills. Use this to tune the thresholds;
671    /// [`no_context_compression`](Self::no_context_compression) turns it off.
672    pub fn context_compression(mut self, trigger_tokens: u32, target_tokens: u32) -> Self {
673        self.config = self
674            .config
675            .context_window_compression(target_tokens)
676            .context_window_trigger_tokens(trigger_tokens);
677        self
678    }
679
680    /// Disable context window compression.
681    ///
682    /// Without it the server ends the session once the model's context window
683    /// is full. Turn it off only when a session is known to be short, or when
684    /// the whole history must stay verbatim for its full length.
685    pub fn no_context_compression(mut self) -> Self {
686        self.config.context_window_compression = None;
687        self
688    }
689
690    // -- Control Plane --
691
692    /// Enable soft turn detection for proactive silence awareness.
693    ///
694    /// When `proactiveAudio` is enabled, the model may choose not to respond.
695    /// After VAD end, if the model stays silent for `timeout`, a lightweight
696    /// "soft turn" updates state and fires watchers without forcing a response.
697    pub fn soft_turn_timeout(mut self, timeout: Duration) -> Self {
698        self.soft_turn_timeout = Some(timeout);
699        self
700    }
701
702    /// Set the steering mode for how the phase machine delivers instructions.
703    ///
704    /// - `InstructionUpdate` (default): Replace system instruction on transition.
705    /// - `ContextInjection`: Inject steering via `send_client_content`.
706    /// - `Hybrid`: Instruction on transition, context injection per turn.
707    pub fn steering_mode(mut self, mode: SteeringMode) -> Self {
708        self.steering_mode = mode;
709        self
710    }
711
712    /// Set when model-role context turns are delivered to the wire.
713    ///
714    /// - `Immediate` (default): Send as a single batched frame during
715    ///   TurnComplete processing.
716    /// - `Deferred`: Queue context and flush before the next user send
717    ///   (`send_audio`/`send_text`/`send_video`).  Eliminates isolated
718    ///   WebSocket frames during silence that can confuse the model.
719    ///
720    /// ```no_run
721    /// # use gemini_adk_fluent_rs::prelude::*;
722    /// Live::builder()
723    ///     .steering_mode(SteeringMode::ContextInjection)
724    ///     .context_delivery(ContextDelivery::Deferred)
725    ///     .phase("greeting")
726    ///         .instruction("Welcome the guest")
727    ///         .done()
728    ///     .initial_phase("greeting");
729    /// ```
730    pub fn context_delivery(mut self, mode: ContextDelivery) -> Self {
731        self.context_delivery = mode;
732        self
733    }
734
735    /// Set the fast-lane delivery (backpressure) policy for every event class.
736    ///
737    /// The event router forwards fast-lane frames (audio, text, transcripts,
738    /// thoughts, VAD, phase) to the fast-lane consumer over a bounded channel.
739    /// By default every class is [`Delivery::Lossless`] — the router awaits
740    /// (`send().await`) when the channel is full, which preserves the historical
741    /// behavior. Opt classes into [`Delivery::LossyDropNewest`] to drop the
742    /// newest frame on overflow instead of stalling the router (and thereby
743    /// stalling control-lane routing too).
744    ///
745    /// ```no_run
746    /// # use gemini_adk_fluent_rs::prelude::*;
747    /// use gemini_adk_fluent_rs::live::{Delivery, DeliveryConfig};
748    /// Live::builder()
749    ///     .delivery(DeliveryConfig::default()
750    ///         .audio(Delivery::LossyDropNewest)
751    ///         .transcript(Delivery::LossyDropNewest));
752    /// ```
753    pub fn delivery(mut self, delivery: DeliveryConfig) -> Self {
754        self.delivery = delivery;
755        self
756    }
757
758    /// Convenience: set the audio class to [`Delivery::LossyDropNewest`] so the
759    /// router never blocks on a slow audio consumer, dropping the newest PCM
760    /// frame on overflow. Other classes keep their current policy.
761    pub fn lossy_audio(mut self) -> Self {
762        self.delivery.audio = Delivery::LossyDropNewest;
763        self
764    }
765
766    /// Convenience: set the transcript class to [`Delivery::LossyDropNewest`].
767    /// Other classes keep their current policy.
768    pub fn lossy_transcript(mut self) -> Self {
769        self.delivery.transcript = Delivery::LossyDropNewest;
770        self
771    }
772
773    /// Install transcript redaction: sensitive strings (card numbers,
774    /// one-time codes, custom patterns) are removed at the event router,
775    /// before callbacks, the transcript buffer, extraction, or persistence
776    /// see the text.
777    ///
778    /// ```no_run
779    /// # use gemini_adk_fluent_rs::prelude::*;
780    /// use gemini_adk_rs::live::redaction::TranscriptRedactor;
781    /// Live::builder()
782    ///     .redaction(TranscriptRedactor::new().card_numbers().long_digits(6));
783    /// ```
784    pub fn redaction(
785        mut self,
786        redactor: gemini_adk_rs::live::redaction::TranscriptRedactor,
787    ) -> Self {
788        self.redactor = Some(redactor);
789        self
790    }
791
792    /// Enable the conversation repair protocol.
793    ///
794    /// Tracks unfulfilled `needs` per phase. After `nudge_after` stalled turns,
795    /// injects a gentle nudge. After `escalate_after` turns, sets
796    /// `repair:escalation` in state for phase guards to handle.
797    pub fn repair(mut self, config: RepairConfig) -> Self {
798        self.repair_config = Some(config);
799        self
800    }
801
802    /// Set a session persistence backend for surviving process restarts.
803    pub fn persistence(mut self, backend: Arc<dyn SessionPersistence>) -> Self {
804        self.persistence = Some(backend);
805        self
806    }
807
808    /// Set the session ID for persistence.
809    pub fn session_id(mut self, id: impl Into<String>) -> Self {
810        self.session_id = Some(id.into());
811        self
812    }
813
814    /// Disable the tool availability advisory on phase transitions.
815    ///
816    /// By default the SDK injects a model-role context turn telling the model
817    /// which tools are available in the new phase.
818    pub fn no_tool_advisory(mut self) -> Self {
819        self.tool_advisory = false;
820        self
821    }
822}
823
824/// Input-audio hardening configuration accumulated by the builder and
825/// applied to the [`LiveHandle`](gemini_adk_rs::live::LiveHandle) right
826/// after connect. Stages run over each outgoing frame **in the order they
827/// were configured** — chain the denoiser before the gate so the gate
828/// calibrates on clean levels (see `Live::mic_denoise`,
829/// `Live::mic_noise_gate`, `Live::mic_processor`, `Live::input_vad`,
830/// `Live::client_interruption_authority`).
831#[derive(Default)]
832pub struct InputAudioConfig {
833    /// Ordered mic-chain stages.
834    pub stages: Vec<InputStage>,
835    /// Replacement input-VAD configuration.
836    pub vad: Option<gemini_genai_rs::vad::VadConfig>,
837    /// Client VAD sends activity marks; server auto-detection is disabled.
838    pub client_authority: bool,
839    /// Turn-commit policy between VAD edges and activity marks.
840    pub turn_commit: Option<gemini_adk_rs::live::TurnCommitConfig>,
841}
842
843/// One stage of the input mic chain — the named stages the SDK ships plus
844/// an open [`Custom`](Self::Custom) slot for any
845/// [`InputAudioProcessor`](gemini_adk_rs::live::InputAudioProcessor).
846pub enum InputStage {
847    /// RNNoise speech enhancement (feature `denoise`).
848    #[cfg(feature = "denoise")]
849    Denoise,
850    /// Level gate: silences frames whose RMS falls below the threshold.
851    NoiseGate {
852        /// RMS threshold in sample units.
853        threshold_rms: f64,
854        /// Quiet frames the gate stays open after the last loud one.
855        hold_frames: u32,
856    },
857    /// Any caller-supplied processor (denoisers, AGC, custom filters).
858    Custom(Box<dyn gemini_adk_rs::live::InputAudioProcessor>),
859}
860
861/// Everything [`InputAudioConfig::build_processors`] hands the handle:
862/// materialized mic-chain stages, optional VAD replacement, client
863/// activity authority, and the optional turn-commit policy.
864pub(crate) type BuiltInputAudio = (
865    Vec<Box<dyn gemini_adk_rs::live::InputAudioProcessor>>,
866    Option<gemini_genai_rs::vad::VadConfig>,
867    bool,
868    Option<gemini_adk_rs::live::TurnCommitConfig>,
869);
870
871impl InputAudioConfig {
872    /// Whether any part of the input path is configured.
873    pub fn is_configured(&self) -> bool {
874        !self.stages.is_empty()
875            || self.vad.is_some()
876            || self.client_authority
877            || self.turn_commit.is_some()
878    }
879
880    /// Materialize the configured stages into runnable processors.
881    pub(crate) fn build_processors(self) -> BuiltInputAudio {
882        let processors = self
883            .stages
884            .into_iter()
885            .map(
886                |stage| -> Box<dyn gemini_adk_rs::live::InputAudioProcessor> {
887                    match stage {
888                        #[cfg(feature = "denoise")]
889                        InputStage::Denoise => Box::new(crate::voice::Denoiser::new(16_000)),
890                        InputStage::NoiseGate {
891                            threshold_rms,
892                            hold_frames,
893                        } => Box::new(crate::voice::NoiseGate::new(threshold_rms, hold_frames)),
894                        InputStage::Custom(processor) => processor,
895                    }
896                },
897            )
898            .collect();
899        (
900            processors,
901            self.vad,
902            self.client_authority,
903            self.turn_commit,
904        )
905    }
906}
907
908#[cfg(test)]
909mod input_audio_tests {
910    use super::*;
911
912    #[test]
913    fn turn_commit_flows_through_build() {
914        let config = InputAudioConfig {
915            turn_commit: Some(gemini_adk_rs::live::TurnCommitConfig::conversational()),
916            ..Default::default()
917        };
918        assert!(config.is_configured());
919        let (_, _, _, turn_commit) = config.build_processors();
920        assert_eq!(
921            turn_commit,
922            Some(gemini_adk_rs::live::TurnCommitConfig::conversational())
923        );
924    }
925
926    struct Doubler;
927    impl gemini_adk_rs::live::InputAudioProcessor for Doubler {
928        fn process_frame(&mut self, frame: &mut Vec<i16>) {
929            for s in frame.iter_mut() {
930                *s = s.saturating_mul(2);
931            }
932        }
933    }
934
935    #[test]
936    fn stages_run_in_configured_order() {
937        let live = crate::live::Live::builder()
938            .mic_processor(Doubler)
939            .mic_noise_gate(400.0, 3)
940            .input_vad(gemini_genai_rs::vad::VadConfig::noisy_street())
941            .client_interruption_authority();
942        assert!(live.input_audio.is_configured());
943        assert_eq!(live.input_audio.stages.len(), 2);
944        assert!(matches!(live.input_audio.stages[0], InputStage::Custom(_)));
945        assert!(matches!(
946            live.input_audio.stages[1],
947            InputStage::NoiseGate { .. }
948        ));
949        let (mut processors, vad, client, _) = live.input_audio.build_processors();
950        assert_eq!(processors.len(), 2);
951        assert_eq!(vad.unwrap().start_threshold_db, 21.0);
952        assert!(client);
953        // The custom stage actually runs: 100 doubles to 200, then the gate
954        // (RMS 200 < 400) silences the frame — order is observable.
955        let mut frame = vec![100i16; 480];
956        for p in processors.iter_mut() {
957            p.process_frame(&mut frame);
958        }
959        assert!(
960            frame.iter().all(|&s| s == 0),
961            "gate should silence the doubled but still-quiet frame"
962        );
963    }
964}
965
966#[cfg(test)]
967mod compression_default_tests {
968    use super::*;
969    use gemini_genai_rs::prelude::{ContextWindowCompressionConfig, SlidingWindow};
970
971    #[test]
972    fn compression_is_on_by_default_at_the_documented_thresholds() {
973        let live = Live::builder();
974        assert_eq!(
975            live.config.context_window_compression,
976            Some(ContextWindowCompressionConfig {
977                sliding_window: Some(SlidingWindow {
978                    target_tokens: Some(DEFAULT_COMPRESSION_TARGET_TOKENS),
979                }),
980                trigger_tokens: Some(DEFAULT_COMPRESSION_TRIGGER_TOKENS),
981            })
982        );
983    }
984
985    #[test]
986    fn explicit_thresholds_replace_the_default() {
987        let live = Live::builder().context_compression(4096, 2048);
988        let cwc = live
989            .config
990            .context_window_compression
991            .expect("compression set");
992        assert_eq!(cwc.trigger_tokens, Some(4096));
993        assert_eq!(cwc.sliding_window.and_then(|w| w.target_tokens), Some(2048));
994    }
995
996    #[test]
997    fn no_context_compression_turns_it_off() {
998        let live = Live::builder().no_context_compression();
999        assert_eq!(live.config.context_window_compression, None);
1000    }
1001}