use crate::llm::{Message, MessageContent, ToolResultBody};
use crate::session::history::estimate_message_tokens;
use super::compact::is_turn_start;
const KEEP_RECENT_TURNS: usize = 3;
const MIN_CLEAR_TOKENS: u64 = 512;
pub(super) const CLEARED_PLACEHOLDER: &str =
"[tool output cleared to save context — re-run the tool if its result is needed again]";
pub(super) struct MicrocompactReport {
pub tokens_before: u64,
pub tokens_after: u64,
pub cleared: usize,
}
pub(super) fn run(messages: &[Message]) -> Option<(Vec<Message>, MicrocompactReport)> {
let turn_starts: Vec<usize> = messages
.iter()
.enumerate()
.filter(|(_, m)| is_turn_start(m))
.map(|(i, _)| i)
.collect();
let keep_from = *turn_starts.iter().rev().nth(KEEP_RECENT_TURNS - 1)?;
let tokens_before = estimate_total(messages);
let mut cleared = 0usize;
let rebuilt: Vec<Message> = messages
.iter()
.enumerate()
.map(|(idx, msg)| {
if idx >= keep_from {
return msg.clone();
}
clear_oversized_results(msg, &mut cleared)
})
.collect();
if cleared == 0 {
return None;
}
let tokens_after = estimate_total(&rebuilt);
Some((
rebuilt,
MicrocompactReport {
tokens_before,
tokens_after,
cleared,
},
))
}
fn clear_oversized_results(msg: &Message, cleared: &mut usize) -> Message {
let has_tool_result = msg
.content
.iter()
.any(|c| matches!(c, MessageContent::ToolResult { .. }));
if !has_tool_result {
return msg.clone();
}
let content: Vec<MessageContent> = msg
.content
.iter()
.map(|c| match c {
MessageContent::ToolResult {
tool_use_id,
output,
is_error,
} if should_clear(output) => {
*cleared += 1;
MessageContent::ToolResult {
tool_use_id: tool_use_id.clone(),
output: ToolResultBody::Text {
text: CLEARED_PLACEHOLDER.to_string(),
},
is_error: *is_error,
}
}
other => other.clone(),
})
.collect();
Message {
role: msg.role,
content: content.into(),
}
}
fn should_clear(output: &ToolResultBody) -> bool {
if already_cleared(output) {
return false;
}
estimate_tool_result_tokens(output) > MIN_CLEAR_TOKENS
}
fn already_cleared(output: &ToolResultBody) -> bool {
matches!(output, ToolResultBody::Text { text } if text == CLEARED_PLACEHOLDER)
}
fn estimate_tool_result_tokens(output: &ToolResultBody) -> u64 {
let probe = Message {
role: crate::llm::Role::User,
content: vec![MessageContent::ToolResult {
tool_use_id: String::new(),
output: output.clone(),
is_error: false,
}]
.into(),
};
estimate_message_tokens(&probe)
}
fn estimate_total(messages: &[Message]) -> u64 {
messages
.iter()
.map(estimate_message_tokens)
.fold(0u64, u64::saturating_add)
}
#[cfg(test)]
mod tests;