Skip to main content

crabtalk_core/agent/
compact.rs

1//! Context compaction — summarize conversation history and replace it.
2
3use crate::model::{Message, Model, Request, Role};
4
5pub(crate) const COMPACT_PROMPT: &str = include_str!("../../prompts/compact.md");
6
7impl<M: Model> super::Agent<M> {
8    /// Summarize the conversation history using the LLM.
9    ///
10    /// Builds the base compact prompt, lets the `compact_hook` (if any) enrich
11    /// it, then sends the history with the enriched prompt as system message.
12    /// Returns the summary text, or `None` if the model produces no content.
13    pub async fn compact(&self, history: &[Message]) -> Option<String> {
14        let model_name = self
15            .config
16            .model
17            .clone()
18            .unwrap_or_else(|| self.model.active_model());
19
20        let prompt = COMPACT_PROMPT.to_owned();
21
22        let mut messages = Vec::with_capacity(2 + history.len());
23        messages.push(Message::system(&prompt));
24        // Include the agent's system prompt as identity context so the
25        // compaction LLM preserves <self>, <identity>, and <profile> info.
26        if !self.config.system_prompt.is_empty() {
27            messages.push(Message::user(format!(
28                "Agent system prompt (preserve identity/profile info):\n{}",
29                self.config.system_prompt
30            )));
31        }
32        let max_len = self.config.compact_tool_max_len;
33        messages.extend(history.iter().cloned().map(|mut m| {
34            if m.role == Role::Tool && m.content.len() > max_len {
35                m.content.truncate(m.content.floor_char_boundary(max_len));
36                m.content.push_str("... [truncated]");
37            }
38            m
39        }));
40
41        let request = Request::new(model_name).with_messages(messages);
42        match self.model.send(&request).await {
43            Ok(response) => response.content().cloned(),
44            Err(e) => {
45                tracing::warn!("compaction LLM call failed: {e}");
46                None
47            }
48        }
49    }
50
51    /// Estimate the token count of conversation history.
52    ///
53    /// Uses a simple heuristic: ~4 characters per token. Counts content,
54    /// reasoning_content, and tool call arguments.
55    pub(crate) fn estimate_tokens(history: &[Message]) -> usize {
56        let total_chars: usize = history
57            .iter()
58            .map(|m| {
59                let mut chars = m.content.len() + m.reasoning_content.len();
60                for tc in &m.tool_calls {
61                    chars += tc.function.arguments.len();
62                }
63                chars
64            })
65            .sum();
66        total_chars / 4
67    }
68}