1use std::sync::{Arc, Mutex};
10
11use crate::context::CompactionReport;
12use crate::context_transformer::ContextTransformer;
13use crate::types::{AgentMessage, LlmMessage};
14
15#[derive(Debug, Clone)]
26pub struct ContextVersion {
27 pub version: u64,
29 pub turn: u64,
31 pub timestamp: u64,
33 pub messages: Vec<LlmMessage>,
35 pub summary: Option<String>,
37}
38
39#[derive(Debug, Clone)]
41pub struct ContextVersionMeta {
42 pub version: u64,
44 pub turn: u64,
46 pub timestamp: u64,
48 pub message_count: usize,
50 pub has_summary: bool,
52}
53
54pub trait ContextVersionStore: Send + Sync {
61 fn save_version(&self, version: &ContextVersion);
63
64 fn load_version(&self, version: u64) -> Option<ContextVersion>;
66
67 fn list_versions(&self) -> Vec<ContextVersionMeta>;
69
70 fn latest_version(&self) -> Option<ContextVersion> {
72 let versions = self.list_versions();
73 versions
74 .last()
75 .and_then(|meta| self.load_version(meta.version))
76 }
77}
78
79pub trait ContextSummarizer: Send + Sync {
91 fn summarize(&self, messages: &[LlmMessage]) -> Option<String>;
96}
97
98pub struct InMemoryVersionStore {
105 versions: Mutex<Vec<ContextVersion>>,
106}
107
108impl InMemoryVersionStore {
109 #[must_use]
110 pub const fn new() -> Self {
111 Self {
112 versions: Mutex::new(Vec::new()),
113 }
114 }
115
116 pub fn len(&self) -> usize {
117 self.versions
118 .lock()
119 .unwrap_or_else(std::sync::PoisonError::into_inner)
120 .len()
121 }
122
123 pub fn is_empty(&self) -> bool {
124 self.len() == 0
125 }
126}
127
128impl Default for InMemoryVersionStore {
129 fn default() -> Self {
130 Self::new()
131 }
132}
133
134impl ContextVersionStore for InMemoryVersionStore {
135 fn save_version(&self, version: &ContextVersion) {
136 let mut guard = self
137 .versions
138 .lock()
139 .unwrap_or_else(std::sync::PoisonError::into_inner);
140 guard.push(version.clone());
141 }
142
143 fn load_version(&self, version: u64) -> Option<ContextVersion> {
144 let guard = self
145 .versions
146 .lock()
147 .unwrap_or_else(std::sync::PoisonError::into_inner);
148 guard.iter().find(|v| v.version == version).cloned()
149 }
150
151 fn list_versions(&self) -> Vec<ContextVersionMeta> {
152 let guard = self
153 .versions
154 .lock()
155 .unwrap_or_else(std::sync::PoisonError::into_inner);
156 guard
157 .iter()
158 .map(|v| ContextVersionMeta {
159 version: v.version,
160 turn: v.turn,
161 timestamp: v.timestamp,
162 message_count: v.messages.len(),
163 has_summary: v.summary.is_some(),
164 })
165 .collect()
166 }
167}
168
169pub struct VersioningTransformer {
194 inner: Box<dyn ContextTransformer>,
195 store: Arc<dyn ContextVersionStore>,
196 summarizer: Option<Arc<dyn ContextSummarizer>>,
197 state: Mutex<VersioningState>,
198}
199
200struct VersioningState {
201 next_version: u64,
202 turn_counter: u64,
203}
204
205impl VersioningTransformer {
206 pub fn new(
208 inner: impl ContextTransformer + 'static,
209 store: Arc<dyn ContextVersionStore>,
210 ) -> Self {
211 Self {
212 inner: Box::new(inner),
213 store,
214 summarizer: None,
215 state: Mutex::new(VersioningState {
216 next_version: 1,
217 turn_counter: 0,
218 }),
219 }
220 }
221
222 #[must_use]
224 pub fn with_summarizer(mut self, summarizer: Arc<dyn ContextSummarizer>) -> Self {
225 self.summarizer = Some(summarizer);
226 self
227 }
228
229 pub fn store(&self) -> &Arc<dyn ContextVersionStore> {
231 &self.store
232 }
233}
234
235impl ContextTransformer for VersioningTransformer {
236 fn transform(
237 &self,
238 messages: &mut Vec<AgentMessage>,
239 overflow: bool,
240 ) -> Option<CompactionReport> {
241 let report = self.inner.transform(messages, overflow)?;
244
245 if report.dropped_messages.is_empty() {
246 return Some(report);
247 }
248
249 let mut state = self
251 .state
252 .lock()
253 .unwrap_or_else(std::sync::PoisonError::into_inner);
254 state.turn_counter += 1;
255
256 let summary = self
257 .summarizer
258 .as_ref()
259 .and_then(|s| s.summarize(&report.dropped_messages));
260
261 let version = ContextVersion {
262 version: state.next_version,
263 turn: state.turn_counter,
264 timestamp: crate::util::now_timestamp(),
265 messages: report.dropped_messages.clone(),
266 summary,
267 };
268
269 state.next_version += 1;
270 drop(state);
271
272 self.store.save_version(&version);
273
274 Some(report)
275 }
276}
277
278#[cfg(test)]
279mod tests {
280 use super::*;
281 use crate::context_transformer::SlidingWindowTransformer;
282 use crate::types::{ContentBlock, UserMessage};
283
284 fn text_message(text: &str) -> AgentMessage {
285 AgentMessage::Llm(LlmMessage::User(UserMessage {
286 content: vec![ContentBlock::Text {
287 text: text.to_owned(),
288 }],
289 timestamp: 0,
290 cache_hint: None,
291 }))
292 }
293
294 #[test]
295 fn versioning_captures_dropped_messages() {
296 let store: Arc<dyn ContextVersionStore> = Arc::new(InMemoryVersionStore::new());
297 let inner = SlidingWindowTransformer::new(250, 100, 1);
298 let transformer = VersioningTransformer::new(inner, Arc::clone(&store));
299
300 let body = "x".repeat(400);
302 let mut messages = vec![
303 text_message(&body),
304 text_message(&body),
305 text_message(&body),
306 text_message(&body),
307 ];
308
309 let report = transformer.transform(&mut messages, false);
310 assert!(report.is_some());
311
312 assert_eq!(messages.len(), 2);
314
315 let versions = store.list_versions();
317 assert_eq!(versions.len(), 1);
318 assert_eq!(versions[0].version, 1);
319 assert_eq!(versions[0].message_count, 2); let v = store.load_version(1).unwrap();
323 assert_eq!(v.messages.len(), 2);
324 assert!(v.summary.is_none());
325 }
326
327 #[test]
328 fn versioning_with_summarizer() {
329 struct TestSummarizer;
330 impl ContextSummarizer for TestSummarizer {
331 fn summarize(&self, messages: &[LlmMessage]) -> Option<String> {
332 Some(format!("Summary of {} messages", messages.len()))
333 }
334 }
335
336 let store: Arc<dyn ContextVersionStore> = Arc::new(InMemoryVersionStore::new());
337 let inner = SlidingWindowTransformer::new(250, 100, 1);
338 let transformer = VersioningTransformer::new(inner, Arc::clone(&store))
339 .with_summarizer(Arc::new(TestSummarizer));
340
341 let body = "x".repeat(400);
342 let mut messages = vec![
343 text_message(&body),
344 text_message(&body),
345 text_message(&body),
346 text_message(&body),
347 ];
348
349 transformer.transform(&mut messages, false);
350
351 let v = store.load_version(1).unwrap();
352 assert_eq!(v.summary.as_deref(), Some("Summary of 2 messages"));
353 }
354
355 #[test]
356 fn no_compaction_no_version_saved() {
357 let store: Arc<dyn ContextVersionStore> = Arc::new(InMemoryVersionStore::new());
358 let inner = SlidingWindowTransformer::new(10_000, 5_000, 1);
359 let transformer = VersioningTransformer::new(inner, Arc::clone(&store));
360
361 let mut messages = vec![text_message("hello"), text_message("world")];
362 let report = transformer.transform(&mut messages, false);
363
364 assert!(report.is_none());
365 assert!(store.list_versions().is_empty());
366 }
367
368 #[test]
369 fn multiple_compactions_increment_version() {
370 let store: Arc<dyn ContextVersionStore> = Arc::new(InMemoryVersionStore::new());
371 let inner = SlidingWindowTransformer::new(250, 100, 1);
372 let transformer = VersioningTransformer::new(inner, Arc::clone(&store));
373
374 let body = "x".repeat(400);
375
376 let mut messages = vec![
378 text_message(&body),
379 text_message(&body),
380 text_message(&body),
381 text_message(&body),
382 ];
383 transformer.transform(&mut messages, false);
384
385 let mut messages = vec![
387 text_message(&body),
388 text_message(&body),
389 text_message(&body),
390 text_message(&body),
391 ];
392 transformer.transform(&mut messages, false);
393
394 let versions = store.list_versions();
395 assert_eq!(versions.len(), 2);
396 assert_eq!(versions[0].version, 1);
397 assert_eq!(versions[1].version, 2);
398 }
399
400 #[test]
401 fn latest_version_returns_most_recent() {
402 let store: Arc<dyn ContextVersionStore> = Arc::new(InMemoryVersionStore::new());
403 let inner = SlidingWindowTransformer::new(250, 100, 1);
404 let transformer = VersioningTransformer::new(inner, Arc::clone(&store));
405
406 let body = "x".repeat(400);
407 for _ in 0..3 {
408 let mut messages = vec![
409 text_message(&body),
410 text_message(&body),
411 text_message(&body),
412 text_message(&body),
413 ];
414 transformer.transform(&mut messages, false);
415 }
416
417 let latest = store.latest_version().unwrap();
418 assert_eq!(latest.version, 3);
419 }
420
421 #[test]
422 fn in_memory_store_load_nonexistent() {
423 let store = InMemoryVersionStore::new();
424 assert!(store.load_version(999).is_none());
425 assert!(store.is_empty());
426 }
427
428 #[test]
429 fn version_meta_fields_correct() {
430 let store: Arc<dyn ContextVersionStore> = Arc::new(InMemoryVersionStore::new());
431 let inner = SlidingWindowTransformer::new(250, 100, 1);
432 let transformer = VersioningTransformer::new(inner, Arc::clone(&store));
433
434 let body = "x".repeat(400);
435 let mut messages = vec![
436 text_message(&body),
437 text_message(&body),
438 text_message(&body),
439 text_message(&body),
440 ];
441 transformer.transform(&mut messages, false);
442
443 let meta = &store.list_versions()[0];
444 assert_eq!(meta.version, 1);
445 assert_eq!(meta.turn, 1);
446 assert!(!meta.has_summary);
447 assert!(meta.timestamp > 0);
448 assert_eq!(meta.message_count, 2);
449 }
450
451 #[test]
452 fn store_accessor() {
453 let store: Arc<dyn ContextVersionStore> = Arc::new(InMemoryVersionStore::new());
454 let inner = SlidingWindowTransformer::new(250, 100, 1);
455 let transformer = VersioningTransformer::new(inner, Arc::clone(&store));
456
457 assert!(transformer.store().list_versions().is_empty());
459 }
460
461 #[test]
464 fn report_dropped_messages_populated_by_compaction() {
465 use crate::context::compact_sliding_window_with;
469
470 let body = "x".repeat(400);
472 let mut messages = vec![
473 text_message(&body), text_message(&body), text_message(&body), text_message(&body), ];
478 let report = compact_sliding_window_with(&mut messages, 250, 1, None).unwrap();
481
482 assert_eq!(report.dropped_messages.len(), 2);
484 assert_eq!(messages.len(), 2);
486 }
487
488 #[test]
489 fn versioning_uses_report_dropped_messages_not_debug_diff() {
490 let store: Arc<dyn ContextVersionStore> = Arc::new(InMemoryVersionStore::new());
493 let inner = SlidingWindowTransformer::new(250, 100, 1);
494 let transformer = VersioningTransformer::new(inner, Arc::clone(&store));
495
496 let body_a = "a".repeat(400); let body_b = "b".repeat(400); let mut messages = vec![
503 text_message(&body_a),
504 text_message(&body_a),
505 text_message(&body_b),
506 text_message(&body_b),
507 ];
508
509 let report = transformer.transform(&mut messages, false);
510 assert!(report.is_some());
511
512 let v = store.load_version(1).unwrap();
513 assert_eq!(v.messages.len(), 2);
515 if let LlmMessage::User(ref u) = v.messages[0] {
518 if let ContentBlock::Text { ref text } = u.content[0] {
519 assert_eq!(text, &body_a);
520 } else {
521 panic!("expected text block");
522 }
523 } else {
524 panic!("expected user message");
525 }
526 }
527
528 #[test]
529 fn custom_messages_excluded_from_dropped_messages() {
530 use crate::context::compact_sliding_window_with;
533 use crate::types::CustomMessage;
534
535 #[derive(Debug)]
536 struct Marker;
537 impl CustomMessage for Marker {
538 fn as_any(&self) -> &dyn std::any::Any {
539 self
540 }
541 }
542
543 let body = "x".repeat(400); let mut messages = vec![
545 text_message(&body), AgentMessage::Custom(Box::new(Marker)), text_message(&body), text_message(&body), ];
550 let report = compact_sliding_window_with(&mut messages, 250, 1, None).unwrap();
552
553 assert_eq!(report.dropped_messages.len(), 1);
555 }
556}