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 #[serde(default)]
275 pub cost_breakdown: CostBreakdown,
276 #[serde(default)]
278 pub price_source: Option<PriceSource>,
279 pub unpriced_tokens: u64,
281 pub turns: u64,
282 pub subagent_turns: u64,
283 pub tool_calls: u64,
284 #[serde(default)]
288 pub web_searches: u64,
289 pub spans: Vec<ToolSpan>,
292 pub age_secs: u64,
294 pub idle_secs: Option<u64>,
296 pub cpu_percent: f32,
297 pub rss_bytes: u64,
298 pub process_count: usize,
299 pub mcp_count: usize,
300 pub tree: Option<ProcNode>,
301 pub attribution: Attribution,
303 pub shares_process: bool,
307 pub parse_warning: Option<String>,
311}
312
313#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq)]
317pub struct CostBreakdown {
318 pub input: f64,
319 pub cache_write_5m: f64,
320 pub cache_write_1h: f64,
321 pub cache_read: f64,
322 pub output: f64,
323 pub web_search: f64,
325}
326
327impl CostBreakdown {
328 pub fn total(&self) -> f64 {
329 self.input + self.cache_write_5m + self.cache_write_1h + self.cache_read + self.output + self.web_search
330 }
331
332 pub fn add(&mut self, o: &CostBreakdown) {
333 self.input += o.input;
334 self.cache_write_5m += o.cache_write_5m;
335 self.cache_write_1h += o.cache_write_1h;
336 self.cache_read += o.cache_read;
337 self.output += o.output;
338 self.web_search += o.web_search;
339 }
340
341 pub fn sub(&mut self, o: &CostBreakdown) {
342 self.input -= o.input;
343 self.cache_write_5m -= o.cache_write_5m;
344 self.cache_write_1h -= o.cache_write_1h;
345 self.cache_read -= o.cache_read;
346 self.output -= o.output;
347 self.web_search -= o.web_search;
348 }
349}
350
351#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
354#[serde(rename_all = "kebab-case")]
355pub enum PriceSource {
356 Builtin,
357 UserFile,
358}
359
360#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
361#[serde(rename_all = "kebab-case")]
362pub enum Attribution {
363 HarnessRegistry,
365 CommandLine,
367 OpenFile,
370 CwdHeuristic,
372 None,
374 TranscriptOnly,
376}
377
378#[derive(Debug, Clone, Default, Serialize, Deserialize)]
379pub struct HostStats {
380 pub hostname: Option<String>,
381 pub cpu_percent: f32,
382 pub cpu_count: usize,
383 pub mem_used_bytes: u64,
384 pub mem_total_bytes: u64,
385}
386
387#[derive(Debug, Clone, Default, Serialize, Deserialize)]
388pub struct Totals {
389 pub agents: usize,
390 pub running: usize,
391 pub idle: usize,
392 pub stopped: usize,
393 pub tokens: u64,
394 pub cost_usd: f64,
395 pub unpriced_tokens: u64,
396 pub processes: usize,
397 pub mcp_processes: usize,
398 pub orphaned_mcp: usize,
399 pub cpu_percent: f32,
400 pub rss_bytes: u64,
401}
402
403pub const SNAPSHOT_SCHEMA_VERSION: u32 = 1;
406
407#[derive(Debug, Clone, Serialize, Deserialize)]
409pub struct Snapshot {
410 pub schema_version: u32,
411 pub taken_at: SystemTime,
412 pub host: HostStats,
413 pub agents: Vec<Agent>,
414 pub orphans: Vec<ProcNode>,
416 pub totals: Totals,
417}
418
419impl Snapshot {
420 pub fn compute_totals(&mut self) {
421 let mut t = Totals::default();
422 for a in &self.agents {
423 t.agents += 1;
424 match a.state {
425 AgentState::Running => t.running += 1,
426 AgentState::Idle => t.idle += 1,
427 AgentState::Stopped => t.stopped += 1,
428 }
429 t.tokens += a.usage.total();
430 t.cost_usd += a.cost_usd;
431 t.unpriced_tokens += a.unpriced_tokens;
432 t.processes += a.process_count;
433 t.mcp_processes += a.mcp_count;
434 t.cpu_percent += a.cpu_percent;
435 t.rss_bytes += a.rss_bytes;
436 }
437 t.orphaned_mcp = self.orphans.len();
438 self.totals = t;
439 }
440}