turn_stats: per-turn analytics sink
new sqlite table at /state/hyperhive-turn-stats.sqlite on each agent's state dir. one row per claude turn captures identity (model, wake_from, result_kind), timing (started/ended_at, duration_ms), cost (input/output/cache_read/cache_creation token counts), behaviour (tool_call_count + per-tool breakdown JSON), and post-turn snapshot metrics (open_threads_count, open_reminders_count). wire additions: - AgentRequest/ManagerRequest::CountPendingReminders + Broker::count_pending_reminders_for(agent) - Bus::observe_stream + take_tool_calls — pumps the existing stdout stream-json, picks out tool_use blocks, accumulates per turn. bin loops fold the breakdown into each row. - TurnStats::open_default + TurnStatRow + record() — best-effort inserts; failures log + don't block the harness. both ag3nt and m1nd bins capture started_at + duration via Instant::elapsed, fetch open-thread + reminder counts from hive-c0re via the existing socket (post-turn, best-effort), and record one row at turn_end. record_kind splits ok / failed / prompt_too_long; failures carry the error message in note. todo entries for host-side vacuum sweep + reading the table back into agent/dashboard badges.
This commit is contained in:
parent
dc1ce1f236
commit
8f5752980f
12 changed files with 476 additions and 3 deletions
|
|
@ -294,6 +294,12 @@ pub struct Bus {
|
|||
/// behavior. Atomic so the consumer can take-and-clear without a
|
||||
/// lock.
|
||||
skip_continue_once: Arc<AtomicBool>,
|
||||
/// Per-turn tool-call counter. Reset by the bin loop between
|
||||
/// turns via `take_tool_calls`. Populated by `observe_stream` as
|
||||
/// the stdout pump parses each stream-json line. Powers the
|
||||
/// `tool_call_count` + `tool_call_breakdown_json` columns on the
|
||||
/// per-turn stats sink.
|
||||
tool_calls: Arc<Mutex<std::collections::HashMap<String, u64>>>,
|
||||
}
|
||||
|
||||
impl Bus {
|
||||
|
|
@ -319,6 +325,7 @@ impl Bus {
|
|||
model: Arc::new(Mutex::new(initial_model)),
|
||||
last_usage: Arc::new(Mutex::new(None)),
|
||||
skip_continue_once: Arc::new(AtomicBool::new(false)),
|
||||
tool_calls: Arc::new(Mutex::new(std::collections::HashMap::new())),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -377,6 +384,43 @@ impl Bus {
|
|||
self.emit(LiveEvent::TokenUsageChanged { usage });
|
||||
}
|
||||
|
||||
/// Walk a stream-json value for `tool_use` blocks and bump the
|
||||
/// per-turn counter for each one we find. Called by the stdout
|
||||
/// pump on every parsed line. Cheap when the line isn't an
|
||||
/// assistant message — the field-check short-circuits.
|
||||
pub fn observe_stream(&self, v: &serde_json::Value) {
|
||||
if v.get("type").and_then(|t| t.as_str()) != Some("assistant") {
|
||||
return;
|
||||
}
|
||||
let Some(content) = v
|
||||
.get("message")
|
||||
.and_then(|m| m.get("content"))
|
||||
.and_then(|c| c.as_array())
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let mut counts = self.tool_calls.lock().unwrap();
|
||||
for block in content {
|
||||
if block.get("type").and_then(|t| t.as_str()) != Some("tool_use") {
|
||||
continue;
|
||||
}
|
||||
let name = block
|
||||
.get("name")
|
||||
.and_then(|n| n.as_str())
|
||||
.unwrap_or("<unnamed>")
|
||||
.to_owned();
|
||||
*counts.entry(name).or_insert(0) += 1;
|
||||
}
|
||||
}
|
||||
|
||||
/// Snapshot + clear the per-turn tool-call counter. The harness
|
||||
/// calls this between turns to fold the breakdown into a
|
||||
/// `turn_stats` row, then start the next turn with an empty map.
|
||||
#[must_use]
|
||||
pub fn take_tool_calls(&self) -> std::collections::HashMap<String, u64> {
|
||||
std::mem::take(&mut *self.tool_calls.lock().unwrap())
|
||||
}
|
||||
|
||||
/// Last known token usage, or `None` if no turn has completed yet.
|
||||
#[must_use]
|
||||
pub fn last_usage(&self) -> Option<TokenUsage> {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue