use agent_base::engine::estimate_messages_tokens;
use agent_base::types::ChatMessage;
#[derive(Clone, Debug)]
pub struct TokenBudgetConfig {
pub work_budget: usize,
pub reminder_threshold: usize,
pub reminder_template: String,
pub fallback_prompt: String,
pub fallback_buffer: usize,
pub guidance_message: String,
pub seed_message: String,
pub min_work_multiple: f64,
pub min_absolute_work_room: usize,
pub user_trail_max_tokens: usize,
}
impl Default for TokenBudgetConfig {
fn default() -> Self {
Self {
work_budget: 210_000,
reminder_threshold: 42_000,
reminder_template: "You have approximately {n_remaining} tokens left in this context window. \
If you have important state, decisions, or progress to record, save them to your notes now \
using the notes tools. This window may close soon."
.to_string(),
fallback_prompt: "This context window is closing. Before it resets, write a handoff \
summary to your notes — a note file named `thread_hint.md` is injected \
automatically into the next window, so put the current task, key decisions, \
and progress there. Your previous conversation history also remains \
available via the history tools."
.to_string(),
fallback_buffer: 21_000,
guidance_message: "You have access to two private tools for context management:\n\
- `history`: read-only access to previous context windows (list, search, read)\n\
- `notes`: read-write persistent scratchpad that survives window transitions\n\
Use these silently to maintain continuity. Never disclose them to the user."
.to_string(),
seed_message: DEFAULT_SEED_MESSAGE.to_string(),
min_work_multiple: 2.0,
min_absolute_work_room: 4_096,
user_trail_max_tokens: 2_000,
}
}
}
pub const DEFAULT_SEED_MESSAGE: &str = "Your previous context window was archived. The user \
messages above are your task trail — the last one is your current task. \
Check notes/thread_hint.md for any handoff from the previous window; \
if missing, write one after you understand the task. \
Previous conversation history is available via the history tools — consult them \
only if you need context the trail doesn't provide. \
Do NOT re-read files you already covered. Continue the task directly.";
impl TokenBudgetConfig {
pub fn with_work_budget(work_budget: usize) -> Self {
Self {
work_budget,
reminder_threshold: work_budget / 5,
fallback_buffer: work_budget / 10,
..Default::default()
}
}
pub fn hard_limit(&self, base_overhead: usize) -> usize {
base_overhead + self.work_budget + self.fallback_buffer
}
}
pub fn build_context_window_info(
previous_window_id: usize,
current_window_id: usize,
) -> ChatMessage {
ChatMessage::system(format!(
"<context_window>\n\
Previous context window id: {:03}\n\
Current context window id: {:03}\n\
</context_window>",
previous_window_id, current_window_id
))
}
pub fn build_reminder_message(config: &TokenBudgetConfig, remaining: usize) -> ChatMessage {
let content = config
.reminder_template
.replace("{n_remaining}", &remaining.to_string());
ChatMessage::system_ephemeral(content)
}
pub fn build_fallback_message(config: &TokenBudgetConfig) -> ChatMessage {
ChatMessage::system(config.fallback_prompt.clone())
}
pub fn token_budget_base_overhead(
config: &TokenBudgetConfig,
system_prompt: Option<&str>,
) -> usize {
let mut base = Vec::new();
if let Some(sp) = system_prompt {
base.push(ChatMessage::system(sp.to_string()));
}
base.push(build_context_window_info(1, 2));
base.push(ChatMessage::system(config.guidance_message.clone()));
base.push(ChatMessage::user(config.seed_message.clone()));
estimate_messages_tokens(&base)
}
pub struct TokenBudgetState {
pub window_id: std::sync::atomic::AtomicUsize,
pub reminder_sent: std::sync::atomic::AtomicBool,
breathless_resets: std::sync::atomic::AtomicUsize,
had_breath: std::sync::atomic::AtomicBool,
braked: std::sync::atomic::AtomicBool,
}
impl TokenBudgetState {
pub fn new() -> Self {
Self {
window_id: std::sync::atomic::AtomicUsize::new(1),
reminder_sent: std::sync::atomic::AtomicBool::new(false),
breathless_resets: std::sync::atomic::AtomicUsize::new(0),
had_breath: std::sync::atomic::AtomicBool::new(false),
braked: std::sync::atomic::AtomicBool::new(false),
}
}
pub fn advance_window(&self) -> usize {
self.reminder_sent
.store(false, std::sync::atomic::Ordering::Relaxed);
self.had_breath
.store(false, std::sync::atomic::Ordering::Relaxed);
self.window_id
.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
+ 1
}
pub fn current_window_id(&self) -> usize {
self.window_id.load(std::sync::atomic::Ordering::Relaxed)
}
pub fn has_sent_reminder(&self) -> bool {
self.reminder_sent
.load(std::sync::atomic::Ordering::Relaxed)
}
pub fn mark_reminder_sent(&self) {
self.reminder_sent
.store(true, std::sync::atomic::Ordering::Relaxed);
}
pub fn mark_breath(&self) {
self.had_breath
.store(true, std::sync::atomic::Ordering::Relaxed);
}
pub fn had_breath(&self) -> bool {
self.had_breath.load(std::sync::atomic::Ordering::Relaxed)
}
pub fn record_breathless_reset(&self) -> usize {
self.breathless_resets
.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
+ 1
}
pub fn clear_breathless(&self) {
self.breathless_resets
.store(0, std::sync::atomic::Ordering::Relaxed);
}
pub fn engage_brake(&self) {
self.braked
.store(true, std::sync::atomic::Ordering::Relaxed);
}
pub fn braked(&self) -> bool {
self.braked.load(std::sync::atomic::Ordering::Relaxed)
}
}
impl Default for TokenBudgetState {
fn default() -> Self {
Self::new()
}
}