use crate::context::estimate_tokens;
use crate::llm::Message;
#[derive(Debug, Clone)]
pub struct HistoryPlan<'a> {
pub to_summarize: &'a [Message],
pub keep_recent: &'a [Message],
}
impl HistoryPlan<'_> {
pub fn is_noop(&self) -> bool {
self.to_summarize.is_empty()
}
}
pub fn plan_history_compaction<'a>(
history: &'a [Message],
budget: usize,
reserve: usize,
) -> HistoryPlan<'a> {
let effective = budget.saturating_sub(reserve);
let mut used = 0usize;
let mut keep_from = history.len();
for (i, msg) in history.iter().enumerate().rev() {
let tokens = estimate_tokens(&msg.content);
if used + tokens > effective {
break;
}
used += tokens;
keep_from = i;
}
HistoryPlan {
to_summarize: &history[..keep_from],
keep_recent: &history[keep_from..],
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::llm::Message;
fn turn(n: usize) -> Message {
Message::user(format!("turn {n}: {}", "x".repeat(27)))
}
#[test]
fn nothing_overflows_is_a_noop() {
let history: Vec<Message> = (0..3).map(turn).collect();
let plan = plan_history_compaction(&history, 1000, 100);
assert!(plan.is_noop());
assert_eq!(plan.keep_recent.len(), 3);
assert!(plan.to_summarize.is_empty());
}
#[test]
fn oldest_turns_overflow_into_summary_keeping_recent() {
let history: Vec<Message> = (0..10).map(turn).collect();
let plan = plan_history_compaction(&history, 40, 5);
assert!(!plan.is_noop());
assert_eq!(
plan.to_summarize.len() + plan.keep_recent.len(),
history.len()
);
assert!(plan.keep_recent.len() < history.len());
assert_eq!(
plan.keep_recent.last().unwrap().content,
history.last().unwrap().content
);
assert_eq!(
plan.to_summarize.first().unwrap().content,
history[0].content
);
}
#[test]
fn single_oversized_turn_goes_to_summary_not_kept_verbatim() {
let big = Message::user("y".repeat(10_000));
let history = vec![big];
let plan = plan_history_compaction(&history, 40, 5);
assert_eq!(plan.keep_recent.len(), 0);
assert_eq!(plan.to_summarize.len(), 1);
}
#[test]
fn empty_history_is_a_noop() {
let plan = plan_history_compaction(&[], 100, 10);
assert!(plan.is_noop());
assert!(plan.keep_recent.is_empty());
}
use proptest::prelude::*;
fn histories() -> impl Strategy<Value = Vec<Message>> {
proptest::collection::vec("[a-z ]{0,120}".prop_map(Message::user), 0..20)
}
fn kept_tokens(plan: &HistoryPlan<'_>) -> usize {
plan.keep_recent
.iter()
.map(|m| estimate_tokens(&m.content))
.sum()
}
proptest! {
#![proptest_config(ProptestConfig { cases: 512, .. ProptestConfig::default() })]
#[test]
fn split_is_a_faithful_partition(
history in histories(),
budget in 0usize..500,
reserve in 0usize..200,
) {
let plan = plan_history_compaction(&history, budget, reserve);
prop_assert_eq!(
plan.to_summarize.len() + plan.keep_recent.len(),
history.len()
);
let rejoined = plan
.to_summarize
.iter()
.chain(plan.keep_recent.iter())
.map(|m| &m.content);
prop_assert!(rejoined.eq(history.iter().map(|m| &m.content)));
}
#[test]
fn kept_turns_fit_the_effective_budget(
history in histories(),
budget in 0usize..500,
reserve in 0usize..200,
) {
let plan = plan_history_compaction(&history, budget, reserve);
prop_assert!(kept_tokens(&plan) <= budget.saturating_sub(reserve));
}
#[test]
fn keeps_as_much_recent_as_fits(
history in histories(),
budget in 0usize..500,
reserve in 0usize..200,
) {
let plan = plan_history_compaction(&history, budget, reserve);
if let Some(newest_overflow) = plan.to_summarize.last() {
let effective = budget.saturating_sub(reserve);
let would_be = kept_tokens(&plan) + estimate_tokens(&newest_overflow.content);
prop_assert!(
would_be > effective,
"a turn that fit was summarized: {would_be} <= {effective}"
);
}
}
#[test]
fn noop_iff_everything_fits(
history in histories(),
budget in 0usize..500,
reserve in 0usize..200,
) {
let plan = plan_history_compaction(&history, budget, reserve);
let total: usize = history.iter().map(|m| estimate_tokens(&m.content)).sum();
prop_assert_eq!(plan.is_noop(), total <= budget.saturating_sub(reserve));
}
}
}