enact-core 0.0.2

Core agent runtime for Enact - Graph-Native AI agents
Documentation
//! Replay - Re-execute from event log
//!
//! Replay enables:
//! - Debugging: re-run an execution to understand what happened
//! - Testing: verify determinism by replaying
//! - Recovery: resume from a checkpoint
//!
//! Replay requires:
//! - Matching schema version
//! - Ordered event log

use super::execution_model::Execution;
use super::ids::ExecutionId;
use super::reducer::{reduce, ExecutionAction, ReducerError};

/// Replay an execution from an action log
///
/// This will recreate the execution state by applying each action
/// through the reducer, ensuring deterministic reconstruction.
pub fn replay(
    execution_id: ExecutionId,
    actions: Vec<ExecutionAction>,
    schema_version: Option<&str>,
) -> Result<Execution, ReplayError> {
    let mut execution = Execution::with_id(execution_id);

    // Set schema version for validation
    if let Some(version) = schema_version {
        execution.schema_version = Some(version.to_string());
    }

    // Apply each action through the reducer
    for (index, action) in actions.into_iter().enumerate() {
        reduce(&mut execution, action)
            .map_err(|e| ReplayError::ReducerError { index, error: e })?;
    }

    Ok(execution)
}

/// Replay error
#[derive(Debug, thiserror::Error)]
pub enum ReplayError {
    #[error("Reducer error at action {index}: {error}")]
    ReducerError { index: usize, error: ReducerError },
    #[error("Schema version mismatch: expected {expected}, got {actual}")]
    SchemaVersionMismatch { expected: String, actual: String },
}

/// Event log for replay
///
/// This is a simple in-memory log. In production, you'd persist this
/// to a database or event store.
#[derive(Debug, Default)]
pub struct EventLog {
    /// Ordered list of actions
    actions: Vec<ExecutionAction>,
}

impl EventLog {
    /// Create a new event log
    pub fn new() -> Self {
        Self::default()
    }

    /// Append an action to the log
    pub fn append(&mut self, action: ExecutionAction) {
        self.actions.push(action);
    }

    /// Get all actions
    pub fn actions(&self) -> &[ExecutionAction] {
        &self.actions
    }

    /// Take all actions (consumes the log)
    pub fn into_actions(self) -> Vec<ExecutionAction> {
        self.actions
    }

    /// Get action count
    pub fn len(&self) -> usize {
        self.actions.len()
    }

    /// Check if empty
    pub fn is_empty(&self) -> bool {
        self.actions.is_empty()
    }
}

#[cfg(test)]
mod tests {
    use super::super::ids::StepType;
    use super::*;

    #[test]
    fn test_replay_simple_execution() {
        let exec_id = ExecutionId::new();

        let actions = vec![
            ExecutionAction::Start,
            ExecutionAction::Complete {
                output: Some("done".into()),
            },
        ];

        let execution = replay(exec_id.clone(), actions, None).unwrap();

        assert_eq!(execution.id.as_str(), exec_id.as_str());
        assert!(execution.state.is_terminal());
        assert_eq!(execution.output, Some("done".to_string()));
    }

    #[test]
    fn test_replay_with_steps() {
        let exec_id = ExecutionId::new();
        let step_id = super::super::ids::StepId::new();

        let actions = vec![
            ExecutionAction::Start,
            ExecutionAction::StepStarted {
                step_id: step_id.clone(),
                parent_step_id: None,
                step_type: StepType::LlmNode,
                name: "test step".into(),
                source: None,
            },
            ExecutionAction::StepCompleted {
                step_id: step_id.clone(),
                output: Some("step output".into()),
                duration_ms: 100,
            },
            ExecutionAction::Complete {
                output: Some("done".into()),
            },
        ];

        let execution = replay(exec_id, actions, None).unwrap();

        assert_eq!(execution.steps.len(), 1);
        let step = execution.get_step(&step_id).unwrap();
        assert_eq!(step.output, Some("step output".to_string()));
    }
}