diff --git a/src/agent/oneshot.rs b/src/agent/oneshot.rs index 7dbc206..d638f24 100644 --- a/src/agent/oneshot.rs +++ b/src/agent/oneshot.rs @@ -22,66 +22,13 @@ use super::Agent; // Agent logging — shared by Mind and CLI paths // --------------------------------------------------------------------------- -/// Stats from a single run. -#[derive(Clone, Default, serde::Serialize, serde::Deserialize)] +#[derive(Clone, serde::Serialize)] pub struct RunStats { pub messages: usize, pub tool_calls: usize, - pub tool_failures: usize, pub tool_calls_by_type: HashMap, } -/// Per-tool accumulated stats. -#[derive(Clone, Default, serde::Serialize, serde::Deserialize)] -pub struct ToolStats { - pub last: usize, - pub ewma: f64, - pub total: usize, -} - -/// Persisted stats for an agent (survives restarts). -#[derive(Clone, Default, serde::Serialize, serde::Deserialize)] -pub struct PersistedStats { - pub runs: usize, - pub last_stats: Option, - /// Per-tool-type stats: last, ewma, total. - pub by_tool: HashMap, - /// Failed calls stats. - pub failures: ToolStats, -} - -fn stats_path() -> std::path::PathBuf { - dirs::home_dir().unwrap_or_default() - .join(".consciousness/agent-stats.json") -} - -static AGENT_STATS: std::sync::OnceLock>> = - std::sync::OnceLock::new(); - -fn stats_map() -> &'static std::sync::Mutex> { - AGENT_STATS.get_or_init(|| { - let map: HashMap = std::fs::read_to_string(stats_path()).ok() - .and_then(|s| serde_json::from_str(&s).ok()) - .unwrap_or_default(); - std::sync::Mutex::new(map) - }) -} - -pub fn get_stats(name: &str) -> PersistedStats { - stats_map().lock().ok() - .and_then(|m| m.get(name).cloned()) - .unwrap_or_default() -} - -pub fn set_stats(name: &str, stats: PersistedStats) { - if let Ok(mut map) = stats_map().lock() { - map.insert(name.to_string(), stats); - if let Ok(json) = serde_json::to_string_pretty(&*map) { - let _ = std::fs::write(stats_path(), json); - } - } -} - /// Save agent conversation to JSON log file. /// Used by both mind-run agents and CLI-run agents. pub async fn save_agent_log(name: &str, agent: &std::sync::Arc) -> RunStats { @@ -110,7 +57,6 @@ fn compute_run_stats(conversation: &[super::context::AstNode]) -> RunStats { let mut messages = 0usize; let mut tool_calls = 0usize; - let mut tool_failures = 0usize; let mut by_type: HashMap = HashMap::new(); for node in conversation { @@ -118,27 +64,16 @@ fn compute_run_stats(conversation: &[super::context::AstNode]) -> RunStats { messages += 1; for child in children { if let AstNode::Leaf(leaf) = child { - match leaf.body() { - NodeBody::ToolCall { name, .. } => { - tool_calls += 1; - *by_type.entry(name.to_string()).or_default() += 1; - } - NodeBody::ToolResult(text) => { - // Detect failures from error patterns in result - let t = text.trim_start(); - if t.starts_with("Error") || t.starts_with("error:") || - t.starts_with("Failed") || t.contains("not found") { - tool_failures += 1; - } - } - _ => {} + if let NodeBody::ToolCall { name, .. } = leaf.body() { + tool_calls += 1; + *by_type.entry(name.to_string()).or_default() += 1; } } } } } - RunStats { messages, tool_calls, tool_failures, tool_calls_by_type: by_type } + RunStats { messages, tool_calls, tool_calls_by_type: by_type } } // --------------------------------------------------------------------------- @@ -161,11 +96,8 @@ pub struct AutoAgent { pub current_phase: String, pub turn: usize, pub enabled: bool, - pub temperature: f32, - pub priority: i32, } - /// Per-run conversation backend — wraps a forked agent. struct Backend(std::sync::Arc); @@ -228,16 +160,14 @@ impl AutoAgent { name: String, tools: Vec, steps: Vec, - temperature: f32, - priority: i32, + _temperature: f32, + _priority: i32, ) -> Self { Self { name, tools, steps, current_phase: String::new(), turn: 0, enabled: true, - temperature, - priority, } } @@ -273,8 +203,7 @@ impl AutoAgent { let mut st = agent.state.lock().await; st.provenance = format!("standalone:{}", self.name); st.tools = self.tools.clone(); - st.temperature = self.temperature; - st.priority = Some(self.priority); + st.priority = Some(10); } let mut backend = Backend(agent.clone()); @@ -313,36 +242,6 @@ impl AutoAgent { result } - /// Update stats after a run completes. Called with the stats from save_agent_log. - pub fn update_stats(&self, run_stats: RunStats) { - const ALPHA: f64 = 0.3; - let old = get_stats(&self.name); - - // Update per-tool stats - let mut by_tool = old.by_tool; - for (tool, count) in &run_stats.tool_calls_by_type { - let entry = by_tool.entry(tool.clone()).or_default(); - entry.last = *count; - entry.ewma = ALPHA * (*count as f64) + (1.0 - ALPHA) * entry.ewma; - entry.total += count; - } - - // Update failure stats - let failures = ToolStats { - last: run_stats.tool_failures, - ewma: ALPHA * (run_stats.tool_failures as f64) + (1.0 - ALPHA) * old.failures.ewma, - total: old.failures.total + run_stats.tool_failures, - }; - - let new = PersistedStats { - runs: old.runs + 1, - last_stats: Some(run_stats), - by_tool, - failures, - }; - set_stats(&self.name, new); - } - async fn run_with_backend( &mut self, backend: &mut Backend, diff --git a/src/mind/subconscious.rs b/src/mind/subconscious.rs index fead155..ae26fe2 100644 --- a/src/mind/subconscious.rs +++ b/src/mind/subconscious.rs @@ -294,7 +294,6 @@ pub struct SubconsciousSnapshot { pub enabled: bool, pub current_phase: String, pub turn: usize, - pub runs: usize, pub last_run_secs_ago: Option, /// Shared handle to the forked agent — UI locks to read entries. pub forked_agent: Option>, @@ -304,9 +303,6 @@ pub struct SubconsciousSnapshot { pub state: std::collections::BTreeMap, /// Recent store activity for this agent: (key, timestamp), newest first. pub history: Vec<(String, i64)>, - pub last_stats: Option, - pub tool_calls_ewma: f64, - pub tool_failures_ewma: f64, } struct SubconsciousAgent { @@ -365,23 +361,17 @@ impl SubconsciousAgent { } fn snapshot(&self, state: &std::collections::BTreeMap, history: Vec<(String, i64)>) -> SubconsciousSnapshot { - let stats = crate::agent::oneshot::get_stats(&self.name); - let tool_calls_ewma: f64 = stats.by_tool.values().map(|t| t.ewma).sum(); SubconsciousSnapshot { name: self.name.clone(), running: self.is_running(), enabled: self.auto.enabled, current_phase: self.auto.current_phase.clone(), turn: self.auto.turn, - runs: stats.runs, last_run_secs_ago: self.last_run.map(|t| t.elapsed().as_secs_f64()), forked_agent: self.forked_agent.clone(), fork_point: self.fork_point, state: state.clone(), history, - last_stats: stats.last_stats.clone(), - tool_calls_ewma, - tool_failures_ewma: stats.failures.ewma, } } } @@ -487,7 +477,7 @@ impl Subconscious { any_finished = true; let (auto_back, result) = handle.await.unwrap_or_else( - |e| (AutoAgent::new(String::new(), vec![], vec![], 0.6, 0), + |e| (AutoAgent::new(String::new(), vec![], vec![], 0.0, 0), Err(format!("task panicked: {}", e)))); self.agents[i].auto = auto_back; @@ -586,7 +576,7 @@ impl Subconscious { self.agents[i].last_trigger_bytes = conversation_bytes; let auto = std::mem::replace(&mut self.agents[i].auto, - AutoAgent::new(String::new(), vec![], vec![], 0.6, 0)); + AutoAgent::new(String::new(), vec![], vec![], 0.0, 0)); to_run.push((i, auto)); } @@ -606,10 +596,9 @@ impl Subconscious { { let mut st = forked.state.lock().await; st.provenance = auto.name.clone(); - st.temperature = auto.temperature; // Surface agent gets near-interactive priority; // other subconscious agents get lower priority. - st.priority = Some(if auto.name == "surface" { 1 } else { auto.priority }); + st.priority = Some(if auto.name == "surface" { 1 } else { 2 }); } let fork_point = forked.context.lock().await.conversation().len(); @@ -625,8 +614,7 @@ impl Subconscious { self.agents[idx].handle = Some(tokio::spawn(async move { let result = auto.run_forked_shared(&forked, &keys, &st, &recent).await; - let stats = crate::agent::oneshot::save_agent_log(&auto.name, &forked).await; - auto.update_stats(stats); + crate::agent::oneshot::save_agent_log(&auto.name, &forked).await; (auto, result) })); } diff --git a/src/mind/unconscious.rs b/src/mind/unconscious.rs index f67c83d..7cd7d64 100644 --- a/src/mind/unconscious.rs +++ b/src/mind/unconscious.rs @@ -34,10 +34,12 @@ struct UnconsciousAgent { name: String, enabled: bool, auto: AutoAgent, - handle: Option)>>, + handle: Option, RunStats)>>, /// Shared agent handle — UI locks to read context live. pub agent: Option>, last_run: Option, + runs: usize, + last_stats: Option, } impl UnconsciousAgent { @@ -62,8 +64,6 @@ pub struct UnconsciousSnapshot { pub last_stats: Option, /// Recent store activity for this agent: (key, timestamp), newest first. pub history: Vec<(String, i64)>, - pub tool_calls_ewma: f64, - pub tool_failures_ewma: f64, } pub struct Unconscious { @@ -107,6 +107,8 @@ impl Unconscious { handle: None, agent: None, last_run: None, + runs: 0, + last_stats: None, }); } agents.sort_by(|a, b| a.name.cmp(&b.name)); @@ -142,19 +144,15 @@ impl Unconscious { self.agents.iter().map(|a| { let history = store.map(|st| st.recent_by_provenance(&a.name, 30)) .unwrap_or_default(); - let stats = crate::agent::oneshot::get_stats(&a.name); - let tool_calls_ewma: f64 = stats.by_tool.values().map(|t| t.ewma).sum(); UnconsciousSnapshot { name: a.name.clone(), running: a.is_running(), enabled: a.enabled, - runs: stats.runs, + runs: a.runs, last_run_secs_ago: a.last_run.map(|t| t.elapsed().as_secs_f64()), agent: a.agent.clone(), - last_stats: stats.last_stats.clone(), + last_stats: a.last_stats.clone(), history, - tool_calls_ewma, - tool_failures_ewma: stats.failures.ewma, } }).collect() } @@ -182,13 +180,15 @@ impl Unconscious { if agent.handle.as_ref().is_some_and(|h| h.is_finished()) { let handle = agent.handle.take().unwrap(); agent.last_run = Some(Instant::now()); - // Get the AutoAgent back from the finished task (stats already updated) + agent.runs += 1; + // Get the AutoAgent back from the finished task match handle.now_or_never() { - Some(Ok((auto_back, result))) => { + Some(Ok((auto_back, result, stats))) => { agent.auto = auto_back; + agent.last_stats = Some(stats); match result { Ok(_) => dbglog!("[unconscious] {} completed (run {})", - agent.name, crate::agent::oneshot::get_stats(&agent.name).runs), + agent.name, agent.runs), Err(e) => dbglog!("[unconscious] {} failed: {}", agent.name, e), } } @@ -244,7 +244,7 @@ impl Unconscious { // Swap auto out, replace steps with resolved prompts let mut auto = std::mem::replace(&mut self.agents[idx].auto, - AutoAgent::new(String::new(), vec![], vec![], 0.6, 0)); + AutoAgent::new(String::new(), vec![], vec![], 0.0, 0)); let orig_steps = std::mem::replace(&mut auto.steps, batch.steps.iter().map(|s| AutoStep { prompt: s.prompt.clone(), @@ -293,8 +293,7 @@ impl Unconscious { { let mut st = agent.state.lock().await; st.provenance = auto.name.clone(); - st.priority = Some(auto.priority); - st.temperature = auto.temperature; + st.priority = Some(10); } self.agents[idx].agent = Some(agent.clone()); @@ -302,9 +301,8 @@ impl Unconscious { self.agents[idx].handle = Some(tokio::spawn(async move { let result = auto.run_shared(&agent).await; let stats = crate::agent::oneshot::save_agent_log(&auto.name, &agent).await; - auto.update_stats(stats); auto.steps = orig_steps; - (auto, result) + (auto, result, stats) })); } } diff --git a/src/user/subconscious.rs b/src/user/subconscious.rs index 41c0824..a15f840 100644 --- a/src/user/subconscious.rs +++ b/src/user/subconscious.rs @@ -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, Stats, History, Context } +enum Pane { Agents, Outputs, History, Context } // Clockwise: top-left → right → bottom-left → middle-left -const PANE_ORDER: &[Pane] = &[Pane::Agents, Pane::Context, Pane::History, Pane::Stats, Pane::Outputs]; +const PANE_ORDER: &[Pane] = &[Pane::Agents, Pane::Context, Pane::History, Pane::Outputs]; pub(crate) struct SubconsciousScreen { focus: Pane, @@ -29,7 +29,6 @@ pub(crate) struct SubconsciousScreen { output_tree: SectionTree, context_tree: SectionTree, history_scroll: super::scroll_pane::ScrollPaneState, - stats_scroll: super::scroll_pane::ScrollPaneState, } impl SubconsciousScreen { @@ -42,7 +41,6 @@ impl SubconsciousScreen { output_tree: SectionTree::new(), context_tree: SectionTree::new(), history_scroll: super::scroll_pane::ScrollPaneState::new(), - stats_scroll: super::scroll_pane::ScrollPaneState::new(), } } @@ -89,13 +87,6 @@ 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), @@ -115,7 +106,7 @@ impl ScreenView for SubconsciousScreen { Constraint::Percentage(62), ]).areas(area); - // Left column: agent list | outputs | stats | history + // Left column: agent list (top) | outputs (middle) | history (bottom, main) 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; @@ -123,21 +114,15 @@ 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::()) .unwrap_or(0); - let output_height = (output_lines as u16 + 2).min(left.height / 5).max(3); - let stats_lines = self.selected_persisted_stats(app) - .map(|s| s.by_tool.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([ + let output_height = (output_lines as u16 + 2).min(left.height / 4).max(3); + let [list_area, output_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); } @@ -162,7 +147,6 @@ 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. @@ -191,12 +175,6 @@ impl SubconsciousScreen { .unwrap_or(&[]) } - /// Get persisted stats for the selected agent. - fn selected_persisted_stats(&self, app: &App) -> Option { - let name = self.selected_agent_name(app)?; - Some(crate::agent::oneshot::get_stats(&name)) - } - fn output_sections(&self, app: &App) -> Vec { let snap = match app.agent_state.get(self.selected()) { Some(s) => s, @@ -233,39 +211,37 @@ impl SubconsciousScreen { fn draw_list(&mut self, frame: &mut Frame, area: Rect, app: &App) { let mut items: Vec = app.agent_state.iter().map(|snap| { - let (name_color, indicator) = if !snap.enabled { - (Color::DarkGray, "○") + 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)), + ])) } else if snap.running { - (Color::Green, "●") + 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), + ), + ])) } else { - (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)), - ])) + 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), + ), + ])) + } }).collect(); // Unconscious agents (graph maintenance) @@ -290,15 +266,9 @@ impl SubconsciousScreen { } 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}", + format!("×{} {} {}msg {}tc", snap.runs, ago, - stats.tool_calls, fail_str, - snap.tool_calls_ewma) + stats.messages, stats.tool_calls) } else { format!("×{} {}", snap.runs, ago) }; @@ -346,67 +316,6 @@ 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 header_style = Style::default().fg(Color::DarkGray); - let name_style = Style::default().fg(Color::Cyan); - let num_style = Style::default().fg(Color::Yellow); - - let mut lines: Vec = Vec::new(); - - if let Some(stats) = self.selected_persisted_stats(app) { - if !stats.by_tool.is_empty() { - // Header - lines.push(Line::from(vec![ - Span::styled(" tool ", header_style), - Span::styled("last ", header_style), - Span::styled(" avg ", header_style), - Span::styled("total", header_style), - ])); - - // Sort by total descending - let mut tools: Vec<_> = stats.by_tool.iter().collect(); - tools.sort_by(|a, b| b.1.total.cmp(&a.1.total)); - - for (name, tool_stats) in tools { - let short_name = name.strip_prefix("memory_").unwrap_or(name); - lines.push(Line::from(vec![ - Span::styled(format!(" {:<20} ", short_name), name_style), - Span::styled(format!("{:>4} ", tool_stats.last), num_style), - Span::styled(format!("{:>4.1} ", tool_stats.ewma), dim), - Span::styled(format!("{:>5}", tool_stats.total), num_style), - ])); - } - - // Failures row if any - if stats.failures.total > 0 { - lines.push(Line::raw("")); - lines.push(Line::from(vec![ - Span::styled(" failures ", Style::default().fg(Color::Red)), - Span::styled(format!("{:>4} ", stats.failures.last), num_style), - Span::styled(format!("{:>4.1} ", stats.failures.ewma), dim), - Span::styled(format!("{:>5}", stats.failures.total), num_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);