const BYTES_PER_TOKEN: usize = 3;
pub struct ContextManager {
budget_tokens: usize,
}
impl ContextManager {
pub fn new(budget_tokens: usize) -> Self {
Self { budget_tokens }
}
pub fn with_default_budget() -> Self {
let config = clawgarden_proto::AppConfig::load();
Self::new(config.agent.context_budget_tokens)
}
pub fn estimate_tokens(text: &str) -> usize {
if text.is_empty() {
return 0;
}
(text.len() / BYTES_PER_TOKEN).max(1)
}
pub fn select_history(&self, history: &[String], user_message: &str) -> Vec<String> {
if history.is_empty() {
return Vec::new();
}
let user_tokens = Self::estimate_tokens(user_message);
let mut remaining = self.budget_tokens.saturating_sub(user_tokens);
let mut selected_indices: Vec<usize> = Vec::new();
for i in (0..history.len()).rev() {
let tokens = Self::estimate_tokens(&history[i]);
if tokens <= remaining {
selected_indices.push(i);
remaining -= tokens;
} else {
break;
}
}
let initial_count = 2.min(history.len());
for i in 0..initial_count {
if !selected_indices.contains(&i) {
let tokens = Self::estimate_tokens(&history[i]);
if tokens <= remaining {
selected_indices.push(i);
remaining -= tokens;
}
}
}
selected_indices.sort();
selected_indices
.into_iter()
.map(|i| history[i].clone())
.collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_history(count: usize, words_per_msg: usize) -> Vec<String> {
(0..count)
.map(|i| {
let words: Vec<String> = (0..words_per_msg)
.map(|j| format!("word{}_{}", i, j))
.collect();
format!("[agent_{}]: {}", i, words.join(" "))
})
.collect()
}
#[test]
fn test_estimate_tokens_empty() {
assert_eq!(ContextManager::estimate_tokens(""), 0);
}
#[test]
fn test_estimate_tokens_nonzero() {
let tokens = ContextManager::estimate_tokens("Hello world");
assert!(tokens > 0);
}
#[test]
fn test_select_history_empty() {
let mgr = ContextManager::with_default_budget();
let result = mgr.select_history(&[], "hello");
assert!(result.is_empty());
}
#[test]
fn test_select_history_fits_all() {
let mgr = ContextManager::new(10000);
let history = make_history(5, 5);
let result = mgr.select_history(&history, "test");
assert_eq!(result.len(), 5, "enough budget includes all");
}
#[test]
fn test_select_history_truncates_recent_first() {
let mgr = ContextManager::new(200);
let history = make_history(20, 10);
let result = mgr.select_history(&history, "test");
assert!(result.len() < 20, "small budget truncates");
assert!(result.last().unwrap().contains("agent_19"));
}
#[test]
fn test_select_history_preserves_initial() {
let mgr = ContextManager::new(10000);
let history = make_history(20, 5);
let result = mgr.select_history(&history, "test");
assert!(
result.first().unwrap().contains("agent_0"),
"initial messages preserved"
);
}
#[test]
fn test_user_message_deducts_budget() {
let mgr = ContextManager::new(500);
let history = make_history(50, 10);
let big_user_msg = "a".repeat(3000); let result = mgr.select_history(&history, &big_user_msg);
assert!(result.len() < 50, "large user message reduces history");;
}
}