Skip to main content

codex_helper_core/fleet/
model.rs

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}