Skip to main content

flashmind_types/
stream.rs

1//! [`AgentStream`] — a stream that yields events and terminates with a result.
2//!
3//! Replaces the `(impl Stream<Item = I>, oneshot::Receiver<O>)` pattern used
4//! throughout the agent runtime. Producers yield [`Outcome::Item`] during
5//! processing and [`Outcome::Done`] as the final value. Consumers iterate
6//! the stream normally and call [`take_result`](AgentStream::take_result)
7//! after exhaustion.
8//!
9//! # Why not just `Stream`?
10//!
11//! The agent loop needs both incremental events (text deltas, tool progress)
12//! and a final assembled result (full response text, token counts, finish reason).
13//! This type packages both into a single handle so callers don't need to juggle
14//! two separate handles or risk deadlocks from dropping the receiver.
15
16use std::pin::Pin;
17use std::task::{Context, Poll};
18
19use futures_core::Stream;
20
21/// A single element in an [`AgentStream`]: either an intermediate event or the
22/// final result.
23pub enum Outcome<I, O> {
24    /// An intermediate item yielded during processing.
25    Item(I),
26    /// The final result — must be yielded exactly once, as the last element.
27    Done(O),
28}
29
30/// A stream that yields events of type `I` and produces a final result `O`.
31///
32/// Implements [`Stream<Item = I>`] so it can be used with `.next().await` and
33/// `StreamExt` combinators. When the inner stream yields [`Outcome::Done`],
34/// the result is stored internally and the stream terminates. Retrieve it
35/// with [`take_result`](Self::take_result).
36///
37/// # Thread safety
38///
39/// The inner stream must satisfy `Send + 'a`, which means `AgentStream` can be
40/// safely moved across thread boundaries (e.g., spawned on a Tokio task).
41///
42/// # Example
43///
44/// ```ignore
45/// fn work() -> AgentStream<'static, String, u32> {
46///     AgentStream::new(async_stream::stream! {
47///         yield Outcome::Item("hello".into());
48///         yield Outcome::Item("world".into());
49///         yield Outcome::Done(42);
50///     })
51/// }
52///
53/// let mut s = work();
54/// while let Some(item) = s.next().await {
55///     println!("{item}");
56/// }
57/// let answer = s.take_result().unwrap();
58/// ```
59pub struct AgentStream<'a, I, O> {
60    inner: Pin<Box<dyn Stream<Item = Outcome<I, O>> + Send + 'a>>,
61    result: Option<O>,
62}
63
64impl<'a, I, O> AgentStream<'a, I, O> {
65    /// Wrap a stream of [`Outcome`] values into an `AgentStream`.
66    pub fn new(inner: impl Stream<Item = Outcome<I, O>> + Send + 'a) -> Self {
67        Self {
68            inner: Box::pin(inner),
69            result: None,
70        }
71    }
72
73    /// Take the final result after the stream has been fully consumed.
74    ///
75    /// Returns `None` if the stream hasn't ended yet or if the producer
76    /// never yielded [`Outcome::Done`].
77    ///
78    /// # Panics
79    ///
80    /// This method does not panic. It always returns an [`Option`], yielding
81    /// `None` when no result is available.
82    pub fn take_result(&mut self) -> Option<O> {
83        self.result.take()
84    }
85}
86
87impl<I, O: Unpin> AgentStream<'_, I, O> {
88    /// Poll for the next item without requiring manual pinning or `StreamExt`.
89    pub async fn next(&mut self) -> Option<I> {
90        use futures_util::StreamExt;
91        StreamExt::next(self).await
92    }
93}
94
95impl<I, O: Unpin> Stream for AgentStream<'_, I, O> {
96    type Item = I;
97
98    fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<I>> {
99        let this = self.get_mut();
100        match this.inner.as_mut().poll_next(cx) {
101            Poll::Ready(Some(Outcome::Item(item))) => Poll::Ready(Some(item)),
102            Poll::Ready(Some(Outcome::Done(result))) => {
103                this.result = Some(result);
104                Poll::Ready(None)
105            }
106            Poll::Ready(None) => Poll::Ready(None),
107            Poll::Pending => Poll::Pending,
108        }
109    }
110}
111
112#[cfg(test)]
113mod tests {
114    use super::*;
115    use futures_util::StreamExt;
116
117    #[tokio::test]
118    async fn yields_items_then_result() {
119        let mut s = AgentStream::new(async_stream::stream! {
120            yield Outcome::Item("a");
121            yield Outcome::Item("b");
122            yield Outcome::Done(42);
123        });
124
125        let items: Vec<&str> = s.by_ref().collect().await;
126        assert_eq!(items, vec!["a", "b"]);
127        assert_eq!(s.take_result(), Some(42));
128    }
129
130    #[tokio::test]
131    async fn empty_stream_no_result() {
132        let mut s: AgentStream<&str, i32> = AgentStream::new(futures_util::stream::empty());
133
134        let items: Vec<&str> = s.by_ref().collect().await;
135        assert!(items.is_empty());
136        assert_eq!(s.take_result(), None);
137    }
138
139    #[tokio::test]
140    async fn result_only() {
141        let mut s: AgentStream<&str, i32> = AgentStream::new(async_stream::stream! {
142            yield Outcome::Done(99);
143        });
144
145        let items: Vec<&str> = s.by_ref().collect().await;
146        assert!(items.is_empty());
147        assert_eq!(s.take_result(), Some(99));
148    }
149
150    #[tokio::test]
151    async fn next_convenience() {
152        let mut s = AgentStream::new(async_stream::stream! {
153            yield Outcome::Item("x");
154            yield Outcome::Item("y");
155            yield Outcome::Done(7);
156        });
157
158        assert_eq!(s.next().await, Some("x"));
159        assert_eq!(s.next().await, Some("y"));
160        assert_eq!(s.next().await, None);
161        assert_eq!(s.take_result(), Some(7));
162    }
163}