Skip to main content

meerkat_core/
state.rs

1//! State projection for the agent loop.
2//!
3//! Canonical transition legality lives in `TurnExecutionAuthority`; this enum
4//! is the persisted/user-facing loop shape.
5
6use serde::{Deserialize, Serialize};
7
8/// States of the core agent loop
9#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
10#[serde(rename_all = "snake_case")]
11pub enum LoopState {
12    /// Waiting for LLM response
13    #[default]
14    CallingLlm,
15    /// No LLM work, waiting for operation completions
16    WaitingForOps,
17    /// Processing buffered operation events
18    DrainingEvents,
19    /// Cleanup on interrupt or budget exhaustion
20    Cancelling,
21    /// Retry logic for transient LLM failures
22    ErrorRecovery,
23    /// Terminal state
24    Completed,
25}
26
27impl LoopState {
28    /// Check if this is a terminal state
29    pub fn is_terminal(&self) -> bool {
30        matches!(self, Self::Completed)
31    }
32
33    /// Check if we're actively waiting for external input
34    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}