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 incremental text delta.
112 pub fn on_text(mut self, f: impl Fn(&str) + Send + Sync + 'static) -> Self {
113 self.callbacks.on_text = Some(Box::new(f));
114 self
115 }
116
117 /// Called when model completes a text response.
118 pub fn on_text_complete(mut self, f: impl Fn(&str) + Send + Sync + 'static) -> Self {
119 self.callbacks.on_text_complete = Some(Box::new(f));
120 self
121 }
122
123 /// Called for input (user speech) transcription: `f(text, is_final)`.
124 ///
125 /// While the user speaks, `is_final` is `false` and `text` is the latest
126 /// partial recognition, which later calls may revise. At the turn boundary
127 /// one call arrives with `is_final == true` carrying the complete
128 /// transcript for the turn — the only value suitable for storage. Requires
129 /// [`transcription`](Self::transcription) or
130 /// [`input_transcription`](Self::input_transcription).
131 pub fn on_input_transcript(
132 mut self,
133 f: impl Fn(&str, /* is_final */ bool) + Send + Sync + 'static,
134 ) -> Self {
135 self.callbacks.on_input_transcript = Some(Box::new(f));
136 self
137 }
138
139 /// Called for output (model speech) transcription: `f(text, is_final)`.
140 ///
141 /// Same partial/final contract as
142 /// [`on_input_transcript`](Self::on_input_transcript): `is_final` is
143 /// `false` for revisable partials and `true` once for the turn's complete
144 /// transcript. Requires [`transcription`](Self::transcription) or
145 /// [`output_transcription`](Self::output_transcription).
146 pub fn on_output_transcript(
147 mut self,
148 f: impl Fn(&str, /* is_final */ bool) + Send + Sync + 'static,
149 ) -> Self {
150 self.callbacks.on_output_transcript = Some(Box::new(f));
151 self
152 }
153
154 /// Called when the model emits a thought/reasoning summary.
155 ///
156 /// Requires `.include_thoughts()` on the session config. Fast lane callback
157 /// (sync, must complete in < 1ms).
158 pub fn on_thought(mut self, f: impl Fn(&str) + Send + Sync + 'static) -> Self {
159 self.callbacks.on_thought = Some(Box::new(f));
160 self
161 }
162
163 /// Called when server VAD detects voice activity start.
164 pub fn on_vad_start(mut self, f: impl Fn() + Send + Sync + 'static) -> Self {
165 self.callbacks.on_vad_start = Some(Box::new(f));
166 self
167 }
168
169 /// Called when server VAD detects voice activity end.
170 pub fn on_vad_end(mut self, f: impl Fn() + Send + Sync + 'static) -> Self {
171 self.callbacks.on_vad_end = Some(Box::new(f));
172 self
173 }
174
175 /// Called when server sends token usage metadata.
176 ///
177 /// Receives a reference to the full [`UsageMetadata`] including prompt,
178 /// response, cached, tool-use, and thoughts token counts plus per-modality
179 /// breakdowns. Fires on the telemetry lane (not the fast lane).
180 pub fn on_usage(mut self, f: impl Fn(&UsageMetadata) + Send + Sync + 'static) -> Self {
181 self.callbacks.on_usage = Some(Box::new(f));
182 self
183 }
184
185 /// Called on wire-level session phase transitions (connecting → active →
186 /// disconnecting …). This is the transport lifecycle, not the
187 /// `PhaseMachine` (see `.phase(..)`).
188 ///
189 /// Receives the new [`SessionPhase`]. Fast lane callback (sync, must
190 /// complete in < 1ms). Use for lightweight UI state updates or metrics.
191 pub fn on_session_phase(mut self, f: impl Fn(SessionPhase) + Send + Sync + 'static) -> Self {
192 self.callbacks.on_session_phase = Some(Box::new(f));
193 self
194 }
195
196 // -- Control Lane Callbacks (async, can block) --
197
198 /// Called when model is interrupted by barge-in.
199 ///
200 /// Awaited before audio forwarding resumes, so a playback flush here is
201 /// guaranteed to land before the next chunk.
202 pub fn on_interrupted<F, Fut>(mut self, f: F) -> Self
203 where
204 F: Fn() -> Fut + Send + Sync + 'static,
205 Fut: Future<Output = ()> + Send + 'static,
206 {
207 self.callbacks.on_interrupted = Some(Arc::new(move || Box::pin(f())));
208 self
209 }
210
211 /// Called when model requests tool execution.
212 /// Return `None` to auto-dispatch, `Some(responses)` to override.
213 /// Receives State for natural state promotion from tool results.
214 pub fn on_tool_call<F, Fut>(mut self, f: F) -> Self
215 where
216 F: Fn(Vec<FunctionCall>, State) -> Fut + Send + Sync + 'static,
217 Fut: Future<Output = Option<Vec<FunctionResponse>>> + Send + 'static,
218 {
219 self.callbacks.on_tool_call = Some(Arc::new(move |calls, state| Box::pin(f(calls, state))));
220 self
221 }
222
223 /// Called when the server cancels pending tool calls.
224 ///
225 /// Receives the list of cancelled tool call IDs. Use to clean up any
226 /// in-flight async work associated with those calls.
227 pub fn on_tool_cancelled<F, Fut>(mut self, f: F) -> Self
228 where
229 F: Fn(Vec<String>) -> Fut + Send + Sync + 'static,
230 Fut: Future<Output = ()> + Send + 'static,
231 {
232 self.callbacks.on_tool_cancelled = Some(Arc::new(move |ids| Box::pin(f(ids))));
233 self
234 }
235
236 /// Called when model turn completes.
237 pub fn on_turn_complete<F, Fut>(mut self, f: F) -> Self
238 where
239 F: Fn() -> Fut + Send + Sync + 'static,
240 Fut: Future<Output = ()> + Send + 'static,
241 {
242 self.callbacks.on_turn_complete = Some(Arc::new(move || Box::pin(f())));
243 self
244 }
245
246 /// Called when the model finishes generating its full intended response.
247 ///
248 /// Fires on the wire `GenerationComplete` event, before any interruption
249 /// truncation. Use this to capture the model's complete output even when
250 /// the user barges in. Paired with `.extract_on_generation()` for structured
251 /// extraction of the pre-truncation response.
252 pub fn on_generation_complete<F, Fut>(mut self, f: F) -> Self
253 where
254 F: Fn() -> Fut + Send + Sync + 'static,
255 Fut: Future<Output = ()> + Send + 'static,
256 {
257 self.callbacks.on_generation_complete = Some(Arc::new(move || Box::pin(f())));
258 self
259 }
260
261 /// Called when server sends GoAway.
262 pub fn on_go_away<F, Fut>(mut self, f: F) -> Self
263 where
264 F: Fn(Duration) -> Fut + Send + Sync + 'static,
265 Fut: Future<Output = ()> + Send + 'static,
266 {
267 self.callbacks.on_go_away = Some(Arc::new(move |d| Box::pin(f(d))));
268 self
269 }
270
271 /// Called when session connects (setup complete).
272 ///
273 /// Receives a `SessionWriter` for sending messages on connect.
274 pub fn on_connected<F, Fut>(mut self, f: F) -> Self
275 where
276 F: Fn(Arc<dyn gemini_genai_rs::session::SessionWriter>) -> Fut + Send + Sync + 'static,
277 Fut: Future<Output = ()> + Send + 'static,
278 {
279 self.callbacks.on_connected = Some(Arc::new(move |w| Box::pin(f(w))));
280 self
281 }
282
283 /// Called when session disconnects.
284 pub fn on_disconnected<F, Fut>(mut self, f: F) -> Self
285 where
286 F: Fn(Option<String>) -> Fut + Send + Sync + 'static,
287 Fut: Future<Output = ()> + Send + 'static,
288 {
289 self.callbacks.on_disconnected = Some(Arc::new(move |r| Box::pin(f(r))));
290 self
291 }
292
293 /// Register an **additive** teardown hook, run on disconnect before
294 /// [`on_disconnected`](Self::on_disconnected).
295 ///
296 /// Every other callback setter replaces: calling `.on_disconnected(..)`
297 /// twice keeps only the second, silently. That is workable for an
298 /// application and unusable for an extension, which cannot know whether the
299 /// application will register a handler after it. Hooks registered here
300 /// accumulate instead, so `with_memory(..)` and the application's own
301 /// `on_disconnected` both run regardless of the order they were written in.
302 ///
303 /// Hooks are awaited in registration order before the session finishes
304 /// tearing down, so this is the seam for flushing durable state. Keep them
305 /// bounded — a hook that hangs delays disconnect.
306 ///
307 /// ```no_run
308 /// # use gemini_adk_fluent_rs::live::Live;
309 /// Live::builder().on_teardown(|| async { /* flush */ });
310 /// ```
311 pub fn on_teardown<F, Fut>(mut self, f: F) -> Self
312 where
313 F: Fn() -> Fut + Send + Sync + 'static,
314 Fut: Future<Output = ()> + Send + 'static,
315 {
316 self.callbacks
317 .on_teardown
318 .push(Arc::new(move || Box::pin(f())));
319 self
320 }
321
322 /// Called after the session resumes following a GoAway disconnect.
323 ///
324 /// Use to re-subscribe to external streams, reset UI state, or log
325 /// resume events. Paired with `.session_resume()` on the builder.
326 pub fn on_resumed<F, Fut>(mut self, f: F) -> Self
327 where
328 F: Fn() -> Fut + Send + Sync + 'static,
329 Fut: Future<Output = ()> + Send + 'static,
330 {
331 self.callbacks.on_resumed = Some(Arc::new(move || Box::pin(f())));
332 self
333 }
334
335 /// Called on non-fatal errors with the error's message.
336 ///
337 /// The argument is a `String`, not a typed error: the runtime funnels
338 /// server errors, codec failures, and processor faults into one
339 /// human-readable message here, and the session keeps running. Fatal
340 /// errors end the session and arrive through
341 /// [`on_disconnected`](Self::on_disconnected) instead.
342 pub fn on_error<F, Fut>(mut self, f: F) -> Self
343 where
344 F: Fn(String) -> Fut + Send + Sync + 'static,
345 Fut: Future<Output = ()> + Send + 'static,
346 {
347 self.callbacks.on_error = Some(Arc::new(move |e| Box::pin(f(e))));
348 self
349 }
350
351 // -- Concurrent callback variants --
352 // These set ExecutionMode::Concurrent so the callback is spawned as a
353 // detached tokio task instead of being awaited inline.
354
355 /// Called when model is interrupted by barge-in (spawned concurrently).
356 ///
357 /// Audio forwarding resumes without waiting for the body, so this is for
358 /// bookkeeping (metrics, a log line) — a playback flush must use the
359 /// blocking [`on_interrupted`](Self::on_interrupted).
360 pub fn on_interrupted_concurrent<F, Fut>(mut self, f: F) -> Self
361 where
362 F: Fn() -> Fut + Send + Sync + 'static,
363 Fut: Future<Output = ()> + Send + 'static,
364 {
365 self.callbacks.on_interrupted = Some(Arc::new(move || Box::pin(f())));
366 self.callbacks.on_interrupted_mode = ExecutionMode::Concurrent;
367 self
368 }
369
370 /// Turn-boundary hook spawned concurrently — for observation only. The
371 /// next turn proceeds without waiting, so context injected from here is
372 /// not guaranteed to precede it; use [`on_turn_boundary`](Self::on_turn_boundary)
373 /// for that.
374 pub fn on_turn_boundary_concurrent<F, Fut>(mut self, f: F) -> Self
375 where
376 F: Fn(gemini_adk_rs::State, Arc<dyn gemini_genai_rs::session::SessionWriter>) -> Fut
377 + Send
378 + Sync
379 + 'static,
380 Fut: Future<Output = ()> + Send + 'static,
381 {
382 self.callbacks.on_turn_boundary =
383 Some(Arc::new(move |state, writer| Box::pin(f(state, writer))));
384 self.callbacks.on_turn_boundary_mode = ExecutionMode::Concurrent;
385 self
386 }
387
388 /// An **additive** teardown hook spawned detached on disconnect rather
389 /// than awaited — the disconnect does not wait for it. For a final metric
390 /// or log line; anything that flushes durable state belongs in
391 /// [`on_teardown`](Self::on_teardown).
392 pub fn on_teardown_concurrent<F, Fut>(mut self, f: F) -> Self
393 where
394 F: Fn() -> Fut + Send + Sync + 'static,
395 Fut: Future<Output = ()> + Send + 'static,
396 {
397 self.callbacks
398 .on_teardown_concurrent
399 .push(Arc::new(move || Box::pin(f())));
400 self
401 }
402
403 /// Called when model turn completes (spawned concurrently).
404 pub fn on_turn_complete_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.on_turn_complete = Some(Arc::new(move || Box::pin(f())));
410 self.callbacks.on_turn_complete_mode = ExecutionMode::Concurrent;
411 self
412 }
413
414 /// Called when the model finishes generating its full intended response (spawned concurrently).
415 pub fn on_generation_complete_concurrent<F, Fut>(mut self, f: F) -> Self
416 where
417 F: Fn() -> Fut + Send + Sync + 'static,
418 Fut: Future<Output = ()> + Send + 'static,
419 {
420 self.callbacks.on_generation_complete = Some(Arc::new(move || Box::pin(f())));
421 self.callbacks.on_generation_complete_mode = ExecutionMode::Concurrent;
422 self
423 }
424
425 /// Called when session connects (spawned concurrently).
426 pub fn on_connected_concurrent<F, Fut>(mut self, f: F) -> Self
427 where
428 F: Fn(Arc<dyn gemini_genai_rs::session::SessionWriter>) -> Fut + Send + Sync + 'static,
429 Fut: Future<Output = ()> + Send + 'static,
430 {
431 self.callbacks.on_connected = Some(Arc::new(move |w| Box::pin(f(w))));
432 self.callbacks.on_connected_mode = ExecutionMode::Concurrent;
433 self
434 }
435
436 /// Called when session disconnects (spawned concurrently).
437 pub fn on_disconnected_concurrent<F, Fut>(mut self, f: F) -> Self
438 where
439 F: Fn(Option<String>) -> Fut + Send + Sync + 'static,
440 Fut: Future<Output = ()> + Send + 'static,
441 {
442 self.callbacks.on_disconnected = Some(Arc::new(move |r| Box::pin(f(r))));
443 self.callbacks.on_disconnected_mode = ExecutionMode::Concurrent;
444 self
445 }
446
447 /// Called after session resumes from GoAway (spawned concurrently).
448 pub fn on_resumed_concurrent<F, Fut>(mut self, f: F) -> Self
449 where
450 F: Fn() -> Fut + Send + Sync + 'static,
451 Fut: Future<Output = ()> + Send + 'static,
452 {
453 self.callbacks.on_resumed = Some(Arc::new(move || Box::pin(f())));
454 self.callbacks.on_resumed_mode = ExecutionMode::Concurrent;
455 self
456 }
457
458 /// Called on non-fatal errors (spawned concurrently).
459 pub fn on_error_concurrent<F, Fut>(mut self, f: F) -> Self
460 where
461 F: Fn(String) -> Fut + Send + Sync + 'static,
462 Fut: Future<Output = ()> + Send + 'static,
463 {
464 self.callbacks.on_error = Some(Arc::new(move |e| Box::pin(f(e))));
465 self.callbacks.on_error_mode = ExecutionMode::Concurrent;
466 self
467 }
468
469 /// Called when server sends GoAway (spawned concurrently).
470 pub fn on_go_away_concurrent<F, Fut>(mut self, f: F) -> Self
471 where
472 F: Fn(Duration) -> Fut + Send + Sync + 'static,
473 Fut: Future<Output = ()> + Send + 'static,
474 {
475 self.callbacks.on_go_away = Some(Arc::new(move |d| Box::pin(f(d))));
476 self.callbacks.on_go_away_mode = ExecutionMode::Concurrent;
477 self
478 }
479
480 /// Called when the server cancels pending tool calls (spawned concurrently).
481 pub fn on_tool_cancelled_concurrent<F, Fut>(mut self, f: F) -> Self
482 where
483 F: Fn(Vec<String>) -> Fut + Send + Sync + 'static,
484 Fut: Future<Output = ()> + Send + 'static,
485 {
486 self.callbacks.on_tool_cancelled = Some(Arc::new(move |ids| Box::pin(f(ids))));
487 self.callbacks.on_tool_cancelled_mode = ExecutionMode::Concurrent;
488 self
489 }
490
491 /// Called when a TurnExtractor produces a result (spawned concurrently).
492 pub fn on_extracted_concurrent<F, Fut>(mut self, f: F) -> Self
493 where
494 F: Fn(String, serde_json::Value) -> Fut + Send + Sync + 'static,
495 Fut: Future<Output = ()> + Send + 'static,
496 {
497 self.callbacks.on_extracted = Some(Arc::new(move |name, value| Box::pin(f(name, value))));
498 self.callbacks.on_extracted_mode = ExecutionMode::Concurrent;
499 self
500 }
501
502 /// Called when a TurnExtractor fails (spawned concurrently).
503 pub fn on_extraction_error_concurrent<F, Fut>(mut self, f: F) -> Self
504 where
505 F: Fn(String, String) -> Fut + Send + Sync + 'static,
506 Fut: Future<Output = ()> + Send + 'static,
507 {
508 self.callbacks.on_extraction_error =
509 Some(Arc::new(move |name, error| Box::pin(f(name, error))));
510 self.callbacks.on_extraction_error_mode = ExecutionMode::Concurrent;
511 self
512 }
513}
514
515#[cfg(test)]
516mod tests {
517 use super::*;
518
519 /// Verify that all four new callback setters are accepted by the builder
520 /// and that the chain returns `Self` (i.e., the type-system accepts them).
521 #[test]
522 fn builder_accepts_new_callbacks() {
523 let _live = Live::builder()
524 // on_session_phase: sync fast-lane
525 .on_session_phase(|_phase| {})
526 // on_tool_cancelled: async control-lane
527 .on_tool_cancelled(|_ids| async {})
528 // on_generation_complete: async control-lane, no args
529 .on_generation_complete(|| async {})
530 // on_resumed: async control-lane, no args
531 .on_resumed(|| async {});
532 // Compiles = test passes
533 }
534
535 /// Verify that the concurrent variants of the new setters also compile.
536 #[test]
537 fn builder_accepts_new_callbacks_concurrent() {
538 let live = Live::builder()
539 .on_tool_cancelled_concurrent(|_ids| async {})
540 .on_generation_complete_concurrent(|| async {})
541 .on_resumed_concurrent(|| async {})
542 .on_interrupted_concurrent(|| async {})
543 .on_turn_boundary_concurrent(|_state, _writer| async {})
544 .on_teardown_concurrent(|| async {});
545 assert_eq!(
546 live.callbacks.on_interrupted_mode,
547 ExecutionMode::Concurrent
548 );
549 assert_eq!(
550 live.callbacks.on_turn_boundary_mode,
551 ExecutionMode::Concurrent
552 );
553 assert_eq!(live.callbacks.on_teardown_concurrent.len(), 1);
554 }
555}