1use serde::{Deserialize, Serialize};
2
3use crate::usage::UsageMetrics;
4
5#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
6#[serde(rename_all = "snake_case")]
7pub enum FleetNodeKind {
8 #[default]
9 Local,
10 Remote,
11}
12
13#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
14#[serde(rename_all = "snake_case")]
15pub enum FleetNodeHealth {
16 #[default]
17 Fresh,
18 Stale,
19 AuthFailed,
20 RateLimited,
21 Unsupported,
22 Unreachable,
23 ParseFailed,
24}
25
26impl FleetNodeHealth {
27 pub fn is_current(self) -> bool {
28 matches!(self, FleetNodeHealth::Fresh)
29 }
30}
31
32#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
33#[serde(rename_all = "snake_case")]
34pub enum FleetWorkUnitKind {
35 #[default]
36 Root,
37 Subagent,
38 Process,
39}
40
41#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
42#[serde(rename_all = "snake_case")]
43pub enum FleetWorkUnitState {
44 #[default]
45 Unknown,
46 Running,
47 WaitingInput,
48 WaitingApproval,
49 Idle,
50 Interrupted,
51 Completed,
52 Errored,
53 Exited,
54}
55
56#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
57#[serde(rename_all = "snake_case")]
58pub enum FleetEvidenceSource {
59 RuntimeStatus,
60 SessionLog,
61 ProcessScan,
62 CachedSnapshot,
63 #[default]
64 Unavailable,
65}
66
67#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
68#[serde(rename_all = "snake_case")]
69pub enum FleetConfidence {
70 High,
71 Medium,
72 Low,
73 #[default]
74 Unknown,
75}
76
77#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
78pub struct FleetEvidence {
79 pub source: FleetEvidenceSource,
80 pub confidence: FleetConfidence,
81 #[serde(default, skip_serializing_if = "Option::is_none")]
82 pub detail: Option<String>,
83}
84
85impl FleetEvidence {
86 pub fn high(source: FleetEvidenceSource) -> Self {
87 Self {
88 source,
89 confidence: FleetConfidence::High,
90 detail: None,
91 }
92 }
93
94 pub fn with_detail(
95 source: FleetEvidenceSource,
96 confidence: FleetConfidence,
97 detail: impl Into<String>,
98 ) -> Self {
99 Self {
100 source,
101 confidence,
102 detail: Some(detail.into()),
103 }
104 }
105}
106
107impl Default for FleetEvidence {
108 fn default() -> Self {
109 Self {
110 source: FleetEvidenceSource::Unavailable,
111 confidence: FleetConfidence::Unknown,
112 detail: None,
113 }
114 }
115}
116
117#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
118pub struct FleetUsageSummary {
119 #[serde(default, skip_serializing_if = "Option::is_none")]
120 pub last_usage: Option<UsageMetrics>,
121 #[serde(default, skip_serializing_if = "Option::is_none")]
122 pub total_usage: Option<UsageMetrics>,
123 #[serde(default, skip_serializing_if = "Option::is_none")]
124 pub turns_total: Option<u64>,
125 #[serde(default, skip_serializing_if = "Option::is_none")]
126 pub turns_with_usage: Option<u64>,
127 #[serde(default, skip_serializing_if = "Option::is_none")]
128 pub last_output_tokens_per_second: Option<f64>,
129 #[serde(default, skip_serializing_if = "Option::is_none")]
130 pub avg_output_tokens_per_second: Option<f64>,
131}
132
133#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
134pub struct FleetWorkUnit {
135 pub node_id: String,
136 pub id: String,
137 #[serde(default, skip_serializing_if = "Option::is_none")]
138 pub parent_id: Option<String>,
139 pub kind: FleetWorkUnitKind,
140 pub state: FleetWorkUnitState,
141 pub evidence: FleetEvidence,
142 #[serde(default, skip_serializing_if = "Option::is_none")]
143 pub session_id: Option<String>,
144 #[serde(default, skip_serializing_if = "Option::is_none")]
145 pub local_thread_id: Option<String>,
146 #[serde(default, skip_serializing_if = "Option::is_none")]
147 pub task_name: Option<String>,
148 #[serde(default, skip_serializing_if = "Option::is_none")]
149 pub cwd: Option<String>,
150 #[serde(default, skip_serializing_if = "Option::is_none")]
151 pub model: Option<String>,
152 #[serde(default, skip_serializing_if = "Option::is_none")]
153 pub station_name: Option<String>,
154 #[serde(default, skip_serializing_if = "Option::is_none")]
155 pub provider_id: Option<String>,
156 #[serde(default, skip_serializing_if = "Option::is_none")]
157 pub last_status: Option<u16>,
158 #[serde(default, skip_serializing_if = "Option::is_none")]
159 pub active_started_at_ms: Option<u64>,
160 #[serde(default, skip_serializing_if = "Option::is_none")]
161 pub last_activity_ms: Option<u64>,
162 #[serde(default, skip_serializing_if = "Option::is_none")]
163 pub last_error: Option<String>,
164 #[serde(default)]
165 pub usage: FleetUsageSummary,
166}
167
168#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
169#[serde(rename_all = "snake_case")]
170pub enum FleetGraphStatus {
171 Available,
172 #[default]
173 Unavailable,
174 Partial,
175}
176
177#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
178#[serde(rename_all = "snake_case")]
179pub enum FleetEdgeStatus {
180 Open,
181 Closed,
182 #[default]
183 Unknown,
184}
185
186#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
187pub struct FleetSubagentEdge {
188 pub node_id: String,
189 pub parent_id: String,
190 pub child_id: String,
191 pub status: FleetEdgeStatus,
192 pub evidence: FleetEvidence,
193}
194
195#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
196pub struct FleetTopology {
197 pub status: FleetGraphStatus,
198 #[serde(default, skip_serializing_if = "Vec::is_empty")]
199 pub edges: Vec<FleetSubagentEdge>,
200 #[serde(default, skip_serializing_if = "Option::is_none")]
201 pub note: Option<String>,
202}
203
204#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
205pub struct FleetProcessSummary {
206 #[serde(default)]
207 pub scan_available: bool,
208 #[serde(default)]
209 pub codex_like_processes: usize,
210 #[serde(default, skip_serializing_if = "Option::is_none")]
211 pub error: Option<String>,
212}
213
214#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
215pub struct FleetNodeSnapshot {
216 pub node_id: String,
217 pub label: String,
218 pub kind: FleetNodeKind,
219 pub health: FleetNodeHealth,
220 pub refreshed_at_ms: u64,
221 #[serde(default, skip_serializing_if = "Option::is_none")]
222 pub stale_since_ms: Option<u64>,
223 #[serde(default, skip_serializing_if = "Option::is_none")]
224 pub snapshot_age_ms: Option<u64>,
225 #[serde(default, skip_serializing_if = "Option::is_none")]
226 pub active_endpoint: Option<String>,
227 #[serde(default, skip_serializing_if = "Option::is_none")]
228 pub last_error: Option<String>,
229 pub processes: FleetProcessSummary,
230 pub topology: FleetTopology,
231 #[serde(default)]
232 pub work_units: Vec<FleetWorkUnit>,
233}
234
235impl FleetNodeSnapshot {
236 pub fn current_work_units(&self) -> impl Iterator<Item = &FleetWorkUnit> {
237 self.work_units.iter().filter(|unit| {
238 matches!(
239 unit.state,
240 FleetWorkUnitState::Running
241 | FleetWorkUnitState::WaitingInput
242 | FleetWorkUnitState::WaitingApproval
243 )
244 })
245 }
246}
247
248#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
249pub struct FleetSnapshot {
250 pub api_version: u32,
251 pub service_name: String,
252 pub refreshed_at_ms: u64,
253 #[serde(default)]
254 pub nodes: Vec<FleetNodeSnapshot>,
255}
256
257impl FleetSnapshot {
258 pub fn active_work_units(&self) -> usize {
259 self.nodes
260 .iter()
261 .map(|node| node.current_work_units().count())
262 .sum()
263 }
264}
265
266pub fn now_ms() -> u64 {
267 std::time::SystemTime::now()
268 .duration_since(std::time::UNIX_EPOCH)
269 .map(|d| d.as_millis() as u64)
270 .unwrap_or(0)
271}