Skip to main content

areev_run/
stream.rs

1//! Streaming (§6.10) — **observational only**. Events describe what the
2//! driver journaled; they never carry authority and never backpressure the
3//! scheduler: `emit` is non-blocking by construction (bounded buffer,
4//! drop-oldest + counter), and the subscriber runs on its own thread. The
5//! §6.10 check is that journals are IDENTICAL with no subscriber, a
6//! subscriber, and a deliberately slow subscriber.
7
8use std::collections::VecDeque;
9use std::sync::{Arc, Condvar, Mutex};
10
11/// One §6.10 event, stamped with where in the run it happened. The enum is
12/// append-only (subscribers match non-exhaustively).
13#[derive(Debug, Clone, PartialEq, serde::Serialize)]
14#[serde(tag = "event")]
15pub enum RunEvent {
16    RunStarted { run_id: String },
17    RunResumed { run_id: String },
18    NodeDispatched {
19        superstep: u64,
20        node: String,
21        task_path: String,
22        attempt: u32,
23        effect_seq: u32,
24    },
25    EffectSettled {
26        superstep: u64,
27        node: String,
28        task_path: String,
29        attempt: u32,
30        effect_seq: u32,
31        ok: bool,
32    },
33    AskRaised { superstep: u64, node: String, tool_call_id: String },
34    CheckpointWritten { superstep: u64, hash: String },
35    /// A streamed text delta from an abstract node's model turn (Wave 2f —
36    /// reserved in the vocabulary now so subscribers can already match it).
37    TokenChunk { node: String, task_path: String, attempt: u32, text: String },
38    /// Always the last event. `dropped_events` is the §6.10 honesty
39    /// counter: how many events the bounded buffer discarded.
40    RunFinished { run_id: String, outcome: String, dropped_events: u64 },
41}
42
43/// A host-supplied event subscriber. Called on the bus's OWN thread — a
44/// slow implementation delays events (and eventually loses the oldest),
45/// never the run.
46pub trait RunObserver: Send + Sync {
47    fn event(&self, ev: &RunEvent);
48}
49
50struct Queue {
51    buf: VecDeque<RunEvent>,
52    dropped: u64,
53    closed: bool,
54}
55
56struct Shared {
57    q: Mutex<Queue>,
58    cv: Condvar,
59}
60
61/// The bounded drop-oldest event bus. One per drive; dropped on drive exit
62/// (Drop drains what is queued, then joins the delivery thread).
63pub(crate) struct EventBus {
64    shared: Arc<Shared>,
65    capacity: usize,
66    worker: Option<std::thread::JoinHandle<()>>,
67}
68
69pub(crate) const EVENT_BUFFER: usize = 1024;
70
71impl EventBus {
72    pub fn new(observer: Arc<dyn RunObserver>, capacity: usize) -> Self {
73        let shared = Arc::new(Shared {
74            q: Mutex::new(Queue { buf: VecDeque::new(), dropped: 0, closed: false }),
75            cv: Condvar::new(),
76        });
77        let worker_shared = Arc::clone(&shared);
78        let worker = std::thread::spawn(move || loop {
79            // One event per iteration: the lock is never held during
80            // delivery, so `emit` stays wait-free for the driver.
81            let ev = {
82                let mut q = worker_shared.q.lock().unwrap_or_else(|e| e.into_inner());
83                loop {
84                    if let Some(ev) = q.buf.pop_front() {
85                        break ev;
86                    }
87                    if q.closed {
88                        return;
89                    }
90                    q = worker_shared
91                        .cv
92                        .wait(q)
93                        .unwrap_or_else(|e| e.into_inner());
94                }
95            };
96            observer.event(&ev);
97        });
98        EventBus { shared, capacity, worker: Some(worker) }
99    }
100
101    /// Non-blocking: over capacity, the OLDEST queued event is discarded
102    /// and counted — the run never waits for a subscriber.
103    pub fn emit(&self, ev: RunEvent) {
104        let mut q = self.shared.q.lock().unwrap_or_else(|e| e.into_inner());
105        if q.buf.len() >= self.capacity {
106            q.buf.pop_front();
107            q.dropped += 1;
108        }
109        q.buf.push_back(ev);
110        drop(q);
111        self.shared.cv.notify_one();
112    }
113
114    pub fn dropped(&self) -> u64 {
115        self.shared.q.lock().unwrap_or_else(|e| e.into_inner()).dropped
116    }
117}
118
119impl Drop for EventBus {
120    fn drop(&mut self) {
121        {
122            let mut q = self.shared.q.lock().unwrap_or_else(|e| e.into_inner());
123            q.closed = true;
124        }
125        self.shared.cv.notify_one();
126        if let Some(w) = self.worker.take() {
127            let _ = w.join();
128        }
129    }
130}
131
132#[cfg(test)]
133mod tests {
134    use super::*;
135    use std::sync::mpsc;
136
137    /// An observer that BLOCKS inside its first delivery until released —
138    /// the §6.10 slow-subscriber case, made deterministic.
139    struct Blocking {
140        entered: mpsc::Sender<()>,
141        release: Mutex<mpsc::Receiver<()>>,
142        seen: Mutex<Vec<RunEvent>>,
143    }
144
145    impl RunObserver for Blocking {
146        fn event(&self, ev: &RunEvent) {
147            let first = {
148                let mut seen = self.seen.lock().unwrap();
149                seen.push(ev.clone());
150                seen.len() == 1
151            };
152            if first {
153                let _ = self.entered.send(());
154                let _ = self.release.lock().unwrap().recv();
155            }
156        }
157    }
158
159    #[test]
160    fn bounded_buffer_drops_oldest_and_counts() {
161        let (entered_tx, entered_rx) = mpsc::channel();
162        let (release_tx, release_rx) = mpsc::channel();
163        let obs = Arc::new(Blocking {
164            entered: entered_tx,
165            release: Mutex::new(release_rx),
166            seen: Mutex::new(Vec::new()),
167        });
168        let bus = EventBus::new(Arc::clone(&obs) as Arc<dyn RunObserver>, 4);
169        let ev = |i: u64| RunEvent::CheckpointWritten { superstep: i, hash: String::new() };
170
171        // First event enters delivery and blocks there.
172        bus.emit(ev(1));
173        entered_rx.recv().unwrap();
174        // Nine more queue behind it; capacity 4 keeps only the newest four.
175        for i in 2..=10 {
176            bus.emit(ev(i));
177        }
178        assert_eq!(bus.dropped(), 5, "events 2..=6 were discarded, oldest first");
179        release_tx.send(()).unwrap();
180        drop(bus); // drains the survivors, then joins
181
182        let seen = obs.seen.lock().unwrap();
183        let got: Vec<u64> = seen
184            .iter()
185            .map(|e| match e {
186                RunEvent::CheckpointWritten { superstep, .. } => *superstep,
187                _ => unreachable!(),
188            })
189            .collect();
190        assert_eq!(got, vec![1, 7, 8, 9, 10], "in-flight + the newest four");
191    }
192}