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, Copy, PartialEq, Eq, Serialize, Deserialize)]
187#[serde(rename_all = "kebab-case")]
188pub enum ProcKind {
189 Agent,
191 Subagent,
193 Mcp,
195 Shell,
197 Tool,
199}
200
201impl ProcKind {
202 pub fn label(self) -> &'static str {
203 match self {
204 ProcKind::Agent => "agent",
205 ProcKind::Subagent => "subagent",
206 ProcKind::Mcp => "mcp",
207 ProcKind::Shell => "shell",
208 ProcKind::Tool => "tool",
209 }
210 }
211}
212
213#[derive(Debug, Clone, Serialize, Deserialize)]
215pub struct ProcNode {
216 pub pid: u32,
217 pub ppid: Option<u32>,
218 pub name: String,
219 pub cmdline: String,
220 pub kind: ProcKind,
221 pub harness: Option<Harness>,
222 pub cpu_percent: f32,
223 pub rss_bytes: u64,
224 pub age_secs: u64,
225 pub cwd: Option<PathBuf>,
226 pub children: Vec<ProcNode>,
227}
228
229impl ProcNode {
230 pub fn totals(&self) -> (f32, u64, usize, usize) {
232 let mut cpu = self.cpu_percent;
233 let mut rss = self.rss_bytes;
234 let mut count = 1;
235 let mut mcp = usize::from(self.kind == ProcKind::Mcp);
236 for c in &self.children {
237 let (ccpu, crss, ccount, cmcp) = c.totals();
238 cpu += ccpu;
239 rss += crss;
240 count += ccount;
241 mcp += cmcp;
242 }
243 (cpu, rss, count, mcp)
244 }
245
246 pub fn walk<'a>(&'a self, depth: usize, f: &mut dyn FnMut(&'a ProcNode, usize)) {
247 f(self, depth);
248 for c in &self.children {
249 c.walk(depth + 1, f);
250 }
251 }
252}
253
254#[derive(Debug, Clone, Serialize, Deserialize)]
256pub struct Agent {
257 pub id: String,
259 pub name: String,
260 pub harness: Harness,
261 pub state: AgentState,
262 pub activity: Activity,
263 pub pid: Option<u32>,
264 pub session_id: Option<String>,
265 pub session_path: Option<PathBuf>,
266 pub cwd: Option<PathBuf>,
267 pub model: Option<String>,
268 pub harness_version: Option<String>,
269 pub usage: TokenUsage,
270 pub cost_usd: f64,
272 pub unpriced_tokens: u64,
274 pub turns: u64,
275 pub subagent_turns: u64,
276 pub tool_calls: u64,
277 #[serde(default)]
281 pub web_searches: u64,
282 pub spans: Vec<ToolSpan>,
285 pub age_secs: u64,
287 pub idle_secs: Option<u64>,
289 pub cpu_percent: f32,
290 pub rss_bytes: u64,
291 pub process_count: usize,
292 pub mcp_count: usize,
293 pub tree: Option<ProcNode>,
294 pub attribution: Attribution,
296 pub shares_process: bool,
300 pub parse_warning: Option<String>,
304}
305
306#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
307#[serde(rename_all = "kebab-case")]
308pub enum Attribution {
309 HarnessRegistry,
311 CommandLine,
313 OpenFile,
316 CwdHeuristic,
318 None,
320 TranscriptOnly,
322}
323
324#[derive(Debug, Clone, Default, Serialize, Deserialize)]
325pub struct HostStats {
326 pub hostname: Option<String>,
327 pub cpu_percent: f32,
328 pub cpu_count: usize,
329 pub mem_used_bytes: u64,
330 pub mem_total_bytes: u64,
331}
332
333#[derive(Debug, Clone, Default, Serialize, Deserialize)]
334pub struct Totals {
335 pub agents: usize,
336 pub running: usize,
337 pub idle: usize,
338 pub stopped: usize,
339 pub tokens: u64,
340 pub cost_usd: f64,
341 pub unpriced_tokens: u64,
342 pub processes: usize,
343 pub mcp_processes: usize,
344 pub orphaned_mcp: usize,
345 pub cpu_percent: f32,
346 pub rss_bytes: u64,
347}
348
349pub const SNAPSHOT_SCHEMA_VERSION: u32 = 1;
352
353#[derive(Debug, Clone, Serialize, Deserialize)]
355pub struct Snapshot {
356 pub schema_version: u32,
357 pub taken_at: SystemTime,
358 pub host: HostStats,
359 pub agents: Vec<Agent>,
360 pub orphans: Vec<ProcNode>,
362 pub totals: Totals,
363}
364
365impl Snapshot {
366 pub fn compute_totals(&mut self) {
367 let mut t = Totals::default();
368 for a in &self.agents {
369 t.agents += 1;
370 match a.state {
371 AgentState::Running => t.running += 1,
372 AgentState::Idle => t.idle += 1,
373 AgentState::Stopped => t.stopped += 1,
374 }
375 t.tokens += a.usage.total();
376 t.cost_usd += a.cost_usd;
377 t.unpriced_tokens += a.unpriced_tokens;
378 t.processes += a.process_count;
379 t.mcp_processes += a.mcp_count;
380 t.cpu_percent += a.cpu_percent;
381 t.rss_bytes += a.rss_bytes;
382 }
383 t.orphaned_mcp = self.orphans.len();
384 self.totals = t;
385 }
386}