Skip to main content

oris_kernel/kernel/
execution_suspension.rs

1//! Execution suspension state machine: handle worker teardown when execution is paused.
2//!
3//! This module defines [ExecutionSuspensionState] for tracking execution lifecycle:
4//! - `Running`: normal execution.
5//! - `Suspended`: execution paused, worker preparing to exit.
6//! - `WaitingInput`: waiting for external input (human, tool, policy).
7
8use serde::{Deserialize, Serialize};
9
10use crate::kernel::identity::RunId;
11
12/// Execution suspension state.
13#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
14pub enum ExecutionSuspensionState {
15    /// Normal execution.
16    Running,
17    /// Execution paused, worker preparing to exit and release resources.
18    Suspended,
19    /// Waiting for external input (human, tool, policy).
20    WaitingInput,
21}
22
23impl Default for ExecutionSuspensionState {
24    fn default() -> Self {
25        ExecutionSuspensionState::Running
26    }
27}
28
29/// Execution suspension: tracks the state of execution suspension.
30#[derive(Clone, Debug, Serialize, Deserialize)]
31pub struct ExecutionSuspension {
32    /// Run this suspension belongs to.
33    pub run_id: RunId,
34    /// Current state.
35    pub state: ExecutionSuspensionState,
36    /// When the state last changed.
37    pub state_changed_at: chrono::DateTime<chrono::Utc>,
38    /// Optional reason for the suspension.
39    pub reason: Option<String>,
40}
41
42impl ExecutionSuspension {
43    /// Creates a new execution suspension in Running state.
44    pub fn new(run_id: RunId) -> Self {
45        Self {
46            run_id,
47            state: ExecutionSuspensionState::Running,
48            state_changed_at: chrono::Utc::now(),
49            reason: None,
50        }
51    }
52
53    /// Transition from Running to Suspended.
54    pub fn suspend(&mut self, reason: Option<String>) -> Result<(), SuspensionError> {
55        if self.state != ExecutionSuspensionState::Running {
56            return Err(SuspensionError::InvalidTransition {
57                from: self.state.clone(),
58                to: "Suspended".into(),
59            });
60        }
61        self.state = ExecutionSuspensionState::Suspended;
62        self.state_changed_at = chrono::Utc::now();
63        self.reason = reason;
64        Ok(())
65    }
66
67    /// Transition from Suspended to WaitingInput.
68    pub fn wait_input(&mut self) -> Result<(), SuspensionError> {
69        if self.state != ExecutionSuspensionState::Suspended {
70            return Err(SuspensionError::InvalidTransition {
71                from: self.state.clone(),
72                to: "WaitingInput".into(),
73            });
74        }
75        self.state = ExecutionSuspensionState::WaitingInput;
76        self.state_changed_at = chrono::Utc::now();
77        Ok(())
78    }
79
80    /// Transition from WaitingInput to Running (resume).
81    pub fn resume(&mut self) -> Result<(), SuspensionError> {
82        if self.state != ExecutionSuspensionState::WaitingInput {
83            return Err(SuspensionError::InvalidTransition {
84                from: self.state.clone(),
85                to: "Running".into(),
86            });
87        }
88        self.state = ExecutionSuspensionState::Running;
89        self.state_changed_at = chrono::Utc::now();
90        self.reason = None;
91        Ok(())
92    }
93
94    /// Check if currently in Running state.
95    pub fn is_running(&self) -> bool {
96        self.state == ExecutionSuspensionState::Running
97    }
98
99    /// Check if currently suspended or waiting for input.
100    pub fn is_suspended(&self) -> bool {
101        matches!(
102            self.state,
103            ExecutionSuspensionState::Suspended | ExecutionSuspensionState::WaitingInput
104        )
105    }
106}
107
108/// Errors for suspension operations.
109#[derive(Debug, thiserror::Error)]
110pub enum SuspensionError {
111    #[error("Invalid state transition from {from:?} to {to}")]
112    InvalidTransition {
113        from: ExecutionSuspensionState,
114        to: String,
115    },
116}
117
118#[cfg(test)]
119mod tests {
120    use super::*;
121
122    #[test]
123    fn new_suspension_is_running() {
124        let susp = ExecutionSuspension::new("run-1".into());
125        assert!(susp.is_running());
126        assert!(!susp.is_suspended());
127    }
128
129    #[test]
130    fn running_to_suspended_transition() {
131        let mut susp = ExecutionSuspension::new("run-1".into());
132        susp.suspend(Some("user requested".into())).unwrap();
133        assert!(!susp.is_running());
134        assert!(susp.is_suspended());
135        assert_eq!(susp.state, ExecutionSuspensionState::Suspended);
136    }
137
138    #[test]
139    fn suspended_to_waiting_input() {
140        let mut susp = ExecutionSuspension::new("run-1".into());
141        susp.suspend(None).unwrap();
142        susp.wait_input().unwrap();
143        assert_eq!(susp.state, ExecutionSuspensionState::WaitingInput);
144    }
145
146    #[test]
147    fn waiting_input_to_running_resume() {
148        let mut susp = ExecutionSuspension::new("run-1".into());
149        susp.suspend(None).unwrap();
150        susp.wait_input().unwrap();
151        susp.resume().unwrap();
152        assert!(susp.is_running());
153    }
154
155    #[test]
156    fn invalid_transition_running_to_waiting() {
157        let mut susp = ExecutionSuspension::new("run-1".into());
158        let err = susp.wait_input().unwrap_err();
159        println!("Error: {:?}", err);
160        assert!(err.to_string().contains("Invalid state transition"));
161    }
162}