agent_engine/runtime/
message_time.rs1use std::sync::Arc;
17
18use chrono::{DateTime, SecondsFormat, Utc};
19
20use crate::SharedMessage;
21
22pub const AT_KEY: &str = agent_core::core::session_journal::MESSAGE_AT_KEY;
24
25#[derive(Clone)]
29pub struct MessageClock(Arc<ClockFn>);
30
31type ClockFn = Box<dyn Fn() -> DateTime<Utc> + Send + Sync>;
32
33impl MessageClock {
34 pub fn new(now: impl Fn() -> DateTime<Utc> + Send + Sync + 'static) -> Self {
35 Self(Arc::new(Box::new(now)))
36 }
37
38 pub fn now(&self) -> DateTime<Utc> {
39 (self.0)()
40 }
41}
42
43impl Default for MessageClock {
44 fn default() -> Self {
45 Self::new(Utc::now)
46 }
47}
48
49impl std::fmt::Debug for MessageClock {
50 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
51 f.write_str("MessageClock")
52 }
53}
54
55pub fn format_time(at: DateTime<Utc>) -> String {
57 at.to_rfc3339_opts(SecondsFormat::Secs, true)
58}
59
60pub fn message_time(message: &serde_json::Value) -> Option<&str> {
62 agent_core::core::session_journal::message_time(message)
63}
64
65pub fn stamp_new(messages: &mut [SharedMessage], floor: usize, at: DateTime<Utc>) -> usize {
69 let mut stamp = None;
70 let mut stamped = 0;
71 let floor = floor.min(messages.len());
72 for message in messages[floor..].iter_mut().rev() {
73 if message.get(AT_KEY).is_some() || !message.is_object() {
74 break;
75 }
76 let ts = stamp.get_or_insert_with(|| format_time(at)).clone();
77 if let Some(object) = Arc::make_mut(message).as_object_mut() {
78 object.insert(AT_KEY.into(), serde_json::Value::String(ts));
79 stamped += 1;
80 }
81 }
82 stamped
83}
84
85#[derive(Debug, Clone, Default)]
91pub struct Stamper {
92 floor: usize,
93}
94
95impl Stamper {
96 pub fn new(settled: usize) -> Self {
97 Self { floor: settled }
98 }
99
100 pub fn stamp(&mut self, messages: &mut [SharedMessage], clock: &MessageClock) -> usize {
103 let floor = self.floor.min(messages.len());
104 self.floor = messages.len();
105 if floor == messages.len() {
106 return 0;
107 }
108 stamp_new(messages, floor, clock.now())
109 }
110
111 pub fn settle(&mut self, settled: usize) {
114 self.floor = settled;
115 }
116}
117
118#[cfg(test)]
119mod tests {
120 use super::*;
121 use serde_json::json;
122
123 fn at(min: u32) -> DateTime<Utc> {
124 chrono::TimeZone::with_ymd_and_hms(&Utc, 2026, 10, 25, 5, min, 0).unwrap()
125 }
126
127 fn msgs(n: usize) -> Vec<SharedMessage> {
128 (0..n)
129 .map(|i| Arc::new(json!({"role": "user", "content": format!("m{i}")})))
130 .collect()
131 }
132
133 fn times(m: &[SharedMessage]) -> Vec<Option<&str>> {
134 m.iter().map(|m| message_time(m)).collect()
135 }
136
137 #[test]
138 fn stamping_is_idempotent_and_only_touches_new_messages() {
139 let mut m = msgs(3);
141 let clock_min = Arc::new(std::sync::atomic::AtomicU32::new(47));
142 let c = clock_min.clone();
143 let clock = MessageClock::new(move || at(c.load(std::sync::atomic::Ordering::SeqCst)));
144 let mut stamper = Stamper::new(m.len());
145 assert_eq!(stamper.stamp(&mut m, &clock), 0);
146 assert_eq!(times(&m), vec![None, None, None]);
147
148 m.extend(msgs(2));
149 assert_eq!(stamper.stamp(&mut m, &clock), 2);
150 let first = "2026-10-25T05:47:00Z";
151 assert_eq!(times(&m), vec![None, None, None, Some(first), Some(first)]);
152 let ptrs: Vec<_> = m.iter().map(Arc::as_ptr).collect();
153
154 clock_min.store(58, std::sync::atomic::Ordering::SeqCst);
156 assert_eq!(stamper.stamp(&mut m, &clock), 0);
157 assert_eq!(stamp_new(&mut m, 0, at(58)), 0, "stops at the stamped tail");
158 assert_eq!(m.iter().map(Arc::as_ptr).collect::<Vec<_>>(), ptrs, "untouched");
159
160 m.extend(msgs(1));
161 assert_eq!(stamper.stamp(&mut m, &clock), 1);
162 assert_eq!(message_time(&m[5]), Some("2026-10-25T05:58:00Z"));
163 assert_eq!(message_time(&m[3]), Some(first));
164 }
165
166 #[test]
167 fn a_shorter_replacement_history_clamps_the_floor() {
168 let clock = MessageClock::new(|| at(1));
169 let mut stamper = Stamper::new(4);
170 let mut head = msgs(2);
172 assert_eq!(stamper.stamp(&mut head, &clock), 0);
173 head.extend(msgs(1));
174 assert_eq!(stamper.stamp(&mut head, &clock), 1);
175 assert_eq!(times(&head), vec![None, None, Some("2026-10-25T05:01:00Z")]);
176 }
177}