Skip to main content

crabtalk_core/agent/
compact.rs

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