use crate::core::types::Message;
use crate::tools::optimize::estimate_tokens;
pub fn estimate_messages_tokens(messages: &[Message]) -> u64 {
messages
.iter()
.map(|m| {
serde_json::to_string(m)
.map(|s| estimate_tokens(&s))
.unwrap_or(0)
})
.sum()
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CompactMode {
Manual,
Auto,
}
pub struct ContextManager {
pub threshold_tokens: u64,
pub mode: CompactMode,
pub native: bool,
pub keep_recent: usize,
}
impl ContextManager {
pub fn new(threshold_tokens: u64, mode: CompactMode, native: bool) -> Self {
ContextManager {
threshold_tokens,
mode,
native,
keep_recent: 6,
}
}
pub fn needs_compaction(&self, messages: &[Message]) -> bool {
if self.native || self.mode != CompactMode::Auto {
return false;
}
estimate_messages_tokens(messages) >= self.threshold_tokens
}
pub fn plan_compaction<'a>(
&self,
messages: &'a [Message],
) -> Option<(&'a [Message], &'a [Message])> {
if messages.len() <= self.keep_recent {
return None;
}
let split = messages.len() - self.keep_recent;
Some((&messages[..split], &messages[split..]))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::types::ContentBlock;
fn msgs(n: usize) -> Vec<Message> {
(0..n)
.map(|i| Message::user(format!("message number {i} with some words")))
.collect()
}
#[test]
fn native_provider_never_compacts() {
let cm = ContextManager::new(1, CompactMode::Auto, true);
assert!(!cm.needs_compaction(&msgs(100)));
}
#[test]
fn manual_mode_never_auto_compacts() {
let cm = ContextManager::new(1, CompactMode::Manual, false);
assert!(!cm.needs_compaction(&msgs(100)));
}
#[test]
fn auto_compacts_over_threshold() {
let big = ContextManager::new(10, CompactMode::Auto, false);
assert!(big.needs_compaction(&msgs(50)));
let high = ContextManager::new(1_000_000, CompactMode::Auto, false);
assert!(!high.needs_compaction(&msgs(2)));
}
#[test]
fn plan_keeps_recent_and_summarizes_old() {
let cm = ContextManager::new(10, CompactMode::Auto, false); let history = msgs(10);
let (old, keep) = cm.plan_compaction(&history).unwrap();
assert_eq!(old.len(), 4);
assert_eq!(keep.len(), 6);
assert!(cm.plan_compaction(&msgs(6)).is_none());
}
#[test]
fn estimate_grows_with_content() {
let small = estimate_messages_tokens(&[Message::user("hi")]);
let big = estimate_messages_tokens(&[Message::assistant(vec![ContentBlock::text(
"a much longer message that clearly has more tokens than the tiny one",
)])]);
assert!(big > small);
}
}