agent_kernel/
lifecycle.rs1use agent_primitives::AgentId;
4use thiserror::Error;
5use tracing::debug;
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
9pub enum AgentState {
10 Init,
12 Ready,
14 Active,
16 Suspended,
18 Retiring,
20 Terminated,
22}
23
24impl AgentState {
25 #[must_use]
27 pub const fn is_active(self) -> bool {
28 matches!(self, Self::Active)
29 }
30
31 #[must_use]
33 pub const fn is_terminal(self) -> bool {
34 matches!(self, Self::Terminated)
35 }
36}
37
38#[derive(Debug, Clone, Copy, PartialEq, Eq)]
40pub enum LifecycleEvent {
41 Boot,
43 Activate,
45 Suspend,
47 Resume,
49 Retire,
51 Terminate,
53 Abort,
55}
56
57#[derive(Debug, Clone, Copy)]
59pub struct Lifecycle {
60 agent_id: AgentId,
61 state: AgentState,
62}
63
64impl Lifecycle {
65 #[must_use]
67 pub const fn new(agent_id: AgentId) -> Self {
68 Self {
69 agent_id,
70 state: AgentState::Init,
71 }
72 }
73
74 #[must_use]
76 pub const fn agent_id(&self) -> AgentId {
77 self.agent_id
78 }
79
80 #[must_use]
82 pub const fn state(&self) -> AgentState {
83 self.state
84 }
85
86 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#[derive(Debug, Error)]
132pub enum LifecycleError {
133 #[error("invalid lifecycle transition from {from:?} via {event:?} for agent {agent_id}")]
135 InvalidTransition {
136 agent_id: AgentId,
138 from: AgentState,
140 event: LifecycleEvent,
142 },
143}
144
145pub 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 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}