agent_kernel/
lifecycle.rs

1//! Lifecycle state machine for MXP agents.
2
3use agent_primitives::AgentId;
4use thiserror::Error;
5use tracing::debug;
6
7/// Discretely states an agent can occupy during its lifetime.
8#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
9pub enum AgentState {
10    /// Kernel constructed but not yet initialized.
11    Init,
12    /// Dependencies are initialized and the agent is ready for activation.
13    Ready,
14    /// Agent is actively handling workloads.
15    Active,
16    /// Agent is temporarily paused but can resume.
17    Suspended,
18    /// Agent is draining in-flight work prior to shut down.
19    Retiring,
20    /// Agent fully terminated; no further work should be scheduled.
21    Terminated,
22}
23
24impl AgentState {
25    /// Returns `true` when the state represents a running agent.
26    #[must_use]
27    pub const fn is_active(self) -> bool {
28        matches!(self, Self::Active)
29    }
30
31    /// Returns `true` once the agent has terminated.
32    #[must_use]
33    pub const fn is_terminal(self) -> bool {
34        matches!(self, Self::Terminated)
35    }
36}
37
38/// Events that trigger lifecycle transitions.
39#[derive(Debug, Clone, Copy, PartialEq, Eq)]
40pub enum LifecycleEvent {
41    /// Finish bootstrapping resources.
42    Boot,
43    /// Begin processing workloads.
44    Activate,
45    /// Pause execution while retaining state.
46    Suspend,
47    /// Resume execution after a suspension.
48    Resume,
49    /// Initiate a graceful shutdown.
50    Retire,
51    /// Finalize shutdown after draining work.
52    Terminate,
53    /// Immediately abort the agent, forcing termination.
54    Abort,
55}
56
57/// Lifecycle state manager.
58#[derive(Debug, Clone, Copy)]
59pub struct Lifecycle {
60    agent_id: AgentId,
61    state: AgentState,
62}
63
64impl Lifecycle {
65    /// Constructs a lifecycle controller for the given agent.
66    #[must_use]
67    pub const fn new(agent_id: AgentId) -> Self {
68        Self {
69            agent_id,
70            state: AgentState::Init,
71        }
72    }
73
74    /// Returns the owning agent identifier.
75    #[must_use]
76    pub const fn agent_id(&self) -> AgentId {
77        self.agent_id
78    }
79
80    /// Returns the current state.
81    #[must_use]
82    pub const fn state(&self) -> AgentState {
83        self.state
84    }
85
86    /// Applies a lifecycle event, returning the resulting state.
87    ///
88    /// # Errors
89    ///
90    /// Returns [`LifecycleError::InvalidTransition`] when the supplied event is not
91    /// allowed from the current state.
92    pub fn transition(&mut self, event: LifecycleEvent) -> LifecycleResult<AgentState> {
93        let next = match (self.state, event) {
94            (AgentState::Init, LifecycleEvent::Boot) => Some(AgentState::Ready),
95            (AgentState::Ready, LifecycleEvent::Activate)
96            | (AgentState::Suspended, LifecycleEvent::Resume) => Some(AgentState::Active),
97            (
98                AgentState::Ready | AgentState::Active | AgentState::Suspended,
99                LifecycleEvent::Retire,
100            ) => Some(AgentState::Retiring),
101            (AgentState::Active, LifecycleEvent::Suspend) => Some(AgentState::Suspended),
102            (AgentState::Retiring | AgentState::Terminated, LifecycleEvent::Terminate)
103            | (_, LifecycleEvent::Abort) => Some(AgentState::Terminated),
104            _ => None,
105        };
106
107        let Some(next_state) = next else {
108            return Err(LifecycleError::InvalidTransition {
109                agent_id: self.agent_id,
110                from: self.state,
111                event,
112            });
113        };
114
115        if next_state != self.state {
116            debug!(
117                agent_id = %self.agent_id,
118                ?self.state,
119                ?next_state,
120                ?event,
121                "agent lifecycle transition"
122            );
123            self.state = next_state;
124        }
125
126        Ok(self.state)
127    }
128}
129
130/// Errors emitted by the lifecycle controller.
131#[derive(Debug, Error)]
132pub enum LifecycleError {
133    /// Transition was not permitted from the current state.
134    #[error("invalid lifecycle transition from {from:?} via {event:?} for agent {agent_id}")]
135    InvalidTransition {
136        /// Identifier of the agent whose transition failed.
137        agent_id: AgentId,
138        /// State prior to the attempted transition.
139        from: AgentState,
140        /// Event that triggered the failure.
141        event: LifecycleEvent,
142    },
143}
144
145/// Result alias used for lifecycle operations.
146pub type LifecycleResult<T> = Result<T, LifecycleError>;
147
148#[cfg(test)]
149mod tests {
150    use super::*;
151
152    fn new_id() -> AgentId {
153        AgentId::random()
154    }
155
156    #[test]
157    fn boot_to_active_flow() {
158        let agent_id = new_id();
159        let mut lifecycle = Lifecycle::new(agent_id);
160
161        assert_eq!(lifecycle.state(), AgentState::Init);
162        lifecycle.transition(LifecycleEvent::Boot).unwrap();
163        assert_eq!(lifecycle.state(), AgentState::Ready);
164        lifecycle.transition(LifecycleEvent::Activate).unwrap();
165        assert!(lifecycle.state().is_active());
166    }
167
168    #[test]
169    fn suspend_and_resume() {
170        let agent_id = new_id();
171        let mut lifecycle = Lifecycle::new(agent_id);
172
173        lifecycle.transition(LifecycleEvent::Boot).unwrap();
174        lifecycle.transition(LifecycleEvent::Activate).unwrap();
175        lifecycle.transition(LifecycleEvent::Suspend).unwrap();
176        assert_eq!(lifecycle.state(), AgentState::Suspended);
177        lifecycle.transition(LifecycleEvent::Resume).unwrap();
178        assert_eq!(lifecycle.state(), AgentState::Active);
179    }
180
181    #[test]
182    fn abort_is_global() {
183        let agent_id = new_id();
184        let mut lifecycle = Lifecycle::new(agent_id);
185
186        lifecycle.transition(LifecycleEvent::Abort).unwrap();
187        assert!(lifecycle.state().is_terminal());
188        // Further aborts keep the state terminal.
189        lifecycle.transition(LifecycleEvent::Abort).unwrap();
190        assert_eq!(lifecycle.state(), AgentState::Terminated);
191    }
192
193    #[test]
194    fn invalid_transition_errors() {
195        let agent_id = new_id();
196        let mut lifecycle = Lifecycle::new(agent_id);
197
198        let err = lifecycle
199            .transition(LifecycleEvent::Activate)
200            .expect_err("activate should fail from init");
201
202        matches!(err, LifecycleError::InvalidTransition { .. });
203    }
204}