gemini_adk_rs/live/
context_writer.rs

1//! Deferred context delivery — flush pending context alongside user content.
2//!
3//! When the control plane produces model-role context turns (tool advisory,
4//! repair nudge, steering modifiers, phase instructions, on_enter_context),
5//! they can be queued in a [`PendingContext`] buffer instead of sent immediately.
6//!
7//! [`DeferredWriter`] wraps any [`SessionWriter`] and transparently drains the
8//! pending queue before forwarding user-initiated sends (`send_audio`,
9//! `send_text`, `send_video`).  This ensures context arrives in the same burst
10//! as user content rather than as isolated WebSocket frames that can confuse
11//! the model or clash with concurrent user input.
12//!
13//! # Architecture
14//!
15//! ```text
16//!   Control lane (lifecycle)         User code (LiveHandle)
17//!          |                                |
18//!   push context to                  send_audio / send_text
19//!   PendingContext                          |
20//!          |                         DeferredWriter
21//!          v                          1. drain PendingContext
22//!   +---------------+                2. send_client_content(drained, false)
23//!   | PendingContext | <-- drain ---  3. forward original send
24//!   +---------------+
25//! ```
26//!
27//! The queue uses `parking_lot::Mutex` for fast, uncontested locking — the
28//! control lane pushes once per turn, and user sends drain before each frame.
29
30use 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::session::{SessionError, SessionWriter};
37
38/// Thread-safe buffer for pending context turns awaiting delivery.
39///
40/// Context is queued by the control plane (lifecycle steps 7d/7e/7f/12/13)
41/// and drained by [`DeferredWriter`] before the next user interaction.
42///
43/// # Thread safety
44///
45/// Uses `parking_lot::Mutex` — fast uncontested locking, no poisoning.
46/// The control lane pushes once per turn; user sends drain once per frame.
47/// Contention is near-zero.
48pub struct PendingContext {
49    buffer: Mutex<Vec<Content>>,
50    /// Whether a prompt (turnComplete:true) should be sent after flushing.
51    prompt: Mutex<bool>,
52}
53
54impl PendingContext {
55    /// Create an empty pending context buffer.
56    pub fn new() -> Self {
57        Self {
58            buffer: Mutex::new(Vec::new()),
59            prompt: Mutex::new(false),
60        }
61    }
62
63    /// Push a single context turn into the buffer.
64    pub fn push(&self, content: Content) {
65        self.buffer.lock().push(content);
66    }
67
68    /// Push multiple context turns into the buffer.
69    pub fn extend(&self, contents: Vec<Content>) {
70        if !contents.is_empty() {
71            self.buffer.lock().extend(contents);
72        }
73    }
74
75    /// Mark that a prompt (turnComplete:true) should follow the next flush.
76    pub fn set_prompt(&self) {
77        *self.prompt.lock() = true;
78    }
79
80    /// Drain all pending context, returning the contents and whether to prompt.
81    ///
82    /// After this call, the buffer is empty and the prompt flag is cleared.
83    pub fn drain(&self) -> (Vec<Content>, bool) {
84        let contents = self.drain_context();
85        let prompt = self.take_prompt();
86        (contents, prompt)
87    }
88
89    /// Drain only context turns, leaving any pending prompt armed.
90    pub fn drain_context(&self) -> Vec<Content> {
91        {
92            let mut buf = self.buffer.lock();
93            std::mem::take(&mut *buf)
94        }
95    }
96
97    /// Take and clear the pending prompt flag without touching queued context.
98    pub fn take_prompt(&self) -> bool {
99        let mut p = self.prompt.lock();
100        std::mem::replace(&mut *p, false)
101    }
102
103    /// Clear any armed prompt without touching queued context.
104    pub fn clear_prompt(&self) {
105        *self.prompt.lock() = false;
106    }
107
108    /// Return whether a prompt is currently armed.
109    pub fn has_prompt(&self) -> bool {
110        *self.prompt.lock()
111    }
112
113    /// Check if the buffer is empty (no pending context or prompt).
114    pub fn is_empty(&self) -> bool {
115        self.buffer.lock().is_empty() && !*self.prompt.lock()
116    }
117}
118
119impl Default for PendingContext {
120    fn default() -> Self {
121        Self::new()
122    }
123}
124
125/// A [`SessionWriter`] wrapper that flushes pending context before user content.
126///
127/// Wraps an inner writer and drains a shared [`PendingContext`] buffer before
128/// forwarding `send_audio`, `send_text`, or `send_video` calls.  This ensures
129/// model-role context turns arrive in the same burst as user content.
130///
131/// # When context is flushed
132///
133/// - **`send_audio`**: Context is flushed as `send_client_content(drained, false)`
134///   immediately before the audio frame.  Audio goes via `realtimeInput` (different
135///   wire message), so they are two frames — but sent back-to-back with no gap.
136///
137/// - **`send_text`**: Context is flushed, then user text is sent.  Both go via
138///   `clientContent`, but as separate messages since the user text needs
139///   `turn_complete: true` to trigger a model response.
140///
141/// - **`send_video`**: Same as audio — flush then forward.
142///
143/// # When context is NOT flushed
144///
145/// `send_tool_response`, `update_instruction`, `send_client_content`,
146/// `signal_activity_start/end`, and `disconnect` do NOT trigger a flush.
147/// These are either internal SDK operations or explicit user control — flushing
148/// context before them would be surprising.
149pub struct DeferredWriter {
150    inner: Arc<dyn SessionWriter>,
151    pending: Arc<PendingContext>,
152}
153
154impl DeferredWriter {
155    /// Create a new deferred writer wrapping the given writer.
156    pub fn new(inner: Arc<dyn SessionWriter>, pending: Arc<PendingContext>) -> Self {
157        Self { inner, pending }
158    }
159
160    /// Flush any pending context to the wire without triggering a model prompt.
161    ///
162    /// User sends (audio/text/video) use this context-only flush so queued
163    /// phase prompts cannot make the model speak while the user is speaking.
164    async fn flush_context(&self) -> Result<(), SessionError> {
165        let contents = self.pending.drain_context();
166        if !contents.is_empty() {
167            self.inner.send_client_content(contents, false).await?;
168        }
169        Ok(())
170    }
171
172    /// Get a reference to the shared pending context buffer.
173    pub fn pending(&self) -> &Arc<PendingContext> {
174        &self.pending
175    }
176}
177
178#[async_trait]
179impl SessionWriter for DeferredWriter {
180    async fn send_audio(&self, data: bytes::Bytes) -> Result<(), SessionError> {
181        self.flush_context().await?;
182        self.inner.send_audio(data).await
183    }
184
185    async fn send_text(&self, text: String) -> Result<(), SessionError> {
186        self.flush_context().await?;
187        self.inner.send_text(text).await
188    }
189
190    async fn send_tool_response(
191        &self,
192        responses: Vec<FunctionResponse>,
193    ) -> Result<(), SessionError> {
194        // Tool responses are SDK-internal — don't flush context here.
195        self.inner.send_tool_response(responses).await
196    }
197
198    async fn send_client_content(
199        &self,
200        turns: Vec<Content>,
201        turn_complete: bool,
202    ) -> Result<(), SessionError> {
203        // Explicit client content calls pass through unchanged.
204        // The caller knows what they're doing.
205        self.inner.send_client_content(turns, turn_complete).await
206    }
207
208    async fn send_video(&self, jpeg_data: bytes::Bytes) -> Result<(), SessionError> {
209        self.flush_context().await?;
210        self.inner.send_video(jpeg_data).await
211    }
212
213    async fn update_instruction(&self, instruction: String) -> Result<(), SessionError> {
214        // Instruction updates are SDK-internal — don't flush context here.
215        self.inner.update_instruction(instruction).await
216    }
217
218    async fn signal_activity_start(&self) -> Result<(), SessionError> {
219        self.inner.signal_activity_start().await
220    }
221
222    async fn signal_activity_end(&self) -> Result<(), SessionError> {
223        self.inner.signal_activity_end().await
224    }
225
226    async fn disconnect(&self) -> Result<(), SessionError> {
227        // Flush any remaining context before disconnecting so it's not lost.
228        let _ = self.flush_context().await;
229        self.inner.disconnect().await
230    }
231}
232
233#[cfg(test)]
234mod tests {
235    use super::*;
236    use std::sync::atomic::{AtomicUsize, Ordering};
237
238    /// Minimal writer that counts calls by type.
239    struct CountingWriter {
240        audio_count: AtomicUsize,
241        text_count: AtomicUsize,
242        client_content_count: AtomicUsize,
243        video_count: AtomicUsize,
244    }
245
246    impl CountingWriter {
247        fn new() -> Self {
248            Self {
249                audio_count: AtomicUsize::new(0),
250                text_count: AtomicUsize::new(0),
251                client_content_count: AtomicUsize::new(0),
252                video_count: AtomicUsize::new(0),
253            }
254        }
255    }
256
257    #[async_trait]
258    impl SessionWriter for CountingWriter {
259        async fn send_audio(&self, _: bytes::Bytes) -> Result<(), SessionError> {
260            self.audio_count.fetch_add(1, Ordering::SeqCst);
261            Ok(())
262        }
263        async fn send_text(&self, _: String) -> Result<(), SessionError> {
264            self.text_count.fetch_add(1, Ordering::SeqCst);
265            Ok(())
266        }
267        async fn send_tool_response(&self, _: Vec<FunctionResponse>) -> Result<(), SessionError> {
268            Ok(())
269        }
270        async fn send_client_content(&self, _: Vec<Content>, _: bool) -> Result<(), SessionError> {
271            self.client_content_count.fetch_add(1, Ordering::SeqCst);
272            Ok(())
273        }
274        async fn send_video(&self, _: bytes::Bytes) -> Result<(), SessionError> {
275            self.video_count.fetch_add(1, Ordering::SeqCst);
276            Ok(())
277        }
278        async fn update_instruction(&self, _: String) -> Result<(), SessionError> {
279            Ok(())
280        }
281        async fn signal_activity_start(&self) -> Result<(), SessionError> {
282            Ok(())
283        }
284        async fn signal_activity_end(&self) -> Result<(), SessionError> {
285            Ok(())
286        }
287        async fn disconnect(&self) -> Result<(), SessionError> {
288            Ok(())
289        }
290    }
291
292    #[test]
293    fn pending_context_push_and_drain() {
294        let pc = PendingContext::new();
295        assert!(pc.is_empty());
296
297        pc.push(Content::model("context 1"));
298        pc.push(Content::model("context 2"));
299        assert!(!pc.is_empty());
300
301        let (contents, prompt) = pc.drain();
302        assert_eq!(contents.len(), 2);
303        assert!(!prompt);
304        assert!(pc.is_empty());
305    }
306
307    #[test]
308    fn pending_context_extend() {
309        let pc = PendingContext::new();
310        pc.extend(vec![
311            Content::model("a"),
312            Content::model("b"),
313            Content::model("c"),
314        ]);
315        let (contents, _) = pc.drain();
316        assert_eq!(contents.len(), 3);
317    }
318
319    #[test]
320    fn pending_context_prompt_flag() {
321        let pc = PendingContext::new();
322        pc.push(Content::model("ctx"));
323        pc.set_prompt();
324        assert!(!pc.is_empty());
325
326        let (contents, prompt) = pc.drain();
327        assert_eq!(contents.len(), 1);
328        assert!(prompt);
329        assert!(pc.is_empty());
330    }
331
332    #[test]
333    fn pending_context_drain_clears() {
334        let pc = PendingContext::new();
335        pc.push(Content::model("a"));
336        pc.set_prompt();
337        let _ = pc.drain();
338
339        // Second drain should be empty
340        let (contents, prompt) = pc.drain();
341        assert!(contents.is_empty());
342        assert!(!prompt);
343    }
344
345    #[tokio::test]
346    async fn deferred_writer_flushes_on_send_audio() {
347        let inner = Arc::new(CountingWriter::new());
348        let pending = Arc::new(PendingContext::new());
349        let writer = DeferredWriter::new(inner.clone(), pending.clone());
350
351        pending.push(Content::model("steering context"));
352        pending.push(Content::model("phase instruction"));
353
354        writer.send_audio(vec![0u8; 100].into()).await.unwrap();
355
356        // Should have flushed: 1 client_content + 1 audio
357        assert_eq!(inner.client_content_count.load(Ordering::SeqCst), 1);
358        assert_eq!(inner.audio_count.load(Ordering::SeqCst), 1);
359        assert!(pending.is_empty());
360    }
361
362    #[tokio::test]
363    async fn deferred_writer_flushes_on_send_text() {
364        let inner = Arc::new(CountingWriter::new());
365        let pending = Arc::new(PendingContext::new());
366        let writer = DeferredWriter::new(inner.clone(), pending.clone());
367
368        pending.push(Content::model("context"));
369
370        writer.send_text("hello".into()).await.unwrap();
371
372        assert_eq!(inner.client_content_count.load(Ordering::SeqCst), 1);
373        assert_eq!(inner.text_count.load(Ordering::SeqCst), 1);
374    }
375
376    #[tokio::test]
377    async fn deferred_writer_flushes_on_send_video() {
378        let inner = Arc::new(CountingWriter::new());
379        let pending = Arc::new(PendingContext::new());
380        let writer = DeferredWriter::new(inner.clone(), pending.clone());
381
382        pending.push(Content::model("context"));
383
384        writer.send_video(vec![0xFFu8; 50].into()).await.unwrap();
385
386        assert_eq!(inner.client_content_count.load(Ordering::SeqCst), 1);
387        assert_eq!(inner.video_count.load(Ordering::SeqCst), 1);
388    }
389
390    #[tokio::test]
391    async fn deferred_writer_no_flush_when_empty() {
392        let inner = Arc::new(CountingWriter::new());
393        let pending = Arc::new(PendingContext::new());
394        let writer = DeferredWriter::new(inner.clone(), pending.clone());
395
396        // No pending context — should just send audio, no client_content
397        writer.send_audio(vec![0u8; 100].into()).await.unwrap();
398
399        assert_eq!(inner.client_content_count.load(Ordering::SeqCst), 0);
400        assert_eq!(inner.audio_count.load(Ordering::SeqCst), 1);
401    }
402
403    #[tokio::test]
404    async fn deferred_writer_keeps_prompt_pending_on_user_audio() {
405        let inner = Arc::new(CountingWriter::new());
406        let pending = Arc::new(PendingContext::new());
407        let writer = DeferredWriter::new(inner.clone(), pending.clone());
408
409        pending.push(Content::model("repair nudge"));
410        pending.set_prompt();
411
412        writer.send_audio(vec![0u8; 100].into()).await.unwrap();
413
414        // User audio only flushes context. Prompt remains armed until an
415        // explicit idle/playback-drained flush.
416        assert_eq!(inner.client_content_count.load(Ordering::SeqCst), 1);
417        assert_eq!(inner.audio_count.load(Ordering::SeqCst), 1);
418        assert!(!pending.is_empty());
419        assert!(pending.take_prompt());
420    }
421
422    #[tokio::test]
423    async fn deferred_writer_does_not_flush_on_tool_response() {
424        let inner = Arc::new(CountingWriter::new());
425        let pending = Arc::new(PendingContext::new());
426        let writer = DeferredWriter::new(inner.clone(), pending.clone());
427
428        pending.push(Content::model("context"));
429
430        writer.send_tool_response(vec![]).await.unwrap();
431
432        // Tool response should NOT flush — context still pending
433        assert_eq!(inner.client_content_count.load(Ordering::SeqCst), 0);
434        assert!(!pending.is_empty());
435    }
436
437    #[tokio::test]
438    async fn deferred_writer_client_content_passes_through() {
439        let inner = Arc::new(CountingWriter::new());
440        let pending = Arc::new(PendingContext::new());
441        let writer = DeferredWriter::new(inner.clone(), pending.clone());
442
443        pending.push(Content::model("queued context"));
444
445        // Explicit client_content should pass through without flushing
446        writer
447            .send_client_content(vec![Content::user("explicit")], true)
448            .await
449            .unwrap();
450
451        assert_eq!(inner.client_content_count.load(Ordering::SeqCst), 1);
452        // Queued context still pending
453        assert!(!pending.is_empty());
454    }
455}