use crate::api::{Message, utils::truncate_at_char_boundary};
use crate::repl::conversation::ConversationHistory;
const COMPACTION_TOOL_RESULT_KEEP_CHARS: usize = 500;
impl ConversationHistory {
pub fn needs_compaction(&self) -> bool {
self.estimate_total_tokens() > self.config.auto_compact_token_limit
}
pub fn set_auto_compact_token_limit(&mut self, n: usize) {
self.config.auto_compact_token_limit = n;
}
pub fn compaction_split_point(&self) -> usize {
let preserve = self.config.compaction_preserve_recent;
if self.messages.len() <= preserve + 5 {
return 0;
}
let mut split = self
.messages
.len()
.saturating_sub(preserve)
.min(self.messages.len() - 1);
while split > 0 && self.messages[split].role != "user" {
split -= 1;
}
while split > 0 {
if let crate::api::MessageContent::Blocks { content } = &self.messages[split].content {
let has_tool_result = content.iter().any(|b| {
matches!(
b,
crate::api::MessageContentBlock::ToolResult { .. }
| crate::api::MessageContentBlock::WebSearchToolResult { .. }
)
});
if has_tool_result {
split -= 1;
continue;
}
}
break;
}
split
}
pub fn truncate_tool_results(&mut self, up_to: usize) {
self.invalidate_cache_anchor();
let threshold = self.config.tool_result_truncate_threshold;
let keep_chars = COMPACTION_TOOL_RESULT_KEEP_CHARS;
for msg in self.messages[..up_to].iter_mut() {
if let crate::api::MessageContent::Blocks { content } = &mut msg.content {
for block in content.iter_mut() {
if let crate::api::MessageContentBlock::ToolResult {
content: result_text,
..
} = block
{
if result_text.len() > threshold {
let original_len = result_text.len();
let actual_keep = keep_chars.min(original_len / 3);
let start_end = truncate_at_char_boundary(result_text, actual_keep);
let end_start = {
let target = original_len.saturating_sub(actual_keep);
let mut i = target;
while i > 0 && !result_text.is_char_boundary(i) {
i -= 1;
}
i
};
let start = &result_text[..start_end];
let end = &result_text[end_start..];
*result_text = format!(
"{}\n...[truncated {} chars]...\n{}",
start, original_len, end
);
}
}
}
}
}
}
pub fn serialize_messages_for_summary(messages: &[Message]) -> String {
let mut parts = Vec::new();
for msg in messages {
let role_label = if msg.role == "user" {
"User"
} else {
"Assistant"
};
match &msg.content {
crate::api::MessageContent::Text { content } => {
parts.push(format!("{}: {}", role_label, content));
}
crate::api::MessageContent::Blocks { content } => {
for block in content {
match block {
crate::api::MessageContentBlock::Text { text, .. } => {
parts.push(format!("{}: {}", role_label, text));
}
crate::api::MessageContentBlock::ToolUse { name, input, .. } => {
let input_str = serde_json::to_string(input).unwrap_or_default();
let input_preview = if input_str.len() > 200 {
format!(
"{}...",
&input_str[..truncate_at_char_boundary(&input_str, 200)]
)
} else {
input_str
};
parts.push(format!("[Tool call: {}({})]", name, input_preview));
}
crate::api::MessageContentBlock::ToolResult { content, .. } => {
let preview = if content.len() > 300 {
format!(
"{}...",
&content[..truncate_at_char_boundary(content, 300)]
)
} else {
content.clone()
};
parts.push(format!("[Tool result: {}]", preview));
}
crate::api::MessageContentBlock::Image { .. } => {
parts.push("[Image attached]".to_string());
}
_ => {}
}
}
}
}
}
parts.join("\n\n")
}
pub fn replace_with_summary(&mut self, summary: String, split_point: usize) {
if split_point == 0 || split_point > self.messages.len() {
return;
}
self.invalidate_cache_anchor();
self.messages.drain(0..split_point);
let summary_msg = Message::user(format!(
"[Conversation Summary]\n\nThe following is a summary of our earlier conversation:\n\n{}",
summary
));
self.messages.insert(0, summary_msg);
self.maintain_cache_anchor();
}
}