Skip to main content

gemini_adk_rs/live/
events.rs

1//! Semantic events emitted by the L1 processor.
2//!
3//! Subscribe via `LiveHandle::events()` (broadcast receiver) or
4//! `LiveHandle::stream()` (a [`futures_util::Stream`]). Sending with no
5//! subscribers is a no-op.
6
7use std::pin::Pin;
8use std::task::{Context, Poll};
9use std::time::Duration;
10
11use bytes::Bytes;
12use futures_util::Stream;
13use tokio::sync::broadcast;
14
15/// How many [`LiveEvent`]s a subscriber can fall behind before it skips the
16/// oldest, unless [`LiveSessionBuilder::event_capacity`](super::builder::LiveSessionBuilder::event_capacity)
17/// says otherwise. Matches the depth the wire layer gives its own
18/// subscribers (`TransportConfig::event_channel_capacity`).
19pub const DEFAULT_EVENT_CAPACITY: usize = 512;
20
21/// Semantic events emitted by the Live session processor.
22///
23/// The L1 equivalent of L0's [`SessionEvent`](gemini_genai_rs::prelude::SessionEvent).
24/// L0 events are wire-level; LiveEvents are semantic (extractions completed,
25/// phases transitioned, tools executed).
26///
27/// Subscribe via [`LiveHandle::events()`](super::handle::LiveHandle::events).
28/// Multiple independent subscribers supported. Sending with no subscribers
29/// is a no-op (`broadcast::send` with 0 receivers stores nothing); the
30/// buffer itself is allocated when the session starts, sized by
31/// [`LiveSessionBuilder::event_capacity`](super::builder::LiveSessionBuilder::event_capacity).
32#[derive(Debug, Clone)]
33#[non_exhaustive]
34pub enum LiveEvent {
35    // -- Fast-lane events (high frequency, sync emission) --
36    /// Raw PCM audio from model. Uses `Bytes` (refcounted) — clone is
37    /// a pointer increment (~2ns), not a deep copy.
38    Audio(Bytes),
39    /// Non-audio media from the model: Gemini 3.8 Live Avatar video
40    /// (`video/mp4`), synchronized with [`Audio`](Self::Audio).
41    Media(gemini_genai_rs::session::InlineMedia),
42    /// Incremental text token from model.
43    TextDelta(String),
44    /// Complete text response (all deltas concatenated).
45    TextComplete(String),
46    /// User speech transcription.
47    InputTranscript {
48        /// The transcribed text content.
49        text: String,
50        /// Whether this is the final transcription for the utterance.
51        is_final: bool,
52    },
53    /// Model speech transcription.
54    OutputTranscript {
55        /// The transcribed text content.
56        text: String,
57        /// Whether this is the final transcription for the utterance.
58        is_final: bool,
59    },
60    /// Model reasoning/thinking content.
61    Thought(String),
62    /// Voice activity detected — user started speaking.
63    VadStart,
64    /// Voice activity ended — user stopped speaking.
65    VadEnd,
66
67    // -- Control-lane events (lower frequency, async emission) --
68    /// A task lifecycle, approval, operation, or foreground changed.
69    TasksChanged(crate::tasks::TaskSessionSnapshot),
70    /// Extraction completed. Emitted for both the top-level result
71    /// AND each flattened key (e.g., "order.items", "order.phase").
72    Extraction {
73        /// Extractor name, or `"extractor.field"` for flattened keys.
74        name: String,
75        /// The extracted JSON value.
76        value: serde_json::Value,
77    },
78    /// Extraction failed.
79    ExtractionError {
80        /// Name of the extractor that failed.
81        name: String,
82        /// Human-readable error description.
83        error: String,
84    },
85    /// A raw extraction field was considered for promotion into authoritative state.
86    StatePromotion {
87        /// Extractor name that produced the field.
88        extractor: String,
89        /// Field name inside the extractor result.
90        field: String,
91        /// State key targeted by the promotion rule.
92        state_key: String,
93        /// Whether the promotion was accepted and written.
94        accepted: bool,
95        /// Human-readable reason for the decision.
96        reason: String,
97        /// Extracted value that was considered.
98        value: serde_json::Value,
99    },
100    /// Phase machine transitioned.
101    PhaseTransition {
102        /// Phase the machine transitioned from.
103        from: String,
104        /// Phase the machine transitioned to.
105        to: String,
106        /// Human-readable reason for the transition.
107        reason: String,
108    },
109    /// Tool dispatched and result obtained.
110    ToolExecution {
111        /// Name of the tool that was called.
112        name: String,
113        /// Arguments passed to the tool.
114        args: serde_json::Value,
115        /// Result returned by the tool.
116        result: serde_json::Value,
117    },
118    /// Tool calls cancelled — either by the server (a `ToolCallCancelled`
119    /// wire event) or locally when a user barge-in interrupted an in-flight
120    /// inline tool. No response is sent for a cancelled call, and a cancelled
121    /// call never advances the governed flow.
122    ToolCancelled {
123        /// IDs of the cancelled tool calls.
124        ids: Vec<String>,
125    },
126    /// Model completed a conversational turn.
127    TurnComplete,
128    /// Model output interrupted by user speech.
129    Interrupted,
130    /// A tool call in a stage with a filler timing has run longer than the
131    /// stage allows: play an earcon or a holding line now.
132    FillerCue {
133        /// The tool still running.
134        tool: String,
135        /// How long it has run, in milliseconds.
136        elapsed_ms: u64,
137    },
138    /// A verbatim stage's required text was compared with what the model
139    /// said this turn.
140    VerbatimChecked {
141        /// The stage.
142        step: String,
143        /// Word-level similarity, 0–1.
144        similarity: f64,
145        /// Whether it was close enough to count as verbatim.
146        passed: bool,
147    },
148    /// The user stayed silent past the active stage's reprompt timing, and
149    /// the model was asked to repeat its question.
150    Reprompted {
151        /// How long the user had been silent, in milliseconds.
152        silence_ms: u64,
153    },
154    /// Session connected to Gemini.
155    Connected,
156    /// Session disconnected.
157    Disconnected {
158        /// Optional reason for disconnection (server-provided or error message).
159        reason: Option<String>,
160    },
161    /// Unrecoverable error.
162    Error(String),
163    /// Server requesting session wind-down.
164    GoAway {
165        /// Time remaining before the server closes the connection.
166        time_left: Duration,
167    },
168
169    // -- Periodic events --
170    /// Aggregated session telemetry snapshot.
171    Telemetry(serde_json::Value),
172    /// Per-turn latency and token metrics.
173    TurnMetrics {
174        /// Turn number (1-indexed).
175        turn: u32,
176        /// End-to-end latency for this turn in milliseconds.
177        latency_ms: u32,
178        /// Number of prompt tokens consumed.
179        prompt_tokens: u32,
180        /// Number of response tokens generated.
181        response_tokens: u32,
182    },
183}
184
185/// A [`futures_util::Stream`] of [`LiveEvent`]s from a Live session.
186///
187/// Created by [`LiveHandle::stream()`](super::handle::LiveHandle::stream).
188/// Wraps the underlying [`broadcast::Receiver`] with stream semantics:
189///
190/// - **Lagged**: if this subscriber falls behind the broadcast buffer, the
191///   missed events are skipped and the stream continues with the next
192///   available event (no error item is yielded).
193/// - **Closed**: when the session's event channel closes, the stream ends
194///   (`next()` returns `None`).
195///
196/// Composes with all `futures`/`tokio-stream` combinators:
197///
198/// ```rust,ignore
199/// use futures_util::StreamExt;
200///
201/// let mut stream = handle.stream();
202/// while let Some(ev) = stream.next().await {
203///     match ev {
204///         LiveEvent::TextDelta(t) => print!("{t}"),
205///         LiveEvent::TurnComplete => println!(),
206///         _ => {}
207///     }
208/// }
209/// ```
210pub struct LiveEventStream {
211    inner: Pin<Box<dyn Stream<Item = LiveEvent> + Send>>,
212}
213
214impl LiveEventStream {
215    /// Wrap a broadcast receiver of [`LiveEvent`]s as a stream.
216    pub(crate) fn new(rx: broadcast::Receiver<LiveEvent>) -> Self {
217        let inner = futures_util::stream::unfold(rx, |mut rx| async move {
218            loop {
219                match rx.recv().await {
220                    Ok(ev) => return Some((ev, rx)),
221                    // Skip lagged (missed) events and keep going.
222                    Err(broadcast::error::RecvError::Lagged(_)) => continue,
223                    // Channel closed: end the stream.
224                    Err(broadcast::error::RecvError::Closed) => return None,
225                }
226            }
227        });
228        Self {
229            inner: Box::pin(inner),
230        }
231    }
232}
233
234impl Stream for LiveEventStream {
235    type Item = LiveEvent;
236
237    fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
238        self.inner.as_mut().poll_next(cx)
239    }
240}
241
242impl std::fmt::Debug for LiveEventStream {
243    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
244        f.debug_struct("LiveEventStream").finish_non_exhaustive()
245    }
246}
247
248#[cfg(test)]
249mod tests {
250    use super::*;
251    use futures_util::StreamExt;
252
253    #[tokio::test]
254    async fn stream_yields_events_in_order_and_ends_on_close() {
255        let (tx, rx) = broadcast::channel::<LiveEvent>(16);
256        let mut stream = LiveEventStream::new(rx);
257
258        tx.send(LiveEvent::VadStart).unwrap();
259        tx.send(LiveEvent::TextDelta("hi".into())).unwrap();
260        tx.send(LiveEvent::TurnComplete).unwrap();
261
262        assert!(matches!(stream.next().await, Some(LiveEvent::VadStart)));
263        match stream.next().await {
264            Some(LiveEvent::TextDelta(t)) => assert_eq!(t, "hi"),
265            other => panic!("expected TextDelta, got {other:?}"),
266        }
267        assert!(matches!(stream.next().await, Some(LiveEvent::TurnComplete)));
268
269        // Closing the channel ends the stream.
270        drop(tx);
271        assert!(stream.next().await.is_none(), "stream ends on Closed");
272    }
273
274    #[tokio::test]
275    async fn stream_skips_lagged_events_and_continues() {
276        // Capacity-2 channel: sending 5 events before polling forces a lag.
277        let (tx, rx) = broadcast::channel::<LiveEvent>(2);
278        let mut stream = LiveEventStream::new(rx);
279
280        for i in 0..5u32 {
281            tx.send(LiveEvent::TextDelta(format!("e{i}"))).unwrap();
282        }
283
284        // The first poll observes the lag, skips it, and yields the oldest
285        // event still buffered (e3), then e4 — no error, no end-of-stream.
286        match stream.next().await {
287            Some(LiveEvent::TextDelta(t)) => assert_eq!(t, "e3"),
288            other => panic!("expected e3 after lag skip, got {other:?}"),
289        }
290        match stream.next().await {
291            Some(LiveEvent::TextDelta(t)) => assert_eq!(t, "e4"),
292            other => panic!("expected e4, got {other:?}"),
293        }
294
295        // The stream is still alive after the lag.
296        tx.send(LiveEvent::TurnComplete).unwrap();
297        assert!(matches!(stream.next().await, Some(LiveEvent::TurnComplete)));
298
299        drop(tx);
300        assert!(stream.next().await.is_none());
301    }
302}