1use serde::{Deserialize, Serialize};
7
8#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
10#[serde(rename_all = "snake_case")]
11pub enum LoopState {
12 #[default]
14 CallingLlm,
15 WaitingForOps,
17 DrainingEvents,
19 Cancelling,
21 ErrorRecovery,
23 Completed,
25}
26
27impl LoopState {
28 pub fn is_terminal(&self) -> bool {
30 matches!(self, Self::Completed)
31 }
32
33 pub fn is_waiting(&self) -> bool {
35 matches!(self, Self::CallingLlm | Self::WaitingForOps)
36 }
37}
38
39impl std::fmt::Display for LoopState {
40 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
41 match self {
42 Self::CallingLlm => write!(f, "calling_llm"),
43 Self::WaitingForOps => write!(f, "waiting_for_ops"),
44 Self::DrainingEvents => write!(f, "draining_events"),
45 Self::Cancelling => write!(f, "cancelling"),
46 Self::ErrorRecovery => write!(f, "error_recovery"),
47 Self::Completed => write!(f, "completed"),
48 }
49 }
50}
51
52#[cfg(test)]
53#[allow(clippy::unwrap_used, clippy::expect_used)]
54mod tests {
55 use super::*;
56 use crate::error::AgentError;
57
58 fn can_transition(from: &LoopState, next: &LoopState) -> bool {
59 use LoopState::{
60 CallingLlm, Cancelling, Completed, DrainingEvents, ErrorRecovery, WaitingForOps,
61 };
62
63 matches!(
64 (from, next),
65 (
66 CallingLlm,
67 WaitingForOps | DrainingEvents | Completed | ErrorRecovery | Cancelling
68 ) | (WaitingForOps, DrainingEvents | Cancelling)
69 | (
70 DrainingEvents | ErrorRecovery,
71 CallingLlm | Completed | Cancelling
72 )
73 | (Cancelling, Completed)
74 )
75 }
76
77 fn transition(state: &mut LoopState, next: LoopState) -> Result<(), AgentError> {
78 if can_transition(state, &next) {
79 *state = next;
80 Ok(())
81 } else {
82 Err(AgentError::InvalidStateTransition {
83 from: format!("{state:?}"),
84 to: format!("{next:?}"),
85 })
86 }
87 }
88
89 #[test]
90 fn test_state_is_terminal() {
91 assert!(LoopState::Completed.is_terminal());
92 assert!(!LoopState::CallingLlm.is_terminal());
93 assert!(!LoopState::WaitingForOps.is_terminal());
94 assert!(!LoopState::DrainingEvents.is_terminal());
95 assert!(!LoopState::Cancelling.is_terminal());
96 assert!(!LoopState::ErrorRecovery.is_terminal());
97 }
98
99 #[test]
100 fn test_state_is_waiting() {
101 assert!(LoopState::CallingLlm.is_waiting());
102 assert!(LoopState::WaitingForOps.is_waiting());
103 assert!(!LoopState::DrainingEvents.is_waiting());
104 assert!(!LoopState::Completed.is_waiting());
105 }
106
107 #[test]
108 fn test_valid_transitions_from_calling_llm() {
109 let state = LoopState::CallingLlm;
110 assert!(can_transition(&state, &LoopState::WaitingForOps));
111 assert!(can_transition(&state, &LoopState::DrainingEvents));
112 assert!(can_transition(&state, &LoopState::Completed));
113 assert!(can_transition(&state, &LoopState::ErrorRecovery));
114 assert!(can_transition(&state, &LoopState::Cancelling));
115
116 assert!(!can_transition(&state, &LoopState::CallingLlm));
117 }
118
119 #[test]
120 fn test_valid_transitions_from_waiting_for_ops() {
121 let state = LoopState::WaitingForOps;
122 assert!(can_transition(&state, &LoopState::DrainingEvents));
123 assert!(can_transition(&state, &LoopState::Cancelling));
124
125 assert!(!can_transition(&state, &LoopState::CallingLlm));
126 assert!(!can_transition(&state, &LoopState::Completed));
127 }
128
129 #[test]
130 fn test_valid_transitions_from_draining_events() {
131 let state = LoopState::DrainingEvents;
132 assert!(can_transition(&state, &LoopState::CallingLlm));
133 assert!(can_transition(&state, &LoopState::Completed));
134 assert!(can_transition(&state, &LoopState::Cancelling));
135
136 assert!(!can_transition(&state, &LoopState::WaitingForOps));
137 assert!(!can_transition(&state, &LoopState::ErrorRecovery));
138 }
139
140 #[test]
141 fn test_valid_transitions_from_cancelling() {
142 let state = LoopState::Cancelling;
143 assert!(can_transition(&state, &LoopState::Completed));
144
145 assert!(!can_transition(&state, &LoopState::CallingLlm));
146 assert!(!can_transition(&state, &LoopState::WaitingForOps));
147 }
148
149 #[test]
150 fn test_valid_transitions_from_error_recovery() {
151 let state = LoopState::ErrorRecovery;
152 assert!(can_transition(&state, &LoopState::CallingLlm));
153 assert!(can_transition(&state, &LoopState::Completed));
154 assert!(can_transition(&state, &LoopState::Cancelling));
155
156 assert!(!can_transition(&state, &LoopState::WaitingForOps));
157 assert!(!can_transition(&state, &LoopState::DrainingEvents));
158 }
159
160 #[test]
161 fn test_completed_is_terminal() {
162 let state = LoopState::Completed;
163
164 assert!(!can_transition(&state, &LoopState::CallingLlm));
165 assert!(!can_transition(&state, &LoopState::WaitingForOps));
166 assert!(!can_transition(&state, &LoopState::DrainingEvents));
167 assert!(!can_transition(&state, &LoopState::Cancelling));
168 assert!(!can_transition(&state, &LoopState::ErrorRecovery));
169 assert!(!can_transition(&state, &LoopState::Completed));
170 }
171
172 #[test]
173 fn test_state_transition_success() {
174 let mut state = LoopState::CallingLlm;
175 assert!(transition(&mut state, LoopState::DrainingEvents).is_ok());
176 assert_eq!(state, LoopState::DrainingEvents);
177
178 assert!(transition(&mut state, LoopState::CallingLlm).is_ok());
179 assert_eq!(state, LoopState::CallingLlm);
180 }
181
182 #[test]
183 fn test_state_transition_failure() {
184 let mut state = LoopState::Completed;
185 let result = transition(&mut state, LoopState::CallingLlm);
186 assert!(result.is_err());
187 assert!(matches!(
188 result.unwrap_err(),
189 AgentError::InvalidStateTransition { .. }
190 ));
191 }
192
193 #[test]
194 fn test_state_serialization() {
195 let states = vec![
196 LoopState::CallingLlm,
197 LoopState::WaitingForOps,
198 LoopState::DrainingEvents,
199 LoopState::Cancelling,
200 LoopState::ErrorRecovery,
201 LoopState::Completed,
202 ];
203
204 for state in states {
205 let json = serde_json::to_value(&state).unwrap();
206 let parsed: LoopState = serde_json::from_value(json).unwrap();
207 assert_eq!(state, parsed);
208 }
209 }
210
211 #[test]
212 fn test_full_happy_path() {
213 let mut state = LoopState::CallingLlm;
214 assert!(transition(&mut state, LoopState::DrainingEvents).is_ok());
215 assert!(transition(&mut state, LoopState::CallingLlm).is_ok());
216 assert!(transition(&mut state, LoopState::Completed).is_ok());
217 assert!(state.is_terminal());
218 }
219
220 #[test]
221 fn test_cancellation_path() {
222 let mut state = LoopState::CallingLlm;
223 assert!(transition(&mut state, LoopState::Cancelling).is_ok());
224 assert!(transition(&mut state, LoopState::Completed).is_ok());
225 assert!(state.is_terminal());
226 }
227
228 #[test]
229 fn test_error_recovery_path() {
230 let mut state = LoopState::CallingLlm;
231 assert!(transition(&mut state, LoopState::ErrorRecovery).is_ok());
232 assert!(transition(&mut state, LoopState::CallingLlm).is_ok());
233 assert!(transition(&mut state, LoopState::Completed).is_ok());
234 }
235}