use crate::error::AgentError;
use crate::types::{Message, Role};
pub struct ConversationHistory {
messages: Vec<Message>,
token_limit: usize,
total_tokens: usize,
}
impl ConversationHistory {
pub fn new(token_limit: usize) -> Self {
Self { messages: Vec::new(), total_tokens: 0, token_limit }
}
pub fn push(&mut self, msg: Message) -> Result<(), AgentError> {
let new_total = self.total_tokens + msg.token_estimate;
if new_total > self.token_limit {
return Err(AgentError::HistoryOverflow { size: new_total, limit: self.token_limit });
}
self.total_tokens = new_total;
self.messages.push(msg);
Ok(())
}
pub fn push_with_eviction(&mut self, msg: Message) {
while self.total_tokens + msg.token_estimate > self.token_limit && !self.messages.is_empty() {
if let Some(pos) = self.messages.iter().position(|m| m.role != Role::System) {
let evicted = self.messages.remove(pos);
self.total_tokens = self.total_tokens.saturating_sub(evicted.token_estimate);
} else {
break;
}
}
self.total_tokens += msg.token_estimate;
self.messages.push(msg);
}
pub fn messages(&self) -> &[Message] { &self.messages }
pub fn total_tokens(&self) -> usize { self.total_tokens }
pub fn len(&self) -> usize { self.messages.len() }
pub fn is_empty(&self) -> bool { self.messages.is_empty() }
pub fn clear_non_system(&mut self) {
self.messages.retain(|m| m.role == Role::System);
self.total_tokens = self.messages.iter().map(|m| m.token_estimate).sum();
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::types::Message;
#[test]
fn test_history_push_within_limit_ok() {
let mut h = ConversationHistory::new(1000);
assert!(h.push(Message::user("hello")).is_ok());
assert_eq!(h.len(), 1);
}
#[test]
fn test_history_push_over_limit_returns_overflow_error() {
let mut h = ConversationHistory::new(1);
let err = h.push(Message::user("this message is definitely too long for a 1 token limit")).unwrap_err();
assert!(matches!(err, AgentError::HistoryOverflow { .. }));
}
#[test]
fn test_history_token_count_accumulates() {
let mut h = ConversationHistory::new(10000);
h.push(Message::user("hello")).unwrap();
h.push(Message::assistant("hi there")).unwrap();
assert!(h.total_tokens() > 0);
}
#[test]
fn test_history_push_with_eviction_stays_within_budget_approximately() {
let mut h = ConversationHistory::new(20);
h.push_with_eviction(Message::system("You are an agent"));
h.push_with_eviction(Message::user("msg 1"));
h.push_with_eviction(Message::user("msg 2"));
h.push_with_eviction(Message::user("a longer message that forces eviction of earlier messages"));
assert!(h.messages().iter().any(|m| m.role == Role::System));
}
#[test]
fn test_history_clear_non_system_removes_user_and_assistant_messages() {
let mut h = ConversationHistory::new(10000);
h.push(Message::system("sys")).unwrap();
h.push(Message::user("user msg")).unwrap();
h.push(Message::assistant("asst msg")).unwrap();
h.clear_non_system();
assert_eq!(h.len(), 1);
assert_eq!(h.messages()[0].role, Role::System);
}
#[test]
fn test_history_is_empty_initially() {
let h = ConversationHistory::new(1000);
assert!(h.is_empty());
}
#[test]
fn test_history_clear_non_system_updates_token_count() {
let mut h = ConversationHistory::new(10000);
h.push(Message::system("sys")).unwrap();
let sys_tokens = h.total_tokens();
h.push(Message::user("a bunch of user content that has tokens")).unwrap();
h.clear_non_system();
assert_eq!(h.total_tokens(), sys_tokens);
}
#[test]
fn test_history_push_multiple_messages_len_correct() {
let mut h = ConversationHistory::new(10000);
for i in 0..5u32 {
h.push(Message::user(format!("msg {i}"))).unwrap();
}
assert_eq!(h.len(), 5);
}
}