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    /// A Tool or child Agent call started.
48    CallableStarted {
49        /// One-based owning turn.
50        turn: u32,
51        /// Callable boundary.
52        kind: CallableKind,
53        /// Canonical model-emitted call.
54        call: ToolCall,
55    },
56    /// A Tool or child Agent call reached a recoverable terminal result.
57    CallableCompleted {
58        /// One-based owning turn.
59        turn: u32,
60        /// Callable boundary.
61        kind: CallableKind,
62        /// Model-emitted call identity.
63        call_id: String,
64        /// Model-facing callable name.
65        name: String,
66        /// Whether execution produced a successful output.
67        success: bool,
68    },
69    /// Shared run-tree resource usage changed.
70    UsageUpdated {
71        /// Latest cumulative usage snapshot.
72        usage: Usage,
73    },
74    /// The Agent reached a terminal model response.
75    Completed {
76        /// Complete canonical outcome.
77        outcome: AgentOutcome,
78    },
79}
80
81pub(crate) trait AgentObserver: Send + Sync {
82    fn emit(&self, event: AgentStreamEvent);
83
84    fn backpressured(&self) -> bool {
85        false
86    }
87}
88
89#[derive(Debug)]
90pub(crate) struct NoopObserver;
91
92impl AgentObserver for NoopObserver {
93    fn emit(&self, _event: AgentStreamEvent) {}
94}
95
96#[derive(Clone, Debug, Default)]
97pub(crate) struct BufferedObserver {
98    events: Arc<Mutex<VecDeque<AgentStreamEvent>>>,
99}
100
101impl BufferedObserver {
102    pub(crate) fn events(&self) -> Arc<Mutex<VecDeque<AgentStreamEvent>>> {
103        self.events.clone()
104    }
105}
106
107impl AgentObserver for BufferedObserver {
108    fn emit(&self, event: AgentStreamEvent) {
109        self.events
110            .lock()
111            .unwrap_or_else(std::sync::PoisonError::into_inner)
112            .push_back(event);
113    }
114
115    fn backpressured(&self) -> bool {
116        true
117    }
118}
119
120pub(crate) async fn emit_agent_event(observer: &dyn AgentObserver, event: AgentStreamEvent) {
121    observer.emit(event);
122    if observer.backpressured() {
123        YieldOnce::new().await;
124    }
125}
126
127struct YieldOnce {
128    yielded: bool,
129}
130
131impl YieldOnce {
132    const fn new() -> Self {
133        Self { yielded: false }
134    }
135}
136
137impl Future for YieldOnce {
138    type Output = ();
139
140    fn poll(mut self: Pin<&mut Self>, context: &mut Context<'_>) -> Poll<Self::Output> {
141        if self.yielded {
142            Poll::Ready(())
143        } else {
144            self.yielded = true;
145            context.waker().wake_by_ref();
146            Poll::Pending
147        }
148    }
149}
150
151/// A borrow-scoped stream that drives the canonical Agent loop when polled.
152#[must_use = "streams do nothing unless polled"]
153pub struct AgentEventStream<'a> {
154    execution: Option<AgentFuture<'a, Result<AgentOutcome, AgentError>>>,
155    events: Arc<Mutex<VecDeque<AgentStreamEvent>>>,
156    failure: Option<AgentError>,
157    finished: bool,
158}
159
160impl<'a> AgentEventStream<'a> {
161    pub(crate) fn new(
162        execution: AgentFuture<'a, Result<AgentOutcome, AgentError>>,
163        events: Arc<Mutex<VecDeque<AgentStreamEvent>>>,
164    ) -> Self {
165        Self {
166            execution: Some(execution),
167            events,
168            failure: None,
169            finished: false,
170        }
171    }
172
173    fn events(&self) -> MutexGuard<'_, VecDeque<AgentStreamEvent>> {
174        self.events
175            .lock()
176            .unwrap_or_else(std::sync::PoisonError::into_inner)
177    }
178
179    fn pop_event(&self) -> Option<AgentStreamEvent> {
180        self.events().pop_front()
181    }
182}
183
184impl Stream for AgentEventStream<'_> {
185    type Item = Result<AgentStreamEvent, AgentError>;
186
187    fn poll_next(self: Pin<&mut Self>, context: &mut Context<'_>) -> Poll<Option<Self::Item>> {
188        let this = self.get_mut();
189        if let Some(event) = this.pop_event() {
190            return Poll::Ready(Some(Ok(event)));
191        }
192        if let Some(execution) = this.execution.as_mut() {
193            match execution.as_mut().poll(context) {
194                Poll::Pending => {
195                    return this
196                        .pop_event()
197                        .map_or(Poll::Pending, |event| Poll::Ready(Some(Ok(event))));
198                }
199                Poll::Ready(Ok(_outcome)) => {
200                    this.execution = None;
201                }
202                Poll::Ready(Err(error)) => {
203                    this.execution = None;
204                    this.failure = Some(error);
205                }
206            }
207        }
208        if let Some(event) = this.pop_event() {
209            return Poll::Ready(Some(Ok(event)));
210        }
211        if let Some(error) = this.failure.take() {
212            return Poll::Ready(Some(Err(error)));
213        }
214        if this.execution.is_none() {
215            this.finished = true;
216        }
217        if this.finished {
218            Poll::Ready(None)
219        } else {
220            Poll::Pending
221        }
222    }
223}
224
225impl std::fmt::Debug for AgentEventStream<'_> {
226    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
227        formatter
228            .debug_struct("AgentEventStream")
229            .field("queued_events", &self.events().len())
230            .field("has_execution", &self.execution.is_some())
231            .field("has_failure", &self.failure.is_some())
232            .field("finished", &self.finished)
233            .finish()
234    }
235}