use super::config::ContextConfig;
use super::token::*;
use crate::types::*;
use std::sync::Arc;
pub fn compact_messages(
messages: Vec<AgentMessage>, config: &ContextConfig, ) -> Vec<AgentMessage> {
compact_messages_with_counter(messages, config, config.token_counter.as_ref())
}
pub fn compact_messages_with_counter(
messages: Vec<AgentMessage>,
config: &ContextConfig,
counter: Option<&Arc<dyn TokenCounter>>,
) -> Vec<AgentMessage> {
let counter = resolve_counter(counter);
let budget = config
.max_context_tokens
.saturating_sub(config.system_prompt_tokens);
if counter.estimate_messages(&messages) <= budget {
return messages;
}
let compacted = level1_truncate_tool_outputs(&messages, config.tool_output_max_lines);
if counter.estimate_messages(&compacted) <= budget {
return compacted;
}
let compacted = level2_summarize_old_turns(&compacted, config.keep_recent);
if counter.estimate_messages(&compacted) <= budget {
return compacted;
}
level3_drop_middle_with_counter(&compacted, config, budget, counter)
}
pub(super) fn level1_truncate_tool_outputs(
messages: &[AgentMessage], max_lines: usize, ) -> Vec<AgentMessage> {
messages
.iter()
.map(|msg| match msg {
AgentMessage::Llm(LlmMessage {
message:
Message::ToolResult {
tool_call_id,
tool_name,
content,
is_error,
timestamp,
},
..
}) => {
let truncated_content: Vec<Content> = content
.iter()
.map(|c| match c {
Content::Text { text } => Content::Text {
text: truncate_text_head_tail(text, max_lines),
},
other => other.clone(), })
.collect();
AgentMessage::Llm(LlmMessage::new(Message::ToolResult {
tool_call_id: tool_call_id.clone(),
tool_name: tool_name.clone(),
content: truncated_content,
is_error: *is_error, timestamp: *timestamp, }))
}
other => other.clone(), })
.collect()
}
pub(super) fn truncate_text_head_tail(
text: &str, max_lines: usize, ) -> String {
let lines: Vec<&str> = text.lines().collect();
if lines.len() <= max_lines {
return text.to_string();
}
let head = max_lines / 2;
let tail = max_lines - head;
let omitted = lines.len() - head - tail;
let mut result = lines[..head].join("\n");
result.push_str(&format!("\n\n[... {} lines truncated ...]\n\n", omitted));
result.push_str(&lines[lines.len() - tail..].join("\n"));
result
}
fn level2_summarize_old_turns(
messages: &[AgentMessage], keep_recent: usize, ) -> Vec<AgentMessage> {
let len = messages.len();
if len <= keep_recent {
return messages.to_vec();
}
let boundary = len - keep_recent;
let mut result = Vec::new();
let mut i = 0;
while i < boundary {
let msg = &messages[i];
match msg {
AgentMessage::Llm(LlmMessage {
message: Message::Assistant { content, .. },
..
}) => {
let text_parts: Vec<&str> = content
.iter()
.filter_map(|c| match c {
Content::Text { text } => {
if text.len() > 200 {
None } else {
Some(text.as_str())
}
}
_ => None,
})
.collect();
let tool_count = content
.iter()
.filter(|c| matches!(c, Content::ToolCall { .. }))
.count();
let summary = if !text_parts.is_empty() {
text_parts.join(" ")
} else if tool_count > 0 {
format!("[Assistant used {} tool(s)]", tool_count)
} else {
"[Assistant response]".into()
};
result.push(AgentMessage::Llm(LlmMessage::new(Message::User {
content: vec![Content::Text {
text: format!("[Summary] {}", summary),
}],
timestamp: now_ms(),
})));
i += 1;
while i < boundary {
if let AgentMessage::Llm(LlmMessage {
message: Message::ToolResult { .. },
..
}) = &messages[i]
{
i += 1;
} else {
break;
}
}
continue;
}
AgentMessage::Llm(LlmMessage {
message: Message::ToolResult { .. },
..
}) => {
i += 1;
continue;
}
other => {
result.push(other.clone());
}
}
i += 1;
}
result.extend_from_slice(&messages[boundary..]);
result
}
fn level3_drop_middle_with_counter(
messages: &[AgentMessage],
config: &ContextConfig,
budget: usize,
counter: &dyn TokenCounter,
) -> Vec<AgentMessage> {
let len = messages.len();
let first_end = config.keep_first.min(len);
let recent_start = len.saturating_sub(config.keep_recent);
if first_end >= recent_start {
return keep_within_budget_with_counter(messages, budget, counter);
}
let first_msgs = &messages[..first_end];
let recent_msgs = &messages[recent_start..];
let removed = recent_start - first_end;
let marker = AgentMessage::Llm(LlmMessage::new(Message::User {
content: vec![Content::Text {
text: format!(
"[Context compacted: {} messages removed to fit context window]",
removed
),
}],
timestamp: now_ms(),
}));
let mut result = first_msgs.to_vec();
result.push(marker);
result.extend_from_slice(recent_msgs);
if counter.estimate_messages(&result) > budget {
return keep_within_budget_with_counter(&result, budget, counter);
}
result
}
fn keep_within_budget_with_counter(
messages: &[AgentMessage],
budget: usize,
counter: &dyn TokenCounter,
) -> Vec<AgentMessage> {
let mut result = Vec::new();
let mut remaining = budget;
for msg in messages.iter().rev() {
let tokens = counter.estimate_message(msg);
if tokens > remaining {
break;
}
remaining -= tokens;
result.push(msg.clone());
}
result.reverse();
if result.len() < messages.len() {
let removed = messages.len() - result.len();
result.insert(
0,
AgentMessage::Llm(LlmMessage::new(Message::User {
content: vec![Content::Text {
text: format!("[Context compacted: {} messages removed]", removed),
}],
timestamp: now_ms(),
})),
);
}
result
}