1use super::errors::SessionError;
7use super::events::{SessionCommand, SessionEvent};
8use super::state::{SessionPhase, SessionState};
9use super::traits::{SessionReader, SessionWriter};
10use crate::protocol::{Content, FunctionResponse};
11use async_trait::async_trait;
12use bytes::Bytes;
13use std::sync::Arc;
14use tokio::sync::{broadcast, mpsc, watch};
15use tokio::task::JoinHandle;
16
17#[derive(Clone)]
22pub struct SessionHandle {
23 command_tx: mpsc::Sender<SessionCommand>,
25 event_tx: broadcast::Sender<SessionEvent>,
27 state: Arc<SessionState>,
29 phase_rx: watch::Receiver<SessionPhase>,
31 task: Arc<tokio::sync::Mutex<Option<JoinHandle<()>>>>,
37 audio_pacer: Option<Arc<tokio::sync::Mutex<crate::transport::TokenBucket>>>,
43}
44
45impl SessionHandle {
46 pub fn new(
48 command_tx: mpsc::Sender<SessionCommand>,
49 event_tx: broadcast::Sender<SessionEvent>,
50 state: Arc<SessionState>,
51 phase_rx: watch::Receiver<SessionPhase>,
52 ) -> Self {
53 Self {
54 command_tx,
55 event_tx,
56 state,
57 phase_rx,
58 task: Arc::new(tokio::sync::Mutex::new(None)),
59 audio_pacer: None,
60 }
61 }
62
63 pub fn with_audio_pacing(mut self, config: crate::transport::BackpressureConfig) -> Self {
68 self.audio_pacer = Some(Arc::new(tokio::sync::Mutex::new(
69 crate::transport::TokenBucket::new(config),
70 )));
71 self
72 }
73
74 pub fn set_task(&self, handle: JoinHandle<()>) {
78 if let Ok(mut guard) = self.task.try_lock() {
80 *guard = Some(handle);
81 }
82 }
83
84 pub async fn join(&self) -> Result<(), tokio::task::JoinError> {
92 let task = self.task.lock().await.take();
93 if let Some(handle) = task {
94 handle.await
95 } else {
96 Ok(())
97 }
98 }
99
100 pub fn subscribe(&self) -> broadcast::Receiver<SessionEvent> {
102 self.event_tx.subscribe()
103 }
104
105 pub fn event_sender(&self) -> &broadcast::Sender<SessionEvent> {
109 &self.event_tx
110 }
111
112 pub fn resume_handle(&self) -> Option<String> {
114 self.state.resume_handle.lock().clone()
115 }
116
117 pub fn phase(&self) -> SessionPhase {
119 self.state.phase()
120 }
121
122 pub fn session_id(&self) -> &str {
124 &self.state.session_id
125 }
126
127 pub async fn wait_for_phase(&self, target: SessionPhase) {
129 let mut rx = self.phase_rx.clone();
130 while *rx.borrow_and_update() != target {
131 if rx.changed().await.is_err() {
132 break;
133 }
134 }
135 }
136
137 pub async fn send_audio(&self, data: impl Into<Bytes>) -> Result<(), SessionError> {
143 let data: Bytes = data.into();
144 if let Some(pacer) = &self.audio_pacer {
145 pacer.lock().await.consume(data.len()).await;
146 }
147 self.send_command(SessionCommand::SendAudio(data)).await
148 }
149
150 pub async fn send_text(&self, text: impl Into<String>) -> Result<(), SessionError> {
152 self.send_command(SessionCommand::SendText(text.into()))
153 .await
154 }
155
156 pub async fn send_tool_response(
158 &self,
159 responses: Vec<FunctionResponse>,
160 ) -> Result<(), SessionError> {
161 self.send_command(SessionCommand::SendToolResponse(responses))
162 .await
163 }
164
165 pub async fn send_video(&self, jpeg_data: impl Into<Bytes>) -> Result<(), SessionError> {
167 self.send_command(SessionCommand::SendVideo(jpeg_data.into()))
168 .await
169 }
170
171 pub async fn update_instruction(
173 &self,
174 instruction: impl Into<String>,
175 ) -> Result<(), SessionError> {
176 self.send_command(SessionCommand::UpdateInstruction(instruction.into()))
177 .await
178 }
179
180 pub async fn signal_activity_start(&self) -> Result<(), SessionError> {
182 self.send_command(SessionCommand::ActivityStart).await
183 }
184
185 pub async fn signal_activity_end(&self) -> Result<(), SessionError> {
187 self.send_command(SessionCommand::ActivityEnd).await
188 }
189
190 pub async fn send_client_content(
193 &self,
194 turns: Vec<Content>,
195 turn_complete: bool,
196 ) -> Result<(), SessionError> {
197 self.send_command(SessionCommand::SendClientContent {
198 turns,
199 turn_complete,
200 })
201 .await
202 }
203
204 pub async fn disconnect(&self) -> Result<(), SessionError> {
206 self.send_command(SessionCommand::Disconnect).await
207 }
208
209 async fn send_command(&self, cmd: SessionCommand) -> Result<(), SessionError> {
211 self.command_tx
212 .send(cmd)
213 .await
214 .map_err(|_| SessionError::ChannelClosed)
215 }
216}
217
218impl std::fmt::Debug for SessionHandle {
219 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
220 f.debug_struct("SessionHandle")
221 .field("session_id", &self.state.session_id)
222 .field("phase", &self.state.phase())
223 .finish()
224 }
225}
226
227#[async_trait]
232impl SessionWriter for SessionHandle {
233 async fn send_audio(&self, data: Bytes) -> Result<(), SessionError> {
234 SessionHandle::send_audio(self, data).await
235 }
236
237 async fn send_text(&self, text: String) -> Result<(), SessionError> {
238 self.send_command(SessionCommand::SendText(text)).await
239 }
240
241 async fn send_tool_response(
242 &self,
243 responses: Vec<FunctionResponse>,
244 ) -> Result<(), SessionError> {
245 self.send_command(SessionCommand::SendToolResponse(responses))
246 .await
247 }
248
249 async fn send_client_content(
250 &self,
251 turns: Vec<Content>,
252 turn_complete: bool,
253 ) -> Result<(), SessionError> {
254 self.send_command(SessionCommand::SendClientContent {
255 turns,
256 turn_complete,
257 })
258 .await
259 }
260
261 async fn send_video(&self, jpeg_data: Bytes) -> Result<(), SessionError> {
262 self.send_command(SessionCommand::SendVideo(jpeg_data))
263 .await
264 }
265
266 async fn update_instruction(&self, instruction: String) -> Result<(), SessionError> {
267 self.send_command(SessionCommand::UpdateInstruction(instruction))
268 .await
269 }
270
271 async fn signal_activity_start(&self) -> Result<(), SessionError> {
272 self.send_command(SessionCommand::ActivityStart).await
273 }
274
275 async fn signal_activity_end(&self) -> Result<(), SessionError> {
276 self.send_command(SessionCommand::ActivityEnd).await
277 }
278
279 async fn disconnect(&self) -> Result<(), SessionError> {
280 self.send_command(SessionCommand::Disconnect).await
281 }
282}
283
284impl SessionReader for SessionHandle {
285 fn subscribe(&self) -> broadcast::Receiver<SessionEvent> {
286 self.event_tx.subscribe()
287 }
288
289 fn phase(&self) -> SessionPhase {
290 self.state.phase()
291 }
292
293 fn session_id(&self) -> &str {
294 &self.state.session_id
295 }
296}
297
298#[cfg(test)]
299mod tests {
300 use super::*;
301
302 #[tokio::test(start_paused = true)]
303 async fn audio_pacing_throttles_producer_to_sustained_rate() {
304 let (command_tx, mut command_rx) = mpsc::channel(64);
305 let (event_tx, _) = broadcast::channel(16);
306 let (phase_tx, phase_rx) = watch::channel(SessionPhase::Active);
307 let state = Arc::new(SessionState::with_events(phase_tx, event_tx.clone()));
308
309 let handle = SessionHandle::new(command_tx, event_tx, state, phase_rx).with_audio_pacing(
311 crate::transport::BackpressureConfig {
312 bucket_capacity: 1000,
313 refill_rate_bps: 1000,
314 },
315 );
316
317 let start = tokio::time::Instant::now();
318 handle.send_audio(vec![0u8; 1000]).await.unwrap();
320 let after_burst = start.elapsed();
321 handle.send_audio(vec![0u8; 1000]).await.unwrap();
322 let after_paced = start.elapsed();
323
324 assert!(after_burst < std::time::Duration::from_millis(50));
325 assert!(
326 after_paced >= std::time::Duration::from_millis(900),
327 "second send should be paced ~1s, was {after_paced:?}"
328 );
329 assert!(command_rx.recv().await.is_some());
331 assert!(command_rx.recv().await.is_some());
332 }
333
334 #[tokio::test]
335 async fn session_handle_join_returns_ok_after_task_completes() {
336 let (command_tx, _command_rx) = mpsc::channel(8);
337 let (event_tx, _) = broadcast::channel(16);
338 let (phase_tx, phase_rx) = watch::channel(SessionPhase::Disconnected);
339 let state = Arc::new(SessionState::with_events(phase_tx, event_tx.clone()));
340
341 let handle = SessionHandle::new(command_tx, event_tx, state, phase_rx);
342
343 let task = tokio::spawn(async {});
345 handle.set_task(task);
346
347 let result = handle.join().await;
349 assert!(
350 result.is_ok(),
351 "join() should return Ok after task completes"
352 );
353 }
354
355 #[tokio::test]
356 async fn session_handle_join_without_task_returns_ok() {
357 let (command_tx, _command_rx) = mpsc::channel(8);
358 let (event_tx, _) = broadcast::channel(16);
359 let (phase_tx, phase_rx) = watch::channel(SessionPhase::Disconnected);
360 let state = Arc::new(SessionState::with_events(phase_tx, event_tx.clone()));
361
362 let handle = SessionHandle::new(command_tx, event_tx, state, phase_rx);
363
364 let result = handle.join().await;
366 assert!(result.is_ok(), "join() without task should return Ok");
367 }
368
369 #[tokio::test]
370 async fn session_handle_join_idempotent() {
371 let (command_tx, _command_rx) = mpsc::channel(8);
372 let (event_tx, _) = broadcast::channel(16);
373 let (phase_tx, phase_rx) = watch::channel(SessionPhase::Disconnected);
374 let state = Arc::new(SessionState::with_events(phase_tx, event_tx.clone()));
375
376 let handle = SessionHandle::new(command_tx, event_tx, state, phase_rx);
377
378 let task = tokio::spawn(async {});
379 handle.set_task(task);
380
381 assert!(handle.join().await.is_ok());
383 assert!(handle.join().await.is_ok());
385 }
386
387 #[tokio::test]
388 async fn session_handle_join_works_on_clone() {
389 let (command_tx, _command_rx) = mpsc::channel(8);
390 let (event_tx, _) = broadcast::channel(16);
391 let (phase_tx, phase_rx) = watch::channel(SessionPhase::Disconnected);
392 let state = Arc::new(SessionState::with_events(phase_tx, event_tx.clone()));
393
394 let handle = SessionHandle::new(command_tx, event_tx, state, phase_rx);
395 let handle_clone = handle.clone();
396
397 let task = tokio::spawn(async {});
398 handle.set_task(task);
399
400 let result = handle_clone.join().await;
402 assert!(result.is_ok(), "join() on clone should work");
403
404 assert!(handle.join().await.is_ok());
406 }
407
408 #[tokio::test]
411 async fn phase_changed_event_emitted_on_transition() {
412 let (phase_tx, _phase_rx) = watch::channel(SessionPhase::Disconnected);
413 let (event_tx, mut event_rx) = broadcast::channel(16);
414 let state = SessionState::with_events(phase_tx, event_tx);
415
416 state.transition_to(SessionPhase::Connecting).unwrap();
417
418 match event_rx.try_recv() {
419 Ok(SessionEvent::PhaseChanged(SessionPhase::Connecting)) => {}
420 other => panic!("expected PhaseChanged(Connecting), got {other:?}"),
421 }
422 }
423
424 #[test]
425 fn phase_changed_not_emitted_without_event_tx() {
426 let (phase_tx, _phase_rx) = watch::channel(SessionPhase::Disconnected);
427 let state = SessionState::new(phase_tx);
428 state.transition_to(SessionPhase::Connecting).unwrap();
430 }
431}