forked from kent/consciousness
agent stats: track tool calls by type with EWMA, add Stats pane
- RunStats now includes tool_calls_by_type HashMap - AutoAgent tracks runs, last_stats, and EWMA for tool calls/failures - Removed duplicate stats fields from individual agent structs - Fixed provenance to use bare agent name (no "agent:" prefix) - Subconscious screen now displays both agent types consistently - Added Stats pane showing tool call breakdown sorted by count Co-Authored-By: Proof of Concept <poc@bcachefs.org>
This commit is contained in:
parent
e9e7458013
commit
314ae9c4cb
4 changed files with 169 additions and 55 deletions
|
|
@ -18,10 +18,10 @@ use super::{App, ScreenView, screen_legend};
|
|||
use super::widgets::{SectionTree, SectionView, section_to_view, pane_block_focused, tree_legend, format_age, format_ts_age};
|
||||
|
||||
#[derive(Clone, Copy, PartialEq)]
|
||||
enum Pane { Agents, Outputs, History, Context }
|
||||
enum Pane { Agents, Outputs, Stats, History, Context }
|
||||
|
||||
// Clockwise: top-left → right → bottom-left → middle-left
|
||||
const PANE_ORDER: &[Pane] = &[Pane::Agents, Pane::Context, Pane::History, Pane::Outputs];
|
||||
const PANE_ORDER: &[Pane] = &[Pane::Agents, Pane::Context, Pane::History, Pane::Stats, Pane::Outputs];
|
||||
|
||||
pub(crate) struct SubconsciousScreen {
|
||||
focus: Pane,
|
||||
|
|
@ -29,6 +29,7 @@ pub(crate) struct SubconsciousScreen {
|
|||
output_tree: SectionTree,
|
||||
context_tree: SectionTree,
|
||||
history_scroll: super::scroll_pane::ScrollPaneState,
|
||||
stats_scroll: super::scroll_pane::ScrollPaneState,
|
||||
}
|
||||
|
||||
impl SubconsciousScreen {
|
||||
|
|
@ -41,6 +42,7 @@ impl SubconsciousScreen {
|
|||
output_tree: SectionTree::new(),
|
||||
context_tree: SectionTree::new(),
|
||||
history_scroll: super::scroll_pane::ScrollPaneState::new(),
|
||||
stats_scroll: super::scroll_pane::ScrollPaneState::new(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -87,6 +89,13 @@ impl ScreenView for SubconsciousScreen {
|
|||
_ => {}
|
||||
}
|
||||
Pane::Outputs => self.output_tree.handle_nav(code, &output_sections, area.height),
|
||||
Pane::Stats => match code {
|
||||
KeyCode::Up => self.stats_scroll.scroll_up(3),
|
||||
KeyCode::Down => self.stats_scroll.scroll_down(3),
|
||||
KeyCode::PageUp => self.stats_scroll.scroll_up(20),
|
||||
KeyCode::PageDown => self.stats_scroll.scroll_down(20),
|
||||
_ => {}
|
||||
}
|
||||
Pane::History => match code {
|
||||
KeyCode::Up => self.history_scroll.scroll_up(3),
|
||||
KeyCode::Down => self.history_scroll.scroll_down(3),
|
||||
|
|
@ -106,7 +115,7 @@ impl ScreenView for SubconsciousScreen {
|
|||
Constraint::Percentage(62),
|
||||
]).areas(area);
|
||||
|
||||
// Left column: agent list (top) | outputs (middle) | history (bottom, main)
|
||||
// Left column: agent list | outputs | stats | history
|
||||
let unc_count = if app.unconscious_state.is_empty() { 0 }
|
||||
else { app.unconscious_state.len() + 1 }; // +1 for separator
|
||||
let agent_count = (app.agent_state.len() + unc_count).max(1) as u16;
|
||||
|
|
@ -114,15 +123,21 @@ impl ScreenView for SubconsciousScreen {
|
|||
let output_lines = app.agent_state.get(self.selected())
|
||||
.map(|s| s.state.values().map(|v| v.lines().count() + 1).sum::<usize>())
|
||||
.unwrap_or(0);
|
||||
let output_height = (output_lines as u16 + 2).min(left.height / 4).max(3);
|
||||
let [list_area, output_area, history_area] = Layout::vertical([
|
||||
let output_height = (output_lines as u16 + 2).min(left.height / 5).max(3);
|
||||
let stats_lines = self.selected_stats(app)
|
||||
.map(|s| s.tool_calls_by_type.len())
|
||||
.unwrap_or(0);
|
||||
let stats_height = (stats_lines as u16 + 2).min(left.height / 5).max(3);
|
||||
let [list_area, output_area, stats_area, history_area] = Layout::vertical([
|
||||
Constraint::Length(list_height),
|
||||
Constraint::Length(output_height),
|
||||
Constraint::Length(stats_height),
|
||||
Constraint::Min(5),
|
||||
]).areas(left);
|
||||
|
||||
self.draw_list(frame, list_area, app);
|
||||
self.draw_outputs(frame, output_area, app);
|
||||
self.draw_stats(frame, stats_area, app);
|
||||
self.draw_history(frame, history_area, app);
|
||||
self.draw_context(frame, right, &context_sections, app);
|
||||
}
|
||||
|
|
@ -147,6 +162,7 @@ impl SubconsciousScreen {
|
|||
self.output_tree = SectionTree::new();
|
||||
self.context_tree = SectionTree::new();
|
||||
self.history_scroll = super::scroll_pane::ScrollPaneState::new();
|
||||
self.stats_scroll = super::scroll_pane::ScrollPaneState::new();
|
||||
}
|
||||
|
||||
/// Get the agent Arc for the selected item, whether subconscious or unconscious.
|
||||
|
|
@ -175,6 +191,19 @@ impl SubconsciousScreen {
|
|||
.unwrap_or(&[])
|
||||
}
|
||||
|
||||
/// Get last run stats for the selected agent.
|
||||
fn selected_stats<'a>(&self, app: &'a App) -> Option<&'a crate::agent::oneshot::RunStats> {
|
||||
let idx = self.selected();
|
||||
let sub_count = app.agent_state.len();
|
||||
if idx < sub_count {
|
||||
return app.agent_state.get(idx)
|
||||
.and_then(|s| s.last_stats.as_ref());
|
||||
}
|
||||
idx.checked_sub(sub_count + 1)
|
||||
.and_then(|i| app.unconscious_state.get(i))
|
||||
.and_then(|s| s.last_stats.as_ref())
|
||||
}
|
||||
|
||||
fn output_sections(&self, app: &App) -> Vec<SectionView> {
|
||||
let snap = match app.agent_state.get(self.selected()) {
|
||||
Some(s) => s,
|
||||
|
|
@ -211,37 +240,39 @@ impl SubconsciousScreen {
|
|||
|
||||
fn draw_list(&mut self, frame: &mut Frame, area: Rect, app: &App) {
|
||||
let mut items: Vec<ListItem> = app.agent_state.iter().map(|snap| {
|
||||
if !snap.enabled {
|
||||
ListItem::from(Line::from(vec![
|
||||
Span::styled(&snap.name, Style::default().fg(Color::DarkGray)),
|
||||
Span::styled(" ○ off", Style::default().fg(Color::DarkGray)),
|
||||
]))
|
||||
let (name_color, indicator) = if !snap.enabled {
|
||||
(Color::DarkGray, "○")
|
||||
} else if snap.running {
|
||||
ListItem::from(Line::from(vec![
|
||||
Span::styled(&snap.name, Style::default().fg(Color::Green)),
|
||||
Span::styled(" ● ", Style::default().fg(Color::Green)),
|
||||
Span::styled(
|
||||
format!("p:{} t:{}", snap.current_phase, snap.turn),
|
||||
Style::default().fg(Color::DarkGray),
|
||||
),
|
||||
]))
|
||||
(Color::Green, "●")
|
||||
} else {
|
||||
let ago = snap.last_run_secs_ago
|
||||
.map(|s| format_age(s))
|
||||
.unwrap_or_else(|| "—".to_string());
|
||||
let entries = snap.forked_agent.as_ref()
|
||||
.and_then(|a| a.context.try_lock().ok())
|
||||
.map(|ctx| ctx.conversation().len().saturating_sub(snap.fork_point))
|
||||
.unwrap_or(0);
|
||||
ListItem::from(Line::from(vec![
|
||||
Span::styled(&snap.name, Style::default().fg(Color::Gray)),
|
||||
Span::styled(" ○ ", Style::default().fg(Color::DarkGray)),
|
||||
Span::styled(
|
||||
format!("{} {}e", ago, entries),
|
||||
Style::default().fg(Color::DarkGray),
|
||||
),
|
||||
]))
|
||||
}
|
||||
(Color::Gray, "○")
|
||||
};
|
||||
let ago = snap.last_run_secs_ago
|
||||
.map(|s| format_age(s))
|
||||
.unwrap_or_else(|| "—".to_string());
|
||||
let detail = if snap.running {
|
||||
format!("p:{} t:{}", snap.current_phase, snap.turn)
|
||||
} else if !snap.enabled {
|
||||
"off".to_string()
|
||||
} else if let Some(ref stats) = snap.last_stats {
|
||||
let fail_str = if stats.tool_failures > 0 {
|
||||
format!(" {}fail", stats.tool_failures)
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
format!("×{} {} {}tc{} avg:{:.1}",
|
||||
snap.runs, ago,
|
||||
stats.tool_calls, fail_str,
|
||||
snap.tool_calls_ewma)
|
||||
} else {
|
||||
format!("×{} {}", snap.runs, ago)
|
||||
};
|
||||
ListItem::from(Line::from(vec![
|
||||
Span::styled(&snap.name, Style::default().fg(name_color)),
|
||||
Span::styled(format!(" {} ", indicator),
|
||||
Style::default().fg(if snap.running { Color::Green } else { Color::DarkGray })),
|
||||
Span::styled(detail, Style::default().fg(Color::DarkGray)),
|
||||
]))
|
||||
}).collect();
|
||||
|
||||
// Unconscious agents (graph maintenance)
|
||||
|
|
@ -266,9 +297,15 @@ impl SubconsciousScreen {
|
|||
} else if !snap.enabled {
|
||||
"off".to_string()
|
||||
} else if let Some(ref stats) = snap.last_stats {
|
||||
format!("×{} {} {}msg {}tc",
|
||||
let fail_str = if stats.tool_failures > 0 {
|
||||
format!(" {}fail", stats.tool_failures)
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
format!("×{} {} {}tc{} avg:{:.1}",
|
||||
snap.runs, ago,
|
||||
stats.messages, stats.tool_calls)
|
||||
stats.tool_calls, fail_str,
|
||||
snap.tool_calls_ewma)
|
||||
} else {
|
||||
format!("×{} {}", snap.runs, ago)
|
||||
};
|
||||
|
|
@ -316,6 +353,42 @@ impl SubconsciousScreen {
|
|||
frame.render_stateful_widget(widget, area, &mut self.output_tree.scroll);
|
||||
}
|
||||
|
||||
fn draw_stats(&mut self, frame: &mut Frame, area: Rect, app: &App) {
|
||||
let dim = Style::default().fg(Color::DarkGray);
|
||||
let name_style = Style::default().fg(Color::Cyan);
|
||||
let count_style = Style::default().fg(Color::Yellow);
|
||||
|
||||
let mut lines: Vec<Line> = Vec::new();
|
||||
|
||||
if let Some(stats) = self.selected_stats(app) {
|
||||
// Sort by count descending
|
||||
let mut tools: Vec<_> = stats.tool_calls_by_type.iter().collect();
|
||||
tools.sort_by(|a, b| b.1.cmp(a.1));
|
||||
|
||||
for (name, count) in tools {
|
||||
lines.push(Line::from(vec![
|
||||
Span::styled(format!(" {:>3} ", count), count_style),
|
||||
Span::styled(name.as_str(), name_style),
|
||||
]));
|
||||
}
|
||||
}
|
||||
|
||||
if lines.is_empty() {
|
||||
lines.push(Line::styled(" (no tool calls)", dim));
|
||||
}
|
||||
|
||||
let mut block = pane_block_focused("tool calls", self.focus == Pane::Stats);
|
||||
if self.focus == Pane::Stats {
|
||||
block = block.title_bottom(Line::styled(
|
||||
" ↑↓:scroll PgUp/Dn ",
|
||||
Style::default().fg(Color::DarkGray),
|
||||
));
|
||||
}
|
||||
let widget = super::scroll_pane::ScrollPane::new(&lines)
|
||||
.block(block);
|
||||
frame.render_stateful_widget(widget, area, &mut self.stats_scroll);
|
||||
}
|
||||
|
||||
fn draw_history(&mut self, frame: &mut Frame, area: Rect, app: &App) {
|
||||
let dim = Style::default().fg(Color::DarkGray);
|
||||
let key_style = Style::default().fg(Color::Yellow);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue