orcs_component/status.rs
1//! Status types for component state reporting.
2//!
3//! Components report their status via the [`Statusable`](super::Statusable) trait.
4//! This enables managers to monitor child component health and progress.
5//!
6//! # Status Lifecycle
7//!
8//! ```text
9//! Initializing → Idle ⇄ Running → Completed
10//! ↓ ↓
11//! Paused Error
12//! ↓ ↓
13//! Aborted ← ───┘
14//! ```
15//!
16//! # Example
17//!
18//! ```
19//! use orcs_component::{Status, StatusDetail, Progress};
20//!
21//! let status = Status::Running;
22//! let detail = StatusDetail {
23//! message: Some("Processing files...".into()),
24//! progress: Some(Progress {
25//! current: 42,
26//! total: Some(100),
27//! unit: Some("files".into()),
28//! }),
29//! metadata: Default::default(),
30//! };
31//!
32//! assert!(status.is_active());
33//! ```
34
35use serde::{Deserialize, Serialize};
36use std::collections::HashMap;
37
38/// Component execution status.
39///
40/// Represents the current state of a component or child.
41///
42/// # State Categories
43///
44/// | Category | States | Can Receive Work |
45/// |----------|--------|------------------|
46/// | Active | `Running`, `AwaitingApproval` | Yes (in progress) |
47/// | Ready | `Idle` | Yes |
48/// | Terminal | `Completed`, `Error`, `Aborted` | No |
49/// | Setup | `Initializing`, `Paused` | No (temporarily) |
50#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
51pub enum Status {
52 /// Component is initializing.
53 ///
54 /// Setup phase before accepting work.
55 #[default]
56 Initializing,
57
58 /// Component is idle, waiting for work.
59 ///
60 /// Ready to accept requests.
61 Idle,
62
63 /// Component is actively processing.
64 Running,
65
66 /// Component is paused (can resume).
67 ///
68 /// Temporarily stopped, can continue with Resume signal.
69 Paused,
70
71 /// Component is waiting for HIL approval.
72 ///
73 /// Blocked on human decision.
74 AwaitingApproval,
75
76 /// Component completed successfully.
77 ///
78 /// Terminal state - no more work will be done.
79 Completed,
80
81 /// Component encountered an error.
82 ///
83 /// Terminal state - may be recoverable with retry.
84 Error,
85
86 /// Component was aborted (by signal).
87 ///
88 /// Terminal state - explicitly stopped.
89 Aborted,
90}
91
92impl Status {
93 /// Returns `true` if the component is actively working.
94 ///
95 /// Active states: `Running`, `AwaitingApproval`
96 ///
97 /// # Example
98 ///
99 /// ```
100 /// use orcs_component::Status;
101 ///
102 /// assert!(Status::Running.is_active());
103 /// assert!(Status::AwaitingApproval.is_active());
104 /// assert!(!Status::Idle.is_active());
105 /// ```
106 #[must_use]
107 pub fn is_active(&self) -> bool {
108 matches!(self, Self::Running | Self::AwaitingApproval)
109 }
110
111 /// Returns `true` if the component is in a terminal state.
112 ///
113 /// Terminal states: `Completed`, `Error`, `Aborted`
114 ///
115 /// # Example
116 ///
117 /// ```
118 /// use orcs_component::Status;
119 ///
120 /// assert!(Status::Completed.is_terminal());
121 /// assert!(Status::Error.is_terminal());
122 /// assert!(Status::Aborted.is_terminal());
123 /// assert!(!Status::Running.is_terminal());
124 /// ```
125 #[must_use]
126 pub fn is_terminal(&self) -> bool {
127 matches!(self, Self::Completed | Self::Error | Self::Aborted)
128 }
129
130 /// Returns `true` if the component can accept new work.
131 ///
132 /// Ready state: `Idle`
133 ///
134 /// # Example
135 ///
136 /// ```
137 /// use orcs_component::Status;
138 ///
139 /// assert!(Status::Idle.is_ready());
140 /// assert!(!Status::Running.is_ready());
141 /// ```
142 #[must_use]
143 pub fn is_ready(&self) -> bool {
144 matches!(self, Self::Idle)
145 }
146}
147
148impl std::fmt::Display for Status {
149 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
150 match self {
151 Self::Initializing => write!(f, "initializing"),
152 Self::Idle => write!(f, "idle"),
153 Self::Running => write!(f, "running"),
154 Self::Paused => write!(f, "paused"),
155 Self::AwaitingApproval => write!(f, "awaiting_approval"),
156 Self::Completed => write!(f, "completed"),
157 Self::Error => write!(f, "error"),
158 Self::Aborted => write!(f, "aborted"),
159 }
160 }
161}
162
163/// Detailed status information.
164///
165/// Optional extended information for debugging and UI display.
166///
167/// # Example
168///
169/// ```
170/// use orcs_component::{StatusDetail, Progress};
171///
172/// let detail = StatusDetail {
173/// message: Some("Compiling crate...".into()),
174/// progress: Some(Progress {
175/// current: 5,
176/// total: Some(10),
177/// unit: Some("crates".into()),
178/// }),
179/// metadata: Default::default(),
180/// };
181/// ```
182#[derive(Debug, Clone, Default, Serialize, Deserialize)]
183pub struct StatusDetail {
184 /// Human-readable status message.
185 pub message: Option<String>,
186
187 /// Progress information (if applicable).
188 pub progress: Option<Progress>,
189
190 /// Additional metadata (for debugging/analytics).
191 pub metadata: HashMap<String, serde_json::Value>,
192}
193
194impl StatusDetail {
195 /// Creates a new [`StatusDetail`] with just a message.
196 ///
197 /// # Example
198 ///
199 /// ```
200 /// use orcs_component::StatusDetail;
201 ///
202 /// let detail = StatusDetail::with_message("Processing...");
203 /// assert_eq!(detail.message, Some("Processing...".into()));
204 /// ```
205 #[must_use]
206 pub fn with_message(message: impl Into<String>) -> Self {
207 Self {
208 message: Some(message.into()),
209 progress: None,
210 metadata: HashMap::new(),
211 }
212 }
213
214 /// Creates a new [`StatusDetail`] with progress.
215 ///
216 /// # Example
217 ///
218 /// ```
219 /// use orcs_component::{StatusDetail, Progress};
220 ///
221 /// let detail = StatusDetail::with_progress(Progress::new(5, Some(10)));
222 /// assert!(detail.progress.is_some());
223 /// ```
224 #[must_use]
225 pub fn with_progress(progress: Progress) -> Self {
226 Self {
227 message: None,
228 progress: Some(progress),
229 metadata: HashMap::new(),
230 }
231 }
232}
233
234/// Progress information for long-running operations.
235///
236/// # Example
237///
238/// ```
239/// use orcs_component::Progress;
240///
241/// // Determinate progress (known total)
242/// let progress = Progress::new(50, Some(100))
243/// .with_unit("files");
244/// assert_eq!(progress.percentage(), Some(50.0));
245///
246/// // Indeterminate progress (unknown total)
247/// let progress = Progress::new(42, None);
248/// assert_eq!(progress.percentage(), None);
249/// ```
250#[derive(Debug, Clone, Serialize, Deserialize)]
251pub struct Progress {
252 /// Current progress value.
253 pub current: u64,
254
255 /// Total value (if known).
256 pub total: Option<u64>,
257
258 /// Unit of measurement (e.g., "files", "tokens", "bytes").
259 pub unit: Option<String>,
260}
261
262impl Progress {
263 /// Creates a new [`Progress`].
264 ///
265 /// # Arguments
266 ///
267 /// * `current` - Current progress value
268 /// * `total` - Total value (None for indeterminate)
269 #[must_use]
270 pub fn new(current: u64, total: Option<u64>) -> Self {
271 Self {
272 current,
273 total,
274 unit: None,
275 }
276 }
277
278 /// Sets the unit of measurement.
279 #[must_use]
280 pub fn with_unit(mut self, unit: impl Into<String>) -> Self {
281 self.unit = Some(unit.into());
282 self
283 }
284
285 /// Returns the progress as a percentage (0.0 - 100.0).
286 ///
287 /// Returns `None` if total is unknown or zero.
288 ///
289 /// # Example
290 ///
291 /// ```
292 /// use orcs_component::Progress;
293 ///
294 /// let progress = Progress::new(25, Some(100));
295 /// assert_eq!(progress.percentage(), Some(25.0));
296 ///
297 /// let progress = Progress::new(42, None);
298 /// assert_eq!(progress.percentage(), None);
299 /// ```
300 #[must_use]
301 pub fn percentage(&self) -> Option<f64> {
302 self.total.and_then(|total| {
303 if total == 0 {
304 None
305 } else {
306 Some((self.current as f64 / total as f64) * 100.0)
307 }
308 })
309 }
310
311 /// Returns `true` if progress is complete.
312 ///
313 /// # Example
314 ///
315 /// ```
316 /// use orcs_component::Progress;
317 ///
318 /// let progress = Progress::new(100, Some(100));
319 /// assert!(progress.is_complete());
320 ///
321 /// let progress = Progress::new(50, Some(100));
322 /// assert!(!progress.is_complete());
323 /// ```
324 #[must_use]
325 pub fn is_complete(&self) -> bool {
326 self.total.is_some_and(|total| self.current >= total)
327 }
328}
329
330#[cfg(test)]
331mod tests {
332 use super::*;
333
334 #[test]
335 fn status_is_active() {
336 assert!(Status::Running.is_active());
337 assert!(Status::AwaitingApproval.is_active());
338 assert!(!Status::Idle.is_active());
339 assert!(!Status::Completed.is_active());
340 }
341
342 #[test]
343 fn status_is_terminal() {
344 assert!(Status::Completed.is_terminal());
345 assert!(Status::Error.is_terminal());
346 assert!(Status::Aborted.is_terminal());
347 assert!(!Status::Running.is_terminal());
348 assert!(!Status::Idle.is_terminal());
349 }
350
351 #[test]
352 fn status_is_ready() {
353 assert!(Status::Idle.is_ready());
354 assert!(!Status::Running.is_ready());
355 assert!(!Status::Initializing.is_ready());
356 }
357
358 #[test]
359 fn status_default() {
360 assert_eq!(Status::default(), Status::Initializing);
361 }
362
363 #[test]
364 fn status_display() {
365 assert_eq!(format!("{}", Status::Running), "running");
366 assert_eq!(format!("{}", Status::AwaitingApproval), "awaiting_approval");
367 }
368
369 #[test]
370 fn progress_percentage() {
371 let progress = Progress::new(50, Some(100));
372 assert_eq!(progress.percentage(), Some(50.0));
373
374 let progress = Progress::new(25, Some(100));
375 assert_eq!(progress.percentage(), Some(25.0));
376
377 let progress = Progress::new(42, None);
378 assert_eq!(progress.percentage(), None);
379
380 let progress = Progress::new(42, Some(0));
381 assert_eq!(progress.percentage(), None);
382 }
383
384 #[test]
385 fn progress_is_complete() {
386 assert!(Progress::new(100, Some(100)).is_complete());
387 assert!(Progress::new(150, Some(100)).is_complete());
388 assert!(!Progress::new(50, Some(100)).is_complete());
389 assert!(!Progress::new(50, None).is_complete());
390 }
391
392 #[test]
393 fn progress_with_unit() {
394 let progress = Progress::new(10, Some(100)).with_unit("files");
395 assert_eq!(progress.unit, Some("files".into()));
396 }
397
398 #[test]
399 fn status_detail_with_message() {
400 let detail = StatusDetail::with_message("Processing...");
401 assert_eq!(detail.message, Some("Processing...".into()));
402 assert!(detail.progress.is_none());
403 }
404
405 #[test]
406 fn status_detail_with_progress() {
407 let detail = StatusDetail::with_progress(Progress::new(5, Some(10)));
408 assert!(detail.message.is_none());
409 assert!(detail.progress.is_some());
410 }
411}