1use std::sync::Arc;
31
32use async_trait::async_trait;
33use parking_lot::Mutex;
34
35use gemini_genai_rs::prelude::{Content, FunctionResponse};
36use gemini_genai_rs::protocol::ContextUpdate;
37use gemini_genai_rs::session::{SessionError, SessionWriter};
38
39pub struct PendingContext {
50 buffer: Mutex<Vec<Content>>,
51 prompt: Mutex<bool>,
53}
54
55impl PendingContext {
56 pub fn new() -> Self {
58 Self {
59 buffer: Mutex::new(Vec::new()),
60 prompt: Mutex::new(false),
61 }
62 }
63
64 pub fn push(&self, content: Content) {
66 self.buffer.lock().push(content);
67 }
68
69 pub fn extend(&self, contents: Vec<Content>) {
71 if !contents.is_empty() {
72 self.buffer.lock().extend(contents);
73 }
74 }
75
76 pub fn set_prompt(&self) {
78 *self.prompt.lock() = true;
79 }
80
81 pub fn drain(&self) -> (Vec<Content>, bool) {
85 let contents = self.drain_context();
86 let prompt = self.take_prompt();
87 (contents, prompt)
88 }
89
90 pub fn drain_context(&self) -> Vec<Content> {
92 {
93 let mut buf = self.buffer.lock();
94 std::mem::take(&mut *buf)
95 }
96 }
97
98 pub fn take_prompt(&self) -> bool {
100 let mut p = self.prompt.lock();
101 std::mem::replace(&mut *p, false)
102 }
103
104 pub fn clear_prompt(&self) {
106 *self.prompt.lock() = false;
107 }
108
109 pub fn has_prompt(&self) -> bool {
111 *self.prompt.lock()
112 }
113
114 pub fn is_empty(&self) -> bool {
116 self.buffer.lock().is_empty() && !*self.prompt.lock()
117 }
118}
119
120impl Default for PendingContext {
121 fn default() -> Self {
122 Self::new()
123 }
124}
125
126pub struct DeferredWriter {
151 inner: Arc<dyn SessionWriter>,
152 pending: Arc<PendingContext>,
153}
154
155impl DeferredWriter {
156 pub fn new(inner: Arc<dyn SessionWriter>, pending: Arc<PendingContext>) -> Self {
158 Self { inner, pending }
159 }
160
161 async fn flush_context(&self) -> Result<(), SessionError> {
166 let contents = self.pending.drain_context();
167 if !contents.is_empty() {
168 self.inner.send_client_content(contents, false).await?;
169 }
170 Ok(())
171 }
172
173 pub fn pending(&self) -> &Arc<PendingContext> {
175 &self.pending
176 }
177}
178
179#[async_trait]
180impl SessionWriter for DeferredWriter {
181 async fn send_audio(&self, data: bytes::Bytes) -> Result<(), SessionError> {
182 self.flush_context().await?;
183 self.inner.send_audio(data).await
184 }
185
186 async fn send_text(&self, text: String) -> Result<(), SessionError> {
187 self.flush_context().await?;
188 self.inner.send_text(text).await
189 }
190
191 async fn send_tool_response(
192 &self,
193 responses: Vec<FunctionResponse>,
194 ) -> Result<(), SessionError> {
195 self.inner.send_tool_response(responses).await
197 }
198
199 async fn send_client_content(
200 &self,
201 turns: Vec<Content>,
202 turn_complete: bool,
203 ) -> Result<(), SessionError> {
204 self.inner.send_client_content(turns, turn_complete).await
207 }
208
209 async fn send_video(&self, jpeg_data: bytes::Bytes) -> Result<(), SessionError> {
210 self.flush_context().await?;
211 self.inner.send_video(jpeg_data).await
212 }
213
214 async fn update_instruction(&self, instruction: String) -> Result<(), SessionError> {
215 self.inner.update_instruction(instruction).await
217 }
218
219 async fn update_context(&self, update: ContextUpdate) -> Result<(), SessionError> {
220 self.inner.update_context(update).await
222 }
223
224 async fn signal_activity_start(&self) -> Result<(), SessionError> {
225 self.inner.signal_activity_start().await
226 }
227
228 async fn signal_activity_end(&self) -> Result<(), SessionError> {
229 self.inner.signal_activity_end().await
230 }
231
232 async fn disconnect(&self) -> Result<(), SessionError> {
233 let _ = self.flush_context().await;
235 self.inner.disconnect().await
236 }
237}
238
239#[cfg(test)]
240mod tests {
241 use super::*;
242 use std::sync::atomic::{AtomicUsize, Ordering};
243
244 struct CountingWriter {
246 audio_count: AtomicUsize,
247 text_count: AtomicUsize,
248 client_content_count: AtomicUsize,
249 video_count: AtomicUsize,
250 }
251
252 impl CountingWriter {
253 fn new() -> Self {
254 Self {
255 audio_count: AtomicUsize::new(0),
256 text_count: AtomicUsize::new(0),
257 client_content_count: AtomicUsize::new(0),
258 video_count: AtomicUsize::new(0),
259 }
260 }
261 }
262
263 #[async_trait]
264 impl SessionWriter for CountingWriter {
265 async fn send_audio(&self, _: bytes::Bytes) -> Result<(), SessionError> {
266 self.audio_count.fetch_add(1, Ordering::SeqCst);
267 Ok(())
268 }
269 async fn send_text(&self, _: String) -> Result<(), SessionError> {
270 self.text_count.fetch_add(1, Ordering::SeqCst);
271 Ok(())
272 }
273 async fn send_tool_response(&self, _: Vec<FunctionResponse>) -> Result<(), SessionError> {
274 Ok(())
275 }
276 async fn send_client_content(&self, _: Vec<Content>, _: bool) -> Result<(), SessionError> {
277 self.client_content_count.fetch_add(1, Ordering::SeqCst);
278 Ok(())
279 }
280 async fn send_video(&self, _: bytes::Bytes) -> Result<(), SessionError> {
281 self.video_count.fetch_add(1, Ordering::SeqCst);
282 Ok(())
283 }
284 async fn update_instruction(&self, _: String) -> Result<(), SessionError> {
285 Ok(())
286 }
287 async fn signal_activity_start(&self) -> Result<(), SessionError> {
288 Ok(())
289 }
290 async fn signal_activity_end(&self) -> Result<(), SessionError> {
291 Ok(())
292 }
293 async fn disconnect(&self) -> Result<(), SessionError> {
294 Ok(())
295 }
296 }
297
298 #[test]
299 fn pending_context_push_and_drain() {
300 let pc = PendingContext::new();
301 assert!(pc.is_empty());
302
303 pc.push(Content::model("context 1"));
304 pc.push(Content::model("context 2"));
305 assert!(!pc.is_empty());
306
307 let (contents, prompt) = pc.drain();
308 assert_eq!(contents.len(), 2);
309 assert!(!prompt);
310 assert!(pc.is_empty());
311 }
312
313 #[test]
314 fn pending_context_extend() {
315 let pc = PendingContext::new();
316 pc.extend(vec![
317 Content::model("a"),
318 Content::model("b"),
319 Content::model("c"),
320 ]);
321 let (contents, _) = pc.drain();
322 assert_eq!(contents.len(), 3);
323 }
324
325 #[test]
326 fn pending_context_prompt_flag() {
327 let pc = PendingContext::new();
328 pc.push(Content::model("ctx"));
329 pc.set_prompt();
330 assert!(!pc.is_empty());
331
332 let (contents, prompt) = pc.drain();
333 assert_eq!(contents.len(), 1);
334 assert!(prompt);
335 assert!(pc.is_empty());
336 }
337
338 #[test]
339 fn pending_context_drain_clears() {
340 let pc = PendingContext::new();
341 pc.push(Content::model("a"));
342 pc.set_prompt();
343 let _ = pc.drain();
344
345 let (contents, prompt) = pc.drain();
347 assert!(contents.is_empty());
348 assert!(!prompt);
349 }
350
351 #[tokio::test]
352 async fn deferred_writer_flushes_on_send_audio() {
353 let inner = Arc::new(CountingWriter::new());
354 let pending = Arc::new(PendingContext::new());
355 let writer = DeferredWriter::new(inner.clone(), pending.clone());
356
357 pending.push(Content::model("steering context"));
358 pending.push(Content::model("phase instruction"));
359
360 writer.send_audio(vec![0u8; 100].into()).await.unwrap();
361
362 assert_eq!(inner.client_content_count.load(Ordering::SeqCst), 1);
364 assert_eq!(inner.audio_count.load(Ordering::SeqCst), 1);
365 assert!(pending.is_empty());
366 }
367
368 #[tokio::test]
369 async fn deferred_writer_flushes_on_send_text() {
370 let inner = Arc::new(CountingWriter::new());
371 let pending = Arc::new(PendingContext::new());
372 let writer = DeferredWriter::new(inner.clone(), pending.clone());
373
374 pending.push(Content::model("context"));
375
376 writer.send_text("hello".into()).await.unwrap();
377
378 assert_eq!(inner.client_content_count.load(Ordering::SeqCst), 1);
379 assert_eq!(inner.text_count.load(Ordering::SeqCst), 1);
380 }
381
382 #[tokio::test]
383 async fn deferred_writer_flushes_on_send_video() {
384 let inner = Arc::new(CountingWriter::new());
385 let pending = Arc::new(PendingContext::new());
386 let writer = DeferredWriter::new(inner.clone(), pending.clone());
387
388 pending.push(Content::model("context"));
389
390 writer.send_video(vec![0xFFu8; 50].into()).await.unwrap();
391
392 assert_eq!(inner.client_content_count.load(Ordering::SeqCst), 1);
393 assert_eq!(inner.video_count.load(Ordering::SeqCst), 1);
394 }
395
396 #[tokio::test]
397 async fn deferred_writer_no_flush_when_empty() {
398 let inner = Arc::new(CountingWriter::new());
399 let pending = Arc::new(PendingContext::new());
400 let writer = DeferredWriter::new(inner.clone(), pending.clone());
401
402 writer.send_audio(vec![0u8; 100].into()).await.unwrap();
404
405 assert_eq!(inner.client_content_count.load(Ordering::SeqCst), 0);
406 assert_eq!(inner.audio_count.load(Ordering::SeqCst), 1);
407 }
408
409 #[tokio::test]
410 async fn deferred_writer_keeps_prompt_pending_on_user_audio() {
411 let inner = Arc::new(CountingWriter::new());
412 let pending = Arc::new(PendingContext::new());
413 let writer = DeferredWriter::new(inner.clone(), pending.clone());
414
415 pending.push(Content::model("repair nudge"));
416 pending.set_prompt();
417
418 writer.send_audio(vec![0u8; 100].into()).await.unwrap();
419
420 assert_eq!(inner.client_content_count.load(Ordering::SeqCst), 1);
423 assert_eq!(inner.audio_count.load(Ordering::SeqCst), 1);
424 assert!(!pending.is_empty());
425 assert!(pending.take_prompt());
426 }
427
428 #[tokio::test]
429 async fn deferred_writer_does_not_flush_on_tool_response() {
430 let inner = Arc::new(CountingWriter::new());
431 let pending = Arc::new(PendingContext::new());
432 let writer = DeferredWriter::new(inner.clone(), pending.clone());
433
434 pending.push(Content::model("context"));
435
436 writer.send_tool_response(vec![]).await.unwrap();
437
438 assert_eq!(inner.client_content_count.load(Ordering::SeqCst), 0);
440 assert!(!pending.is_empty());
441 }
442
443 #[tokio::test]
444 async fn deferred_writer_client_content_passes_through() {
445 let inner = Arc::new(CountingWriter::new());
446 let pending = Arc::new(PendingContext::new());
447 let writer = DeferredWriter::new(inner.clone(), pending.clone());
448
449 pending.push(Content::model("queued context"));
450
451 writer
453 .send_client_content(vec![Content::user("explicit")], true)
454 .await
455 .unwrap();
456
457 assert_eq!(inner.client_content_count.load(Ordering::SeqCst), 1);
458 assert!(!pending.is_empty());
460 }
461}