1use serde::{Deserialize, Serialize};
4use std::path::PathBuf;
5use std::time::SystemTime;
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, PartialOrd, Ord)]
9#[serde(rename_all = "kebab-case")]
10pub enum Harness {
11 Claude,
12 Codex,
13 Gemini,
14 OpenCode,
15 Aider,
16 Copilot,
17 Cursor,
18 Unknown,
19}
20
21impl Harness {
22 pub fn label(self) -> &'static str {
23 match self {
24 Harness::Claude => "claude",
25 Harness::Codex => "codex",
26 Harness::Gemini => "gemini",
27 Harness::OpenCode => "opencode",
28 Harness::Aider => "aider",
29 Harness::Copilot => "copilot",
30 Harness::Cursor => "cursor",
31 Harness::Unknown => "unknown",
32 }
33 }
34}
35
36#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, PartialOrd, Ord)]
42#[serde(rename_all = "kebab-case")]
43pub enum AgentState {
44 Running,
45 Idle,
46 Stopped,
47}
48
49impl AgentState {
50 pub fn label(self) -> &'static str {
51 match self {
52 AgentState::Running => "running",
53 AgentState::Idle => "idle",
54 AgentState::Stopped => "stopped",
55 }
56 }
57}
58
59#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
61#[serde(rename_all = "kebab-case")]
62pub enum Activity {
63 Working,
65 Waiting,
67 #[default]
68 Unknown,
69}
70
71#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq)]
73pub struct TokenUsage {
74 pub input: u64,
75 pub cache_write_5m: u64,
76 pub cache_write_1h: u64,
77 pub cache_read: u64,
78 pub output: u64,
79}
80
81impl TokenUsage {
82 pub fn cache_write(&self) -> u64 {
83 self.cache_write_5m + self.cache_write_1h
84 }
85
86 pub fn total(&self) -> u64 {
88 self.input + self.cache_write() + self.cache_read + self.output
89 }
90
91 pub fn add(&mut self, other: &TokenUsage) {
92 self.input += other.input;
93 self.cache_write_5m += other.cache_write_5m;
94 self.cache_write_1h += other.cache_write_1h;
95 self.cache_read += other.cache_read;
96 self.output += other.output;
97 }
98
99 pub fn sub(&mut self, other: &TokenUsage) {
100 self.input = self.input.saturating_sub(other.input);
101 self.cache_write_5m = self.cache_write_5m.saturating_sub(other.cache_write_5m);
102 self.cache_write_1h = self.cache_write_1h.saturating_sub(other.cache_write_1h);
103 self.cache_read = self.cache_read.saturating_sub(other.cache_read);
104 self.output = self.output.saturating_sub(other.output);
105 }
106}
107
108#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
116pub struct ToolSpan {
117 pub id: String,
119 pub name: String,
121 pub started_at: SystemTime,
122 pub duration_ms: Option<u64>,
124 pub sidechain: bool,
126 pub error: bool,
128}
129
130impl ToolSpan {
131 pub fn is_open(&self) -> bool {
132 self.duration_ms.is_none()
133 }
134
135 pub fn elapsed_ms(&self, now: SystemTime) -> u64 {
137 match self.duration_ms {
138 Some(ms) => ms,
139 None => now.duration_since(self.started_at).map(|d| d.as_millis() as u64).unwrap_or(0),
140 }
141 }
142
143 pub fn ended_at(&self, now: SystemTime) -> SystemTime {
144 self.started_at + std::time::Duration::from_millis(self.elapsed_ms(now))
145 }
146}
147
148#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
150#[serde(rename_all = "kebab-case")]
151pub enum ProcKind {
152 Agent,
154 Subagent,
156 Mcp,
158 Shell,
160 Tool,
162}
163
164impl ProcKind {
165 pub fn label(self) -> &'static str {
166 match self {
167 ProcKind::Agent => "agent",
168 ProcKind::Subagent => "subagent",
169 ProcKind::Mcp => "mcp",
170 ProcKind::Shell => "shell",
171 ProcKind::Tool => "tool",
172 }
173 }
174}
175
176#[derive(Debug, Clone, Serialize, Deserialize)]
178pub struct ProcNode {
179 pub pid: u32,
180 pub ppid: Option<u32>,
181 pub name: String,
182 pub cmdline: String,
183 pub kind: ProcKind,
184 pub harness: Option<Harness>,
185 pub cpu_percent: f32,
186 pub rss_bytes: u64,
187 pub age_secs: u64,
188 pub cwd: Option<PathBuf>,
189 pub children: Vec<ProcNode>,
190}
191
192impl ProcNode {
193 pub fn totals(&self) -> (f32, u64, usize, usize) {
195 let mut cpu = self.cpu_percent;
196 let mut rss = self.rss_bytes;
197 let mut count = 1;
198 let mut mcp = usize::from(self.kind == ProcKind::Mcp);
199 for c in &self.children {
200 let (ccpu, crss, ccount, cmcp) = c.totals();
201 cpu += ccpu;
202 rss += crss;
203 count += ccount;
204 mcp += cmcp;
205 }
206 (cpu, rss, count, mcp)
207 }
208
209 pub fn walk<'a>(&'a self, depth: usize, f: &mut dyn FnMut(&'a ProcNode, usize)) {
210 f(self, depth);
211 for c in &self.children {
212 c.walk(depth + 1, f);
213 }
214 }
215}
216
217#[derive(Debug, Clone, Serialize, Deserialize)]
219pub struct Agent {
220 pub id: String,
222 pub name: String,
223 pub harness: Harness,
224 pub state: AgentState,
225 pub activity: Activity,
226 pub pid: Option<u32>,
227 pub session_id: Option<String>,
228 pub session_path: Option<PathBuf>,
229 pub cwd: Option<PathBuf>,
230 pub model: Option<String>,
231 pub harness_version: Option<String>,
232 pub usage: TokenUsage,
233 pub cost_usd: f64,
235 pub unpriced_tokens: u64,
237 pub turns: u64,
238 pub subagent_turns: u64,
239 pub tool_calls: u64,
240 pub spans: Vec<ToolSpan>,
243 pub age_secs: u64,
245 pub idle_secs: Option<u64>,
247 pub cpu_percent: f32,
248 pub rss_bytes: u64,
249 pub process_count: usize,
250 pub mcp_count: usize,
251 pub tree: Option<ProcNode>,
252 pub attribution: Attribution,
254 pub shares_process: bool,
258 pub parse_warning: Option<String>,
262}
263
264#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
265#[serde(rename_all = "kebab-case")]
266pub enum Attribution {
267 HarnessRegistry,
269 CommandLine,
271 CwdHeuristic,
273 None,
275 TranscriptOnly,
277}
278
279#[derive(Debug, Clone, Default, Serialize, Deserialize)]
280pub struct HostStats {
281 pub hostname: Option<String>,
282 pub cpu_percent: f32,
283 pub cpu_count: usize,
284 pub mem_used_bytes: u64,
285 pub mem_total_bytes: u64,
286}
287
288#[derive(Debug, Clone, Default, Serialize, Deserialize)]
289pub struct Totals {
290 pub agents: usize,
291 pub running: usize,
292 pub idle: usize,
293 pub stopped: usize,
294 pub tokens: u64,
295 pub cost_usd: f64,
296 pub unpriced_tokens: u64,
297 pub processes: usize,
298 pub mcp_processes: usize,
299 pub orphaned_mcp: usize,
300 pub cpu_percent: f32,
301 pub rss_bytes: u64,
302}
303
304pub const SNAPSHOT_SCHEMA_VERSION: u32 = 1;
307
308#[derive(Debug, Clone, Serialize, Deserialize)]
310pub struct Snapshot {
311 pub schema_version: u32,
312 pub taken_at: SystemTime,
313 pub host: HostStats,
314 pub agents: Vec<Agent>,
315 pub orphans: Vec<ProcNode>,
317 pub totals: Totals,
318}
319
320impl Snapshot {
321 pub fn compute_totals(&mut self) {
322 let mut t = Totals::default();
323 for a in &self.agents {
324 t.agents += 1;
325 match a.state {
326 AgentState::Running => t.running += 1,
327 AgentState::Idle => t.idle += 1,
328 AgentState::Stopped => t.stopped += 1,
329 }
330 t.tokens += a.usage.total();
331 t.cost_usd += a.cost_usd;
332 t.unpriced_tokens += a.unpriced_tokens;
333 t.processes += a.process_count;
334 t.mcp_processes += a.mcp_count;
335 t.cpu_percent += a.cpu_percent;
336 t.rss_bytes += a.rss_bytes;
337 }
338 t.orphaned_mcp = self.orphans.len();
339 self.totals = t;
340 }
341}