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}