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