Compare commits
2 commits
e9e7458013
...
f408bb5d86
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f408bb5d86 | ||
|
|
314ae9c4cb |
4 changed files with 269 additions and 63 deletions
|
|
@ -22,13 +22,66 @@ use super::Agent;
|
||||||
// Agent logging — shared by Mind and CLI paths
|
// Agent logging — shared by Mind and CLI paths
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
#[derive(Clone, serde::Serialize)]
|
/// Stats from a single run.
|
||||||
|
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
|
||||||
pub struct RunStats {
|
pub struct RunStats {
|
||||||
pub messages: usize,
|
pub messages: usize,
|
||||||
pub tool_calls: usize,
|
pub tool_calls: usize,
|
||||||
|
pub tool_failures: usize,
|
||||||
pub tool_calls_by_type: HashMap<String, usize>,
|
pub tool_calls_by_type: HashMap<String, usize>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 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<RunStats>,
|
||||||
|
/// Per-tool-type stats: last, ewma, total.
|
||||||
|
pub by_tool: HashMap<String, ToolStats>,
|
||||||
|
/// 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::Mutex<HashMap<String, PersistedStats>>> =
|
||||||
|
std::sync::OnceLock::new();
|
||||||
|
|
||||||
|
fn stats_map() -> &'static std::sync::Mutex<HashMap<String, PersistedStats>> {
|
||||||
|
AGENT_STATS.get_or_init(|| {
|
||||||
|
let map: HashMap<String, PersistedStats> = 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.
|
/// Save agent conversation to JSON log file.
|
||||||
/// Used by both mind-run agents and CLI-run agents.
|
/// Used by both mind-run agents and CLI-run agents.
|
||||||
pub async fn save_agent_log(name: &str, agent: &std::sync::Arc<Agent>) -> RunStats {
|
pub async fn save_agent_log(name: &str, agent: &std::sync::Arc<Agent>) -> RunStats {
|
||||||
|
|
@ -57,6 +110,7 @@ fn compute_run_stats(conversation: &[super::context::AstNode]) -> RunStats {
|
||||||
|
|
||||||
let mut messages = 0usize;
|
let mut messages = 0usize;
|
||||||
let mut tool_calls = 0usize;
|
let mut tool_calls = 0usize;
|
||||||
|
let mut tool_failures = 0usize;
|
||||||
let mut by_type: HashMap<String, usize> = HashMap::new();
|
let mut by_type: HashMap<String, usize> = HashMap::new();
|
||||||
|
|
||||||
for node in conversation {
|
for node in conversation {
|
||||||
|
|
@ -64,16 +118,27 @@ fn compute_run_stats(conversation: &[super::context::AstNode]) -> RunStats {
|
||||||
messages += 1;
|
messages += 1;
|
||||||
for child in children {
|
for child in children {
|
||||||
if let AstNode::Leaf(leaf) = child {
|
if let AstNode::Leaf(leaf) = child {
|
||||||
if let NodeBody::ToolCall { name, .. } = leaf.body() {
|
match leaf.body() {
|
||||||
tool_calls += 1;
|
NodeBody::ToolCall { name, .. } => {
|
||||||
*by_type.entry(name.to_string()).or_default() += 1;
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
RunStats { messages, tool_calls, tool_calls_by_type: by_type }
|
RunStats { messages, tool_calls, tool_failures, tool_calls_by_type: by_type }
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
@ -96,8 +161,11 @@ pub struct AutoAgent {
|
||||||
pub current_phase: String,
|
pub current_phase: String,
|
||||||
pub turn: usize,
|
pub turn: usize,
|
||||||
pub enabled: bool,
|
pub enabled: bool,
|
||||||
|
pub temperature: f32,
|
||||||
|
pub priority: i32,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/// Per-run conversation backend — wraps a forked agent.
|
/// Per-run conversation backend — wraps a forked agent.
|
||||||
struct Backend(std::sync::Arc<Agent>);
|
struct Backend(std::sync::Arc<Agent>);
|
||||||
|
|
||||||
|
|
@ -160,14 +228,16 @@ impl AutoAgent {
|
||||||
name: String,
|
name: String,
|
||||||
tools: Vec<agent_tools::Tool>,
|
tools: Vec<agent_tools::Tool>,
|
||||||
steps: Vec<AutoStep>,
|
steps: Vec<AutoStep>,
|
||||||
_temperature: f32,
|
temperature: f32,
|
||||||
_priority: i32,
|
priority: i32,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
name, tools, steps,
|
name, tools, steps,
|
||||||
current_phase: String::new(),
|
current_phase: String::new(),
|
||||||
turn: 0,
|
turn: 0,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
|
temperature,
|
||||||
|
priority,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -203,7 +273,8 @@ impl AutoAgent {
|
||||||
let mut st = agent.state.lock().await;
|
let mut st = agent.state.lock().await;
|
||||||
st.provenance = format!("standalone:{}", self.name);
|
st.provenance = format!("standalone:{}", self.name);
|
||||||
st.tools = self.tools.clone();
|
st.tools = self.tools.clone();
|
||||||
st.priority = Some(10);
|
st.temperature = self.temperature;
|
||||||
|
st.priority = Some(self.priority);
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut backend = Backend(agent.clone());
|
let mut backend = Backend(agent.clone());
|
||||||
|
|
@ -242,6 +313,36 @@ impl AutoAgent {
|
||||||
result
|
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(
|
async fn run_with_backend(
|
||||||
&mut self,
|
&mut self,
|
||||||
backend: &mut Backend,
|
backend: &mut Backend,
|
||||||
|
|
|
||||||
|
|
@ -294,6 +294,7 @@ pub struct SubconsciousSnapshot {
|
||||||
pub enabled: bool,
|
pub enabled: bool,
|
||||||
pub current_phase: String,
|
pub current_phase: String,
|
||||||
pub turn: usize,
|
pub turn: usize,
|
||||||
|
pub runs: usize,
|
||||||
pub last_run_secs_ago: Option<f64>,
|
pub last_run_secs_ago: Option<f64>,
|
||||||
/// Shared handle to the forked agent — UI locks to read entries.
|
/// Shared handle to the forked agent — UI locks to read entries.
|
||||||
pub forked_agent: Option<Arc<crate::agent::Agent>>,
|
pub forked_agent: Option<Arc<crate::agent::Agent>>,
|
||||||
|
|
@ -303,6 +304,9 @@ pub struct SubconsciousSnapshot {
|
||||||
pub state: std::collections::BTreeMap<String, String>,
|
pub state: std::collections::BTreeMap<String, String>,
|
||||||
/// Recent store activity for this agent: (key, timestamp), newest first.
|
/// Recent store activity for this agent: (key, timestamp), newest first.
|
||||||
pub history: Vec<(String, i64)>,
|
pub history: Vec<(String, i64)>,
|
||||||
|
pub last_stats: Option<crate::agent::oneshot::RunStats>,
|
||||||
|
pub tool_calls_ewma: f64,
|
||||||
|
pub tool_failures_ewma: f64,
|
||||||
}
|
}
|
||||||
|
|
||||||
struct SubconsciousAgent {
|
struct SubconsciousAgent {
|
||||||
|
|
@ -361,17 +365,23 @@ impl SubconsciousAgent {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn snapshot(&self, state: &std::collections::BTreeMap<String, String>, history: Vec<(String, i64)>) -> SubconsciousSnapshot {
|
fn snapshot(&self, state: &std::collections::BTreeMap<String, String>, 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 {
|
SubconsciousSnapshot {
|
||||||
name: self.name.clone(),
|
name: self.name.clone(),
|
||||||
running: self.is_running(),
|
running: self.is_running(),
|
||||||
enabled: self.auto.enabled,
|
enabled: self.auto.enabled,
|
||||||
current_phase: self.auto.current_phase.clone(),
|
current_phase: self.auto.current_phase.clone(),
|
||||||
turn: self.auto.turn,
|
turn: self.auto.turn,
|
||||||
|
runs: stats.runs,
|
||||||
last_run_secs_ago: self.last_run.map(|t| t.elapsed().as_secs_f64()),
|
last_run_secs_ago: self.last_run.map(|t| t.elapsed().as_secs_f64()),
|
||||||
forked_agent: self.forked_agent.clone(),
|
forked_agent: self.forked_agent.clone(),
|
||||||
fork_point: self.fork_point,
|
fork_point: self.fork_point,
|
||||||
state: state.clone(),
|
state: state.clone(),
|
||||||
history,
|
history,
|
||||||
|
last_stats: stats.last_stats.clone(),
|
||||||
|
tool_calls_ewma,
|
||||||
|
tool_failures_ewma: stats.failures.ewma,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -477,7 +487,7 @@ impl Subconscious {
|
||||||
any_finished = true;
|
any_finished = true;
|
||||||
|
|
||||||
let (auto_back, result) = handle.await.unwrap_or_else(
|
let (auto_back, result) = handle.await.unwrap_or_else(
|
||||||
|e| (AutoAgent::new(String::new(), vec![], vec![], 0.0, 0),
|
|e| (AutoAgent::new(String::new(), vec![], vec![], 0.6, 0),
|
||||||
Err(format!("task panicked: {}", e))));
|
Err(format!("task panicked: {}", e))));
|
||||||
self.agents[i].auto = auto_back;
|
self.agents[i].auto = auto_back;
|
||||||
|
|
||||||
|
|
@ -576,7 +586,7 @@ impl Subconscious {
|
||||||
self.agents[i].last_trigger_bytes = conversation_bytes;
|
self.agents[i].last_trigger_bytes = conversation_bytes;
|
||||||
|
|
||||||
let auto = std::mem::replace(&mut self.agents[i].auto,
|
let auto = std::mem::replace(&mut self.agents[i].auto,
|
||||||
AutoAgent::new(String::new(), vec![], vec![], 0.0, 0));
|
AutoAgent::new(String::new(), vec![], vec![], 0.6, 0));
|
||||||
to_run.push((i, auto));
|
to_run.push((i, auto));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -596,9 +606,10 @@ impl Subconscious {
|
||||||
{
|
{
|
||||||
let mut st = forked.state.lock().await;
|
let mut st = forked.state.lock().await;
|
||||||
st.provenance = auto.name.clone();
|
st.provenance = auto.name.clone();
|
||||||
|
st.temperature = auto.temperature;
|
||||||
// Surface agent gets near-interactive priority;
|
// Surface agent gets near-interactive priority;
|
||||||
// other subconscious agents get lower priority.
|
// other subconscious agents get lower priority.
|
||||||
st.priority = Some(if auto.name == "surface" { 1 } else { 2 });
|
st.priority = Some(if auto.name == "surface" { 1 } else { auto.priority });
|
||||||
}
|
}
|
||||||
let fork_point = forked.context.lock().await.conversation().len();
|
let fork_point = forked.context.lock().await.conversation().len();
|
||||||
|
|
||||||
|
|
@ -614,7 +625,8 @@ impl Subconscious {
|
||||||
|
|
||||||
self.agents[idx].handle = Some(tokio::spawn(async move {
|
self.agents[idx].handle = Some(tokio::spawn(async move {
|
||||||
let result = auto.run_forked_shared(&forked, &keys, &st, &recent).await;
|
let result = auto.run_forked_shared(&forked, &keys, &st, &recent).await;
|
||||||
crate::agent::oneshot::save_agent_log(&auto.name, &forked).await;
|
let stats = crate::agent::oneshot::save_agent_log(&auto.name, &forked).await;
|
||||||
|
auto.update_stats(stats);
|
||||||
(auto, result)
|
(auto, result)
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -34,12 +34,10 @@ struct UnconsciousAgent {
|
||||||
name: String,
|
name: String,
|
||||||
enabled: bool,
|
enabled: bool,
|
||||||
auto: AutoAgent,
|
auto: AutoAgent,
|
||||||
handle: Option<tokio::task::JoinHandle<(AutoAgent, Result<String, String>, RunStats)>>,
|
handle: Option<tokio::task::JoinHandle<(AutoAgent, Result<String, String>)>>,
|
||||||
/// Shared agent handle — UI locks to read context live.
|
/// Shared agent handle — UI locks to read context live.
|
||||||
pub agent: Option<std::sync::Arc<crate::agent::Agent>>,
|
pub agent: Option<std::sync::Arc<crate::agent::Agent>>,
|
||||||
last_run: Option<Instant>,
|
last_run: Option<Instant>,
|
||||||
runs: usize,
|
|
||||||
last_stats: Option<RunStats>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl UnconsciousAgent {
|
impl UnconsciousAgent {
|
||||||
|
|
@ -64,6 +62,8 @@ pub struct UnconsciousSnapshot {
|
||||||
pub last_stats: Option<RunStats>,
|
pub last_stats: Option<RunStats>,
|
||||||
/// Recent store activity for this agent: (key, timestamp), newest first.
|
/// Recent store activity for this agent: (key, timestamp), newest first.
|
||||||
pub history: Vec<(String, i64)>,
|
pub history: Vec<(String, i64)>,
|
||||||
|
pub tool_calls_ewma: f64,
|
||||||
|
pub tool_failures_ewma: f64,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Unconscious {
|
pub struct Unconscious {
|
||||||
|
|
@ -107,8 +107,6 @@ impl Unconscious {
|
||||||
handle: None,
|
handle: None,
|
||||||
agent: None,
|
agent: None,
|
||||||
last_run: None,
|
last_run: None,
|
||||||
runs: 0,
|
|
||||||
last_stats: None,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
agents.sort_by(|a, b| a.name.cmp(&b.name));
|
agents.sort_by(|a, b| a.name.cmp(&b.name));
|
||||||
|
|
@ -144,15 +142,19 @@ impl Unconscious {
|
||||||
self.agents.iter().map(|a| {
|
self.agents.iter().map(|a| {
|
||||||
let history = store.map(|st| st.recent_by_provenance(&a.name, 30))
|
let history = store.map(|st| st.recent_by_provenance(&a.name, 30))
|
||||||
.unwrap_or_default();
|
.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 {
|
UnconsciousSnapshot {
|
||||||
name: a.name.clone(),
|
name: a.name.clone(),
|
||||||
running: a.is_running(),
|
running: a.is_running(),
|
||||||
enabled: a.enabled,
|
enabled: a.enabled,
|
||||||
runs: a.runs,
|
runs: stats.runs,
|
||||||
last_run_secs_ago: a.last_run.map(|t| t.elapsed().as_secs_f64()),
|
last_run_secs_ago: a.last_run.map(|t| t.elapsed().as_secs_f64()),
|
||||||
agent: a.agent.clone(),
|
agent: a.agent.clone(),
|
||||||
last_stats: a.last_stats.clone(),
|
last_stats: stats.last_stats.clone(),
|
||||||
history,
|
history,
|
||||||
|
tool_calls_ewma,
|
||||||
|
tool_failures_ewma: stats.failures.ewma,
|
||||||
}
|
}
|
||||||
}).collect()
|
}).collect()
|
||||||
}
|
}
|
||||||
|
|
@ -180,15 +182,13 @@ impl Unconscious {
|
||||||
if agent.handle.as_ref().is_some_and(|h| h.is_finished()) {
|
if agent.handle.as_ref().is_some_and(|h| h.is_finished()) {
|
||||||
let handle = agent.handle.take().unwrap();
|
let handle = agent.handle.take().unwrap();
|
||||||
agent.last_run = Some(Instant::now());
|
agent.last_run = Some(Instant::now());
|
||||||
agent.runs += 1;
|
// Get the AutoAgent back from the finished task (stats already updated)
|
||||||
// Get the AutoAgent back from the finished task
|
|
||||||
match handle.now_or_never() {
|
match handle.now_or_never() {
|
||||||
Some(Ok((auto_back, result, stats))) => {
|
Some(Ok((auto_back, result))) => {
|
||||||
agent.auto = auto_back;
|
agent.auto = auto_back;
|
||||||
agent.last_stats = Some(stats);
|
|
||||||
match result {
|
match result {
|
||||||
Ok(_) => dbglog!("[unconscious] {} completed (run {})",
|
Ok(_) => dbglog!("[unconscious] {} completed (run {})",
|
||||||
agent.name, agent.runs),
|
agent.name, crate::agent::oneshot::get_stats(&agent.name).runs),
|
||||||
Err(e) => dbglog!("[unconscious] {} failed: {}", agent.name, e),
|
Err(e) => dbglog!("[unconscious] {} failed: {}", agent.name, e),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -244,7 +244,7 @@ impl Unconscious {
|
||||||
|
|
||||||
// Swap auto out, replace steps with resolved prompts
|
// Swap auto out, replace steps with resolved prompts
|
||||||
let mut auto = std::mem::replace(&mut self.agents[idx].auto,
|
let mut auto = std::mem::replace(&mut self.agents[idx].auto,
|
||||||
AutoAgent::new(String::new(), vec![], vec![], 0.0, 0));
|
AutoAgent::new(String::new(), vec![], vec![], 0.6, 0));
|
||||||
let orig_steps = std::mem::replace(&mut auto.steps,
|
let orig_steps = std::mem::replace(&mut auto.steps,
|
||||||
batch.steps.iter().map(|s| AutoStep {
|
batch.steps.iter().map(|s| AutoStep {
|
||||||
prompt: s.prompt.clone(),
|
prompt: s.prompt.clone(),
|
||||||
|
|
@ -293,7 +293,8 @@ impl Unconscious {
|
||||||
{
|
{
|
||||||
let mut st = agent.state.lock().await;
|
let mut st = agent.state.lock().await;
|
||||||
st.provenance = auto.name.clone();
|
st.provenance = auto.name.clone();
|
||||||
st.priority = Some(10);
|
st.priority = Some(auto.priority);
|
||||||
|
st.temperature = auto.temperature;
|
||||||
}
|
}
|
||||||
|
|
||||||
self.agents[idx].agent = Some(agent.clone());
|
self.agents[idx].agent = Some(agent.clone());
|
||||||
|
|
@ -301,8 +302,9 @@ impl Unconscious {
|
||||||
self.agents[idx].handle = Some(tokio::spawn(async move {
|
self.agents[idx].handle = Some(tokio::spawn(async move {
|
||||||
let result = auto.run_shared(&agent).await;
|
let result = auto.run_shared(&agent).await;
|
||||||
let stats = crate::agent::oneshot::save_agent_log(&auto.name, &agent).await;
|
let stats = crate::agent::oneshot::save_agent_log(&auto.name, &agent).await;
|
||||||
|
auto.update_stats(stats);
|
||||||
auto.steps = orig_steps;
|
auto.steps = orig_steps;
|
||||||
(auto, result, stats)
|
(auto, result)
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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};
|
use super::widgets::{SectionTree, SectionView, section_to_view, pane_block_focused, tree_legend, format_age, format_ts_age};
|
||||||
|
|
||||||
#[derive(Clone, Copy, PartialEq)]
|
#[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
|
// 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 {
|
pub(crate) struct SubconsciousScreen {
|
||||||
focus: Pane,
|
focus: Pane,
|
||||||
|
|
@ -29,6 +29,7 @@ pub(crate) struct SubconsciousScreen {
|
||||||
output_tree: SectionTree,
|
output_tree: SectionTree,
|
||||||
context_tree: SectionTree,
|
context_tree: SectionTree,
|
||||||
history_scroll: super::scroll_pane::ScrollPaneState,
|
history_scroll: super::scroll_pane::ScrollPaneState,
|
||||||
|
stats_scroll: super::scroll_pane::ScrollPaneState,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SubconsciousScreen {
|
impl SubconsciousScreen {
|
||||||
|
|
@ -41,6 +42,7 @@ impl SubconsciousScreen {
|
||||||
output_tree: SectionTree::new(),
|
output_tree: SectionTree::new(),
|
||||||
context_tree: SectionTree::new(),
|
context_tree: SectionTree::new(),
|
||||||
history_scroll: super::scroll_pane::ScrollPaneState::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::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 {
|
Pane::History => match code {
|
||||||
KeyCode::Up => self.history_scroll.scroll_up(3),
|
KeyCode::Up => self.history_scroll.scroll_up(3),
|
||||||
KeyCode::Down => self.history_scroll.scroll_down(3),
|
KeyCode::Down => self.history_scroll.scroll_down(3),
|
||||||
|
|
@ -106,7 +115,7 @@ impl ScreenView for SubconsciousScreen {
|
||||||
Constraint::Percentage(62),
|
Constraint::Percentage(62),
|
||||||
]).areas(area);
|
]).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 }
|
let unc_count = if app.unconscious_state.is_empty() { 0 }
|
||||||
else { app.unconscious_state.len() + 1 }; // +1 for separator
|
else { app.unconscious_state.len() + 1 }; // +1 for separator
|
||||||
let agent_count = (app.agent_state.len() + unc_count).max(1) as u16;
|
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())
|
let output_lines = app.agent_state.get(self.selected())
|
||||||
.map(|s| s.state.values().map(|v| v.lines().count() + 1).sum::<usize>())
|
.map(|s| s.state.values().map(|v| v.lines().count() + 1).sum::<usize>())
|
||||||
.unwrap_or(0);
|
.unwrap_or(0);
|
||||||
let output_height = (output_lines as u16 + 2).min(left.height / 4).max(3);
|
let output_height = (output_lines as u16 + 2).min(left.height / 5).max(3);
|
||||||
let [list_area, output_area, history_area] = Layout::vertical([
|
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([
|
||||||
Constraint::Length(list_height),
|
Constraint::Length(list_height),
|
||||||
Constraint::Length(output_height),
|
Constraint::Length(output_height),
|
||||||
|
Constraint::Length(stats_height),
|
||||||
Constraint::Min(5),
|
Constraint::Min(5),
|
||||||
]).areas(left);
|
]).areas(left);
|
||||||
|
|
||||||
self.draw_list(frame, list_area, app);
|
self.draw_list(frame, list_area, app);
|
||||||
self.draw_outputs(frame, output_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_history(frame, history_area, app);
|
||||||
self.draw_context(frame, right, &context_sections, app);
|
self.draw_context(frame, right, &context_sections, app);
|
||||||
}
|
}
|
||||||
|
|
@ -147,6 +162,7 @@ impl SubconsciousScreen {
|
||||||
self.output_tree = SectionTree::new();
|
self.output_tree = SectionTree::new();
|
||||||
self.context_tree = SectionTree::new();
|
self.context_tree = SectionTree::new();
|
||||||
self.history_scroll = super::scroll_pane::ScrollPaneState::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.
|
/// Get the agent Arc for the selected item, whether subconscious or unconscious.
|
||||||
|
|
@ -175,6 +191,12 @@ impl SubconsciousScreen {
|
||||||
.unwrap_or(&[])
|
.unwrap_or(&[])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get persisted stats for the selected agent.
|
||||||
|
fn selected_persisted_stats(&self, app: &App) -> Option<crate::agent::oneshot::PersistedStats> {
|
||||||
|
let name = self.selected_agent_name(app)?;
|
||||||
|
Some(crate::agent::oneshot::get_stats(&name))
|
||||||
|
}
|
||||||
|
|
||||||
fn output_sections(&self, app: &App) -> Vec<SectionView> {
|
fn output_sections(&self, app: &App) -> Vec<SectionView> {
|
||||||
let snap = match app.agent_state.get(self.selected()) {
|
let snap = match app.agent_state.get(self.selected()) {
|
||||||
Some(s) => s,
|
Some(s) => s,
|
||||||
|
|
@ -211,37 +233,39 @@ impl SubconsciousScreen {
|
||||||
|
|
||||||
fn draw_list(&mut self, frame: &mut Frame, area: Rect, app: &App) {
|
fn draw_list(&mut self, frame: &mut Frame, area: Rect, app: &App) {
|
||||||
let mut items: Vec<ListItem> = app.agent_state.iter().map(|snap| {
|
let mut items: Vec<ListItem> = app.agent_state.iter().map(|snap| {
|
||||||
if !snap.enabled {
|
let (name_color, indicator) = if !snap.enabled {
|
||||||
ListItem::from(Line::from(vec![
|
(Color::DarkGray, "○")
|
||||||
Span::styled(&snap.name, Style::default().fg(Color::DarkGray)),
|
|
||||||
Span::styled(" ○ off", Style::default().fg(Color::DarkGray)),
|
|
||||||
]))
|
|
||||||
} else if snap.running {
|
} else if snap.running {
|
||||||
ListItem::from(Line::from(vec![
|
(Color::Green, "●")
|
||||||
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 {
|
} else {
|
||||||
let ago = snap.last_run_secs_ago
|
(Color::Gray, "○")
|
||||||
.map(|s| format_age(s))
|
};
|
||||||
.unwrap_or_else(|| "—".to_string());
|
let ago = snap.last_run_secs_ago
|
||||||
let entries = snap.forked_agent.as_ref()
|
.map(|s| format_age(s))
|
||||||
.and_then(|a| a.context.try_lock().ok())
|
.unwrap_or_else(|| "—".to_string());
|
||||||
.map(|ctx| ctx.conversation().len().saturating_sub(snap.fork_point))
|
let detail = if snap.running {
|
||||||
.unwrap_or(0);
|
format!("p:{} t:{}", snap.current_phase, snap.turn)
|
||||||
ListItem::from(Line::from(vec![
|
} else if !snap.enabled {
|
||||||
Span::styled(&snap.name, Style::default().fg(Color::Gray)),
|
"off".to_string()
|
||||||
Span::styled(" ○ ", Style::default().fg(Color::DarkGray)),
|
} else if let Some(ref stats) = snap.last_stats {
|
||||||
Span::styled(
|
let fail_str = if stats.tool_failures > 0 {
|
||||||
format!("{} {}e", ago, entries),
|
format!(" {}fail", stats.tool_failures)
|
||||||
Style::default().fg(Color::DarkGray),
|
} 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();
|
}).collect();
|
||||||
|
|
||||||
// Unconscious agents (graph maintenance)
|
// Unconscious agents (graph maintenance)
|
||||||
|
|
@ -266,9 +290,15 @@ impl SubconsciousScreen {
|
||||||
} else if !snap.enabled {
|
} else if !snap.enabled {
|
||||||
"off".to_string()
|
"off".to_string()
|
||||||
} else if let Some(ref stats) = snap.last_stats {
|
} 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,
|
snap.runs, ago,
|
||||||
stats.messages, stats.tool_calls)
|
stats.tool_calls, fail_str,
|
||||||
|
snap.tool_calls_ewma)
|
||||||
} else {
|
} else {
|
||||||
format!("×{} {}", snap.runs, ago)
|
format!("×{} {}", snap.runs, ago)
|
||||||
};
|
};
|
||||||
|
|
@ -316,6 +346,67 @@ impl SubconsciousScreen {
|
||||||
frame.render_stateful_widget(widget, area, &mut self.output_tree.scroll);
|
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<Line> = 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) {
|
fn draw_history(&mut self, frame: &mut Frame, area: Rect, app: &App) {
|
||||||
let dim = Style::default().fg(Color::DarkGray);
|
let dim = Style::default().fg(Color::DarkGray);
|
||||||
let key_style = Style::default().fg(Color::Yellow);
|
let key_style = Style::default().fg(Color::Yellow);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue