gemini_genai_rs/session/
events.rs

1//! Session events, commands, and turn tracking.
2//!
3//! [`SessionEvent`] — events emitted by the server for application consumption.
4//! [`SessionCommand`] — commands sent from the application to the transport.
5//! [`Turn`] — tracking for a single model response turn.
6//! [`recv_event`] — broadcast lag-tolerant event receiver.
7
8use super::errors::SessionError;
9use super::state::SessionPhase;
10use crate::protocol::{Content, FunctionCall, FunctionResponse, UsageMetadata};
11use bytes::Bytes;
12use std::time::{Duration, Instant};
13use tokio::sync::broadcast;
14
15// ---------------------------------------------------------------------------
16// Events (server -> application)
17// ---------------------------------------------------------------------------
18
19/// Events emitted by the session, consumed by application code.
20#[derive(Debug, Clone)]
21#[non_exhaustive]
22pub enum SessionEvent {
23    /// Session connected and setup complete.
24    Connected,
25    /// Incremental text from model response.
26    TextDelta(String),
27    /// Complete text of a finished model turn.
28    TextComplete(String),
29    /// Audio data from model response (PCM16 samples, base64-decoded).
30    ///
31    /// Uses [`bytes::Bytes`] for zero-copy fan-out: cloning a `Bytes` handle
32    /// bumps an `Arc` refcount instead of copying the underlying data.
33    AudioData(bytes::Bytes),
34    /// Input transcription from server.
35    InputTranscription(String),
36    /// Output transcription from server.
37    OutputTranscription(String),
38    /// Thought/reasoning summary from the model (when includeThoughts is enabled).
39    Thought(String),
40    /// Model requested tool calls.
41    ToolCall(Vec<FunctionCall>),
42    /// Server cancelled pending tool calls.
43    ToolCallCancelled(Vec<String>),
44    /// Model turn is complete (it's the user's turn now).
45    TurnComplete,
46    /// Model finished generating its full response.
47    ///
48    /// Fires even if the generation was interrupted — tells you the model's
49    /// internal generation pipeline has stopped. Distinct from `TurnComplete`
50    /// which is the turn-taking signal.
51    GenerationComplete,
52    /// Model was interrupted by barge-in.
53    Interrupted,
54    /// Session phase changed.
55    PhaseChanged(SessionPhase),
56    /// Server sent GoAway: it will close the connection after `time_left`
57    /// (when it said how long). The transport reconnects on its own; this
58    /// is the moment to drain buffered audio and expect a resumption handle.
59    GoAway(Option<Duration>),
60    /// Session disconnected (with optional reason).
61    Disconnected(Option<String>),
62    /// Non-fatal error. The session loop keeps running (reconnecting where
63    /// the transport allows); match on the [`SessionError`] variants to tell
64    /// a refused setup from a dropped socket.
65    Error(SessionError),
66    /// Session resumption update with handle, resumability, and consumed index.
67    SessionResumeUpdate(ResumeInfo),
68    /// Server-side voice activity detected (user started speaking).
69    VoiceActivityStart,
70    /// Server-side voice activity ended (user stopped speaking).
71    VoiceActivityEnd,
72    /// Token usage metadata from server (for context window tracking).
73    ///
74    /// Contains full token breakdown: prompt, response, cached, tool-use,
75    /// thinking tokens, plus per-modality details.
76    Usage(UsageMetadata),
77}
78
79/// Session resumption information from the server.
80#[derive(Debug, Clone)]
81pub struct ResumeInfo {
82    /// Opaque handle for session resumption.
83    pub handle: String,
84    /// Whether the session is currently resumable.
85    pub resumable: bool,
86    /// Index of the last client message consumed by the server.
87    pub last_consumed_index: Option<String>,
88}
89
90// ---------------------------------------------------------------------------
91// Commands (application -> server)
92// ---------------------------------------------------------------------------
93
94/// Commands sent from application code to the session transport.
95#[derive(Debug, Clone)]
96pub enum SessionCommand {
97    /// Send audio data (raw PCM16 bytes, will be base64-encoded).
98    ///
99    /// [`Bytes`] both ways: the same handle type as [`SessionEvent::AudioData`],
100    /// so audio can be forwarded without a copy.
101    SendAudio(Bytes),
102    /// Send a text message.
103    SendText(String),
104    /// Send tool responses.
105    SendToolResponse(Vec<FunctionResponse>),
106    /// Signal activity start (client VAD detected speech).
107    ActivityStart,
108    /// Signal activity end (client VAD detected silence).
109    ActivityEnd,
110    /// Send client content (conversation history or context injection).
111    SendClientContent {
112        /// Conversation turns to include.
113        turns: Vec<Content>,
114        /// Whether this completes the client's turn.
115        turn_complete: bool,
116    },
117    /// Send video/image data (raw JPEG bytes, will be base64-encoded).
118    SendVideo(Bytes),
119    /// Update system instruction mid-session (sends client_content with role=system).
120    UpdateInstruction(String),
121    /// Gracefully disconnect.
122    Disconnect,
123}
124
125// ---------------------------------------------------------------------------
126// Turn tracking
127// ---------------------------------------------------------------------------
128
129/// Represents a single model response turn.
130#[derive(Debug, Clone)]
131pub struct Turn {
132    /// Unique turn identifier.
133    pub id: String,
134    /// Accumulated text parts.
135    pub text: String,
136    /// Whether this turn included audio.
137    pub has_audio: bool,
138    /// Tool calls requested in this turn.
139    pub tool_calls: Vec<FunctionCall>,
140    /// When the turn started.
141    pub started_at: Instant,
142    /// When the turn completed (if complete).
143    pub completed_at: Option<Instant>,
144    /// Whether the turn was interrupted.
145    pub interrupted: bool,
146}
147
148impl Turn {
149    /// Create a new turn.
150    pub fn new() -> Self {
151        Self {
152            id: uuid::Uuid::new_v4().to_string(),
153            text: String::new(),
154            has_audio: false,
155            tool_calls: Vec::new(),
156            started_at: Instant::now(),
157            completed_at: None,
158            interrupted: false,
159        }
160    }
161
162    /// Duration of the turn.
163    pub fn duration(&self) -> std::time::Duration {
164        let end = self.completed_at.unwrap_or_else(Instant::now);
165        end.duration_since(self.started_at)
166    }
167}
168
169impl Default for Turn {
170    fn default() -> Self {
171        Self::new()
172    }
173}
174
175// ---------------------------------------------------------------------------
176// Broadcast lag helper
177// ---------------------------------------------------------------------------
178
179/// Receive the next event from a broadcast receiver, handling lag gracefully.
180///
181/// If the receiver falls behind (too slow to keep up with the sender), the
182/// skipped events are logged and the next available event is returned.
183/// Returns `None` when the channel is closed.
184///
185/// # Example
186///
187/// ```no_run
188/// # use gemini_genai_rs::session::{SessionHandle, recv_event};
189/// # async fn run(handle: SessionHandle) {
190/// let mut events = handle.subscribe();
191/// while let Some(event) = recv_event(&mut events).await {
192///     // handle event
193/// }
194/// # }
195/// ```
196pub async fn recv_event(rx: &mut broadcast::Receiver<SessionEvent>) -> Option<SessionEvent> {
197    loop {
198        match rx.recv().await {
199            Ok(event) => return Some(event),
200            Err(broadcast::error::RecvError::Lagged(n)) => {
201                tracing::warn!(skipped = n, "Event subscriber lagged, skipped {n} events");
202                continue;
203            }
204            Err(broadcast::error::RecvError::Closed) => return None,
205        }
206    }
207}
208
209#[cfg(test)]
210mod tests {
211    use super::*;
212
213    #[tokio::test]
214    async fn recv_event_returns_events_normally() {
215        let (tx, mut rx) = broadcast::channel(16);
216
217        tx.send(SessionEvent::Connected).unwrap();
218        tx.send(SessionEvent::TurnComplete).unwrap();
219
220        let event = recv_event(&mut rx).await;
221        assert!(matches!(event, Some(SessionEvent::Connected)));
222
223        let event = recv_event(&mut rx).await;
224        assert!(matches!(event, Some(SessionEvent::TurnComplete)));
225    }
226
227    #[tokio::test]
228    async fn recv_event_returns_none_on_closed_channel() {
229        let (tx, mut rx) = broadcast::channel::<SessionEvent>(16);
230        drop(tx);
231
232        let event = recv_event(&mut rx).await;
233        assert!(event.is_none(), "should return None when channel is closed");
234    }
235
236    #[tokio::test]
237    async fn recv_event_handles_lag() {
238        // Create a tiny broadcast channel (capacity 2)
239        let (tx, mut rx) = broadcast::channel(2);
240
241        // Send 4 events — the receiver will lag behind
242        for i in 0..4 {
243            let _ = tx.send(SessionEvent::TextDelta(format!("msg{i}")));
244        }
245
246        // recv_event should skip the lagged events and return the next available
247        let event = recv_event(&mut rx).await;
248        assert!(event.is_some(), "should get an event after lag");
249    }
250}