Skip to main content

agentic_workflow/engine/
fsm.rs

1use std::collections::HashMap;
2
3use chrono::Utc;
4use uuid::Uuid;
5
6use crate::types::{
7    State, StateMachine, Transition, TransitionRecord,
8    WorkflowError, WorkflowResult,
9};
10
11/// Finite state machine engine.
12pub struct FsmEngine {
13    machines: HashMap<String, StateMachine>,
14    history: HashMap<String, Vec<TransitionRecord>>,
15}
16
17impl FsmEngine {
18    pub fn new() -> Self {
19        Self {
20            machines: HashMap::new(),
21            history: HashMap::new(),
22        }
23    }
24
25    /// Create a new state machine.
26    pub fn create_fsm(
27        &mut self,
28        name: &str,
29        states: Vec<State>,
30        transitions: Vec<Transition>,
31        initial_state: &str,
32    ) -> WorkflowResult<String> {
33        // Validate initial state exists
34        if !states.iter().any(|s| s.name == initial_state) {
35            return Err(WorkflowError::InvalidTransition {
36                from: "".to_string(),
37                to: initial_state.to_string(),
38            });
39        }
40
41        let id = Uuid::new_v4().to_string();
42        let now = Utc::now();
43
44        let fsm = StateMachine {
45            id: id.clone(),
46            name: name.to_string(),
47            states,
48            transitions,
49            initial_state: initial_state.to_string(),
50            current_state: initial_state.to_string(),
51            context: HashMap::new(),
52            created_at: now,
53            updated_at: now,
54        };
55
56        self.machines.insert(id.clone(), fsm);
57        self.history.insert(id.clone(), Vec::new());
58        Ok(id)
59    }
60
61    /// Attempt a state transition.
62    pub fn transition(
63        &mut self,
64        fsm_id: &str,
65        event: &str,
66    ) -> WorkflowResult<String> {
67        let fsm = self
68            .machines
69            .get_mut(fsm_id)
70            .ok_or_else(|| WorkflowError::Internal(format!("FSM not found: {}", fsm_id)))?;
71
72        let current = fsm.current_state.clone();
73
74        // Find matching transition
75        let transition = fsm
76            .transitions
77            .iter()
78            .find(|t| t.from == current && t.event == event)
79            .ok_or_else(|| WorkflowError::InvalidTransition {
80                from: current.clone(),
81                to: format!("(event: {})", event),
82            })?
83            .clone();
84
85        let to_state = transition.to.clone();
86
87        // Record the transition
88        let record = TransitionRecord {
89            fsm_id: fsm_id.to_string(),
90            from_state: current.clone(),
91            to_state: to_state.clone(),
92            event: event.to_string(),
93            timestamp: Utc::now(),
94            context_snapshot: fsm.context.clone(),
95        };
96
97        // Apply the transition
98        fsm.current_state = to_state.clone();
99        fsm.updated_at = Utc::now();
100
101        self.history
102            .entry(fsm_id.to_string())
103            .or_default()
104            .push(record);
105
106        Ok(to_state)
107    }
108
109    /// Get current state.
110    pub fn current_state(&self, fsm_id: &str) -> WorkflowResult<&str> {
111        let fsm = self
112            .machines
113            .get(fsm_id)
114            .ok_or_else(|| WorkflowError::Internal(format!("FSM not found: {}", fsm_id)))?;
115
116        Ok(&fsm.current_state)
117    }
118
119    /// Get valid next transitions.
120    pub fn valid_next(&self, fsm_id: &str) -> WorkflowResult<Vec<&Transition>> {
121        let fsm = self
122            .machines
123            .get(fsm_id)
124            .ok_or_else(|| WorkflowError::Internal(format!("FSM not found: {}", fsm_id)))?;
125
126        Ok(fsm.valid_transitions())
127    }
128
129    /// Get transition history.
130    pub fn get_history(&self, fsm_id: &str) -> WorkflowResult<&[TransitionRecord]> {
131        self.history
132            .get(fsm_id)
133            .map(|v| v.as_slice())
134            .ok_or_else(|| WorkflowError::Internal(format!("FSM not found: {}", fsm_id)))
135    }
136
137    /// Get a state machine.
138    pub fn get_fsm(&self, fsm_id: &str) -> WorkflowResult<&StateMachine> {
139        self.machines
140            .get(fsm_id)
141            .ok_or_else(|| WorkflowError::Internal(format!("FSM not found: {}", fsm_id)))
142    }
143
144    /// Generate a Mermaid state diagram.
145    pub fn diagram(&self, fsm_id: &str) -> WorkflowResult<String> {
146        let fsm = self.get_fsm(fsm_id)?;
147        let mut lines = vec!["stateDiagram-v2".to_string()];
148
149        lines.push(format!("    [*] --> {}", fsm.initial_state));
150
151        for t in &fsm.transitions {
152            lines.push(format!("    {} --> {} : {}", t.from, t.to, t.event));
153        }
154
155        for state in &fsm.states {
156            if state.is_terminal {
157                lines.push(format!("    {} --> [*]", state.name));
158            }
159        }
160
161        Ok(lines.join("\n"))
162    }
163}
164
165impl Default for FsmEngine {
166    fn default() -> Self {
167        Self::new()
168    }
169}
170
171#[cfg(test)]
172mod tests {
173    use super::*;
174
175    fn order_states() -> (Vec<State>, Vec<Transition>) {
176        let states = vec![
177            State { name: "Created".into(), description: None, entry_action: None, exit_action: None, is_terminal: false },
178            State { name: "Paid".into(), description: None, entry_action: None, exit_action: None, is_terminal: false },
179            State { name: "Shipped".into(), description: None, entry_action: None, exit_action: None, is_terminal: false },
180            State { name: "Delivered".into(), description: None, entry_action: None, exit_action: None, is_terminal: true },
181        ];
182
183        let transitions = vec![
184            Transition { from: "Created".into(), to: "Paid".into(), event: "pay".into(), guard: None, action: None },
185            Transition { from: "Paid".into(), to: "Shipped".into(), event: "ship".into(), guard: None, action: None },
186            Transition { from: "Shipped".into(), to: "Delivered".into(), event: "deliver".into(), guard: None, action: None },
187        ];
188
189        (states, transitions)
190    }
191
192    #[test]
193    fn test_fsm_transitions() {
194        let mut engine = FsmEngine::new();
195        let (states, transitions) = order_states();
196        let fid = engine.create_fsm("order", states, transitions, "Created").unwrap();
197
198        assert_eq!(engine.current_state(&fid).unwrap(), "Created");
199        engine.transition(&fid, "pay").unwrap();
200        assert_eq!(engine.current_state(&fid).unwrap(), "Paid");
201        engine.transition(&fid, "ship").unwrap();
202        assert_eq!(engine.current_state(&fid).unwrap(), "Shipped");
203
204        // Invalid transition
205        assert!(engine.transition(&fid, "pay").is_err());
206    }
207}