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 first_item_ms: Option<u64>,
64 finished_ms: Option<u64>,
65}
66
67impl Slot {
68 fn into_record(self) -> Option<EffectRecord> {
70 Some(EffectRecord {
71 published: self.tool_output,
72 id: self.id,
73 key: self.key,
74 kind: self.kind,
75 outcome: self.outcome?,
76 events: self
77 .events
78 .and_then(|items| StreamRecord::from_items(items).ok()),
79 stream_origin: self.stream_origin,
80 parent: self.origin.parent,
81 scope: self.origin.scope,
82 purpose: self.origin.purpose,
83 turn: None,
84 started_ms: self.started_ms,
85 first_item_ms: self.first_item_ms,
86 finished_ms: self.finished_ms,
87 reviewed: None,
88 disposition: Disposition::Ran,
89 decided_by: None,
90 })
91 }
92}
93
94impl RecordBuffer {
95 pub fn new() -> Self {
98 Self::default()
99 }
100
101 #[must_use]
105 pub fn keeping_stream_events(mut self) -> Self {
106 self.keep_events = true;
107 self
108 }
109
110 #[must_use]
116 pub fn timed(mut self) -> Self {
117 self.timed = true;
118 self
119 }
120
121 pub fn take(&self, id: EffectId) -> Option<EffectRecord> {
124 let mut slots = self.slots();
125 let position = slots
126 .iter()
127 .rposition(|slot| slot.id == id && slot.outcome.is_some())?;
128 slots.remove(position).into_record()
129 }
130
131 pub fn take_resolved(&self) -> Vec<EffectRecord> {
134 self.take_resolved_where(|_| true)
135 }
136
137 pub fn take_resolved_where(&self, mut take: impl FnMut(EffectId) -> bool) -> Vec<EffectRecord> {
140 let mut slots = self.slots();
141 let (taken, left) = std::mem::take(&mut *slots)
142 .into_iter()
143 .partition(|slot| slot.outcome.is_some() && take(slot.id));
144 *slots = left;
145 drop(slots);
146 taken.into_iter().filter_map(Slot::into_record).collect()
147 }
148
149 pub fn records(&self) -> Vec<EffectRecord> {
151 let slots = self.slots();
152 let resolved: Vec<Slot> = slots
153 .iter()
154 .filter(|slot| slot.outcome.is_some())
155 .cloned()
156 .collect();
157 drop(slots);
158 resolved.into_iter().filter_map(Slot::into_record).collect()
159 }
160
161 pub fn contains(&self, id: EffectId) -> bool {
163 self.slots().iter().any(|slot| slot.id == id)
164 }
165
166 pub fn remove(&self, id: EffectId) -> Option<HandlerKey> {
169 let mut slots = self.slots();
170 let position = slots.iter().rposition(|slot| slot.id == id)?;
171 Some(slots.remove(position).key)
172 }
173
174 pub fn kept_events(&self, id: EffectId) -> Option<usize> {
177 let slots = self.slots();
178 let slot = slots.iter().rev().find(|slot| slot.id == id)?;
179 slot.events.as_ref().map(Vec::len)
180 }
181
182 pub fn resolved(&self) -> usize {
184 let slots = self.slots();
185 slots.iter().filter(|slot| slot.outcome.is_some()).count()
186 }
187
188 pub fn in_flight(&self) -> usize {
190 let slots = self.slots();
191 slots.iter().filter(|slot| slot.outcome.is_none()).count()
192 }
193
194 fn slots(&self) -> MutexGuard<'_, Vec<Slot>> {
195 self.slots.lock().unwrap_or_else(PoisonError::into_inner)
196 }
197
198 fn with_slot(&self, id: EffectId, keep: impl Fn(&Slot) -> bool, edit: impl FnOnce(&mut Slot)) {
201 let mut slots = self.slots();
202 if let Some(slot) = slots
203 .iter_mut()
204 .rev()
205 .find(|slot| slot.id == id && keep(slot))
206 {
207 edit(slot);
208 }
209 }
210
211 fn now(&self) -> Option<u64> {
213 self.timed.then(crate::now_ms)
214 }
215}
216
217impl std::fmt::Debug for RecordBuffer {
218 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
219 f.debug_struct("RecordBuffer")
220 .field("resolved", &self.resolved())
221 .field("in_flight", &self.in_flight())
222 .finish()
223 }
224}
225
226impl Recorder for RecordBuffer {
227 fn handlers(&self, _handlers: Vec<HandlerDescriptor>) {}
228
229 fn begin(&self, id: EffectId, key: HandlerKey, kind: EffectKind, origin: DispatchContext) {
230 let events = (self.keep_events && kind.streams()).then(Vec::new);
231 let started_ms = self.now();
232 self.slots().push(Slot {
233 id,
234 key,
235 kind,
236 origin,
237 outcome: None,
238 events,
239 stream_origin: None,
240 tool_output: None,
241 started_ms,
242 first_item_ms: None,
243 finished_ms: None,
244 });
245 }
246
247 fn discard(&self, id: EffectId) {
248 self.remove(id);
249 }
250
251 fn patch(&self, id: EffectId, kind: EffectKind) {
252 self.with_slot(id, |_| true, |slot| slot.kind = kind);
253 }
254
255 fn keep_events(&self) -> bool {
256 self.keep_events
257 }
258
259 fn event(&self, id: EffectId, item: &Item<StreamEvent>) {
260 if !self.keep_events && !self.timed {
261 return;
262 }
263 self.with_slot(
264 id,
265 |_| true,
266 |slot| {
267 slot.first_item_ms = slot.first_item_ms.or_else(|| self.now());
268 if let Some(events) = &mut slot.events {
269 events.push(item.clone());
270 }
271 },
272 );
273 }
274
275 fn origin(&self, id: EffectId, origin: &crate::message::Origin) {
276 let keeps = |slot: &Slot| slot.events.is_some();
277 self.with_slot(id, keeps, |slot| {
278 slot.stream_origin = Some(origin.clone());
279 });
280 }
281
282 fn tool_output(&self, id: EffectId, output: ToolResultContext) {
283 let open = |slot: &Slot| slot.outcome.is_none();
284 self.with_slot(id, open, |slot| slot.tool_output = Some(output));
285 }
286
287 fn resolve(&self, id: EffectId, outcome: Result<Answered, ErrorReport>) {
288 let finished_ms = self.now();
289 self.with_slot(
290 id,
291 |_| true,
292 |slot| {
293 slot.outcome = Some(outcome);
294 slot.finished_ms = finished_ms;
295 },
296 );
297 }
298}
299
300#[cfg(test)]
301mod tests;