gemini_adk_fluent_rs/live/callbacks.rs
1//! Event callback registration methods for `Live`.
2//!
3//! # The lane rule
4//!
5//! Every callback runs on one of two lanes, and the lane decides what the
6//! body may do:
7//!
8//! - **Fast lane** (`on_audio`, `on_text`, `on_text_complete`,
9//! `on_input_transcript`, `on_output_transcript`, `on_thought`,
10//! `on_vad_start`, `on_vad_end`, `on_session_phase`, `on_usage`): a sync
11//! `Fn` invoked inline on the event-dispatch path. It must return in well
12//! under a millisecond — no allocation, no locks, no I/O. A channel
13//! `try_send` is the right shape; anything heavier goes to the other lane
14//! through that channel.
15//! - **Control lane** (everything returning a future): async, may block, runs
16//! in the processor's control loop. By default each is **awaited inline**
17//! ([`ExecutionMode::Blocking`]) so ordering and state are consistent. A
18//! `_concurrent` twin — `on_turn_complete_concurrent`, `on_connected_concurrent`,
19//! … — registers the same body as a **detached task**
20//! ([`ExecutionMode::Concurrent`]) for fire-and-forget work (logging,
21//! analytics, webhooks). Twins exist only where fire-and-forget is
22//! meaningful; hooks whose return value feeds the session (`on_tool_call`,
23//! `before_tool_response`) have none.
24//!
25//! Registering a callback twice keeps the last registration, except
26//! [`on_teardown`](Live::on_teardown), which accumulates.
27
28use std::future::Future;
29use std::sync::Arc;
30use std::time::Duration;
31
32use bytes::Bytes;
33
34use gemini_adk_rs::State;
35use gemini_adk_rs::live::ExecutionMode;
36use gemini_genai_rs::prelude::*;
37
38use super::Live;
39
40impl Live {
41 // -- Outbound Interceptors --
42
43 /// Intercept tool responses before they are sent back to Gemini.
44 ///
45 /// Use this to rewrite, augment, or filter tool results based on
46 /// conversation state. The callback receives the tool responses and the
47 /// shared `State`, and returns (potentially modified) responses.
48 ///
49 /// # Example
50 /// ```no_run
51 /// # use gemini_adk_fluent_rs::prelude::*;
52 /// # #[derive(Default, serde::Serialize, serde::Deserialize)]
53 /// # struct OrderState { items: Vec<String> }
54 /// Live::builder().before_tool_response(|responses, state| async move {
55 /// let order: OrderState = state.get("OrderState").unwrap_or_default();
56 /// responses.into_iter().map(|mut r| {
57 /// r.response["current_order"] = serde_json::to_value(&order).unwrap();
58 /// r
59 /// }).collect()
60 /// });
61 /// ```
62 pub fn before_tool_response<F, Fut>(mut self, f: F) -> Self
63 where
64 F: Fn(Vec<FunctionResponse>, gemini_adk_rs::State) -> Fut + Send + Sync + 'static,
65 Fut: Future<Output = Vec<FunctionResponse>> + Send + 'static,
66 {
67 self.callbacks.before_tool_response = Some(Arc::new(move |responses, state| {
68 Box::pin(f(responses, state))
69 }));
70 self
71 }
72
73 /// Hook called at turn boundaries — after extractors run, before `on_turn_complete`.
74 ///
75 /// Receives the shared `State` and a `SessionWriter` for injecting content
76 /// into the conversation. Use for context stuffing, K/V data injection,
77 /// condensed state summaries, or any outbound content interleaving.
78 ///
79 /// # Example
80 /// ```no_run
81 /// # use gemini_adk_fluent_rs::prelude::*;
82 /// Live::builder().on_turn_boundary(|state, writer| async move {
83 /// let summary = state.get::<String>("summary").unwrap_or_default();
84 /// writer.send_client_content(
85 /// vec![Content::user(format!("[Context: {summary}]"))],
86 /// false,
87 /// ).await.ok();
88 /// });
89 /// ```
90 pub fn on_turn_boundary<F, Fut>(mut self, f: F) -> Self
91 where
92 F: Fn(gemini_adk_rs::State, Arc<dyn gemini_genai_rs::session::SessionWriter>) -> Fut
93 + Send
94 + Sync
95 + 'static,
96 Fut: Future<Output = ()> + Send + 'static,
97 {
98 self.callbacks.on_turn_boundary =
99 Some(Arc::new(move |state, writer| Box::pin(f(state, writer))));
100 self
101 }
102
103 // -- Fast Lane Callbacks (sync, < 1ms) --
104
105 /// Called for each audio chunk from the model (PCM16 24kHz).
106 pub fn on_audio(mut self, f: impl Fn(&Bytes) + Send + Sync + 'static) -> Self {
107 self.callbacks.on_audio = Some(Box::new(f));
108 self
109 }
110
111 /// Called for each non-audio media chunk from the model: Gemini 3.8 Live
112 /// Avatar video (`video/mp4`, 24 FPS), synchronized with
113 /// [`on_audio`](Self::on_audio). Suppressed during barge-in, like audio.
114 pub fn on_media(
115 mut self,
116 f: impl Fn(&gemini_genai_rs::session::InlineMedia) + Send + Sync + 'static,
117 ) -> Self {
118 self.callbacks.on_media = Some(Box::new(f));
119 self
120 }
121
122 /// Called for each incremental text delta.
123 pub fn on_text(mut self, f: impl Fn(&str) + Send + Sync + 'static) -> Self {
124 self.callbacks.on_text = Some(Box::new(f));
125 self
126 }
127
128 /// Called when model completes a text response.
129 pub fn on_text_complete(mut self, f: impl Fn(&str) + Send + Sync + 'static) -> Self {
130 self.callbacks.on_text_complete = Some(Box::new(f));
131 self
132 }
133
134 /// Called for input (user speech) transcription: `f(text, is_final)`.
135 ///
136 /// While the user speaks, `is_final` is `false` and `text` is the latest
137 /// partial recognition, which later calls may revise. At the turn boundary
138 /// one call arrives with `is_final == true` carrying the complete
139 /// transcript for the turn — the only value suitable for storage. Requires
140 /// [`transcription`](Self::transcription) or
141 /// [`input_transcription`](Self::input_transcription).
142 pub fn on_input_transcript(
143 mut self,
144 f: impl Fn(&str, /* is_final */ bool) + Send + Sync + 'static,
145 ) -> Self {
146 self.callbacks.on_input_transcript = Some(Box::new(f));
147 self
148 }
149
150 /// Called for output (model speech) transcription: `f(text, is_final)`.
151 ///
152 /// Same partial/final contract as
153 /// [`on_input_transcript`](Self::on_input_transcript): `is_final` is
154 /// `false` for revisable partials and `true` once for the turn's complete
155 /// transcript. Requires [`transcription`](Self::transcription) or
156 /// [`output_transcription`](Self::output_transcription).
157 pub fn on_output_transcript(
158 mut self,
159 f: impl Fn(&str, /* is_final */ bool) + Send + Sync + 'static,
160 ) -> Self {
161 self.callbacks.on_output_transcript = Some(Box::new(f));
162 self
163 }
164
165 /// Called when the model emits a thought/reasoning summary.
166 ///
167 /// Requires `.include_thoughts()` on the session config. Fast lane callback
168 /// (sync, must complete in < 1ms).
169 pub fn on_thought(mut self, f: impl Fn(&str) + Send + Sync + 'static) -> Self {
170 self.callbacks.on_thought = Some(Box::new(f));
171 self
172 }
173
174 /// Called when server VAD detects voice activity start.
175 pub fn on_vad_start(mut self, f: impl Fn() + Send + Sync + 'static) -> Self {
176 self.callbacks.on_vad_start = Some(Box::new(f));
177 self
178 }
179
180 /// Called when server VAD detects voice activity end.
181 pub fn on_vad_end(mut self, f: impl Fn() + Send + Sync + 'static) -> Self {
182 self.callbacks.on_vad_end = Some(Box::new(f));
183 self
184 }
185
186 /// Called when server sends token usage metadata.
187 ///
188 /// Receives a reference to the full [`UsageMetadata`] including prompt,
189 /// response, cached, tool-use, and thoughts token counts plus per-modality
190 /// breakdowns. Fires on the telemetry lane (not the fast lane).
191 pub fn on_usage(mut self, f: impl Fn(&UsageMetadata) + Send + Sync + 'static) -> Self {
192 self.callbacks.on_usage = Some(Box::new(f));
193 self
194 }
195
196 /// Called on wire-level session phase transitions (connecting → active →
197 /// disconnecting …). This is the transport lifecycle, not the
198 /// `PhaseMachine` (see `.phase(..)`).
199 ///
200 /// Receives the new [`SessionPhase`]. Fast lane callback (sync, must
201 /// complete in < 1ms). Use for lightweight UI state updates or metrics.
202 pub fn on_session_phase(mut self, f: impl Fn(SessionPhase) + Send + Sync + 'static) -> Self {
203 self.callbacks.on_session_phase = Some(Box::new(f));
204 self
205 }
206
207 // -- Control Lane Callbacks (async, can block) --
208
209 /// Called when model is interrupted by barge-in.
210 ///
211 /// Awaited before audio forwarding resumes, so a playback flush here is
212 /// guaranteed to land before the next chunk.
213 pub fn on_interrupted<F, Fut>(mut self, f: F) -> Self
214 where
215 F: Fn() -> Fut + Send + Sync + 'static,
216 Fut: Future<Output = ()> + Send + 'static,
217 {
218 self.callbacks.on_interrupted = Some(Arc::new(move || Box::pin(f())));
219 self
220 }
221
222 /// Called when model requests tool execution.
223 /// Return `None` to auto-dispatch, `Some(responses)` to override.
224 /// Receives State for natural state promotion from tool results.
225 pub fn on_tool_call<F, Fut>(mut self, f: F) -> Self
226 where
227 F: Fn(Vec<FunctionCall>, State) -> Fut + Send + Sync + 'static,
228 Fut: Future<Output = Option<Vec<FunctionResponse>>> + Send + 'static,
229 {
230 self.callbacks.on_tool_call = Some(Arc::new(move |calls, state| Box::pin(f(calls, state))));
231 self
232 }
233
234 /// Called when the server cancels pending tool calls.
235 ///
236 /// Receives the list of cancelled tool call IDs. Use to clean up any
237 /// in-flight async work associated with those calls.
238 pub fn on_tool_cancelled<F, Fut>(mut self, f: F) -> Self
239 where
240 F: Fn(Vec<String>) -> Fut + Send + Sync + 'static,
241 Fut: Future<Output = ()> + Send + 'static,
242 {
243 self.callbacks.on_tool_cancelled = Some(Arc::new(move |ids| Box::pin(f(ids))));
244 self
245 }
246
247 /// Called when model turn completes.
248 pub fn on_turn_complete<F, Fut>(mut self, f: F) -> Self
249 where
250 F: Fn() -> Fut + Send + Sync + 'static,
251 Fut: Future<Output = ()> + Send + 'static,
252 {
253 self.callbacks.on_turn_complete = Some(Arc::new(move || Box::pin(f())));
254 self
255 }
256
257 /// Called when the model finishes generating its full intended response.
258 ///
259 /// Fires on the wire `GenerationComplete` event, before the turn
260 /// completes. Paired with `.extract_on_generation()` for structured
261 /// extraction as soon as the response is generated. Current Live models
262 /// send no `GenerationComplete` for an interrupted turn; the partial
263 /// output transcript callbacks carry everything the model produced.
264 pub fn on_generation_complete<F, Fut>(mut self, f: F) -> Self
265 where
266 F: Fn() -> Fut + Send + Sync + 'static,
267 Fut: Future<Output = ()> + Send + 'static,
268 {
269 self.callbacks.on_generation_complete = Some(Arc::new(move || Box::pin(f())));
270 self
271 }
272
273 /// Called when server sends GoAway.
274 pub fn on_go_away<F, Fut>(mut self, f: F) -> Self
275 where
276 F: Fn(Duration) -> Fut + Send + Sync + 'static,
277 Fut: Future<Output = ()> + Send + 'static,
278 {
279 self.callbacks.on_go_away = Some(Arc::new(move |d| Box::pin(f(d))));
280 self
281 }
282
283 /// Called when session connects (setup complete).
284 ///
285 /// Receives a `SessionWriter` for sending messages on connect.
286 pub fn on_connected<F, Fut>(mut self, f: F) -> Self
287 where
288 F: Fn(Arc<dyn gemini_genai_rs::session::SessionWriter>) -> Fut + Send + Sync + 'static,
289 Fut: Future<Output = ()> + Send + 'static,
290 {
291 self.callbacks.on_connected = Some(Arc::new(move |w| Box::pin(f(w))));
292 self
293 }
294
295 /// Called when session disconnects.
296 pub fn on_disconnected<F, Fut>(mut self, f: F) -> Self
297 where
298 F: Fn(Option<String>) -> Fut + Send + Sync + 'static,
299 Fut: Future<Output = ()> + Send + 'static,
300 {
301 self.callbacks.on_disconnected = Some(Arc::new(move |r| Box::pin(f(r))));
302 self
303 }
304
305 /// Register an **additive** teardown hook, run on disconnect before
306 /// [`on_disconnected`](Self::on_disconnected).
307 ///
308 /// Every other callback setter replaces: calling `.on_disconnected(..)`
309 /// twice keeps only the second, silently. That is workable for an
310 /// application and unusable for an extension, which cannot know whether the
311 /// application will register a handler after it. Hooks registered here
312 /// accumulate instead, so `with_memory(..)` and the application's own
313 /// `on_disconnected` both run regardless of the order they were written in.
314 ///
315 /// Hooks are awaited in registration order before the session finishes
316 /// tearing down, so this is the seam for flushing durable state. Keep them
317 /// bounded — a hook that hangs delays disconnect.
318 ///
319 /// ```no_run
320 /// # use gemini_adk_fluent_rs::live::Live;
321 /// Live::builder().on_teardown(|| async { /* flush */ });
322 /// ```
323 pub fn on_teardown<F, Fut>(mut self, f: F) -> Self
324 where
325 F: Fn() -> Fut + Send + Sync + 'static,
326 Fut: Future<Output = ()> + Send + 'static,
327 {
328 self.callbacks
329 .on_teardown
330 .push(Arc::new(move || Box::pin(f())));
331 self
332 }
333
334 /// Called after the session resumes following a GoAway disconnect.
335 ///
336 /// Use to re-subscribe to external streams, reset UI state, or log
337 /// resume events. Paired with `.session_resume()` on the builder.
338 pub fn on_resumed<F, Fut>(mut self, f: F) -> Self
339 where
340 F: Fn() -> Fut + Send + Sync + 'static,
341 Fut: Future<Output = ()> + Send + 'static,
342 {
343 self.callbacks.on_resumed = Some(Arc::new(move || Box::pin(f())));
344 self
345 }
346
347 /// Called on non-fatal errors with the error's message.
348 ///
349 /// The argument is a `String`, not a typed error: the runtime funnels
350 /// server errors, codec failures, and processor faults into one
351 /// human-readable message here, and the session keeps running. Fatal
352 /// errors end the session and arrive through
353 /// [`on_disconnected`](Self::on_disconnected) instead.
354 pub fn on_error<F, Fut>(mut self, f: F) -> Self
355 where
356 F: Fn(String) -> Fut + Send + Sync + 'static,
357 Fut: Future<Output = ()> + Send + 'static,
358 {
359 self.callbacks.on_error = Some(Arc::new(move |e| Box::pin(f(e))));
360 self
361 }
362
363 // -- Concurrent callback variants --
364 // These set ExecutionMode::Concurrent so the callback is spawned as a
365 // detached tokio task instead of being awaited inline.
366
367 /// Called when model is interrupted by barge-in (spawned concurrently).
368 ///
369 /// Audio forwarding resumes without waiting for the body, so this is for
370 /// bookkeeping (metrics, a log line) — a playback flush must use the
371 /// blocking [`on_interrupted`](Self::on_interrupted).
372 pub fn on_interrupted_concurrent<F, Fut>(mut self, f: F) -> Self
373 where
374 F: Fn() -> Fut + Send + Sync + 'static,
375 Fut: Future<Output = ()> + Send + 'static,
376 {
377 self.callbacks.on_interrupted = Some(Arc::new(move || Box::pin(f())));
378 self.callbacks.on_interrupted_mode = ExecutionMode::Concurrent;
379 self
380 }
381
382 /// Turn-boundary hook spawned concurrently — for observation only. The
383 /// next turn proceeds without waiting, so context injected from here is
384 /// not guaranteed to precede it; use [`on_turn_boundary`](Self::on_turn_boundary)
385 /// for that.
386 pub fn on_turn_boundary_concurrent<F, Fut>(mut self, f: F) -> Self
387 where
388 F: Fn(gemini_adk_rs::State, Arc<dyn gemini_genai_rs::session::SessionWriter>) -> Fut
389 + Send
390 + Sync
391 + 'static,
392 Fut: Future<Output = ()> + Send + 'static,
393 {
394 self.callbacks.on_turn_boundary =
395 Some(Arc::new(move |state, writer| Box::pin(f(state, writer))));
396 self.callbacks.on_turn_boundary_mode = ExecutionMode::Concurrent;
397 self
398 }
399
400 /// An **additive** teardown hook spawned detached on disconnect rather
401 /// than awaited — the disconnect does not wait for it. For a final metric
402 /// or log line; anything that flushes durable state belongs in
403 /// [`on_teardown`](Self::on_teardown).
404 pub fn on_teardown_concurrent<F, Fut>(mut self, f: F) -> Self
405 where
406 F: Fn() -> Fut + Send + Sync + 'static,
407 Fut: Future<Output = ()> + Send + 'static,
408 {
409 self.callbacks
410 .on_teardown_concurrent
411 .push(Arc::new(move || Box::pin(f())));
412 self
413 }
414
415 /// Called when model turn completes (spawned concurrently).
416 pub fn on_turn_complete_concurrent<F, Fut>(mut self, f: F) -> Self
417 where
418 F: Fn() -> Fut + Send + Sync + 'static,
419 Fut: Future<Output = ()> + Send + 'static,
420 {
421 self.callbacks.on_turn_complete = Some(Arc::new(move || Box::pin(f())));
422 self.callbacks.on_turn_complete_mode = ExecutionMode::Concurrent;
423 self
424 }
425
426 /// Called when the model finishes generating its full intended response (spawned concurrently).
427 pub fn on_generation_complete_concurrent<F, Fut>(mut self, f: F) -> Self
428 where
429 F: Fn() -> Fut + Send + Sync + 'static,
430 Fut: Future<Output = ()> + Send + 'static,
431 {
432 self.callbacks.on_generation_complete = Some(Arc::new(move || Box::pin(f())));
433 self.callbacks.on_generation_complete_mode = ExecutionMode::Concurrent;
434 self
435 }
436
437 /// Called when session connects (spawned concurrently).
438 pub fn on_connected_concurrent<F, Fut>(mut self, f: F) -> Self
439 where
440 F: Fn(Arc<dyn gemini_genai_rs::session::SessionWriter>) -> Fut + Send + Sync + 'static,
441 Fut: Future<Output = ()> + Send + 'static,
442 {
443 self.callbacks.on_connected = Some(Arc::new(move |w| Box::pin(f(w))));
444 self.callbacks.on_connected_mode = ExecutionMode::Concurrent;
445 self
446 }
447
448 /// Called when session disconnects (spawned concurrently).
449 pub fn on_disconnected_concurrent<F, Fut>(mut self, f: F) -> Self
450 where
451 F: Fn(Option<String>) -> Fut + Send + Sync + 'static,
452 Fut: Future<Output = ()> + Send + 'static,
453 {
454 self.callbacks.on_disconnected = Some(Arc::new(move |r| Box::pin(f(r))));
455 self.callbacks.on_disconnected_mode = ExecutionMode::Concurrent;
456 self
457 }
458
459 /// Called after session resumes from GoAway (spawned concurrently).
460 pub fn on_resumed_concurrent<F, Fut>(mut self, f: F) -> Self
461 where
462 F: Fn() -> Fut + Send + Sync + 'static,
463 Fut: Future<Output = ()> + Send + 'static,
464 {
465 self.callbacks.on_resumed = Some(Arc::new(move || Box::pin(f())));
466 self.callbacks.on_resumed_mode = ExecutionMode::Concurrent;
467 self
468 }
469
470 /// Called on non-fatal errors (spawned concurrently).
471 pub fn on_error_concurrent<F, Fut>(mut self, f: F) -> Self
472 where
473 F: Fn(String) -> Fut + Send + Sync + 'static,
474 Fut: Future<Output = ()> + Send + 'static,
475 {
476 self.callbacks.on_error = Some(Arc::new(move |e| Box::pin(f(e))));
477 self.callbacks.on_error_mode = ExecutionMode::Concurrent;
478 self
479 }
480
481 /// Called when server sends GoAway (spawned concurrently).
482 pub fn on_go_away_concurrent<F, Fut>(mut self, f: F) -> Self
483 where
484 F: Fn(Duration) -> Fut + Send + Sync + 'static,
485 Fut: Future<Output = ()> + Send + 'static,
486 {
487 self.callbacks.on_go_away = Some(Arc::new(move |d| Box::pin(f(d))));
488 self.callbacks.on_go_away_mode = ExecutionMode::Concurrent;
489 self
490 }
491
492 /// Called when the server cancels pending tool calls (spawned concurrently).
493 pub fn on_tool_cancelled_concurrent<F, Fut>(mut self, f: F) -> Self
494 where
495 F: Fn(Vec<String>) -> Fut + Send + Sync + 'static,
496 Fut: Future<Output = ()> + Send + 'static,
497 {
498 self.callbacks.on_tool_cancelled = Some(Arc::new(move |ids| Box::pin(f(ids))));
499 self.callbacks.on_tool_cancelled_mode = ExecutionMode::Concurrent;
500 self
501 }
502
503 /// Called when a TurnExtractor produces a result (spawned concurrently).
504 pub fn on_extracted_concurrent<F, Fut>(mut self, f: F) -> Self
505 where
506 F: Fn(String, serde_json::Value) -> Fut + Send + Sync + 'static,
507 Fut: Future<Output = ()> + Send + 'static,
508 {
509 self.callbacks.on_extracted = Some(Arc::new(move |name, value| Box::pin(f(name, value))));
510 self.callbacks.on_extracted_mode = ExecutionMode::Concurrent;
511 self
512 }
513
514 /// Called when a TurnExtractor fails (spawned concurrently).
515 pub fn on_extraction_error_concurrent<F, Fut>(mut self, f: F) -> Self
516 where
517 F: Fn(String, String) -> Fut + Send + Sync + 'static,
518 Fut: Future<Output = ()> + Send + 'static,
519 {
520 self.callbacks.on_extraction_error =
521 Some(Arc::new(move |name, error| Box::pin(f(name, error))));
522 self.callbacks.on_extraction_error_mode = ExecutionMode::Concurrent;
523 self
524 }
525}
526
527#[cfg(test)]
528mod tests {
529 use super::*;
530
531 /// Verify that all four new callback setters are accepted by the builder
532 /// and that the chain returns `Self` (i.e., the type-system accepts them).
533 #[test]
534 fn builder_accepts_new_callbacks() {
535 let _live = Live::builder()
536 // on_session_phase: sync fast-lane
537 .on_session_phase(|_phase| {})
538 // on_tool_cancelled: async control-lane
539 .on_tool_cancelled(|_ids| async {})
540 // on_generation_complete: async control-lane, no args
541 .on_generation_complete(|| async {})
542 // on_resumed: async control-lane, no args
543 .on_resumed(|| async {});
544 // Compiles = test passes
545 }
546
547 /// Verify that the concurrent variants of the new setters also compile.
548 #[test]
549 fn builder_accepts_new_callbacks_concurrent() {
550 let live = Live::builder()
551 .on_tool_cancelled_concurrent(|_ids| async {})
552 .on_generation_complete_concurrent(|| async {})
553 .on_resumed_concurrent(|| async {})
554 .on_interrupted_concurrent(|| async {})
555 .on_turn_boundary_concurrent(|_state, _writer| async {})
556 .on_teardown_concurrent(|| async {});
557 assert_eq!(
558 live.callbacks.on_interrupted_mode,
559 ExecutionMode::Concurrent
560 );
561 assert_eq!(
562 live.callbacks.on_turn_boundary_mode,
563 ExecutionMode::Concurrent
564 );
565 assert_eq!(live.callbacks.on_teardown_concurrent.len(), 1);
566 }
567}