//! When a durable session should compact. use crate::Usage; /// Decides, after a completed turn, whether an [`crate::InfiniteSession`] /// should proactively compact — and what to say in the optional checkpoint /// turn that runs first. Injected by the caller so the driver stays free of /// any app-specific policy. pub trait CompactionPolicy { /// Given the last turn's context [`Usage`], compact now (before the window /// fills)? fn should_compact(&self, usage: Usage) -> bool; /// Prompt for a pre-compaction checkpoint turn (a chance for the agent to /// flush durable state before detail collapses into a summary), or `None` /// to compact without one. Default: `None`. fn checkpoint_prompt(&self) -> Option<&str> { None } } /// Compact once the context reaches `percent` of the model window. /// /// The window is the one the model reported this turn ([`Usage::context_window`]), /// falling back to [`PercentPolicy::default_window`] when the turn reported /// none (e.g. a degenerate turn with no `result` usage). `percent == 0` /// disables proactive compaction entirely. #[derive(Debug, Clone, Default)] pub struct PercentPolicy { /// Watermark as a percent of the context window (e.g. `75`). `0` disables. pub percent: u8, /// Window to assume when the turn didn't report one. `None` → never /// compact until a window is observed. pub default_window: Option, /// Prompt for the pre-compaction checkpoint turn; `None` skips it. pub checkpoint_prompt: Option, } impl CompactionPolicy for PercentPolicy { fn should_compact(&self, usage: Usage) -> bool { if self.percent == 0 { return false; } let Some(window) = usage .context_window .or(self.default_window) .filter(|&w| w > 0) else { return false; }; usage.context_tokens.saturating_mul(100) >= u64::from(self.percent) * window } fn checkpoint_prompt(&self) -> Option<&str> { self.checkpoint_prompt.as_deref() } } /// A policy that never compacts. Turns run until the session overflows and the /// reactive path in [`crate::InfiniteSession::run`] takes over. #[derive(Debug, Clone, Copy, Default)] pub struct NeverCompact; impl CompactionPolicy for NeverCompact { fn should_compact(&self, _usage: Usage) -> bool { false } } #[cfg(test)] mod tests { use super::{CompactionPolicy, NeverCompact, PercentPolicy}; use crate::Usage; fn policy(percent: u8, default_window: Option) -> PercentPolicy { PercentPolicy { percent, default_window, checkpoint_prompt: None, } } #[test] fn fires_at_or_above_watermark() { let p = policy(75, None); // 75% of a 200k window = 150k. assert!(!p.should_compact(Usage { context_tokens: 149_999, context_window: Some(200_000), })); assert!(p.should_compact(Usage { context_tokens: 150_000, context_window: Some(200_000), })); } #[test] fn zero_percent_disables() { assert!(!policy(0, Some(200_000)).should_compact(Usage { context_tokens: 199_999, context_window: Some(200_000), })); } #[test] fn falls_back_to_default_window_when_unreported() { let p = policy(50, Some(100_000)); assert!(p.should_compact(Usage { context_tokens: 50_000, context_window: None, })); // Reported window takes precedence over the default. assert!(!p.should_compact(Usage { context_tokens: 50_000, context_window: Some(200_000), })); } #[test] fn no_window_anywhere_never_fires() { assert!(!policy(75, None).should_compact(Usage { context_tokens: u64::MAX, context_window: None, })); } #[test] fn never_compact_is_never() { assert!(!NeverCompact.should_compact(Usage { context_tokens: u64::MAX, context_window: Some(1), })); } }