Skip to main content

ainl_mission/
lifecycle.rs

1//! [`MissionLifecycle`] — host implements persistence and side effects.
2
3use ainl_contracts::{Mission, MissionId, MissionState};
4use thiserror::Error;
5
6use crate::state_machine::{transition, StateMachineError};
7
8/// Host-facing lifecycle operations (implemented by kernel glue).
9pub trait MissionLifecycle {
10    /// Load the current mission record.
11    fn load_mission(&self, mission_id: &MissionId) -> Result<Mission, MissionLifecycleError>;
12
13    /// Persist an updated mission (state and timestamps).
14    fn save_mission(&self, mission: &Mission) -> Result<(), MissionLifecycleError>;
15
16    /// Apply `new_state` with validation and persistence.
17    fn set_state(
18        &self,
19        mission_id: &MissionId,
20        new_state: MissionState,
21    ) -> Result<MissionState, MissionLifecycleError> {
22        let mut mission = self.load_mission(mission_id)?;
23        let next = transition(mission.state, new_state).map_err(MissionLifecycleError::State)?;
24        mission.state = next;
25        self.save_mission(&mission)?;
26        Ok(next)
27    }
28
29    /// `AwaitingInput` → `Initializing`.
30    fn initialize(&self, mission_id: &MissionId) -> Result<MissionState, MissionLifecycleError> {
31        self.set_state(mission_id, MissionState::Initializing)
32    }
33
34    /// `Initializing` → `Running` (or resume path `Paused` → `Running`).
35    fn start_running(&self, mission_id: &MissionId) -> Result<MissionState, MissionLifecycleError> {
36        let mission = self.load_mission(mission_id)?;
37        let target = match mission.state {
38            MissionState::Initializing | MissionState::Paused | MissionState::OrchestratorTurn => {
39                MissionState::Running
40            }
41            other => other,
42        };
43        self.set_state(mission_id, target)
44    }
45
46    fn pause(&self, mission_id: &MissionId) -> Result<MissionState, MissionLifecycleError> {
47        self.set_state(mission_id, MissionState::Paused)
48    }
49
50    fn resume(&self, mission_id: &MissionId) -> Result<MissionState, MissionLifecycleError> {
51        self.set_state(mission_id, MissionState::Running)
52    }
53
54    fn enter_orchestrator_turn(
55        &self,
56        mission_id: &MissionId,
57    ) -> Result<MissionState, MissionLifecycleError> {
58        self.set_state(mission_id, MissionState::OrchestratorTurn)
59    }
60
61    fn complete(&self, mission_id: &MissionId) -> Result<MissionState, MissionLifecycleError> {
62        self.set_state(mission_id, MissionState::Completed)
63    }
64
65    fn cancel(&self, mission_id: &MissionId) -> Result<MissionState, MissionLifecycleError> {
66        self.set_state(mission_id, MissionState::Cancelled)
67    }
68}
69
70/// Lifecycle persistence or transition error.
71#[derive(Debug, Error)]
72pub enum MissionLifecycleError {
73    #[error("state machine: {0}")]
74    State(#[from] StateMachineError),
75    #[error("not found: {0}")]
76    NotFound(String),
77    #[error("persist: {0}")]
78    Persist(String),
79}
80
81#[cfg(test)]
82mod tests {
83    use super::*;
84    use ainl_contracts::MissionCapabilityFlags;
85    use chrono::Utc;
86    use std::cell::RefCell;
87    use std::collections::HashMap;
88
89    struct MemLifecycle {
90        missions: RefCell<HashMap<String, Mission>>,
91    }
92
93    impl MemLifecycle {
94        fn with_mission(state: MissionState) -> Self {
95            let m = Mission {
96                mission_id: MissionId("m1".into()),
97                objective_md: "test".into(),
98                state,
99                milestone_ids: vec![],
100                mission_root: None,
101                created_at: Utc::now(),
102                last_orchestrator_turn_at: None,
103                capability_flags: MissionCapabilityFlags::default(),
104            };
105            let mut map = HashMap::new();
106            map.insert("m1".into(), m);
107            Self {
108                missions: RefCell::new(map),
109            }
110        }
111    }
112
113    impl MissionLifecycle for MemLifecycle {
114        fn load_mission(&self, mission_id: &MissionId) -> Result<Mission, MissionLifecycleError> {
115            self.missions
116                .borrow()
117                .get(mission_id.as_str())
118                .cloned()
119                .ok_or_else(|| MissionLifecycleError::NotFound(mission_id.as_str().into()))
120        }
121
122        fn save_mission(&self, mission: &Mission) -> Result<(), MissionLifecycleError> {
123            self.missions
124                .borrow_mut()
125                .insert(mission.mission_id.as_str().into(), mission.clone());
126            Ok(())
127        }
128    }
129
130    #[test]
131    fn initialize_then_run() {
132        let lc = MemLifecycle::with_mission(MissionState::AwaitingInput);
133        let id = MissionId("m1".into());
134        assert_eq!(lc.initialize(&id).unwrap(), MissionState::Initializing);
135        assert_eq!(lc.start_running(&id).unwrap(), MissionState::Running);
136    }
137
138    #[test]
139    fn pause_resume() {
140        let lc = MemLifecycle::with_mission(MissionState::Running);
141        let id = MissionId("m1".into());
142        assert_eq!(lc.pause(&id).unwrap(), MissionState::Paused);
143        assert_eq!(lc.resume(&id).unwrap(), MissionState::Running);
144    }
145}