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