WIP: ContextEntry/ContextSection data structures for incremental token counting
New types — not yet wired to callers: - ContextEntry: wraps ConversationEntry with cached token count and timestamp - ContextSection: named group of entries with cached token total. Private entries/tokens, read via entries()/tokens(). Mutation via push(entry), set(index, entry), del(index). - ContextState: system/identity/journal/conversation sections + working_stack - ConversationEntry::System variant for system prompt entries Token counting happens once at push time. Sections maintain their totals incrementally via push/set/del. No more recomputing from scratch on every budget check. Does not compile — callers need updating. Co-Authored-By: Proof of Concept <poc@bcachefs.org>
This commit is contained in:
parent
776ac527f1
commit
62996e27d7
10 changed files with 450 additions and 403 deletions
|
|
@ -376,7 +376,7 @@ pub(crate) struct InteractScreen {
|
|||
call_timeout_secs: u64,
|
||||
// State sync with agent — double buffer
|
||||
last_generation: u64,
|
||||
last_entries: Vec<crate::agent::context::ConversationEntry>,
|
||||
last_entries: Vec<crate::agent::context::ContextEntry>,
|
||||
pending_display_count: usize,
|
||||
/// Reference to agent for state sync
|
||||
agent: std::sync::Arc<tokio::sync::Mutex<crate::agent::Agent>>,
|
||||
|
|
@ -482,21 +482,21 @@ impl InteractScreen {
|
|||
|
||||
for i in (0..self.last_entries.len()).rev() {
|
||||
// Check if this entry is out of bounds or doesn't match
|
||||
let matches = i < entries.len() && self.last_entries[i] == entries[i];
|
||||
let matches = i < entries.len() && self.last_entries[i].entry == entries[i].entry;
|
||||
|
||||
if !matches {
|
||||
pop = i;
|
||||
}
|
||||
|
||||
// Only stop at assistant if it matches - otherwise keep going
|
||||
if matches && self.last_entries[i].message().role == Role::Assistant {
|
||||
if matches && self.last_entries[i].entry.message().role == Role::Assistant {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
while self.last_entries.len() > pop {
|
||||
let popped = self.last_entries.pop().unwrap();
|
||||
for (target, _, _) in Self::route_entry(&popped) {
|
||||
for (target, _, _) in Self::route_entry(&popped.entry) {
|
||||
match target {
|
||||
PaneTarget::Conversation | PaneTarget::ConversationAssistant
|
||||
=> self.conversation.pop_line(),
|
||||
|
|
@ -510,7 +510,7 @@ impl InteractScreen {
|
|||
// Phase 2: push new entries
|
||||
let start = self.last_entries.len();
|
||||
for entry in entries.iter().skip(start) {
|
||||
for (target, text, marker) in Self::route_entry(entry) {
|
||||
for (target, text, marker) in Self::route_entry(&entry.entry) {
|
||||
match target {
|
||||
PaneTarget::Conversation => {
|
||||
self.conversation.current_color = Color::Cyan;
|
||||
|
|
|
|||
|
|
@ -23,9 +23,9 @@ impl ConsciousScreen {
|
|||
Self { agent, tree: SectionTree::new() }
|
||||
}
|
||||
|
||||
fn read_context_state(&self) -> Vec<crate::agent::context::ContextSection> {
|
||||
fn read_context_sections(&self) -> Vec<crate::agent::context::ContextSection> {
|
||||
match self.agent.try_lock() {
|
||||
Ok(ag) => ag.context_state_summary(),
|
||||
Ok(ag) => ag.context_sections().iter().map(|s| (*s).clone()).collect(),
|
||||
Err(_) => Vec::new(),
|
||||
}
|
||||
}
|
||||
|
|
@ -39,7 +39,7 @@ impl ScreenView for ConsciousScreen {
|
|||
for event in events {
|
||||
if let ratatui::crossterm::event::Event::Key(key) = event {
|
||||
if key.kind != ratatui::crossterm::event::KeyEventKind::Press { continue; }
|
||||
let context_state = self.read_context_state();
|
||||
let context_state = self.read_context_sections();
|
||||
self.tree.handle_nav(key.code, &context_state, area.height);
|
||||
}
|
||||
}
|
||||
|
|
@ -64,9 +64,9 @@ impl ScreenView for ConsciousScreen {
|
|||
if !app.status.context_budget.is_empty() {
|
||||
lines.push(Line::raw(format!(" Budget: {}", app.status.context_budget)));
|
||||
}
|
||||
let context_state = self.read_context_state();
|
||||
let context_state = self.read_context_sections();
|
||||
if !context_state.is_empty() {
|
||||
let total: usize = context_state.iter().map(|s| s.tokens).sum();
|
||||
let total: usize = context_state.iter().map(|s| s.tokens()).sum();
|
||||
lines.push(Line::raw(""));
|
||||
lines.push(Line::styled(
|
||||
" (↑/↓ select, →/Enter expand, ← collapse, PgUp/PgDn scroll)",
|
||||
|
|
|
|||
|
|
@ -16,7 +16,8 @@ use ratatui::{
|
|||
|
||||
use super::{App, ScreenView, screen_legend};
|
||||
use super::widgets::{SectionTree, pane_block_focused, render_scrollable, tree_legend, format_age, format_ts_age};
|
||||
use crate::agent::context::ContextSection;
|
||||
use crate::agent::context::{ContextSection, ContextEntry, ConversationEntry};
|
||||
use crate::agent::api::Message;
|
||||
|
||||
#[derive(Clone, Copy, PartialEq)]
|
||||
enum Pane { Agents, Outputs, History, Context }
|
||||
|
|
@ -135,12 +136,13 @@ impl SubconsciousScreen {
|
|||
None => return Vec::new(),
|
||||
};
|
||||
snap.state.iter().map(|(key, val)| {
|
||||
ContextSection {
|
||||
name: key.clone(),
|
||||
let mut section = ContextSection::new(key.clone());
|
||||
section.push(ContextEntry {
|
||||
entry: ConversationEntry::Message(Message::user(val)),
|
||||
tokens: 0,
|
||||
content: val.clone(),
|
||||
children: Vec::new(),
|
||||
}
|
||||
timestamp: None,
|
||||
});
|
||||
section
|
||||
}).collect()
|
||||
}
|
||||
|
||||
|
|
@ -151,7 +153,15 @@ impl SubconsciousScreen {
|
|||
};
|
||||
snap.forked_agent.as_ref()
|
||||
.and_then(|agent| agent.try_lock().ok())
|
||||
.map(|ag| ag.conversation_sections_from(snap.fork_point))
|
||||
.map(|ag| {
|
||||
// Build a single section from the forked conversation entries
|
||||
let entries = ag.conversation_entries_from(snap.fork_point);
|
||||
let mut section = ContextSection::new("Conversation");
|
||||
for e in entries {
|
||||
section.push(e.clone());
|
||||
}
|
||||
vec![section]
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
|
|
@ -172,7 +182,7 @@ impl SubconsciousScreen {
|
|||
.unwrap_or_else(|| "—".to_string());
|
||||
let entries = snap.forked_agent.as_ref()
|
||||
.and_then(|a| a.try_lock().ok())
|
||||
.map(|ag| ag.context.entries.len().saturating_sub(snap.fork_point))
|
||||
.map(|ag| ag.context.conversation.len().saturating_sub(snap.fork_point))
|
||||
.unwrap_or(0);
|
||||
ListItem::from(Line::from(vec![
|
||||
Span::styled(&snap.name, Style::default().fg(Color::Gray)),
|
||||
|
|
|
|||
|
|
@ -99,27 +99,24 @@ impl SectionTree {
|
|||
}
|
||||
|
||||
/// Total nodes in the tree (regardless of expand state).
|
||||
/// Each section is 1 node, each entry within is 1 node.
|
||||
fn total_nodes(&self, sections: &[ContextSection]) -> usize {
|
||||
fn count_all(section: &ContextSection) -> usize {
|
||||
1 + section.children.iter().map(|c| count_all(c)).sum::<usize>()
|
||||
}
|
||||
sections.iter().map(|s| count_all(s)).sum()
|
||||
sections.iter().map(|s| 1 + s.entries().len()).sum()
|
||||
}
|
||||
|
||||
pub fn item_count(&self, sections: &[ContextSection]) -> usize {
|
||||
fn count(section: &ContextSection, expanded: &std::collections::HashSet<usize>, idx: &mut usize) -> usize {
|
||||
let my_idx = *idx;
|
||||
*idx += 1;
|
||||
let mut total = 1;
|
||||
if expanded.contains(&my_idx) {
|
||||
for child in §ion.children {
|
||||
total += count(child, expanded, idx);
|
||||
}
|
||||
}
|
||||
total
|
||||
}
|
||||
let mut idx = 0;
|
||||
sections.iter().map(|s| count(s, &self.expanded, &mut idx)).sum()
|
||||
let mut total = 0;
|
||||
for section in sections {
|
||||
let my_idx = idx;
|
||||
idx += 1;
|
||||
total += 1;
|
||||
if self.expanded.contains(&my_idx) {
|
||||
total += section.entries().len();
|
||||
idx += section.entries().len();
|
||||
}
|
||||
}
|
||||
total
|
||||
}
|
||||
|
||||
pub fn handle_nav(&mut self, code: KeyCode, sections: &[ContextSection], height: u16) {
|
||||
|
|
@ -193,27 +190,24 @@ impl SectionTree {
|
|||
pub fn render_sections(&self, sections: &[ContextSection], lines: &mut Vec<Line>) {
|
||||
let mut idx = 0;
|
||||
for section in sections {
|
||||
self.render_one(section, 0, lines, &mut idx);
|
||||
self.render_one(section, lines, &mut idx);
|
||||
}
|
||||
}
|
||||
|
||||
fn render_one(
|
||||
&self,
|
||||
section: &ContextSection,
|
||||
depth: usize,
|
||||
lines: &mut Vec<Line>,
|
||||
idx: &mut usize,
|
||||
) {
|
||||
let my_idx = *idx;
|
||||
let selected = self.selected == Some(my_idx);
|
||||
let expanded = self.expanded.contains(&my_idx);
|
||||
let has_children = !section.children.is_empty();
|
||||
let has_content = !section.content.is_empty();
|
||||
let expandable = has_children || has_content;
|
||||
let expandable = !section.is_empty();
|
||||
|
||||
let indent = " ".repeat(depth + 1);
|
||||
let marker = if !expandable { " " } else if expanded { "▼" } else { "▶" };
|
||||
let label = format!("{}{} {:30} {:>6} tokens", indent, marker, section.name, section.tokens);
|
||||
let label = format!(" {} {:30} {:>6} tokens",
|
||||
marker, format!("{} ({})", section.name, section.len()), section.tokens());
|
||||
let style = if selected {
|
||||
Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD)
|
||||
} else {
|
||||
|
|
@ -223,25 +217,44 @@ impl SectionTree {
|
|||
*idx += 1;
|
||||
|
||||
if expanded {
|
||||
if has_children {
|
||||
for child in §ion.children {
|
||||
self.render_one(child, depth + 1, lines, idx);
|
||||
}
|
||||
} else if has_content {
|
||||
let content_indent = format!("{} │ ", " ".repeat(depth + 1));
|
||||
let content_lines: Vec<&str> = section.content.lines().collect();
|
||||
let show = content_lines.len().min(50);
|
||||
for line in &content_lines[..show] {
|
||||
lines.push(Line::styled(
|
||||
format!("{}{}", content_indent, line),
|
||||
Style::default().fg(Color::DarkGray),
|
||||
));
|
||||
}
|
||||
if content_lines.len() > 50 {
|
||||
lines.push(Line::styled(
|
||||
format!("{}... ({} more lines)", content_indent, content_lines.len() - 50),
|
||||
Style::default().fg(Color::DarkGray),
|
||||
));
|
||||
for ce in section.entries() {
|
||||
let entry_selected = self.selected == Some(*idx);
|
||||
let entry_expanded = self.expanded.contains(idx);
|
||||
let text = ce.entry.message().content_text();
|
||||
let preview: String = text.chars().take(60).collect();
|
||||
let preview = preview.replace('\n', " ");
|
||||
let label = if preview.len() < text.len() {
|
||||
format!(" {}...", preview)
|
||||
} else {
|
||||
format!(" {}", preview)
|
||||
};
|
||||
let entry_marker = if text.len() > 60 {
|
||||
if entry_expanded { "▼" } else { "▶" }
|
||||
} else { " " };
|
||||
let entry_label = format!(" {} {:>6} {}", entry_marker, ce.tokens, label);
|
||||
let style = if entry_selected {
|
||||
Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD)
|
||||
} else {
|
||||
Style::default().fg(Color::DarkGray)
|
||||
};
|
||||
lines.push(Line::styled(entry_label, style));
|
||||
*idx += 1;
|
||||
|
||||
if entry_expanded {
|
||||
let content_lines: Vec<&str> = text.lines().collect();
|
||||
let show = content_lines.len().min(50);
|
||||
for line in &content_lines[..show] {
|
||||
lines.push(Line::styled(
|
||||
format!(" │ {}", line),
|
||||
Style::default().fg(Color::DarkGray),
|
||||
));
|
||||
}
|
||||
if content_lines.len() > 50 {
|
||||
lines.push(Line::styled(
|
||||
format!(" ... ({} more lines)", content_lines.len() - 50),
|
||||
Style::default().fg(Color::DarkGray),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue