Skip to main content

agy_bridge/streaming/
writer.rs

1//! The sending/writing side of the streaming channel pair.
2
3use std::sync::{
4    Arc, Mutex,
5    atomic::{AtomicBool, Ordering},
6};
7
8use tokio::sync::mpsc;
9
10use super::types::{
11    ChatResponseSharedState, ResponseEvent, StreamChunk, StreamError, StreamSubscriptions,
12    ToolCallEvent,
13};
14use crate::types::Step;
15
16/// Error returned when sending to a [`ChatResponseWriter`] channel fails.
17///
18/// This wraps the underlying channel error to avoid leaking the
19/// `tokio::sync::mpsc::error::SendError<T>` generic into the public API.
20///
21/// # Example
22///
23/// ```
24/// use agy_bridge::streaming::WriterError;
25///
26/// let err = WriterError::new("receiver dropped");
27/// assert_eq!(err.to_string(), "receiver dropped");
28/// ```
29#[derive(Debug)]
30pub struct WriterError {
31    /// Human-readable description of the failure.
32    pub message: String,
33}
34
35impl WriterError {
36    /// Create a new writer error.
37    pub fn new(message: impl Into<String>) -> Self {
38        Self {
39            message: message.into(),
40        }
41    }
42}
43
44impl std::fmt::Display for WriterError {
45    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
46        write!(f, "{}", self.message)
47    }
48}
49
50impl std::error::Error for WriterError {}
51
52impl<T> From<mpsc::error::SendError<T>> for WriterError {
53    fn from(err: mpsc::error::SendError<T>) -> Self {
54        Self {
55            message: format!("channel send failed: {err}"),
56        }
57    }
58}
59
60/// The sending side of a [`ChatResponseHandle`](super::handle::ChatResponseHandle),
61/// held by the Python bridge thread that drives the SDK's async iterator.
62pub struct ChatResponseWriter {
63    /// Sends text tokens.
64    pub(crate) text_tx: mpsc::Sender<String>,
65    /// Sends thinking tokens.
66    pub(crate) thought_tx: mpsc::Sender<String>,
67    /// Sends tool call events.
68    pub(crate) tool_call_tx: mpsc::Sender<ToolCallEvent>,
69    /// Sends a stream error (at most one).
70    pub(crate) error_tx: mpsc::Sender<StreamError>,
71    /// Sends ordered [`ResponseEvent`]s for the resolve timeline.
72    pub(crate) event_tx: mpsc::Sender<ResponseEvent>,
73    /// Sends [`Step`] objects as they are produced.
74    ///
75    /// The sender must be held to keep the channel alive for
76    /// [`ChatResponseHandle::take_step_stream()`](super::handle::ChatResponseHandle::take_step_stream).
77    /// It will be actively written once step-level streaming is wired through
78    /// the command loop.
79    pub(crate) step_tx: mpsc::Sender<Step>,
80    /// Sends unified [`StreamChunk`]s.
81    pub(crate) chunk_tx: mpsc::Sender<StreamChunk>,
82    /// Per-view subscription flags, shared with the handle.
83    ///
84    /// The writer consults these before fanning out so it never sends to a
85    /// channel the consumer isn't draining (which would block it forever).
86    pub(crate) subs: Arc<StreamSubscriptions>,
87    /// Shared state to send metadata updates back to the handle.
88    pub(crate) shared_state: Arc<Mutex<ChatResponseSharedState>>,
89}
90
91impl ChatResponseWriter {
92    /// Fan a streamed item out to a single optional "view" channel.
93    ///
94    /// This is the deadlock-safe primitive the bridge uses for its multi-channel
95    /// fan-out. It:
96    ///
97    /// * **Skips** the send entirely when no consumer has subscribed to this
98    ///   view. Its receiver is never drained, so a real send would fill the
99    ///   bounded buffer and block the writer forever — silently stalling the
100    ///   whole stream. This is the root cause of the "no progress" hangs.
101    /// * Treats a **dropped receiver** mid-stream as an implicit unsubscribe:
102    ///   it clears the flag and returns, so subsequent items skip immediately.
103    ///
104    /// A single view channel must never block or abort the entire stream.
105    pub(crate) async fn fan_out<T>(
106        subscribed: &AtomicBool,
107        tx: &mpsc::Sender<T>,
108        item: T,
109        channel: &'static str,
110    ) {
111        if !subscribed.load(Ordering::Acquire) {
112            return;
113        }
114        if let Err(e) = tx.send(item).await {
115            subscribed.store(false, Ordering::Release);
116            tracing::debug!(channel, error = %e, "fan-out receiver dropped; unsubscribing view");
117        }
118    }
119
120    /// Send a text token.
121    ///
122    /// # Errors
123    ///
124    /// Returns [`WriterError`] if the receiver has been dropped.
125    pub async fn send_text(&self, text: String) -> Result<(), WriterError> {
126        self.text_tx.send(text).await.map_err(WriterError::from)
127    }
128
129    /// Send a thinking token.
130    ///
131    /// # Errors
132    ///
133    /// Returns [`WriterError`] if the receiver has been dropped.
134    pub async fn send_thought(&self, thought: String) -> Result<(), WriterError> {
135        self.thought_tx
136            .send(thought)
137            .await
138            .map_err(WriterError::from)
139    }
140
141    /// Send a tool call event.
142    ///
143    /// # Errors
144    ///
145    /// Returns [`WriterError`] if the receiver has been dropped.
146    pub async fn send_tool_call(&self, event: ToolCallEvent) -> Result<(), WriterError> {
147        self.tool_call_tx
148            .send(event)
149            .await
150            .map_err(WriterError::from)
151    }
152
153    /// Send an error.
154    ///
155    /// # Errors
156    ///
157    /// Returns [`WriterError`] if the receiver has been dropped.
158    pub async fn send_error(&self, error: StreamError) -> Result<(), WriterError> {
159        self.error_tx.send(error).await.map_err(WriterError::from)
160    }
161
162    /// Send a response event.
163    ///
164    /// # Errors
165    ///
166    /// Returns [`WriterError`] if the receiver has been dropped.
167    pub async fn send_event(&self, event: ResponseEvent) -> Result<(), WriterError> {
168        self.event_tx.send(event).await.map_err(WriterError::from)
169    }
170
171    /// Send a step.
172    ///
173    /// # Errors
174    ///
175    /// Returns [`WriterError`] if the receiver has been dropped.
176    pub async fn send_step(&self, step: crate::types::Step) -> Result<(), WriterError> {
177        self.step_tx.send(step).await.map_err(WriterError::from)
178    }
179
180    /// Send a unified stream chunk.
181    ///
182    /// # Errors
183    ///
184    /// Returns [`WriterError`] if the receiver has been dropped.
185    pub async fn send_chunk(&self, chunk: StreamChunk) -> Result<(), WriterError> {
186        self.chunk_tx.send(chunk).await.map_err(WriterError::from)
187    }
188
189    /// Store usage metadata in the shared state so the handle can read it
190    /// after the stream completes.
191    pub fn set_usage(&self, usage: crate::types::UsageMetadata) {
192        match self.shared_state.lock() {
193            Ok(mut state) => {
194                state.usage = Some(usage);
195            }
196            Err(e) => {
197                tracing::error!(
198                    error = %e,
199                    "ChatResponseWriter shared_state mutex poisoned in set_usage"
200                );
201            }
202        }
203    }
204
205    /// Store structured output in the shared state so the handle can read it
206    /// after the stream completes.
207    pub fn set_structured_output(&self, value: serde_json::Value) {
208        match self.shared_state.lock() {
209            Ok(mut state) => {
210                state.structured_output = Some(value);
211            }
212            Err(e) => {
213                tracing::error!(
214                    error = %e,
215                    "ChatResponseWriter shared_state mutex poisoned in set_structured_output"
216                );
217            }
218        }
219    }
220}