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}