Skip to main content

runifold_agent/
stream.rs

1use std::{
2    collections::VecDeque,
3    future::Future,
4    pin::Pin,
5    sync::{Arc, Mutex, MutexGuard},
6    task::{Context, Poll},
7};
8
9use futures_core::Stream;
10use runifold_core::Usage;
11use runifold_model::{ModelStreamEvent, ToolCall};
12use serde::{Deserialize, Serialize};
13
14use crate::{AgentError, AgentFuture, AgentOutcome};
15
16/// The callable boundary represented by an Agent stream event.
17#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
18#[non_exhaustive]
19pub enum CallableKind {
20    /// A locally registered Tool.
21    Tool,
22    /// A child Agent route.
23    Agent,
24}
25
26/// One real-time event from the canonical Agent execution loop.
27#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
28#[non_exhaustive]
29pub enum AgentStreamEvent {
30    /// The Agent execution started.
31    Started {
32        /// Stable local Agent name.
33        agent: String,
34    },
35    /// A model turn started.
36    TurnStarted {
37        /// One-based turn number.
38        turn: u32,
39    },
40    /// One provider-neutral model streaming event.
41    Model {
42        /// One-based owning turn.
43        turn: u32,
44        /// Canonical event accepted by the model stream accumulator.
45        event: ModelStreamEvent,
46    },
47    /// One dynamic context source completed retrieval.
48    ContextRetrieved {
49        /// Operator-visible source name.
50        source: String,
51        /// Number of documents injected into the transcript.
52        documents: usize,
53    },
54    /// A Tool or child Agent call started.
55    CallableStarted {
56        /// One-based owning turn.
57        turn: u32,
58        /// Callable boundary.
59        kind: CallableKind,
60        /// Canonical model-emitted call.
61        call: ToolCall,
62    },
63    /// A Tool or child Agent call reached a recoverable terminal result.
64    CallableCompleted {
65        /// One-based owning turn.
66        turn: u32,
67        /// Callable boundary.
68        kind: CallableKind,
69        /// Model-emitted call identity.
70        call_id: String,
71        /// Model-facing callable name.
72        name: String,
73        /// Whether execution produced a successful output.
74        success: bool,
75    },
76    /// Shared run-tree resource usage changed.
77    UsageUpdated {
78        /// Latest cumulative usage snapshot.
79        usage: Usage,
80    },
81    /// The Agent reached a terminal model response.
82    Completed {
83        /// Complete canonical outcome.
84        outcome: AgentOutcome,
85    },
86}
87
88pub(crate) trait AgentObserver: Send + Sync {
89    fn emit(&self, event: AgentStreamEvent);
90
91    fn backpressured(&self) -> bool {
92        false
93    }
94}
95
96#[derive(Debug)]
97pub(crate) struct NoopObserver;
98
99impl AgentObserver for NoopObserver {
100    fn emit(&self, _event: AgentStreamEvent) {}
101}
102
103#[derive(Clone, Debug, Default)]
104pub(crate) struct BufferedObserver {
105    events: Arc<Mutex<VecDeque<AgentStreamEvent>>>,
106}
107
108impl BufferedObserver {
109    pub(crate) fn events(&self) -> Arc<Mutex<VecDeque<AgentStreamEvent>>> {
110        self.events.clone()
111    }
112}
113
114impl AgentObserver for BufferedObserver {
115    fn emit(&self, event: AgentStreamEvent) {
116        self.events
117            .lock()
118            .unwrap_or_else(std::sync::PoisonError::into_inner)
119            .push_back(event);
120    }
121
122    fn backpressured(&self) -> bool {
123        true
124    }
125}
126
127pub(crate) async fn emit_agent_event(observer: &dyn AgentObserver, event: AgentStreamEvent) {
128    observer.emit(event);
129    if observer.backpressured() {
130        YieldOnce::new().await;
131    }
132}
133
134struct YieldOnce {
135    yielded: bool,
136}
137
138impl YieldOnce {
139    const fn new() -> Self {
140        Self { yielded: false }
141    }
142}
143
144impl Future for YieldOnce {
145    type Output = ();
146
147    fn poll(mut self: Pin<&mut Self>, context: &mut Context<'_>) -> Poll<Self::Output> {
148        if self.yielded {
149            Poll::Ready(())
150        } else {
151            self.yielded = true;
152            context.waker().wake_by_ref();
153            Poll::Pending
154        }
155    }
156}
157
158/// A borrow-scoped stream that drives the canonical Agent loop when polled.
159#[must_use = "streams do nothing unless polled"]
160pub struct AgentEventStream<'a> {
161    execution: Option<AgentFuture<'a, Result<AgentOutcome, AgentError>>>,
162    events: Arc<Mutex<VecDeque<AgentStreamEvent>>>,
163    failure: Option<AgentError>,
164    finished: bool,
165}
166
167impl<'a> AgentEventStream<'a> {
168    pub(crate) fn new(
169        execution: AgentFuture<'a, Result<AgentOutcome, AgentError>>,
170        events: Arc<Mutex<VecDeque<AgentStreamEvent>>>,
171    ) -> Self {
172        Self {
173            execution: Some(execution),
174            events,
175            failure: None,
176            finished: false,
177        }
178    }
179
180    fn events(&self) -> MutexGuard<'_, VecDeque<AgentStreamEvent>> {
181        self.events
182            .lock()
183            .unwrap_or_else(std::sync::PoisonError::into_inner)
184    }
185
186    fn pop_event(&self) -> Option<AgentStreamEvent> {
187        self.events().pop_front()
188    }
189}
190
191impl Stream for AgentEventStream<'_> {
192    type Item = Result<AgentStreamEvent, AgentError>;
193
194    fn poll_next(self: Pin<&mut Self>, context: &mut Context<'_>) -> Poll<Option<Self::Item>> {
195        let this = self.get_mut();
196        if let Some(event) = this.pop_event() {
197            return Poll::Ready(Some(Ok(event)));
198        }
199        if let Some(execution) = this.execution.as_mut() {
200            match execution.as_mut().poll(context) {
201                Poll::Pending => {
202                    return this
203                        .pop_event()
204                        .map_or(Poll::Pending, |event| Poll::Ready(Some(Ok(event))));
205                }
206                Poll::Ready(Ok(_outcome)) => {
207                    this.execution = None;
208                }
209                Poll::Ready(Err(error)) => {
210                    this.execution = None;
211                    this.failure = Some(error);
212                }
213            }
214        }
215        if let Some(event) = this.pop_event() {
216            return Poll::Ready(Some(Ok(event)));
217        }
218        if let Some(error) = this.failure.take() {
219            return Poll::Ready(Some(Err(error)));
220        }
221        if this.execution.is_none() {
222            this.finished = true;
223        }
224        if this.finished {
225            Poll::Ready(None)
226        } else {
227            Poll::Pending
228        }
229    }
230}
231
232impl std::fmt::Debug for AgentEventStream<'_> {
233    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
234        formatter
235            .debug_struct("AgentEventStream")
236            .field("queued_events", &self.events().len())
237            .field("has_execution", &self.execution.is_some())
238            .field("has_failure", &self.failure.is_some())
239            .field("finished", &self.finished)
240            .finish()
241    }
242}