Skip to main content

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}