enact_core/kernel/
replay.rs1use super::execution_model::Execution;
13use super::ids::ExecutionId;
14use super::reducer::{reduce, ExecutionAction, ReducerError};
15
16pub 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 if let Some(version) = schema_version {
29 execution.schema_version = Some(version.to_string());
30 }
31
32 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#[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#[derive(Debug, Default)]
55pub struct EventLog {
56 actions: Vec<ExecutionAction>,
58}
59
60impl EventLog {
61 pub fn new() -> Self {
63 Self::default()
64 }
65
66 pub fn append(&mut self, action: ExecutionAction) {
68 self.actions.push(action);
69 }
70
71 pub fn actions(&self) -> &[ExecutionAction] {
73 &self.actions
74 }
75
76 pub fn into_actions(self) -> Vec<ExecutionAction> {
78 self.actions
79 }
80
81 pub fn len(&self) -> usize {
83 self.actions.len()
84 }
85
86 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}