Split context_state_summary: ContextBudget for compaction, UI-only for display

context_state_summary() was used for both compaction decisions (just
needs token counts) and debug screen display (needs full tree with
labels). Split into:

- Agent::context_budget() -> ContextBudget: cheap token counting by
  category, used by compact(), restore_from_log(), mind event loop
- ContextBudget::format(): replaces sections_budget_string() which
  fragily pattern-matched on section name strings
- context_state_summary(): now UI-only, formatting code stays here

Also extracted entry_sections() as shared helper with include_memories
param — false for context_state_summary (memories have own section),
true for conversation_sections_from() (subconscious screen shows all).

Co-Authored-By: Proof of Concept <poc@bcachefs.org>
This commit is contained in:
Kent Overstreet 2026-04-07 19:02:58 -04:00
parent 9e49398689
commit cf1c64f936
4 changed files with 130 additions and 86 deletions

View file

@ -336,33 +336,29 @@ impl ContextState {
}
}
/// Total tokens used across all context sections.
pub fn sections_used(sections: &[ContextSection]) -> usize {
sections.iter().map(|s| s.tokens).sum()
/// Token budget per context category — cheap to compute, no formatting.
pub struct ContextBudget {
pub system: usize,
pub identity: usize,
pub journal: usize,
pub memory: usize,
pub conversation: usize,
}
/// Budget status string derived from context sections.
pub fn sections_budget_string(sections: &[ContextSection]) -> String {
let window = context_window();
if window == 0 { return String::new(); }
let used: usize = sections.iter().map(|s| s.tokens).sum();
let free = window.saturating_sub(used);
let pct = |n: usize| if n == 0 { 0 } else { ((n * 100) / window).max(1) };
let parts: Vec<String> = sections.iter()
.map(|s| {
// Short label from section name
let label = match s.name.as_str() {
n if n.starts_with("System") => "sys",
n if n.starts_with("Personality") => "id",
n if n.starts_with("Journal") => "jnl",
n if n.starts_with("Working") => "stack",
n if n.starts_with("Memory") => "mem",
n if n.starts_with("Conversation") => "conv",
_ => return String::new(),
};
format!("{}:{}%", label, pct(s.tokens))
})
.filter(|s| !s.is_empty())
.collect();
format!("{} free:{}%", parts.join(" "), pct(free))
impl ContextBudget {
pub fn total(&self) -> usize {
self.system + self.identity + self.journal + self.memory + self.conversation
}
pub fn format(&self) -> String {
let window = context_window();
if window == 0 { return String::new(); }
let used = self.total();
let free = window.saturating_sub(used);
let pct = |n: usize| if n == 0 { 0 } else { ((n * 100) / window).max(1) };
format!("sys:{}% id:{}% jnl:{}% mem:{}% conv:{}% free:{}%",
pct(self.system), pct(self.identity), pct(self.journal),
pct(self.memory), pct(self.conversation), pct(free))
}
}