meerkat_core/compact.rs
1//! Compactor trait — provider-agnostic context compaction.
2//!
3//! The `Compactor` trait defines how and when to compact (summarize) the
4//! conversation history to reclaim context window space. Implementations
5//! live in `meerkat-session` (behind the `session-compaction` feature).
6
7use crate::types::Message;
8use serde::{Deserialize, Serialize};
9
10/// Metadata key used to persist compaction cadence across session reuse.
11pub const SESSION_COMPACTION_CADENCE_KEY: &str = "session_compaction_cadence";
12
13/// Durable session-scoped cadence state for compaction decisions.
14#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
15#[serde(rename_all = "snake_case")]
16pub struct SessionCompactionCadence {
17 /// Monotonic index of pre-LLM boundaries seen in this session.
18 pub session_boundary_index: u64,
19 /// Boundary index where compaction last completed successfully.
20 #[serde(default, skip_serializing_if = "Option::is_none")]
21 pub last_compaction_boundary_index: Option<u64>,
22}
23
24/// Context provided to `Compactor::should_compact` for trigger decisions.
25#[derive(Debug, Clone)]
26pub struct CompactionContext {
27 /// Input token count from the last LLM response.
28 pub last_input_tokens: u64,
29 /// Total number of messages in the session.
30 pub message_count: usize,
31 /// Estimated history tokens (JSON bytes / 4).
32 pub estimated_history_tokens: u64,
33 /// Session-scoped pre-LLM boundary index where compaction last occurred, if ever.
34 pub last_compaction_boundary_index: Option<u64>,
35 /// Current session-scoped pre-LLM boundary index.
36 pub session_boundary_index: u64,
37}
38
39/// Result of a compaction rebuild.
40#[derive(Debug, Clone)]
41pub struct CompactionResult {
42 /// The rebuilt message history (summary + retained recent turns).
43 pub messages: Vec<Message>,
44 /// Messages that were removed from history (for future memory indexing).
45 pub discarded: Vec<Message>,
46}
47
48/// Configuration for the default compactor implementation.
49#[derive(Debug, Clone)]
50pub struct CompactionConfig {
51 /// Compaction triggers when `last_input_tokens >= auto_compact_threshold`.
52 pub auto_compact_threshold: u64,
53 /// Number of recent complete turns to retain after compaction.
54 pub recent_turn_budget: usize,
55 /// Maximum tokens for the compaction summary LLM response.
56 pub max_summary_tokens: u32,
57 /// Minimum session-scoped LLM boundaries between consecutive compactions.
58 pub min_turns_between_compactions: u32,
59}
60
61impl Default for CompactionConfig {
62 fn default() -> Self {
63 Self {
64 auto_compact_threshold: 100_000,
65 recent_turn_budget: 4,
66 max_summary_tokens: 4096,
67 min_turns_between_compactions: 3,
68 }
69 }
70}
71
72/// Provider-agnostic compaction strategy.
73///
74/// Determines when to compact and how to rebuild the history after summarization.
75pub trait Compactor: Send + Sync {
76 /// Check whether compaction should run given the current context.
77 fn should_compact(&self, ctx: &CompactionContext) -> bool;
78
79 /// Return the prompt to send to the LLM for summarization.
80 fn compaction_prompt(&self) -> &str;
81
82 /// Maximum tokens the summarization response may consume.
83 fn max_summary_tokens(&self) -> u32;
84
85 /// Prepare messages for the summarization LLM call.
86 ///
87 /// Called before sending the history to the LLM for summarization.
88 /// Implementations may strip content that is not suitable for the
89 /// summarization pass (e.g. base64-encoded images).
90 ///
91 /// The default implementation returns an unmodified clone.
92 fn prepare_for_summarization(&self, messages: &[Message]) -> Vec<Message> {
93 messages.to_vec()
94 }
95
96 /// Rebuild the session history from a summary and current messages.
97 ///
98 /// The system prompt is extracted from `messages` directly (the first
99 /// `Message::System` if present). No dual source of truth.
100 ///
101 /// The implementation should:
102 /// 1. Preserve any `Message::System` verbatim.
103 /// 2. Inject a summary message.
104 /// 3. Retain recent complete turns per `recent_turn_budget`.
105 /// 4. Return everything else as `discarded`.
106 fn rebuild_history(&self, messages: &[Message], summary: &str) -> CompactionResult;
107}