Skip to main content

atman_runtime/
message_stream.rs

1//! Incrementally maintained message accumulator.  `window()` returns a
2//! `MessageWindow` anchored at the last compaction summary (zero-copy);
3//! `full_messages()` returns a shared `Arc<Vec<Message>>` of every message.
4
5use std::ops::Deref;
6use std::sync::{Arc, Mutex};
7
8use crate::compaction::is_compaction_summary;
9use crate::event::EventEnvelope;
10use crate::message::Message;
11
12#[derive(Clone)]
13pub struct MessageWindow {
14    messages: Arc<Vec<Message>>,
15    start: usize,
16}
17
18impl MessageWindow {
19    pub fn to_vec(&self) -> Vec<Message> {
20        self.as_slice().to_vec()
21    }
22
23    fn as_slice(&self) -> &[Message] {
24        &self.messages[self.start..]
25    }
26}
27
28impl Deref for MessageWindow {
29    type Target = [Message];
30
31    fn deref(&self) -> &Self::Target {
32        self.as_slice()
33    }
34}
35
36struct Acc {
37    compacted: Vec<(u64, Message)>,
38    full_raw: Vec<(u64, Message)>,
39    replayed: usize,
40    full_cache: Arc<Vec<Message>>,
41    window_cache: MessageWindow,
42}
43
44pub struct MessageStream {
45    events: Arc<Mutex<Vec<EventEnvelope>>>,
46    initial_compacted: Vec<(u64, Message)>,
47    initial_raw: Vec<(u64, Message)>,
48    acc: Mutex<Acc>,
49}
50
51impl MessageStream {
52    pub fn new(events: Arc<Mutex<Vec<EventEnvelope>>>) -> Self {
53        let empty = Arc::new(Vec::new());
54        Self {
55            events,
56            initial_compacted: Vec::new(),
57            initial_raw: Vec::new(),
58            acc: Mutex::new(Acc {
59                compacted: Vec::new(),
60                full_raw: Vec::new(),
61                replayed: 0,
62                full_cache: Arc::clone(&empty),
63                window_cache: MessageWindow {
64                    messages: empty,
65                    start: 0,
66                },
67            }),
68        }
69    }
70
71    pub fn with_initial(
72        events: Arc<Mutex<Vec<EventEnvelope>>>,
73        compacted: Vec<(u64, Message)>,
74        raw: Vec<(u64, Message)>,
75    ) -> Self {
76        let full: Arc<Vec<Message>> = Arc::new(raw.iter().map(|(_, msg)| msg.clone()).collect());
77        let start = full.iter().rposition(is_compaction_summary).unwrap_or(0);
78        let window = MessageWindow {
79            messages: Arc::clone(&full),
80            start,
81        };
82        Self {
83            events,
84            initial_compacted: compacted,
85            initial_raw: raw,
86            acc: Mutex::new(Acc {
87                compacted: Vec::new(),
88                full_raw: Vec::new(),
89                replayed: 0,
90                full_cache: full,
91                window_cache: window,
92            }),
93        }
94    }
95
96    pub fn full_messages(&self) -> Arc<Vec<Message>> {
97        let events = self.events.lock().expect("events poisoned");
98        let mut acc = self.acc.lock().expect("acc poisoned");
99        self.ensure_fresh_locked(&events, &mut acc);
100        Arc::clone(&acc.full_cache)
101    }
102
103    pub fn window(&self) -> MessageWindow {
104        let events = self.events.lock().expect("events poisoned");
105        let mut acc = self.acc.lock().expect("acc poisoned");
106        self.ensure_fresh_locked(&events, &mut acc);
107        acc.window_cache.clone()
108    }
109
110    fn ensure_fresh_locked(&self, events: &[EventEnvelope], acc: &mut Acc) {
111        if acc.compacted.is_empty() {
112            acc.compacted = self.initial_compacted.clone();
113            acc.full_raw = self.initial_raw.clone();
114        }
115        if acc.replayed >= events.len() {
116            return;
117        }
118        for ev in &events[acc.replayed..] {
119            crate::projection::message_window::apply_envelope_to_messages(ev, &mut acc.compacted);
120            match &ev.event {
121                crate::event::Event::UserMsg {
122                    message,
123                    flow_run_id: None,
124                    ..
125                }
126                | crate::event::Event::AssistantMsg {
127                    message,
128                    flow_run_id: None,
129                    ..
130                }
131                | crate::event::Event::ToolResultMsg {
132                    message,
133                    flow_run_id: None,
134                    ..
135                }
136                | crate::event::Event::SystemMsg { message, .. } => {
137                    acc.full_raw.push((ev.seq, message.clone()));
138                }
139                _ => {}
140            }
141        }
142        acc.replayed = events.len();
143
144        let compacted: Vec<Message> = acc.compacted.iter().map(|(_, msg)| msg).cloned().collect();
145        let start = compacted
146            .iter()
147            .rposition(is_compaction_summary)
148            .unwrap_or(0);
149        let compacted_arc = Arc::new(compacted);
150        acc.window_cache = MessageWindow {
151            messages: Arc::clone(&compacted_arc),
152            start,
153        };
154
155        let raw: Vec<Message> = acc.full_raw.iter().map(|(_, msg)| msg).cloned().collect();
156        acc.full_cache = Arc::new(raw);
157    }
158}
159
160#[cfg(test)]
161mod tests {
162    use super::*;
163    use crate::event::TurnId;
164    use crate::event::{Event, EventEnvelope};
165    use crate::message::{MessagePart, MessageRole};
166
167    fn user(text: &str) -> Message {
168        Message {
169            role: MessageRole::User,
170            parts: vec![MessagePart::Text {
171                text: text.to_string(),
172            }],
173            turn_id: TurnId::now(),
174        }
175    }
176
177    fn assistant(text: &str) -> Message {
178        Message {
179            role: MessageRole::Assistant,
180            parts: vec![MessagePart::Text {
181                text: text.to_string(),
182            }],
183            turn_id: TurnId::now(),
184        }
185    }
186
187    fn compact_summary(text: &str) -> Message {
188        Message::system_compact_summary(TurnId::now(), text, 0, 1, 2)
189    }
190
191    fn make_msg_event(ty: &str, msg: &Message, _seq: u64) -> Event {
192        match ty {
193            "user_msg" => Event::UserMsg {
194                turn_id: msg.turn_id.clone(),
195                flow_run_id: None,
196                message: msg.clone(),
197            },
198            "assistant_msg" => Event::AssistantMsg {
199                turn_id: msg.turn_id.clone(),
200                flow_run_id: None,
201                message: msg.clone(),
202            },
203            "system_msg" => Event::SystemMsg {
204                turn_id: msg.turn_id.clone(),
205                message: msg.clone(),
206            },
207            _ => unreachable!(),
208        }
209    }
210
211    fn make_context_compact(
212        range_start: u64,
213        range_end: u64,
214        before_tokens: u64,
215        after_tokens: u64,
216        summary_text: &str,
217        replacement_msg_seq: u64,
218    ) -> Event {
219        Event::ContextCompact {
220            session_id: "test".into(),
221            before_tokens,
222            after_tokens,
223            compacted_range_start: range_start,
224            compacted_range_end: range_end,
225            summary_text: Some(summary_text.into()),
226            replacement_msg_seq: Some(replacement_msg_seq),
227        }
228    }
229
230    fn event_envelopes(events: Vec<Event>) -> Arc<Mutex<Vec<EventEnvelope>>> {
231        Arc::new(Mutex::new(
232            events
233                .into_iter()
234                .enumerate()
235                .map(|(i, event)| EventEnvelope::new((i + 1) as u64, event))
236                .collect(),
237        ))
238    }
239
240    #[test]
241    fn full_messages_filters_only_message_events() {
242        let u1 = user("hello");
243        let a1 = assistant("hi there");
244        let events = event_envelopes(vec![
245            make_msg_event("user_msg", &u1, 1),
246            Event::TurnStart {
247                turn_id: TurnId::now(),
248            },
249            make_msg_event("assistant_msg", &a1, 2),
250            Event::LlmCall {
251                model: "m".into(),
252                provider: "p".into(),
253                usage: crate::provider::TokenUsage::default(),
254                wallclock_ms: 0,
255                ttft_ms: None,
256                tokens_per_second: None,
257                status: crate::event::LlmCallStatus::Ok,
258                run_id: None,
259                node_id: None,
260            },
261        ]);
262        let ms = MessageStream::new(events);
263        let msgs = ms.full_messages();
264        assert_eq!(msgs.len(), 2);
265        assert_eq!(msgs[0].text_concat(), "hello");
266        assert_eq!(msgs[1].text_concat(), "hi there");
267    }
268
269    #[test]
270    fn window_no_summary_returns_all() {
271        let events = vec![
272            make_msg_event("user_msg", &user("a"), 1),
273            make_msg_event("assistant_msg", &assistant("b"), 2),
274            make_msg_event("user_msg", &user("c"), 3),
275        ];
276        let ms = MessageStream::new(event_envelopes(events));
277        assert_eq!(ms.window().len(), 3);
278    }
279
280    #[test]
281    fn window_single_summary_starts_from_it() {
282        let s1 = compact_summary("summary 1");
283        let events = vec![
284            make_msg_event("user_msg", &user("old"), 1),
285            make_msg_event("assistant_msg", &assistant("old"), 2),
286            make_msg_event("system_msg", &s1, 3),
287            make_msg_event("user_msg", &user("new"), 4),
288            make_msg_event("assistant_msg", &assistant("new"), 5),
289        ];
290        let ms = MessageStream::new(event_envelopes(events));
291        let w = ms.window();
292        assert_eq!(w.len(), 3);
293        assert!(matches!(w[0].parts[0], MessagePart::CompactSummary { .. }));
294    }
295
296    #[test]
297    fn window_multiple_summaries_uses_last() {
298        let s1 = compact_summary("summary 1");
299        let s2 = compact_summary("summary 2");
300        let events = vec![
301            make_msg_event("system_msg", &s1, 1),
302            make_msg_event("user_msg", &user("m1"), 2),
303            make_msg_event("system_msg", &s2, 3),
304            make_msg_event("user_msg", &user("m2"), 4),
305        ];
306        let ms = MessageStream::new(event_envelopes(events));
307        let w = ms.window();
308        assert_eq!(w.len(), 2);
309        assert!(matches!(w[0].parts[0], MessagePart::CompactSummary { .. }));
310        if let MessagePart::CompactSummary { summary, .. } = &w[0].parts[0] {
311            assert_eq!(summary, "summary 2");
312        }
313    }
314
315    #[test]
316    fn window_no_prefix_before_summary() {
317        let s1 = compact_summary("summary");
318        let events = vec![
319            make_msg_event("user_msg", &user("very old"), 1),
320            make_msg_event("assistant_msg", &assistant("very old"), 2),
321            make_msg_event("system_msg", &s1, 3),
322            make_msg_event("user_msg", &user("new"), 4),
323        ];
324        let ms = MessageStream::new(event_envelopes(events));
325        let w = ms.window();
326        assert_eq!(w.len(), 2);
327        assert!(matches!(w[0].parts[0], MessagePart::CompactSummary { .. }));
328        assert_eq!(w[1].text_concat(), "new");
329    }
330
331    #[test]
332    fn window_empty_stream_returns_empty() {
333        let ms = MessageStream::new(event_envelopes(Vec::new()));
334        assert!(ms.window().is_empty());
335    }
336
337    #[test]
338    fn context_compact_replaces_range_with_summary() {
339        let events = vec![
340            make_msg_event("user_msg", &user("old u1"), 1),
341            make_msg_event("assistant_msg", &assistant("old a1"), 2),
342            make_msg_event("user_msg", &user("old u2"), 3),
343            make_msg_event("system_msg", &compact_summary("summary"), 4),
344            make_context_compact(0, 2, 100, 50, "compaction summary text", 4),
345            make_msg_event("user_msg", &user("after compact"), 5),
346        ];
347        let ms = MessageStream::new(event_envelopes(events));
348        let w = ms.window();
349        assert_eq!(w.len(), 2);
350        assert!(matches!(w[0].parts[0], MessagePart::CompactSummary { .. }));
351    }
352
353    #[test]
354    fn multiple_compactions_applied_in_order() {
355        let events = vec![
356            make_msg_event("user_msg", &user("a"), 1),
357            make_msg_event("assistant_msg", &assistant("b"), 2),
358            make_msg_event("system_msg", &compact_summary("s1"), 3),
359            make_context_compact(0, 1, 200, 100, "first summary", 3),
360            make_msg_event("user_msg", &user("c"), 4),
361            make_msg_event("assistant_msg", &assistant("d"), 5),
362            make_msg_event("system_msg", &compact_summary("s2"), 6),
363            make_context_compact(1, 2, 150, 80, "second summary", 6),
364            make_msg_event("user_msg", &user("e"), 7),
365        ];
366        let ms = MessageStream::new(event_envelopes(events));
367        let w = ms.window();
368        assert_eq!(w.len(), 2);
369        assert!(matches!(w[0].parts[0], MessagePart::CompactSummary { .. }));
370        if let MessagePart::CompactSummary { summary, .. } = &w[0].parts[0] {
371            assert_eq!(summary, "second summary");
372        }
373    }
374
375    #[test]
376    fn compact_then_user_message_produces_summary_plus_user() {
377        let events = vec![
378            make_msg_event("user_msg", &user("old u1"), 1),
379            make_msg_event("assistant_msg", &assistant("old a1"), 2),
380            make_msg_event("user_msg", &user("old u2"), 3),
381            make_msg_event("system_msg", &compact_summary("compact summary"), 4),
382            make_context_compact(0, 2, 200, 100, "compact summary", 4),
383            make_msg_event("user_msg", &user("new message after compact"), 5),
384        ];
385        let ms = MessageStream::new(event_envelopes(events));
386        let w = ms.window();
387        assert_eq!(w.len(), 2);
388        assert!(matches!(w[0].parts[0], MessagePart::CompactSummary { .. }));
389        assert_eq!(w[1].text_concat(), "new message after compact");
390    }
391
392    #[test]
393    fn no_compaction_window_equals_full_messages() {
394        let events = vec![
395            make_msg_event("user_msg", &user("first"), 1),
396            make_msg_event("assistant_msg", &assistant("second"), 2),
397            make_msg_event("user_msg", &user("third"), 3),
398        ];
399        let ms = MessageStream::new(event_envelopes(events));
400        assert_eq!(ms.full_messages().len(), 3);
401        assert_eq!(ms.window().len(), 3);
402    }
403
404    #[test]
405    fn full_messages_retains_compacted_history() {
406        let events = vec![
407            make_msg_event("user_msg", &user("old u1"), 1),
408            make_msg_event("assistant_msg", &assistant("old a1"), 2),
409            make_msg_event("user_msg", &user("old u2"), 3),
410            make_msg_event("system_msg", &compact_summary("summary"), 4),
411            make_context_compact(0, 2, 200, 100, "summary", 4),
412            make_msg_event("user_msg", &user("after compact"), 5),
413        ];
414        let ms = MessageStream::new(event_envelopes(events));
415        // Window: only compact summary + messages after it
416        let w = ms.window();
417        assert_eq!(w.len(), 2);
418        // Full: all messages including compacted ones
419        let f = ms.full_messages();
420        assert_eq!(f.len(), 5, "full must retain compacted messages");
421        assert_eq!(f[0].text_concat(), "old u1");
422        assert_eq!(f[1].text_concat(), "old a1");
423        assert_eq!(f[2].text_concat(), "old u2");
424    }
425
426    #[test]
427    fn third_compaction_replaces_second_summary() {
428        let events = vec![
429            make_msg_event("user_msg", &user("a"), 1),
430            make_msg_event("assistant_msg", &assistant("b"), 2),
431            make_msg_event("system_msg", &compact_summary("s1"), 3),
432            make_context_compact(0, 1, 100, 50, "s1 text", 3),
433            make_msg_event("user_msg", &user("c"), 4),
434            make_msg_event("system_msg", &compact_summary("s2"), 5),
435            make_context_compact(0, 1, 80, 40, "s2 text", 6),
436            make_msg_event("user_msg", &user("d"), 6),
437            make_msg_event("system_msg", &compact_summary("s3"), 7),
438            make_context_compact(0, 1, 70, 30, "s3 text", 9),
439            make_msg_event("user_msg", &user("final"), 8),
440        ];
441        let ms = MessageStream::new(event_envelopes(events));
442        let w = ms.window();
443        assert_eq!(w.len(), 2);
444        if let MessagePart::CompactSummary { summary, .. } = &w[0].parts[0] {
445            assert_eq!(summary, "s3 text");
446        }
447        assert_eq!(w[1].text_concat(), "final");
448    }
449
450    #[test]
451    fn reopened_session_keeps_initial_messages_after_new_events() {
452        let initial_compacted = vec![
453            (1, compact_summary("compaction summary")),
454            (2, assistant("tail assistant")),
455        ];
456        let initial_raw = vec![
457            (1, compact_summary("compaction summary")),
458            (2, assistant("tail assistant")),
459        ];
460        let events = Arc::new(Mutex::new(Vec::new()));
461        let ms = MessageStream::with_initial(events.clone(), initial_compacted, initial_raw);
462
463        events.lock().unwrap().push(EventEnvelope::new(
464            1,
465            Event::TurnStart {
466                turn_id: TurnId::now(),
467            },
468        ));
469        events.lock().unwrap().push(EventEnvelope::new(
470            2,
471            Event::UserMsg {
472                turn_id: TurnId::now(),
473                flow_run_id: None,
474                message: user("latest user"),
475            },
476        ));
477
478        let w = ms.window();
479        assert_eq!(w.len(), 3);
480        assert!(matches!(w[0].parts[0], MessagePart::CompactSummary { .. }));
481        assert_eq!(w[1].text_concat(), "tail assistant");
482        assert_eq!(w[2].text_concat(), "latest user");
483    }
484
485    /// Regression: after a runtime ContextCompact, full_messages() must still
486    /// contain the pre-compact messages.
487    #[test]
488    fn full_messages_retains_pre_compact_history_after_runtime_compact() {
489        let initial_compacted = vec![
490            (1, compact_summary("prior summary")),
491            (2, user("old user")),
492            (3, assistant("old assistant")),
493        ];
494        let initial_raw = vec![
495            (1, compact_summary("prior summary")),
496            (2, user("old user")),
497            (3, assistant("old assistant")),
498        ];
499        let events = Arc::new(Mutex::new(Vec::new()));
500        let ms = MessageStream::with_initial(events.clone(), initial_compacted, initial_raw);
501
502        events.lock().unwrap().push(EventEnvelope::new(
503            10,
504            Event::UserMsg {
505                turn_id: TurnId::now(),
506                flow_run_id: None,
507                message: user("new user before compact"),
508            },
509        ));
510        events.lock().unwrap().push(EventEnvelope::new(
511            11,
512            Event::AssistantMsg {
513                turn_id: TurnId::now(),
514                flow_run_id: None,
515                message: assistant("new assistant before compact"),
516            },
517        ));
518
519        let before = ms.full_messages();
520        assert_eq!(before.len(), 5, "pre-compact full should have all 5 msgs");
521
522        events.lock().unwrap().push(EventEnvelope::new(
523            12,
524            Event::SystemMsg {
525                turn_id: TurnId::now(),
526                message: compact_summary("runtime summary"),
527            },
528        ));
529        events.lock().unwrap().push(EventEnvelope::new(
530            13,
531            Event::ContextCompact {
532                session_id: "test".into(),
533                before_tokens: 1000,
534                after_tokens: 100,
535                compacted_range_start: 1,
536                compacted_range_end: 2,
537                summary_text: Some("runtime summary".into()),
538                replacement_msg_seq: Some(12),
539            },
540        ));
541
542        events.lock().unwrap().push(EventEnvelope::new(
543            14,
544            Event::UserMsg {
545                turn_id: TurnId::now(),
546                flow_run_id: None,
547                message: user("after compact user"),
548            },
549        ));
550
551        let w = ms.window();
552        assert_eq!(w.len(), 4, "window after compact");
553        assert!(matches!(w[0].parts[0], MessagePart::CompactSummary { .. }));
554
555        let full = ms.full_messages();
556        assert!(
557            full.len() >= 6,
558            "full must retain pre-compact history, got {} msgs: {:?}",
559            full.len(),
560            full.iter().map(|m| m.text_concat()).collect::<Vec<_>>()
561        );
562        let texts: Vec<String> = full.iter().map(|m| m.text_concat()).collect();
563        assert!(
564            texts.iter().any(|t| t.contains("old user")),
565            "full must contain pre-compact 'old user', got: {:?}",
566            texts
567        );
568        assert!(
569            texts.iter().any(|t| t.contains("old assistant")),
570            "full must contain pre-compact 'old assistant', got: {:?}",
571            texts
572        );
573    }
574}