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}
255
256#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
257#[serde(rename_all = "kebab-case")]
258pub enum Attribution {
259 HarnessRegistry,
261 CommandLine,
263 CwdHeuristic,
265 None,
267 TranscriptOnly,
269}
270
271#[derive(Debug, Clone, Default, Serialize, Deserialize)]
272pub struct HostStats {
273 pub hostname: Option<String>,
274 pub cpu_percent: f32,
275 pub cpu_count: usize,
276 pub mem_used_bytes: u64,
277 pub mem_total_bytes: u64,
278}
279
280#[derive(Debug, Clone, Default, Serialize, Deserialize)]
281pub struct Totals {
282 pub agents: usize,
283 pub running: usize,
284 pub idle: usize,
285 pub stopped: usize,
286 pub tokens: u64,
287 pub cost_usd: f64,
288 pub unpriced_tokens: u64,
289 pub processes: usize,
290 pub mcp_processes: usize,
291 pub orphaned_mcp: usize,
292 pub cpu_percent: f32,
293 pub rss_bytes: u64,
294}
295
296pub const SNAPSHOT_SCHEMA_VERSION: u32 = 1;
299
300#[derive(Debug, Clone, Serialize, Deserialize)]
302pub struct Snapshot {
303 pub schema_version: u32,
304 pub taken_at: SystemTime,
305 pub host: HostStats,
306 pub agents: Vec<Agent>,
307 pub orphans: Vec<ProcNode>,
309 pub totals: Totals,
310}
311
312impl Snapshot {
313 pub fn compute_totals(&mut self) {
314 let mut t = Totals::default();
315 for a in &self.agents {
316 t.agents += 1;
317 match a.state {
318 AgentState::Running => t.running += 1,
319 AgentState::Idle => t.idle += 1,
320 AgentState::Stopped => t.stopped += 1,
321 }
322 t.tokens += a.usage.total();
323 t.cost_usd += a.cost_usd;
324 t.unpriced_tokens += a.unpriced_tokens;
325 t.processes += a.process_count;
326 t.mcp_processes += a.mcp_count;
327 t.cpu_percent += a.cpu_percent;
328 t.rss_bytes += a.rss_bytes;
329 }
330 t.orphaned_mcp = self.orphans.len();
331 self.totals = t;
332 }
333}