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}