zeroclaw/src/agent/loop_/history.rs

107 lines
3.7 KiB
Rust

use crate::providers::{ChatMessage, Provider};
use crate::util::truncate_with_ellipsis;
use anyhow::Result;
use std::fmt::Write;
/// Keep this many most-recent non-system messages after compaction.
const COMPACTION_KEEP_RECENT_MESSAGES: usize = 20;
/// Safety cap for compaction source transcript passed to the summarizer.
const COMPACTION_MAX_SOURCE_CHARS: usize = 12_000;
/// Max characters retained in stored compaction summary.
const COMPACTION_MAX_SUMMARY_CHARS: usize = 2_000;
/// Trim conversation history to prevent unbounded growth.
/// Preserves the system prompt (first message if role=system) and the most recent messages.
pub(super) fn trim_history(history: &mut Vec<ChatMessage>, max_history: usize) {
// Nothing to trim if within limit
let has_system = history.first().map_or(false, |m| m.role == "system");
let non_system_count = if has_system {
history.len() - 1
} else {
history.len()
};
if non_system_count <= max_history {
return;
}
let start = if has_system { 1 } else { 0 };
let to_remove = non_system_count - max_history;
history.drain(start..start + to_remove);
}
pub(super) fn build_compaction_transcript(messages: &[ChatMessage]) -> String {
let mut transcript = String::new();
for msg in messages {
let role = msg.role.to_uppercase();
let _ = writeln!(transcript, "{role}: {}", msg.content.trim());
}
if transcript.chars().count() > COMPACTION_MAX_SOURCE_CHARS {
truncate_with_ellipsis(&transcript, COMPACTION_MAX_SOURCE_CHARS)
} else {
transcript
}
}
pub(super) fn apply_compaction_summary(
history: &mut Vec<ChatMessage>,
start: usize,
compact_end: usize,
summary: &str,
) {
let summary_msg = ChatMessage::assistant(format!("[Compaction summary]\n{}", summary.trim()));
history.splice(start..compact_end, std::iter::once(summary_msg));
}
pub(super) async fn auto_compact_history(
history: &mut Vec<ChatMessage>,
provider: &dyn Provider,
model: &str,
max_history: usize,
) -> Result<bool> {
let has_system = history.first().map_or(false, |m| m.role == "system");
let non_system_count = if has_system {
history.len().saturating_sub(1)
} else {
history.len()
};
if non_system_count <= max_history {
return Ok(false);
}
let start = if has_system { 1 } else { 0 };
let keep_recent = COMPACTION_KEEP_RECENT_MESSAGES.min(non_system_count);
let compact_count = non_system_count.saturating_sub(keep_recent);
if compact_count == 0 {
return Ok(false);
}
let compact_end = start + compact_count;
let to_compact: Vec<ChatMessage> = history[start..compact_end].to_vec();
let transcript = build_compaction_transcript(&to_compact);
let summarizer_system = "You are a conversation compaction engine. Summarize older chat history into concise context for future turns. Preserve: user preferences, commitments, decisions, unresolved tasks, key facts. Omit: filler, repeated chit-chat, verbose tool logs. Output plain text bullet points only.";
let summarizer_user = format!(
"Summarize the following conversation history for context preservation. Keep it short (max 12 bullet points).\n\n{}",
transcript
);
let summary_raw = provider
.chat_with_system(Some(summarizer_system), &summarizer_user, model, 0.2)
.await
.unwrap_or_else(|_| {
// Fallback to deterministic local truncation when summarization fails.
truncate_with_ellipsis(&transcript, COMPACTION_MAX_SUMMARY_CHARS)
});
let summary = truncate_with_ellipsis(&summary_raw, COMPACTION_MAX_SUMMARY_CHARS);
apply_compaction_summary(history, start, compact_end, &summary);
Ok(true)
}