use crate::messages::{Message, MessageContent};
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum TrimStrategy {
First,
Last,
}
fn estimate_tokens(msg: &Message) -> usize {
let text_len = msg.content().as_text().len();
text_len.div_ceil(4)
}
pub fn trim_messages(
messages: &[Message],
max_tokens: usize,
strategy: &TrimStrategy,
) -> Vec<Message> {
let total: usize = messages.iter().map(estimate_tokens).sum();
if total <= max_tokens {
return messages.to_vec();
}
match strategy {
TrimStrategy::First => {
let mut result = Vec::new();
let mut budget = max_tokens;
for msg in messages.iter().rev() {
let tokens = estimate_tokens(msg);
if tokens <= budget {
result.push(msg.clone());
budget -= tokens;
} else {
break;
}
}
result.reverse();
result
}
TrimStrategy::Last => {
let mut result = Vec::new();
let mut budget = max_tokens;
for msg in messages {
let tokens = estimate_tokens(msg);
if tokens <= budget {
result.push(msg.clone());
budget -= tokens;
} else {
break;
}
}
result
}
}
}
pub fn merge_message_runs(messages: &[Message]) -> Vec<Message> {
if messages.is_empty() {
return Vec::new();
}
let mut result: Vec<Message> = Vec::new();
for msg in messages {
let should_merge = result
.last()
.is_some_and(|last| last.message_type() == msg.message_type());
if should_merge {
if let Some(last) = result.last_mut() {
let combined = format!("{}\n{}", last.content().as_text(), msg.content().as_text());
let new_content = MessageContent::Text(combined);
replace_content(last, new_content);
}
} else {
result.push(msg.clone());
}
}
result
}
fn replace_content(msg: &mut Message, new_content: MessageContent) {
match msg {
Message::Human { content, .. }
| Message::AI { content, .. }
| Message::System { content, .. }
| Message::Tool { content, .. }
| Message::Chat { content, .. } => {
*content = new_content;
}
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
#[test]
fn test_trim_messages_first_strategy() {
let messages = vec![
Message::system("You are a helpful assistant"),
Message::human("Hello there"),
Message::ai("Hi"),
];
let trimmed = trim_messages(&messages, 2, &TrimStrategy::First);
assert!(!trimmed.is_empty());
assert!(trimmed.len() < messages.len());
assert_eq!(trimmed.last().map(Message::message_type), Some("ai"));
}
#[test]
fn test_trim_messages_last_strategy() {
let messages = vec![
Message::human("Hi"),
Message::ai("Hello! How can I help you today with your questions?"),
Message::human("Tell me about Rust"),
];
let trimmed = trim_messages(&messages, 3, &TrimStrategy::Last);
assert!(!trimmed.is_empty());
assert_eq!(trimmed[0].message_type(), "human");
}
#[test]
fn test_trim_messages_within_budget() {
let messages = vec![Message::human("Hi"), Message::ai("Hello")];
let trimmed = trim_messages(&messages, 1000, &TrimStrategy::First);
assert_eq!(trimmed.len(), 2);
}
#[test]
fn test_trim_messages_empty() {
let trimmed = trim_messages(&[], 100, &TrimStrategy::First);
assert!(trimmed.is_empty());
}
#[test]
fn test_merge_message_runs() {
let messages = vec![
Message::human("Hello"),
Message::human("How are you?"),
Message::ai("I'm fine"),
Message::ai("Thanks for asking"),
Message::human("Great"),
];
let merged = merge_message_runs(&messages);
assert_eq!(merged.len(), 3);
assert_eq!(merged[0].content().as_text(), "Hello\nHow are you?");
assert_eq!(merged[1].content().as_text(), "I'm fine\nThanks for asking");
assert_eq!(merged[2].content().as_text(), "Great");
}
#[test]
fn test_merge_message_runs_no_consecutive() {
let messages = vec![
Message::human("Hello"),
Message::ai("Hi"),
Message::human("How are you?"),
];
let merged = merge_message_runs(&messages);
assert_eq!(merged.len(), 3);
}
#[test]
fn test_merge_message_runs_empty() {
let merged = merge_message_runs(&[]);
assert!(merged.is_empty());
}
#[test]
fn test_merge_message_runs_single() {
let messages = vec![Message::human("Hello")];
let merged = merge_message_runs(&messages);
assert_eq!(merged.len(), 1);
assert_eq!(merged[0].content().as_text(), "Hello");
}
}