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