Skip to main content

enact_core/kernel/
replay.rs

1//! Replay - Re-execute from event log
2//!
3//! Replay enables:
4//! - Debugging: re-run an execution to understand what happened
5//! - Testing: verify determinism by replaying
6//! - Recovery: resume from a checkpoint
7//!
8//! Replay requires:
9//! - Matching schema version
10//! - Ordered event log
11
12use super::execution_model::Execution;
13use super::ids::ExecutionId;
14use super::reducer::{reduce, ExecutionAction, ReducerError};
15
16/// Replay an execution from an action log
17///
18/// This will recreate the execution state by applying each action
19/// through the reducer, ensuring deterministic reconstruction.
20pub fn replay(
21    execution_id: ExecutionId,
22    actions: Vec<ExecutionAction>,
23    schema_version: Option<&str>,
24) -> Result<Execution, ReplayError> {
25    let mut execution = Execution::with_id(execution_id);
26
27    // Set schema version for validation
28    if let Some(version) = schema_version {
29        execution.schema_version = Some(version.to_string());
30    }
31
32    // Apply each action through the reducer
33    for (index, action) in actions.into_iter().enumerate() {
34        reduce(&mut execution, action)
35            .map_err(|e| ReplayError::ReducerError { index, error: e })?;
36    }
37
38    Ok(execution)
39}
40
41/// Replay error
42#[derive(Debug, thiserror::Error)]
43pub enum ReplayError {
44    #[error("Reducer error at action {index}: {error}")]
45    ReducerError { index: usize, error: ReducerError },
46    #[error("Schema version mismatch: expected {expected}, got {actual}")]
47    SchemaVersionMismatch { expected: String, actual: String },
48}
49
50/// Event log for replay
51///
52/// This is a simple in-memory log. In production, you'd persist this
53/// to a database or event store.
54#[derive(Debug, Default)]
55pub struct EventLog {
56    /// Ordered list of actions
57    actions: Vec<ExecutionAction>,
58}
59
60impl EventLog {
61    /// Create a new event log
62    pub fn new() -> Self {
63        Self::default()
64    }
65
66    /// Append an action to the log
67    pub fn append(&mut self, action: ExecutionAction) {
68        self.actions.push(action);
69    }
70
71    /// Get all actions
72    pub fn actions(&self) -> &[ExecutionAction] {
73        &self.actions
74    }
75
76    /// Take all actions (consumes the log)
77    pub fn into_actions(self) -> Vec<ExecutionAction> {
78        self.actions
79    }
80
81    /// Get action count
82    pub fn len(&self) -> usize {
83        self.actions.len()
84    }
85
86    /// Check if empty
87    pub fn is_empty(&self) -> bool {
88        self.actions.is_empty()
89    }
90}
91
92#[cfg(test)]
93mod tests {
94    use super::super::ids::StepType;
95    use super::*;
96
97    #[test]
98    fn test_replay_simple_execution() {
99        let exec_id = ExecutionId::new();
100
101        let actions = vec![
102            ExecutionAction::Start,
103            ExecutionAction::Complete {
104                output: Some("done".into()),
105            },
106        ];
107
108        let execution = replay(exec_id.clone(), actions, None).unwrap();
109
110        assert_eq!(execution.id.as_str(), exec_id.as_str());
111        assert!(execution.state.is_terminal());
112        assert_eq!(execution.output, Some("done".to_string()));
113    }
114
115    #[test]
116    fn test_replay_with_steps() {
117        let exec_id = ExecutionId::new();
118        let step_id = super::super::ids::StepId::new();
119
120        let actions = vec![
121            ExecutionAction::Start,
122            ExecutionAction::StepStarted {
123                step_id: step_id.clone(),
124                parent_step_id: None,
125                step_type: StepType::LlmNode,
126                name: "test step".into(),
127                source: None,
128            },
129            ExecutionAction::StepCompleted {
130                step_id: step_id.clone(),
131                output: Some("step output".into()),
132                duration_ms: 100,
133            },
134            ExecutionAction::Complete {
135                output: Some("done".into()),
136            },
137        ];
138
139        let execution = replay(exec_id, actions, None).unwrap();
140
141        assert_eq!(execution.steps.len(), 1);
142        let step = execution.get_step(&step_id).unwrap();
143        assert_eq!(step.output, Some("step output".to_string()));
144    }
145}