use crate::store::MessageRecord;
use crate::token::estimate_record_tokens;
const PRUNE_MINIMUM: usize = 20_000;
const PRUNE_PROTECT: usize = 40_000;
const PRUNE_PROTECTED_TOOLS: &[&str] = &["skill"];
pub const PRUNED_PLACEHOLDER: &str = "[Old tool result content cleared]";
#[derive(Debug, Clone)]
pub struct PruneResult {
pub pruned_count: usize,
pub tokens_freed: usize,
}
#[derive(Debug, Clone, Copy)]
struct PruneTarget {
index: usize,
}
#[must_use]
pub fn prune_tool_outputs(messages: &mut [MessageRecord], skip_turns: usize) -> PruneResult {
if messages.is_empty() {
return PruneResult {
pruned_count: 0,
tokens_freed: 0,
};
}
let mut turn_count = 0usize;
let mut skip_until: Option<usize> = None;
for (i, msg) in messages.iter().enumerate().rev() {
if !msg.is_compaction && msg.role == crate::store::MessageRole::User {
turn_count += 1;
if turn_count >= skip_turns {
skip_until = Some(i);
break;
}
}
}
let mut protected = 0usize;
let mut targets: Vec<PruneTarget> = Vec::new();
let mut estimated_freed = 0usize;
for (idx, msg) in messages.iter().enumerate().rev() {
if msg.is_summary {
break;
}
if skip_until.is_some() && protected < PRUNE_PROTECT {
protected += estimate_record_tokens(msg);
continue;
}
if msg.role != crate::store::MessageRole::Tool {
continue;
}
if let Some(ref tool_name) = msg.tool_name {
if PRUNE_PROTECTED_TOOLS.contains(&tool_name.as_str()) {
continue;
}
}
if msg.is_summary {
continue;
}
let tokens = estimate_record_tokens(msg);
if protected < PRUNE_PROTECT {
protected += tokens;
continue;
}
targets.push(PruneTarget { index: idx });
estimated_freed += tokens;
}
if estimated_freed < PRUNE_MINIMUM {
return PruneResult {
pruned_count: 0,
tokens_freed: 0,
};
}
for target in &targets {
let msg = &mut messages[target.index];
msg.is_summary = true;
msg.content = vec![crate::provider::ContentPart::text(PRUNED_PLACEHOLDER)];
}
PruneResult {
pruned_count: targets.len(),
tokens_freed: estimated_freed,
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
use crate::provider::ContentPart;
use uuid::Uuid;
fn make_tool_msg(name: &str, output: &str) -> MessageRecord {
MessageRecord {
id: Uuid::now_v7(),
session_id: Uuid::now_v7(),
role: crate::store::MessageRole::Tool,
content: vec![ContentPart::text(output)],
tool_calls: Vec::new(),
tool_call_id: Some("call_1".to_owned()),
tool_name: Some(name.to_owned()),
usage: None,
created_at: chrono::Utc::now(),
is_compaction: false,
is_summary: false,
compaction_meta: None,
}
}
#[test]
fn empty_messages() {
let mut messages = Vec::new();
let result = prune_tool_outputs(&mut messages, 2);
assert_eq!(result.pruned_count, 0);
}
#[test]
fn skill_tools_are_protected() {
let mut messages = vec![make_tool_msg("skill", "important skill output")];
let result = prune_tool_outputs(&mut messages, 0);
assert_eq!(result.pruned_count, 0);
}
#[test]
fn small_output_not_pruned() {
let mut messages = vec![make_tool_msg("read_file", "short")];
let result = prune_tool_outputs(&mut messages, 0);
assert_eq!(result.pruned_count, 0);
}
}