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}