diff --git a/poc-agent/src/agent.rs b/poc-agent/src/agent.rs index 11e267d..9040491 100644 --- a/poc-agent/src/agent.rs +++ b/poc-agent/src/agent.rs @@ -57,111 +57,6 @@ struct DispatchState { /// Mutable context state — the structured regions of the context window. /// /// Each field is a different dimension of awareness. The struct renders -/// itself to text for inclusion in the context message sent to the model. -/// Tools can update individual fields mid-session. -#[derive(Debug, Clone)] -pub struct ContextState { - /// System prompt (identity, instructions, loaded from prompt file). - pub system_prompt: String, - /// Identity files: (filename, contents). Transparent structure for - /// debug inspection and per-file budget control. - pub personality: Vec<(String, String)>, - /// Journal entries rendered as text — bridges old conversation. - pub journal: String, - /// Working stack — what the agent is currently doing. - /// Top of stack (last element) is the current focus. - pub working_stack: Vec, -} - -/// Path to working stack instructions, included in context before the stack state. -const WORKING_STACK_INSTRUCTIONS: &str = "/home/kent/.config/poc-agent/working-stack.md"; - -/// Path to persisted working stack state. -const WORKING_STACK_FILE: &str = "/home/kent/.claude/memory/working-stack.json"; - -impl ContextState { - /// Render the context message for the model. Personality + working stack. - /// Journal is rendered separately as its own message in the conversation. - pub fn render_context_message(&self) -> String { - let mut parts: Vec = self.personality.iter() - .map(|(name, content)| format!("## {}\n\n{}", name, content)) - .collect(); - - // Always include working stack section — instructions + current state - let instructions = std::fs::read_to_string(WORKING_STACK_INSTRUCTIONS) - .unwrap_or_default(); - let mut stack_section = instructions; - - if self.working_stack.is_empty() { - stack_section.push_str("\n## Current stack\n\n(empty)\n"); - } else { - stack_section.push_str("\n## Current stack\n\n"); - for (i, item) in self.working_stack.iter().enumerate() { - if i == self.working_stack.len() - 1 { - stack_section.push_str(&format!("→ {}\n", item)); - } else { - stack_section.push_str(&format!(" [{}] {}\n", i, item)); - } - } - } - parts.push(stack_section); - - parts.join("\n\n---\n\n") - } -} - -/// Breakdown of context window usage by category, in tokens. -/// -/// Categories: -/// id — static identity context (system prompt + CLAUDE.md + memory files) -/// mem — dynamically recalled content from poc-memory (future) -/// jnl — journal entries bridging old conversation -/// conv — raw recent conversation messages -/// free — unused context window (headroom before compaction) -/// -/// Token estimates are derived from char proportions scaled by the -/// API-reported prompt_tokens count. Before the first API call, uses -/// chars/4 as a rough approximation. -#[derive(Debug, Clone, Default)] -pub struct ContextBudget { - pub identity_tokens: usize, - pub memory_tokens: usize, - pub journal_tokens: usize, - pub conversation_tokens: usize, - /// Model's context window size in tokens. - pub window_tokens: usize, -} - -impl ContextBudget { - pub fn used(&self) -> usize { - self.identity_tokens + self.memory_tokens + self.journal_tokens + self.conversation_tokens - } - - pub fn free(&self) -> usize { - self.window_tokens.saturating_sub(self.used()) - } - - /// Format as a compact status string with percentages of the token window. - /// Non-zero values always show at least 1%. - pub fn status_string(&self) -> String { - let total = self.window_tokens; - if total == 0 { - return String::new(); - } - let pct = |n: usize| { - if n == 0 { return 0; } - ((n * 100) / total).max(1) - }; - format!( - "id:{}% mem:{}% jnl:{}% conv:{}% free:{}%", - pct(self.identity_tokens), - pct(self.memory_tokens), - pct(self.journal_tokens), - pct(self.conversation_tokens), - pct(self.free()), - ) - } -} pub struct Agent { client: ApiClient, diff --git a/poc-agent/src/types.rs b/poc-agent/src/types.rs index 94725e5..8995f0f 100644 --- a/poc-agent/src/types.rs +++ b/poc-agent/src/types.rs @@ -316,3 +316,65 @@ impl ToolDef { } } } + +/// Mutable context state — the structured regions of the context window. +#[derive(Debug, Clone)] +pub struct ContextState { + pub system_prompt: String, + pub personality: Vec<(String, String)>, + pub journal: String, + pub working_stack: Vec, +} + +pub const WORKING_STACK_INSTRUCTIONS: &str = "/home/kent/.config/poc-agent/working-stack.md"; +pub const WORKING_STACK_FILE: &str = "/home/kent/.claude/memory/working-stack.json"; + +impl ContextState { + pub fn render_context_message(&self) -> String { + let mut parts: Vec = self.personality.iter() + .map(|(name, content)| format!("## {}\n\n{}", name, content)) + .collect(); + let instructions = std::fs::read_to_string(WORKING_STACK_INSTRUCTIONS).unwrap_or_default(); + let mut stack_section = instructions; + if self.working_stack.is_empty() { + stack_section.push_str("\n## Current stack\n\n(empty)\n"); + } else { + stack_section.push_str("\n## Current stack\n\n"); + for (i, item) in self.working_stack.iter().enumerate() { + if i == self.working_stack.len() - 1 { + stack_section.push_str(&format!("→ {}\n", item)); + } else { + stack_section.push_str(&format!(" [{}] {}\n", i, item)); + } + } + } + parts.push(stack_section); + parts.join("\n\n---\n\n") + } +} + +#[derive(Debug, Clone, Default)] +pub struct ContextBudget { + pub identity_tokens: usize, + pub memory_tokens: usize, + pub journal_tokens: usize, + pub conversation_tokens: usize, + pub window_tokens: usize, +} + +impl ContextBudget { + pub fn used(&self) -> usize { + self.identity_tokens + self.memory_tokens + self.journal_tokens + self.conversation_tokens + } + pub fn free(&self) -> usize { + self.window_tokens.saturating_sub(self.used()) + } + pub fn status_string(&self) -> String { + let total = self.window_tokens; + if total == 0 { return String::new(); } + let pct = |n: usize| if n == 0 { 0 } else { ((n * 100) / total).max(1) }; + format!("id:{}% mem:{}% jnl:{}% conv:{}% free:{}%", + pct(self.identity_tokens), pct(self.memory_tokens), + pct(self.journal_tokens), pct(self.conversation_tokens), pct(self.free())) + } +}