feat(hive-claude): add InfiniteSession (name+store+compaction policy)
This commit is contained in:
parent
bf93a81e1d
commit
80d819e444
8 changed files with 501 additions and 283 deletions
130
hive-claude/src/policy.rs
Normal file
130
hive-claude/src/policy.rs
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
//! 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),
|
||||
}));
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue