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}