134 lines
4.1 KiB
Rust
134 lines
4.1 KiB
Rust
//! 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<u64>,
|
|
/// Prompt for the pre-compaction checkpoint turn; `None` skips it.
|
|
pub checkpoint_prompt: Option<String>,
|
|
}
|
|
|
|
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<u64>) -> 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),
|
|
}));
|
|
}
|
|
}
|