1use chrono::Utc;
2use parking_lot::RwLock;
3use std::sync::atomic::{AtomicU64, Ordering};
4
5use ai_agents_core::{AgentError, Result, StateMachineSnapshot, StateTransitionEvent};
6
7use super::config::{StateConfig, StateDefinition, ToolRef, Transition};
8
9pub struct StateMachine {
10 config: StateConfig,
11 state_guard: RwLock<()>,
13 generation: AtomicU64,
15 current: RwLock<String>,
16 previous: RwLock<Option<String>>,
17 turn_count: RwLock<u32>,
18 no_transition_count: RwLock<u32>,
19 history: RwLock<Vec<StateTransitionEvent>>,
20}
21
22impl StateMachine {
23 pub fn new(config: StateConfig) -> Result<Self> {
24 config.validate()?;
25 let initial = Self::resolve_initial_state(&config)?;
26 Ok(Self {
27 config,
28 state_guard: RwLock::new(()),
29 generation: AtomicU64::new(1),
30 current: RwLock::new(initial),
31 previous: RwLock::new(None),
32 turn_count: RwLock::new(0),
33 no_transition_count: RwLock::new(0),
34 history: RwLock::new(Vec::new()),
35 })
36 }
37
38 fn resolve_initial_state(config: &StateConfig) -> Result<String> {
39 let mut path = config.initial.clone();
40 let mut current_def = config.states.get(&config.initial);
41
42 while let Some(def) = current_def {
43 if let (Some(initial_sub), Some(sub_states)) = (&def.initial, &def.states) {
44 path = format!("{}.{}", path, initial_sub);
45 current_def = sub_states.get(initial_sub);
46 } else {
47 break;
48 }
49 }
50
51 Ok(path)
52 }
53
54 pub fn current(&self) -> String {
55 self.current.read().clone()
56 }
57
58 pub fn previous(&self) -> Option<String> {
59 self.previous.read().clone()
60 }
61
62 pub fn current_definition(&self) -> Option<StateDefinition> {
63 let current = self.current.read();
64 self.config.get_state(¤t).cloned()
65 }
66
67 pub fn get_definition(&self, state: &str) -> Option<&StateDefinition> {
68 self.config.get_state(state)
69 }
70
71 pub fn get_parent_definition(&self) -> Option<StateDefinition> {
72 let current = self.current.read();
73 let parts: Vec<&str> = current.split('.').collect();
74 if parts.len() <= 1 {
75 return None;
76 }
77 let parent_path = parts[..parts.len() - 1].join(".");
78 self.config.get_state(&parent_path).cloned()
79 }
80
81 pub fn current_tool_scope_snapshot(&self) -> (u64, Vec<Vec<ToolRef>>) {
83 let _guard = self.state_guard.read();
84 let current = self.current.read().clone();
85 let mut path = String::new();
86 let mut scopes = Vec::new();
87
88 for part in current.split('.') {
89 if !path.is_empty() {
90 path.push('.');
91 }
92 path.push_str(part);
93 if let Some(tools) = self
94 .config
95 .get_state(&path)
96 .and_then(|definition| definition.tools.clone())
97 {
98 scopes.push(tools);
99 }
100 }
101
102 (self.generation.load(Ordering::SeqCst), scopes)
103 }
104
105 pub fn generation(&self) -> u64 {
107 let _guard = self.state_guard.read();
108 self.generation.load(Ordering::SeqCst)
109 }
110
111 pub fn transition_to(&self, state: &str, reason: &str) -> Result<()> {
113 let _guard = self.state_guard.write();
114 let current_path = self.current.read().clone();
115 let resolved_path = self.config.resolve_full_path(¤t_path, state);
116
117 if self.config.get_state(&resolved_path).is_none() {
118 return Err(AgentError::InvalidSpec(format!(
119 "Unknown state: {} (resolved from {})",
120 resolved_path, state
121 )));
122 }
123
124 let final_path = self.resolve_to_leaf_state(&resolved_path)?;
125
126 let from = {
127 let mut current = self.current.write();
128 let mut previous = self.previous.write();
129 let from = current.clone();
130 *previous = Some(from.clone());
131 *current = final_path.clone();
132 from
133 };
134
135 *self.turn_count.write() = 0;
136 *self.no_transition_count.write() = 0;
137
138 let event = StateTransitionEvent {
139 from,
140 to: final_path,
141 reason: reason.to_string(),
142 timestamp: Utc::now(),
143 };
144 self.history.write().push(event);
145 self.generation.fetch_add(1, Ordering::SeqCst);
146
147 Ok(())
148 }
149
150 fn resolve_to_leaf_state(&self, path: &str) -> Result<String> {
151 let mut current_path = path.to_string();
152
153 loop {
154 let def = self.config.get_state(¤t_path).ok_or_else(|| {
155 AgentError::InvalidSpec(format!("State not found: {}", current_path))
156 })?;
157
158 if let (Some(initial_sub), Some(sub_states)) = (&def.initial, &def.states)
159 && sub_states.contains_key(initial_sub)
160 {
161 current_path = format!("{}.{}", current_path, initial_sub);
162 continue;
163 }
164 break;
165 }
166
167 Ok(current_path)
168 }
169
170 pub fn available_transitions(&self) -> Vec<Transition> {
171 let mut transitions = Vec::new();
172
173 if let Some(def) = self.current_definition() {
174 transitions.extend(def.transitions.clone());
175 }
176
177 transitions.extend(self.config.global_transitions.clone());
178
179 transitions.sort_by_key(|transition| std::cmp::Reverse(transition.priority));
180 transitions
181 }
182
183 pub fn auto_transitions(&self) -> Vec<Transition> {
184 self.available_transitions()
185 .into_iter()
186 .filter(|t| t.auto)
187 .collect()
188 }
189
190 pub fn history(&self) -> Vec<StateTransitionEvent> {
191 self.history.read().clone()
192 }
193
194 pub fn increment_turn(&self) {
195 let _guard = self.state_guard.write();
196 *self.turn_count.write() += 1;
197 }
198
199 pub fn turn_count(&self) -> u32 {
200 *self.turn_count.read()
201 }
202
203 pub fn increment_no_transition(&self) {
204 let _guard = self.state_guard.write();
205 *self.no_transition_count.write() += 1;
206 }
207
208 pub fn no_transition_count(&self) -> u32 {
209 *self.no_transition_count.read()
210 }
211
212 pub fn reset_no_transition(&self) {
213 let _guard = self.state_guard.write();
214 *self.no_transition_count.write() = 0;
215 }
216
217 pub fn check_fallback(&self) -> Option<String> {
218 if let Some(max) = self.config.max_no_transition
219 && self.no_transition_count() >= max
220 {
221 return self.config.fallback.clone();
222 }
223 None
224 }
225
226 pub fn reset(&self) {
228 let _guard = self.state_guard.write();
229 let initial =
230 Self::resolve_initial_state(&self.config).unwrap_or(self.config.initial.clone());
231 let changed = *self.current.read() != initial
232 || self.previous.read().is_some()
233 || *self.turn_count.read() != 0
234 || *self.no_transition_count.read() != 0
235 || !self.history.read().is_empty();
236 if !changed {
237 return;
238 }
239
240 *self.current.write() = initial;
241 *self.previous.write() = None;
242 *self.turn_count.write() = 0;
243 *self.no_transition_count.write() = 0;
244 self.history.write().clear();
245 self.generation.fetch_add(1, Ordering::SeqCst);
246 }
247
248 pub fn snapshot(&self) -> StateMachineSnapshot {
250 let _guard = self.state_guard.read();
251 StateMachineSnapshot {
252 current_state: self.current.read().clone(),
253 previous_state: self.previous.read().clone(),
254 turn_count: *self.turn_count.read(),
255 no_transition_count: *self.no_transition_count.read(),
256 history: self.history.read().clone(),
257 }
258 }
259
260 pub fn restore(&self, snapshot: StateMachineSnapshot) -> Result<()> {
262 if self.config.get_state(&snapshot.current_state).is_none() {
263 return Err(AgentError::InvalidSpec(format!(
264 "Snapshot contains unknown state: {}",
265 snapshot.current_state
266 )));
267 }
268
269 let _guard = self.state_guard.write();
270 let current_history = self.history.read();
271 let history_matches = current_history.len() == snapshot.history.len()
272 && current_history
273 .iter()
274 .zip(&snapshot.history)
275 .all(|(left, right)| {
276 left.from == right.from
277 && left.to == right.to
278 && left.reason == right.reason
279 && left.timestamp == right.timestamp
280 });
281 let changed = *self.current.read() != snapshot.current_state
282 || *self.previous.read() != snapshot.previous_state
283 || *self.turn_count.read() != snapshot.turn_count
284 || *self.no_transition_count.read() != snapshot.no_transition_count
285 || !history_matches;
286 drop(current_history);
287 if !changed {
288 return Ok(());
289 }
290
291 *self.current.write() = snapshot.current_state;
292 *self.previous.write() = snapshot.previous_state;
293 *self.turn_count.write() = snapshot.turn_count;
294 *self.no_transition_count.write() = snapshot.no_transition_count;
295 *self.history.write() = snapshot.history;
296 self.generation.fetch_add(1, Ordering::SeqCst);
297 Ok(())
298 }
299
300 pub fn config(&self) -> &StateConfig {
301 &self.config
302 }
303
304 pub fn check_timeout(&self) -> Option<String> {
305 let def = self.current_definition()?;
306 let max_turns = def.max_turns?;
307 let timeout_to = def.timeout_to.as_ref()?;
308 if self.turn_count() >= max_turns {
309 let current_path = self.current.read().clone();
310 Some(self.config.resolve_full_path(¤t_path, timeout_to))
311 } else {
312 None
313 }
314 }
315
316 pub fn current_depth(&self) -> usize {
317 self.current.read().split('.').count()
318 }
319
320 pub fn is_in_sub_state(&self) -> bool {
321 self.current_depth() > 1
322 }
323
324 pub fn parent_state(&self) -> Option<String> {
325 let current = self.current.read();
326 let parts: Vec<&str> = current.split('.').collect();
327 if parts.len() > 1 {
328 Some(parts[..parts.len() - 1].join("."))
329 } else {
330 None
331 }
332 }
333
334 pub fn root_state(&self) -> String {
335 let current = self.current.read();
336 current.split('.').next().unwrap_or(¤t).to_string()
337 }
338
339 pub fn is_on_cooldown(&self, target: &str, cooldown_turns: u32) -> bool {
341 let history = self.history.read();
342 let total_transitions = history.len();
343 if total_transitions == 0 || cooldown_turns == 0 {
344 return false;
345 }
346 let start = total_transitions.saturating_sub(cooldown_turns as usize);
348 history[start..].iter().any(|e| e.to == target)
349 }
350}
351
352#[cfg(test)]
353mod tests {
354 use super::super::config::TransitionTiming;
355 use super::*;
356 use std::collections::HashMap;
357
358 fn create_test_config() -> StateConfig {
359 let mut states = HashMap::new();
360 states.insert(
361 "greeting".into(),
362 StateDefinition {
363 prompt: Some("Welcome!".into()),
364 transitions: vec![Transition {
365 to: "support".into(),
366 when: "needs help".into(),
367 guard: None,
368 intent: None,
369 auto: true,
370 priority: 10,
371 cooldown_turns: None,
372 timing: TransitionTiming::PostResponse,
373 requires_response: false,
374 run_extractors: false,
375 }],
376 ..Default::default()
377 },
378 );
379 states.insert(
380 "support".into(),
381 StateDefinition {
382 prompt: Some("How can I help?".into()),
383 max_turns: Some(3),
384 timeout_to: Some("escalation".into()),
385 ..Default::default()
386 },
387 );
388 states.insert(
389 "escalation".into(),
390 StateDefinition {
391 prompt: Some("Escalating...".into()),
392 ..Default::default()
393 },
394 );
395 StateConfig {
396 initial: "greeting".into(),
397 states,
398 global_transitions: vec![],
399 fallback: None,
400 max_no_transition: None,
401 regenerate_on_transition: true,
402 }
403 }
404
405 fn create_hierarchical_config() -> StateConfig {
406 let mut sub_states = HashMap::new();
407 sub_states.insert(
408 "gathering_info".into(),
409 StateDefinition {
410 prompt: Some("Gathering info".into()),
411 transitions: vec![Transition {
412 to: "proposing".into(),
413 when: "understood".into(),
414 guard: None,
415 intent: None,
416 auto: true,
417 priority: 0,
418 cooldown_turns: None,
419 timing: TransitionTiming::PostResponse,
420 requires_response: false,
421 run_extractors: false,
422 }],
423 ..Default::default()
424 },
425 );
426 sub_states.insert(
427 "proposing".into(),
428 StateDefinition {
429 prompt: Some("Proposing solution".into()),
430 transitions: vec![Transition {
431 to: "^closing".into(),
432 when: "resolved".into(),
433 guard: None,
434 intent: None,
435 auto: true,
436 priority: 0,
437 cooldown_turns: None,
438 timing: TransitionTiming::PostResponse,
439 requires_response: false,
440 run_extractors: false,
441 }],
442 ..Default::default()
443 },
444 );
445
446 let mut states = HashMap::new();
447 states.insert(
448 "problem_solving".into(),
449 StateDefinition {
450 prompt: Some("Problem solving".into()),
451 initial: Some("gathering_info".into()),
452 states: Some(sub_states),
453 ..Default::default()
454 },
455 );
456 states.insert(
457 "closing".into(),
458 StateDefinition {
459 prompt: Some("Thank you".into()),
460 ..Default::default()
461 },
462 );
463
464 StateConfig {
465 initial: "problem_solving".into(),
466 states,
467 global_transitions: vec![],
468 fallback: None,
469 max_no_transition: None,
470 regenerate_on_transition: true,
471 }
472 }
473
474 #[test]
475 fn test_new_state_machine() {
476 let config = create_test_config();
477 let sm = StateMachine::new(config).unwrap();
478 assert_eq!(sm.current(), "greeting");
479 assert!(sm.previous().is_none());
480 assert_eq!(sm.turn_count(), 0);
481 }
482
483 #[test]
484 fn test_transition() {
485 let config = create_test_config();
486 let sm = StateMachine::new(config).unwrap();
487 sm.transition_to("support", "user asked for help").unwrap();
488 assert_eq!(sm.current(), "support");
489 assert_eq!(sm.previous(), Some("greeting".into()));
490 assert_eq!(sm.history().len(), 1);
491 }
492
493 #[test]
494 fn test_turn_counting() {
495 let config = create_test_config();
496 let sm = StateMachine::new(config).unwrap();
497 assert_eq!(sm.turn_count(), 0);
498 sm.increment_turn();
499 sm.increment_turn();
500 assert_eq!(sm.turn_count(), 2);
501 sm.transition_to("support", "reason").unwrap();
502 assert_eq!(sm.turn_count(), 0);
503 }
504
505 #[test]
506 fn test_timeout_check() {
507 let config = create_test_config();
508 let sm = StateMachine::new(config).unwrap();
509 sm.transition_to("support", "needs help").unwrap();
510 assert!(sm.check_timeout().is_none());
511 sm.increment_turn();
512 sm.increment_turn();
513 sm.increment_turn();
514 assert_eq!(sm.check_timeout(), Some("escalation".into()));
515 }
516
517 #[test]
518 fn test_snapshot_restore() {
519 let config = create_test_config();
520 let sm = StateMachine::new(config.clone()).unwrap();
521 sm.transition_to("support", "reason").unwrap();
522 sm.increment_turn();
523
524 let snapshot = sm.snapshot();
525 assert_eq!(snapshot.current_state, "support");
526 assert_eq!(snapshot.turn_count, 1);
527
528 let sm2 = StateMachine::new(config).unwrap();
529 sm2.restore(snapshot).unwrap();
530 assert_eq!(sm2.current(), "support");
531 assert_eq!(sm2.turn_count(), 1);
532 }
533
534 #[test]
535 fn test_reset() {
536 let config = create_test_config();
537 let sm = StateMachine::new(config).unwrap();
538 sm.transition_to("support", "reason").unwrap();
539 sm.increment_turn();
540 sm.reset();
541 assert_eq!(sm.current(), "greeting");
542 assert!(sm.previous().is_none());
543 assert_eq!(sm.turn_count(), 0);
544 assert!(sm.history().is_empty());
545 }
546
547 #[test]
548 fn generation_tracks_only_successful_transition_reset_and_restore_changes() {
549 let sm = StateMachine::new(create_test_config()).unwrap();
550 let initial_generation = sm.generation();
551 let initial_snapshot = sm.snapshot();
552
553 sm.reset();
554 sm.restore(initial_snapshot.clone()).unwrap();
555 assert_eq!(sm.generation(), initial_generation);
556
557 assert!(sm.transition_to("missing", "invalid").is_err());
558 assert_eq!(sm.generation(), initial_generation);
559
560 sm.transition_to("support", "valid").unwrap();
561 let transition_generation = sm.generation();
562 assert_eq!(transition_generation, initial_generation + 1);
563 let transitioned_snapshot = sm.snapshot();
564
565 sm.restore(transitioned_snapshot.clone()).unwrap();
566 assert_eq!(sm.generation(), transition_generation);
567 sm.increment_turn();
568 assert_eq!(sm.generation(), transition_generation);
569
570 sm.reset();
571 let reset_generation = sm.generation();
572 assert_eq!(reset_generation, transition_generation + 1);
573 sm.reset();
574 assert_eq!(sm.generation(), reset_generation);
575
576 sm.restore(transitioned_snapshot).unwrap();
577 assert_eq!(sm.generation(), reset_generation + 1);
578 sm.restore(initial_snapshot).unwrap();
579 assert_eq!(sm.generation(), reset_generation + 2);
580 }
581
582 #[test]
583 fn current_tool_scope_snapshot_keeps_complete_ancestor_chain() {
584 let yaml = r#"
585initial: root
586states:
587 root:
588 tools: [root_tool]
589 initial: middle
590 states:
591 middle:
592 initial: leaf
593 states:
594 leaf:
595 tools: []
596"#;
597 let config: StateConfig = serde_yaml::from_str(yaml).unwrap();
598 let sm = StateMachine::new(config).unwrap();
599
600 let (generation, scopes) = sm.current_tool_scope_snapshot();
601
602 assert_eq!(generation, sm.generation());
603 assert_eq!(scopes.len(), 2);
604 assert_eq!(scopes[0][0].id(), "root_tool");
605 assert!(scopes[1].is_empty());
606 }
607
608 #[test]
609 fn test_hierarchical_initial_state() {
610 let config = create_hierarchical_config();
611 let sm = StateMachine::new(config).unwrap();
612 assert_eq!(sm.current(), "problem_solving.gathering_info");
613 }
614
615 #[test]
616 fn test_hierarchical_transition_sibling() {
617 let config = create_hierarchical_config();
618 let sm = StateMachine::new(config).unwrap();
619 assert_eq!(sm.current(), "problem_solving.gathering_info");
620
621 sm.transition_to("proposing", "understood").unwrap();
622 assert_eq!(sm.current(), "problem_solving.proposing");
623 }
624
625 #[test]
626 fn test_hierarchical_transition_parent() {
627 let config = create_hierarchical_config();
628 let sm = StateMachine::new(config).unwrap();
629 sm.transition_to("proposing", "understood").unwrap();
630 sm.transition_to("^closing", "resolved").unwrap();
631 assert_eq!(sm.current(), "closing");
632 }
633
634 #[test]
635 fn test_current_depth() {
636 let config = create_hierarchical_config();
637 let sm = StateMachine::new(config).unwrap();
638 assert_eq!(sm.current_depth(), 2);
639 assert!(sm.is_in_sub_state());
640
641 sm.transition_to("^closing", "done").unwrap();
642 assert_eq!(sm.current_depth(), 1);
643 assert!(!sm.is_in_sub_state());
644 }
645
646 #[test]
647 fn test_parent_state() {
648 let config = create_hierarchical_config();
649 let sm = StateMachine::new(config).unwrap();
650 assert_eq!(sm.parent_state(), Some("problem_solving".into()));
651
652 sm.transition_to("^closing", "done").unwrap();
653 assert!(sm.parent_state().is_none());
654 }
655
656 #[test]
657 fn test_root_state() {
658 let config = create_hierarchical_config();
659 let sm = StateMachine::new(config).unwrap();
660 assert_eq!(sm.root_state(), "problem_solving");
661
662 sm.transition_to("^closing", "done").unwrap();
663 assert_eq!(sm.root_state(), "closing");
664 }
665
666 #[test]
667 fn test_no_transition_count() {
668 let config = create_test_config();
669 let sm = StateMachine::new(config).unwrap();
670
671 assert_eq!(sm.no_transition_count(), 0);
672 sm.increment_no_transition();
673 sm.increment_no_transition();
674 assert_eq!(sm.no_transition_count(), 2);
675
676 sm.reset_no_transition();
677 assert_eq!(sm.no_transition_count(), 0);
678 }
679
680 #[test]
681 fn test_fallback() {
682 let mut config = create_test_config();
683 config.fallback = Some("escalation".into());
684 config.max_no_transition = Some(3);
685
686 let sm = StateMachine::new(config).unwrap();
687 assert!(sm.check_fallback().is_none());
688
689 sm.increment_no_transition();
690 sm.increment_no_transition();
691 sm.increment_no_transition();
692 assert_eq!(sm.check_fallback(), Some("escalation".into()));
693 }
694
695 #[test]
696 fn test_global_transitions() {
697 let mut config = create_test_config();
698 config.global_transitions = vec![Transition {
699 to: "escalation".into(),
700 when: "user is angry".into(),
701 guard: None,
702 intent: None,
703 auto: true,
704 priority: 100,
705 cooldown_turns: None,
706 timing: TransitionTiming::PostResponse,
707 requires_response: false,
708 run_extractors: false,
709 }];
710
711 let sm = StateMachine::new(config).unwrap();
712 let transitions = sm.available_transitions();
713
714 assert!(
715 transitions
716 .iter()
717 .any(|t| t.to == "escalation" && t.priority == 100)
718 );
719 assert_eq!(transitions[0].to, "escalation");
720 }
721
722 #[test]
723 fn test_get_parent_definition() {
724 let config = create_hierarchical_config();
725 let sm = StateMachine::new(config).unwrap();
726
727 let parent = sm.get_parent_definition();
728 assert!(parent.is_some());
729 assert_eq!(parent.unwrap().prompt, Some("Problem solving".into()));
730
731 sm.transition_to("^closing", "done").unwrap();
732 assert!(sm.get_parent_definition().is_none());
733 }
734}