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, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default, PartialOrd, Ord)]
111#[serde(rename_all = "kebab-case")]
112pub enum SpanKind {
113 #[default]
115 Tool,
116 Inference,
120 Turn,
123}
124
125impl SpanKind {
126 pub const ALL: [SpanKind; 3] = [SpanKind::Tool, SpanKind::Inference, SpanKind::Turn];
127
128 pub fn label(self) -> &'static str {
129 match self {
130 SpanKind::Tool => "tool",
131 SpanKind::Inference => "inference",
132 SpanKind::Turn => "turn",
133 }
134 }
135}
136
137#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
148pub struct ToolSpan {
149 pub id: String,
152 pub name: String,
155 pub started_at: SystemTime,
156 pub duration_ms: Option<u64>,
158 pub sidechain: bool,
160 pub error: bool,
162 #[serde(default)]
164 pub kind: SpanKind,
165}
166
167impl ToolSpan {
168 pub fn is_open(&self) -> bool {
169 self.duration_ms.is_none()
170 }
171
172 pub fn elapsed_ms(&self, now: SystemTime) -> u64 {
174 match self.duration_ms {
175 Some(ms) => ms,
176 None => now.duration_since(self.started_at).map(|d| d.as_millis() as u64).unwrap_or(0),
177 }
178 }
179
180 pub fn ended_at(&self, now: SystemTime) -> SystemTime {
181 self.started_at + std::time::Duration::from_millis(self.elapsed_ms(now))
182 }
183}
184
185#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
190pub struct McpServer {
191 pub name: String,
194 pub pid: Option<u32>,
195 pub cmdline: Option<String>,
196 pub cpu_percent: f32,
197 pub rss_bytes: u64,
198 pub age_secs: Option<u64>,
199 pub calls: u64,
201 pub errors: u64,
203 pub last_call: Option<SystemTime>,
204 pub matched_by: McpMatch,
207}
208
209#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
211#[serde(rename_all = "kebab-case")]
212pub enum McpMatch {
213 ProcessOnly,
216 TranscriptOnly,
219 Name,
221 Sole,
224}
225
226#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
228#[serde(rename_all = "kebab-case")]
229pub enum ProcKind {
230 Agent,
232 Subagent,
234 Mcp,
236 Shell,
238 Tool,
240}
241
242impl ProcKind {
243 pub fn label(self) -> &'static str {
244 match self {
245 ProcKind::Agent => "agent",
246 ProcKind::Subagent => "subagent",
247 ProcKind::Mcp => "mcp",
248 ProcKind::Shell => "shell",
249 ProcKind::Tool => "tool",
250 }
251 }
252}
253
254#[derive(Debug, Clone, Serialize, Deserialize)]
256pub struct ProcNode {
257 pub pid: u32,
258 pub ppid: Option<u32>,
259 pub name: String,
260 pub cmdline: String,
261 pub kind: ProcKind,
262 pub harness: Option<Harness>,
263 pub cpu_percent: f32,
264 pub rss_bytes: u64,
265 pub age_secs: u64,
266 pub cwd: Option<PathBuf>,
267 pub children: Vec<ProcNode>,
268}
269
270impl ProcNode {
271 pub fn totals(&self) -> (f32, u64, usize, usize) {
276 let mut cpu = self.cpu_percent;
277 let mut rss = self.rss_bytes;
278 let mut count = 1;
279 let mut mcp = usize::from(self.kind == ProcKind::Mcp);
280 for c in &self.children {
281 let (ccpu, crss, ccount, cmcp) = c.totals();
282 cpu += ccpu;
283 rss += crss;
284 count += ccount;
285 if self.kind != ProcKind::Mcp {
286 mcp += cmcp;
287 }
288 }
289 (cpu, rss, count, mcp)
290 }
291
292 pub fn mcp_roots(&self) -> Vec<&ProcNode> {
296 let mut out = Vec::new();
297 self.collect_mcp_roots(&mut out);
298 out
299 }
300
301 fn collect_mcp_roots<'a>(&'a self, out: &mut Vec<&'a ProcNode>) {
302 if self.kind == ProcKind::Mcp {
303 out.push(self);
304 return;
305 }
306 for c in &self.children {
307 c.collect_mcp_roots(out);
308 }
309 }
310
311 pub fn walk<'a>(&'a self, depth: usize, f: &mut dyn FnMut(&'a ProcNode, usize)) {
312 f(self, depth);
313 for c in &self.children {
314 c.walk(depth + 1, f);
315 }
316 }
317}
318
319#[derive(Debug, Clone, Serialize, Deserialize)]
321pub struct Agent {
322 pub id: String,
324 pub name: String,
325 pub harness: Harness,
326 pub state: AgentState,
327 pub activity: Activity,
328 pub pid: Option<u32>,
329 pub session_id: Option<String>,
330 pub session_path: Option<PathBuf>,
331 pub cwd: Option<PathBuf>,
332 pub model: Option<String>,
333 pub harness_version: Option<String>,
334 pub usage: TokenUsage,
335 pub cost_usd: f64,
337 #[serde(default)]
340 pub cost_breakdown: CostBreakdown,
341 #[serde(default)]
343 pub price_source: Option<PriceSource>,
344 pub unpriced_tokens: u64,
346 pub turns: u64,
347 pub subagent_turns: u64,
348 pub tool_calls: u64,
349 #[serde(default)]
353 pub web_searches: u64,
354 pub spans: Vec<ToolSpan>,
357 pub age_secs: u64,
359 pub idle_secs: Option<u64>,
361 pub cpu_percent: f32,
362 pub rss_bytes: u64,
363 pub process_count: usize,
364 pub mcp_count: usize,
365 #[serde(default)]
368 pub mcp_servers: Vec<McpServer>,
369 pub tree: Option<ProcNode>,
370 pub attribution: Attribution,
372 #[serde(default)]
377 pub shares_process: bool,
378 pub parse_warning: Option<String>,
382}
383
384#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq)]
388pub struct CostBreakdown {
389 pub input: f64,
390 pub cache_write_5m: f64,
391 pub cache_write_1h: f64,
392 pub cache_read: f64,
393 pub output: f64,
394 pub web_search: f64,
396}
397
398impl CostBreakdown {
399 pub fn total(&self) -> f64 {
400 self.input + self.cache_write_5m + self.cache_write_1h + self.cache_read + self.output + self.web_search
401 }
402
403 pub fn add(&mut self, o: &CostBreakdown) {
404 self.input += o.input;
405 self.cache_write_5m += o.cache_write_5m;
406 self.cache_write_1h += o.cache_write_1h;
407 self.cache_read += o.cache_read;
408 self.output += o.output;
409 self.web_search += o.web_search;
410 }
411
412 pub fn sub(&mut self, o: &CostBreakdown) {
413 self.input -= o.input;
414 self.cache_write_5m -= o.cache_write_5m;
415 self.cache_write_1h -= o.cache_write_1h;
416 self.cache_read -= o.cache_read;
417 self.output -= o.output;
418 self.web_search -= o.web_search;
419 }
420}
421
422#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
425#[serde(rename_all = "kebab-case")]
426pub enum PriceSource {
427 Builtin,
428 UserFile,
429}
430
431#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
432#[serde(rename_all = "kebab-case")]
433pub enum Attribution {
434 HarnessRegistry,
436 CommandLine,
438 OpenFile,
441 CwdHeuristic,
443 None,
445 TranscriptOnly,
447}
448
449#[derive(Debug, Clone, Default, Serialize, Deserialize)]
450pub struct HostStats {
451 pub hostname: Option<String>,
452 pub cpu_percent: f32,
453 pub cpu_count: usize,
454 pub mem_used_bytes: u64,
455 pub mem_total_bytes: u64,
456}
457
458#[derive(Debug, Clone, Default, Serialize, Deserialize)]
459pub struct Totals {
460 pub agents: usize,
461 pub running: usize,
462 pub idle: usize,
463 pub stopped: usize,
464 pub tokens: u64,
465 pub cost_usd: f64,
466 pub unpriced_tokens: u64,
467 pub processes: usize,
468 pub mcp_processes: usize,
469 pub orphaned_mcp: usize,
470 pub cpu_percent: f32,
471 pub rss_bytes: u64,
472}
473
474pub const SNAPSHOT_SCHEMA_VERSION: u32 = 1;
477
478#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
483pub struct OrphanOrigin {
484 pub pid: u32,
485 pub first_seen: SystemTime,
486 pub orphaned_at: Option<SystemTime>,
489 pub parent: Option<OrphanParent>,
490}
491
492#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
494pub struct OrphanParent {
495 pub pid: u32,
496 pub agent_id: String,
497 pub name: String,
498}
499
500#[derive(Debug, Clone, Serialize, Deserialize)]
502pub struct Snapshot {
503 pub schema_version: u32,
504 pub taken_at: SystemTime,
505 pub host: HostStats,
506 pub agents: Vec<Agent>,
507 pub orphans: Vec<ProcNode>,
509 #[serde(default)]
511 pub orphan_origins: Vec<OrphanOrigin>,
512 pub totals: Totals,
513}
514
515impl Snapshot {
516 pub fn compute_totals(&mut self) {
517 let mut t = Totals::default();
518 for a in &self.agents {
519 t.agents += 1;
520 match a.state {
521 AgentState::Running => t.running += 1,
522 AgentState::Idle => t.idle += 1,
523 AgentState::Stopped => t.stopped += 1,
524 }
525 t.tokens += a.usage.total();
526 t.cost_usd += a.cost_usd;
527 t.unpriced_tokens += a.unpriced_tokens;
528 t.processes += a.process_count;
529 t.mcp_processes += a.mcp_count;
530 t.cpu_percent += a.cpu_percent;
531 t.rss_bytes += a.rss_bytes;
532 }
533 t.orphaned_mcp = self.orphans.len();
534 self.totals = t;
535 }
536}