1use std::sync::{Arc, Mutex, MutexGuard, PoisonError};
21
22use super::{DispatchContext, Recorder};
23use crate::{
24 effect::{
25 Answered, Disposition, EffectId, EffectKind, EffectRecord, HandlerDescriptor, HandlerKey,
26 },
27 error::ErrorReport,
28 streaming::{Item, StreamEvent, StreamRecord},
29 tool::ToolResultContext,
30};
31
32#[derive(Clone, Default)]
45pub struct RecordBuffer {
46 slots: Arc<Mutex<Vec<Slot>>>,
47 keep_events: bool,
48 timed: bool,
49}
50
51#[derive(Clone)]
53struct Slot {
54 id: EffectId,
55 key: HandlerKey,
56 kind: EffectKind,
57 origin: DispatchContext,
58 outcome: Option<Result<Answered, ErrorReport>>,
59 events: Option<Vec<Item<StreamEvent>>>,
60 stream_origin: Option<crate::message::Origin>,
61 tool_output: Option<ToolResultContext>,
62 started_ms: Option<u64>,
63 finished_ms: Option<u64>,
64}
65
66impl Slot {
67 fn into_record(self) -> Option<EffectRecord> {
69 Some(EffectRecord {
70 published: self.tool_output,
71 id: self.id,
72 key: self.key,
73 kind: self.kind,
74 outcome: self.outcome?,
75 events: self
76 .events
77 .and_then(|items| StreamRecord::from_items(items).ok()),
78 stream_origin: self.stream_origin,
79 parent: self.origin.parent,
80 scope: self.origin.scope,
81 purpose: self.origin.purpose,
82 turn: None,
83 started_ms: self.started_ms,
84 finished_ms: self.finished_ms,
85 reviewed: None,
86 disposition: Disposition::Ran,
87 decided_by: None,
88 })
89 }
90}
91
92impl RecordBuffer {
93 pub fn new() -> Self {
96 Self::default()
97 }
98
99 #[must_use]
103 pub fn keeping_stream_events(mut self) -> Self {
104 self.keep_events = true;
105 self
106 }
107
108 #[must_use]
113 pub fn timed(mut self) -> Self {
114 self.timed = true;
115 self
116 }
117
118 pub fn take(&self, id: EffectId) -> Option<EffectRecord> {
121 let mut slots = self.slots();
122 let position = slots
123 .iter()
124 .rposition(|slot| slot.id == id && slot.outcome.is_some())?;
125 slots.remove(position).into_record()
126 }
127
128 pub fn take_resolved(&self) -> Vec<EffectRecord> {
131 self.take_resolved_where(|_| true)
132 }
133
134 pub fn take_resolved_where(&self, mut take: impl FnMut(EffectId) -> bool) -> Vec<EffectRecord> {
137 let mut slots = self.slots();
138 let (taken, left) = std::mem::take(&mut *slots)
139 .into_iter()
140 .partition(|slot| slot.outcome.is_some() && take(slot.id));
141 *slots = left;
142 drop(slots);
143 taken.into_iter().filter_map(Slot::into_record).collect()
144 }
145
146 pub fn records(&self) -> Vec<EffectRecord> {
148 let slots = self.slots();
149 let resolved: Vec<Slot> = slots
150 .iter()
151 .filter(|slot| slot.outcome.is_some())
152 .cloned()
153 .collect();
154 drop(slots);
155 resolved.into_iter().filter_map(Slot::into_record).collect()
156 }
157
158 pub fn contains(&self, id: EffectId) -> bool {
160 self.slots().iter().any(|slot| slot.id == id)
161 }
162
163 pub fn remove(&self, id: EffectId) -> Option<HandlerKey> {
166 let mut slots = self.slots();
167 let position = slots.iter().rposition(|slot| slot.id == id)?;
168 Some(slots.remove(position).key)
169 }
170
171 pub fn kept_events(&self, id: EffectId) -> Option<usize> {
174 let slots = self.slots();
175 let slot = slots.iter().rev().find(|slot| slot.id == id)?;
176 slot.events.as_ref().map(Vec::len)
177 }
178
179 pub fn resolved(&self) -> usize {
181 let slots = self.slots();
182 slots.iter().filter(|slot| slot.outcome.is_some()).count()
183 }
184
185 pub fn in_flight(&self) -> usize {
187 let slots = self.slots();
188 slots.iter().filter(|slot| slot.outcome.is_none()).count()
189 }
190
191 fn slots(&self) -> MutexGuard<'_, Vec<Slot>> {
192 self.slots.lock().unwrap_or_else(PoisonError::into_inner)
193 }
194
195 fn with_slot(&self, id: EffectId, keep: impl Fn(&Slot) -> bool, edit: impl FnOnce(&mut Slot)) {
198 let mut slots = self.slots();
199 if let Some(slot) = slots
200 .iter_mut()
201 .rev()
202 .find(|slot| slot.id == id && keep(slot))
203 {
204 edit(slot);
205 }
206 }
207
208 fn now(&self) -> Option<u64> {
210 use web_time::{SystemTime, UNIX_EPOCH};
211 if !self.timed {
212 return None;
213 }
214 let since = SystemTime::now().duration_since(UNIX_EPOCH).ok()?;
215 Some(u64::try_from(since.as_millis()).unwrap_or(u64::MAX))
216 }
217}
218
219impl std::fmt::Debug for RecordBuffer {
220 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
221 f.debug_struct("RecordBuffer")
222 .field("resolved", &self.resolved())
223 .field("in_flight", &self.in_flight())
224 .finish()
225 }
226}
227
228impl Recorder for RecordBuffer {
229 fn handlers(&self, _handlers: Vec<HandlerDescriptor>) {}
230
231 fn begin(&self, id: EffectId, key: HandlerKey, kind: EffectKind, origin: DispatchContext) {
232 let events = (self.keep_events && kind.streams()).then(Vec::new);
233 let started_ms = self.now();
234 self.slots().push(Slot {
235 id,
236 key,
237 kind,
238 origin,
239 outcome: None,
240 events,
241 stream_origin: None,
242 tool_output: None,
243 started_ms,
244 finished_ms: None,
245 });
246 }
247
248 fn discard(&self, id: EffectId) {
249 self.remove(id);
250 }
251
252 fn patch(&self, id: EffectId, kind: EffectKind) {
253 self.with_slot(id, |_| true, |slot| slot.kind = kind);
254 }
255
256 fn keep_events(&self) -> bool {
257 self.keep_events
258 }
259
260 fn event(&self, id: EffectId, item: &Item<StreamEvent>) {
261 self.with_slot(
262 id,
263 |_| true,
264 |slot| {
265 if let Some(events) = &mut slot.events {
266 events.push(item.clone());
267 }
268 },
269 );
270 }
271
272 fn origin(&self, id: EffectId, origin: &crate::message::Origin) {
273 let keeps = |slot: &Slot| slot.events.is_some();
274 self.with_slot(id, keeps, |slot| {
275 slot.stream_origin = Some(origin.clone());
276 });
277 }
278
279 fn tool_output(&self, id: EffectId, output: ToolResultContext) {
280 let open = |slot: &Slot| slot.outcome.is_none();
281 self.with_slot(id, open, |slot| slot.tool_output = Some(output));
282 }
283
284 fn resolve(&self, id: EffectId, outcome: Result<Answered, ErrorReport>) {
285 let finished_ms = self.now();
286 self.with_slot(
287 id,
288 |_| true,
289 |slot| {
290 slot.outcome = Some(outcome);
291 slot.finished_ms = finished_ms;
292 },
293 );
294 }
295}
296
297#[cfg(test)]
298mod tests;