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, TerminalRequirementFailure};
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    /// An invalid terminal candidate scheduled a bounded repair turn.
87    TerminalRepairScheduled {
88        /// One-based repair attempt number.
89        attempt: u32,
90        /// Safe reason the candidate was rejected.
91        failure: TerminalRequirementFailure,
92    },
93}
94
95pub(crate) trait AgentObserver: Send + Sync {
96    fn emit(&self, event: AgentStreamEvent);
97
98    fn backpressured(&self) -> bool {
99        false
100    }
101}
102
103#[derive(Debug)]
104pub(crate) struct NoopObserver;
105
106impl AgentObserver for NoopObserver {
107    fn emit(&self, _event: AgentStreamEvent) {}
108}
109
110#[derive(Clone, Debug, Default)]
111pub(crate) struct BufferedObserver {
112    events: Arc<Mutex<VecDeque<AgentStreamEvent>>>,
113}
114
115impl BufferedObserver {
116    pub(crate) fn events(&self) -> Arc<Mutex<VecDeque<AgentStreamEvent>>> {
117        self.events.clone()
118    }
119}
120
121impl AgentObserver for BufferedObserver {
122    fn emit(&self, event: AgentStreamEvent) {
123        self.events
124            .lock()
125            .unwrap_or_else(std::sync::PoisonError::into_inner)
126            .push_back(event);
127    }
128
129    fn backpressured(&self) -> bool {
130        true
131    }
132}
133
134pub(crate) async fn emit_agent_event(observer: &dyn AgentObserver, event: AgentStreamEvent) {
135    observer.emit(event);
136    if observer.backpressured() {
137        YieldOnce::new().await;
138    }
139}
140
141struct YieldOnce {
142    yielded: bool,
143}
144
145impl YieldOnce {
146    const fn new() -> Self {
147        Self { yielded: false }
148    }
149}
150
151impl Future for YieldOnce {
152    type Output = ();
153
154    fn poll(mut self: Pin<&mut Self>, context: &mut Context<'_>) -> Poll<Self::Output> {
155        if self.yielded {
156            Poll::Ready(())
157        } else {
158            self.yielded = true;
159            context.waker().wake_by_ref();
160            Poll::Pending
161        }
162    }
163}
164
165/// A borrow-scoped stream that drives the canonical Agent loop when polled.
166#[must_use = "streams do nothing unless polled"]
167pub struct AgentEventStream<'a> {
168    execution: Option<AgentFuture<'a, Result<AgentOutcome, AgentError>>>,
169    events: Arc<Mutex<VecDeque<AgentStreamEvent>>>,
170    failure: Option<AgentError>,
171    finished: bool,
172}
173
174impl<'a> AgentEventStream<'a> {
175    pub(crate) fn new(
176        execution: AgentFuture<'a, Result<AgentOutcome, AgentError>>,
177        events: Arc<Mutex<VecDeque<AgentStreamEvent>>>,
178    ) -> Self {
179        Self {
180            execution: Some(execution),
181            events,
182            failure: None,
183            finished: false,
184        }
185    }
186
187    fn events(&self) -> MutexGuard<'_, VecDeque<AgentStreamEvent>> {
188        self.events
189            .lock()
190            .unwrap_or_else(std::sync::PoisonError::into_inner)
191    }
192
193    fn pop_event(&self) -> Option<AgentStreamEvent> {
194        self.events().pop_front()
195    }
196}
197
198impl Stream for AgentEventStream<'_> {
199    type Item = Result<AgentStreamEvent, AgentError>;
200
201    fn poll_next(self: Pin<&mut Self>, context: &mut Context<'_>) -> Poll<Option<Self::Item>> {
202        let this = self.get_mut();
203        if let Some(event) = this.pop_event() {
204            return Poll::Ready(Some(Ok(event)));
205        }
206        if let Some(execution) = this.execution.as_mut() {
207            match execution.as_mut().poll(context) {
208                Poll::Pending => {
209                    return this
210                        .pop_event()
211                        .map_or(Poll::Pending, |event| Poll::Ready(Some(Ok(event))));
212                }
213                Poll::Ready(Ok(_outcome)) => {
214                    this.execution = None;
215                }
216                Poll::Ready(Err(error)) => {
217                    this.execution = None;
218                    this.failure = Some(error);
219                }
220            }
221        }
222        if let Some(event) = this.pop_event() {
223            return Poll::Ready(Some(Ok(event)));
224        }
225        if let Some(error) = this.failure.take() {
226            return Poll::Ready(Some(Err(error)));
227        }
228        if this.execution.is_none() {
229            this.finished = true;
230        }
231        if this.finished {
232            Poll::Ready(None)
233        } else {
234            Poll::Pending
235        }
236    }
237}
238
239impl std::fmt::Debug for AgentEventStream<'_> {
240    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
241        formatter
242            .debug_struct("AgentEventStream")
243            .field("queued_events", &self.events().len())
244            .field("has_execution", &self.execution.is_some())
245            .field("has_failure", &self.failure.is_some())
246            .field("finished", &self.finished)
247            .finish()
248    }
249}