use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BudgetPolicy {
pub total_window: usize,
pub system_reserve_pct: f32,
pub tool_def_reserve_pct: f32,
pub user_prompt_reserve_pct: f32,
pub recent_turns_keep_verbatim: usize,
pub trigger_compaction_at_pct: f32,
pub freshness_aware: bool,
pub vitals_aware: bool,
pub soft_total_cap: usize,
}
impl Default for BudgetPolicy {
fn default() -> Self {
Self {
total_window: 30_000,
system_reserve_pct: 0.10,
tool_def_reserve_pct: 0.10,
user_prompt_reserve_pct: 0.05,
recent_turns_keep_verbatim: 6,
trigger_compaction_at_pct: 0.70,
freshness_aware: true,
vitals_aware: true,
soft_total_cap: 30_000,
}
}
}
impl BudgetPolicy {
#[must_use]
pub fn system_budget(&self) -> usize {
((self.total_window as f32) * self.system_reserve_pct) as usize
}
#[must_use]
pub fn tool_def_budget(&self) -> usize {
((self.total_window as f32) * self.tool_def_reserve_pct) as usize
}
#[must_use]
pub fn user_prompt_budget(&self) -> usize {
((self.total_window as f32) * self.user_prompt_reserve_pct) as usize
}
#[must_use]
pub fn flexible_budget(&self) -> usize {
self.total_window
.saturating_sub(self.system_budget())
.saturating_sub(self.tool_def_budget())
.saturating_sub(self.user_prompt_budget())
}
#[must_use]
pub fn should_compact(&self, used_tokens: usize) -> bool {
let trigger = ((self.total_window as f32) * self.trigger_compaction_at_pct) as usize;
used_tokens >= trigger
}
#[must_use]
pub fn for_window(input_window_tokens: usize) -> Self {
let mut p = Self::default();
p.total_window = input_window_tokens.min(p.soft_total_cap.max(input_window_tokens));
p.soft_total_cap = p.soft_total_cap.min(input_window_tokens.max(1));
p
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_reserves_sum_below_total() {
let p = BudgetPolicy::default();
let reserves =
p.system_budget() + p.tool_def_budget() + p.user_prompt_budget();
assert!(reserves < p.total_window);
assert!(p.flexible_budget() > 0);
}
#[test]
fn compaction_triggers_at_threshold() {
let p = BudgetPolicy::default();
assert!(!p.should_compact(0));
assert!(p.should_compact((p.total_window as f32 * 0.71) as usize));
}
#[test]
fn for_window_respects_soft_cap() {
let p = BudgetPolicy::for_window(8_000);
assert!(p.total_window <= 30_000);
assert!(p.flexible_budget() > 0);
}
}