use serde::{Deserialize, Serialize};
use std::path::PathBuf;
use std::time::SystemTime;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, PartialOrd, Ord)]
#[serde(rename_all = "kebab-case")]
pub enum Harness {
Claude,
Codex,
Gemini,
OpenCode,
Aider,
Copilot,
Cursor,
Unknown,
}
impl Harness {
pub fn label(self) -> &'static str {
match self {
Harness::Claude => "claude",
Harness::Codex => "codex",
Harness::Gemini => "gemini",
Harness::OpenCode => "opencode",
Harness::Aider => "aider",
Harness::Copilot => "copilot",
Harness::Cursor => "cursor",
Harness::Unknown => "unknown",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, PartialOrd, Ord)]
#[serde(rename_all = "kebab-case")]
pub enum AgentState {
Running,
Idle,
Stopped,
}
impl AgentState {
pub fn label(self) -> &'static str {
match self {
AgentState::Running => "running",
AgentState::Idle => "idle",
AgentState::Stopped => "stopped",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "kebab-case")]
pub enum Activity {
Working,
Waiting,
#[default]
Unknown,
}
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq)]
pub struct TokenUsage {
pub input: u64,
pub cache_write_5m: u64,
pub cache_write_1h: u64,
pub cache_read: u64,
pub output: u64,
}
impl TokenUsage {
pub fn cache_write(&self) -> u64 {
self.cache_write_5m + self.cache_write_1h
}
pub fn total(&self) -> u64 {
self.input + self.cache_write() + self.cache_read + self.output
}
pub fn add(&mut self, other: &TokenUsage) {
self.input += other.input;
self.cache_write_5m += other.cache_write_5m;
self.cache_write_1h += other.cache_write_1h;
self.cache_read += other.cache_read;
self.output += other.output;
}
pub fn sub(&mut self, other: &TokenUsage) {
self.input = self.input.saturating_sub(other.input);
self.cache_write_5m = self.cache_write_5m.saturating_sub(other.cache_write_5m);
self.cache_write_1h = self.cache_write_1h.saturating_sub(other.cache_write_1h);
self.cache_read = self.cache_read.saturating_sub(other.cache_read);
self.output = self.output.saturating_sub(other.output);
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default, PartialOrd, Ord)]
#[serde(rename_all = "kebab-case")]
pub enum SpanKind {
#[default]
Tool,
Inference,
Turn,
}
impl SpanKind {
pub const ALL: [SpanKind; 3] = [SpanKind::Tool, SpanKind::Inference, SpanKind::Turn];
pub fn label(self) -> &'static str {
match self {
SpanKind::Tool => "tool",
SpanKind::Inference => "inference",
SpanKind::Turn => "turn",
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ToolSpan {
pub id: String,
pub name: String,
pub started_at: SystemTime,
pub duration_ms: Option<u64>,
pub sidechain: bool,
pub error: bool,
#[serde(default)]
pub kind: SpanKind,
}
impl ToolSpan {
pub fn is_open(&self) -> bool {
self.duration_ms.is_none()
}
pub fn elapsed_ms(&self, now: SystemTime) -> u64 {
match self.duration_ms {
Some(ms) => ms,
None => now.duration_since(self.started_at).map(|d| d.as_millis() as u64).unwrap_or(0),
}
}
pub fn ended_at(&self, now: SystemTime) -> SystemTime {
self.started_at + std::time::Duration::from_millis(self.elapsed_ms(now))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum ProcKind {
Agent,
Subagent,
Mcp,
Shell,
Tool,
}
impl ProcKind {
pub fn label(self) -> &'static str {
match self {
ProcKind::Agent => "agent",
ProcKind::Subagent => "subagent",
ProcKind::Mcp => "mcp",
ProcKind::Shell => "shell",
ProcKind::Tool => "tool",
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProcNode {
pub pid: u32,
pub ppid: Option<u32>,
pub name: String,
pub cmdline: String,
pub kind: ProcKind,
pub harness: Option<Harness>,
pub cpu_percent: f32,
pub rss_bytes: u64,
pub age_secs: u64,
pub cwd: Option<PathBuf>,
pub children: Vec<ProcNode>,
}
impl ProcNode {
pub fn totals(&self) -> (f32, u64, usize, usize) {
let mut cpu = self.cpu_percent;
let mut rss = self.rss_bytes;
let mut count = 1;
let mut mcp = usize::from(self.kind == ProcKind::Mcp);
for c in &self.children {
let (ccpu, crss, ccount, cmcp) = c.totals();
cpu += ccpu;
rss += crss;
count += ccount;
mcp += cmcp;
}
(cpu, rss, count, mcp)
}
pub fn walk<'a>(&'a self, depth: usize, f: &mut dyn FnMut(&'a ProcNode, usize)) {
f(self, depth);
for c in &self.children {
c.walk(depth + 1, f);
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Agent {
pub id: String,
pub name: String,
pub harness: Harness,
pub state: AgentState,
pub activity: Activity,
pub pid: Option<u32>,
pub session_id: Option<String>,
pub session_path: Option<PathBuf>,
pub cwd: Option<PathBuf>,
pub model: Option<String>,
pub harness_version: Option<String>,
pub usage: TokenUsage,
pub cost_usd: f64,
pub unpriced_tokens: u64,
pub turns: u64,
pub subagent_turns: u64,
pub tool_calls: u64,
#[serde(default)]
pub web_searches: u64,
pub spans: Vec<ToolSpan>,
pub age_secs: u64,
pub idle_secs: Option<u64>,
pub cpu_percent: f32,
pub rss_bytes: u64,
pub process_count: usize,
pub mcp_count: usize,
pub tree: Option<ProcNode>,
pub attribution: Attribution,
pub shares_process: bool,
pub parse_warning: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum Attribution {
HarnessRegistry,
CommandLine,
CwdHeuristic,
None,
TranscriptOnly,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct HostStats {
pub hostname: Option<String>,
pub cpu_percent: f32,
pub cpu_count: usize,
pub mem_used_bytes: u64,
pub mem_total_bytes: u64,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct Totals {
pub agents: usize,
pub running: usize,
pub idle: usize,
pub stopped: usize,
pub tokens: u64,
pub cost_usd: f64,
pub unpriced_tokens: u64,
pub processes: usize,
pub mcp_processes: usize,
pub orphaned_mcp: usize,
pub cpu_percent: f32,
pub rss_bytes: u64,
}
pub const SNAPSHOT_SCHEMA_VERSION: u32 = 1;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Snapshot {
pub schema_version: u32,
pub taken_at: SystemTime,
pub host: HostStats,
pub agents: Vec<Agent>,
pub orphans: Vec<ProcNode>,
pub totals: Totals,
}
impl Snapshot {
pub fn compute_totals(&mut self) {
let mut t = Totals::default();
for a in &self.agents {
t.agents += 1;
match a.state {
AgentState::Running => t.running += 1,
AgentState::Idle => t.idle += 1,
AgentState::Stopped => t.stopped += 1,
}
t.tokens += a.usage.total();
t.cost_usd += a.cost_usd;
t.unpriced_tokens += a.unpriced_tokens;
t.processes += a.process_count;
t.mcp_processes += a.mcp_count;
t.cpu_percent += a.cpu_percent;
t.rss_bytes += a.rss_bytes;
}
t.orphaned_mcp = self.orphans.len();
self.totals = t;
}
}