use crate::llm::ChatMessage;
const DEFAULT_CONTEXT_LIMIT: usize = 100_000;
const COMPACTION_THRESHOLD: f64 = 0.8;
const TOKENS_PER_WORD: f64 = 1.3;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CompactionStrategy {
Summarize {
keep_recent: usize,
},
Truncate {
keep_recent: usize,
},
MoveToWorkspace,
}
impl Default for CompactionStrategy {
fn default() -> Self {
Self::Summarize { keep_recent: 5 }
}
}
pub struct ContextMonitor {
context_limit: usize,
threshold_ratio: f64,
}
impl ContextMonitor {
pub fn new() -> Self {
Self {
context_limit: DEFAULT_CONTEXT_LIMIT,
threshold_ratio: COMPACTION_THRESHOLD,
}
}
pub fn with_limit(mut self, limit: usize) -> Self {
self.context_limit = limit;
self
}
pub fn with_threshold(mut self, ratio: f64) -> Self {
self.threshold_ratio = ratio.clamp(0.5, 0.95);
self
}
pub fn estimate_tokens(&self, messages: &[ChatMessage]) -> usize {
messages.iter().map(estimate_message_tokens).sum()
}
pub fn needs_compaction(&self, messages: &[ChatMessage]) -> bool {
let tokens = self.estimate_tokens(messages);
let threshold = (self.context_limit as f64 * self.threshold_ratio) as usize;
tokens >= threshold
}
pub fn usage_percent(&self, messages: &[ChatMessage]) -> f64 {
let tokens = self.estimate_tokens(messages);
(tokens as f64 / self.context_limit as f64) * 100.0
}
pub fn suggest_compaction(&self, messages: &[ChatMessage]) -> Option<CompactionStrategy> {
if !self.needs_compaction(messages) {
return None;
}
let tokens = self.estimate_tokens(messages);
let overage = tokens as f64 / self.context_limit as f64;
if overage > 0.95 {
Some(CompactionStrategy::Truncate { keep_recent: 3 })
} else if overage > 0.85 {
Some(CompactionStrategy::Summarize { keep_recent: 5 })
} else {
Some(CompactionStrategy::MoveToWorkspace)
}
}
pub fn limit(&self) -> usize {
self.context_limit
}
pub fn threshold(&self) -> usize {
(self.context_limit as f64 * self.threshold_ratio) as usize
}
}
impl Default for ContextMonitor {
fn default() -> Self {
Self::new()
}
}
fn estimate_message_tokens(message: &ChatMessage) -> usize {
let word_count = message.content.split_whitespace().count();
let overhead = 4;
(word_count as f64 * TOKENS_PER_WORD) as usize + overhead
}
pub fn estimate_text_tokens(text: &str) -> usize {
let word_count = text.split_whitespace().count();
(word_count as f64 * TOKENS_PER_WORD) as usize
}
#[derive(Debug, Clone)]
pub struct ContextBreakdown {
pub total_tokens: usize,
pub system_tokens: usize,
pub user_tokens: usize,
pub assistant_tokens: usize,
pub tool_tokens: usize,
pub message_count: usize,
}
impl ContextBreakdown {
pub fn analyze(messages: &[ChatMessage]) -> Self {
let mut breakdown = Self {
total_tokens: 0,
system_tokens: 0,
user_tokens: 0,
assistant_tokens: 0,
tool_tokens: 0,
message_count: messages.len(),
};
for message in messages {
let tokens = estimate_message_tokens(message);
breakdown.total_tokens += tokens;
match message.role {
crate::llm::Role::System => breakdown.system_tokens += tokens,
crate::llm::Role::User => breakdown.user_tokens += tokens,
crate::llm::Role::Assistant => breakdown.assistant_tokens += tokens,
crate::llm::Role::Tool => breakdown.tool_tokens += tokens,
}
}
breakdown
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_token_estimation() {
let msg = ChatMessage::user("Hello, how are you today?");
let tokens = estimate_message_tokens(&msg);
assert!(tokens > 0);
assert!(tokens < 20);
}
#[test]
fn test_needs_compaction() {
let monitor = ContextMonitor::new().with_limit(100);
let small: Vec<ChatMessage> = vec![ChatMessage::user("Hello")];
assert!(!monitor.needs_compaction(&small));
let large_content = "word ".repeat(1000);
let large: Vec<ChatMessage> = vec![ChatMessage::user(&large_content)];
assert!(monitor.needs_compaction(&large));
}
#[test]
fn test_suggest_compaction() {
let monitor = ContextMonitor::new().with_limit(100);
let small: Vec<ChatMessage> = vec![ChatMessage::user("Hello")];
assert!(monitor.suggest_compaction(&small).is_none());
}
#[test]
fn test_context_breakdown() {
let messages = vec![
ChatMessage::system("You are a helpful assistant."),
ChatMessage::user("Hello"),
ChatMessage::assistant("Hi there!"),
];
let breakdown = ContextBreakdown::analyze(&messages);
assert_eq!(breakdown.message_count, 3);
assert!(breakdown.system_tokens > 0);
assert!(breakdown.user_tokens > 0);
assert!(breakdown.assistant_tokens > 0);
}
}