enact-core 0.0.2

Core agent runtime for Enact - Graph-Native AI agents
Documentation
//! Execution State Machine
//!
//! Defines the explicit state machine for execution lifecycle.
//! This is the authoritative definition - all state transitions
//! must go through the reducer.

use serde::{Deserialize, Serialize};

/// Execution lifecycle state - explicit state machine
///
/// @see packages/enact-schemas/src/streaming.schemas.ts - executionEventDataSchema.status
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
#[serde(rename_all = "PascalCase")]
pub enum ExecutionState {
    /// Execution created but not yet started
    #[default]
    Created,
    /// Execution is in planning phase (Agentic DAG planning)
    Planning,
    /// Execution is actively running
    Running,
    /// Execution is paused (can be resumed)
    Paused,
    /// Execution is waiting for external input (approval, tool result, etc.)
    Waiting(WaitReason),
    /// Execution completed successfully
    Completed,
    /// Execution failed with an error
    Failed,
    /// Execution was cancelled by user/system
    Cancelled,
}

impl ExecutionState {
    /// Check if execution is in a terminal state
    pub fn is_terminal(&self) -> bool {
        matches!(self, Self::Completed | Self::Failed | Self::Cancelled)
    }

    /// Check if execution can be resumed
    pub fn can_resume(&self) -> bool {
        matches!(self, Self::Paused | Self::Waiting(_))
    }

    /// Check if execution is active (running or waiting)
    pub fn is_active(&self) -> bool {
        matches!(self, Self::Running | Self::Waiting(_))
    }
}

/// Reason for waiting state
///
/// This enum covers all documented wait scenarios for long-running executions.
/// @see docs/TECHNICAL/27-LONG-RUNNING-EXECUTIONS.md
/// @see docs/TECHNICAL/31-MID-EXECUTION-GUIDANCE.md
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum WaitReason {
    /// Waiting for human approval (HITL)
    Approval,
    /// Waiting for user input
    UserInput,
    /// Waiting due to rate limits
    RateLimit,
    /// Waiting for an external dependency (API, tool, resource)
    External,
    /// Waiting for checkpoint operation to complete
    Checkpoint,
    /// Waiting due to cost threshold reached (enforcement middleware)
    CostThreshold,
    /// Waiting due to memory pressure (context compaction needed)
    MemoryPressure,
    /// Waiting for sub-agent execution to complete
    SubAgent,
}

/// Step lifecycle state
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
#[serde(rename_all = "PascalCase")]
pub enum StepState {
    /// Step created/queued
    #[default]
    Pending,
    /// Step is running
    Running,
    /// Step completed successfully
    Completed,
    /// Step failed
    Failed,
    /// Step was skipped
    Skipped,
}

impl StepState {
    /// Check if step is in a terminal state
    pub fn is_terminal(&self) -> bool {
        matches!(self, Self::Completed | Self::Failed | Self::Skipped)
    }
}

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

    #[test]
    fn test_execution_state_terminal() {
        assert!(!ExecutionState::Created.is_terminal());
        assert!(!ExecutionState::Planning.is_terminal());
        assert!(!ExecutionState::Running.is_terminal());
        assert!(!ExecutionState::Paused.is_terminal());
        assert!(ExecutionState::Completed.is_terminal());
        assert!(ExecutionState::Failed.is_terminal());
        assert!(ExecutionState::Cancelled.is_terminal());
    }

    #[test]
    fn test_execution_state_can_resume() {
        assert!(!ExecutionState::Created.can_resume());
        assert!(!ExecutionState::Planning.can_resume());
        assert!(!ExecutionState::Running.can_resume());
        assert!(ExecutionState::Paused.can_resume());
        assert!(ExecutionState::Waiting(WaitReason::Approval).can_resume());
    }

    #[test]
    fn test_execution_state_is_active() {
        assert!(!ExecutionState::Created.is_active());
        assert!(!ExecutionState::Planning.is_active());
        assert!(ExecutionState::Running.is_active());
        assert!(!ExecutionState::Paused.is_active());
        assert!(ExecutionState::Waiting(WaitReason::Approval).is_active());
        assert!(ExecutionState::Waiting(WaitReason::External).is_active());
        assert!(!ExecutionState::Completed.is_active());
        assert!(!ExecutionState::Failed.is_active());
        assert!(!ExecutionState::Cancelled.is_active());
    }

    #[test]
    fn test_execution_state_serde() {
        // Test all variants serialize/deserialize correctly
        let states = vec![
            ExecutionState::Created,
            ExecutionState::Planning,
            ExecutionState::Running,
            ExecutionState::Paused,
            ExecutionState::Waiting(WaitReason::Approval),
            ExecutionState::Waiting(WaitReason::UserInput),
            ExecutionState::Waiting(WaitReason::RateLimit),
            ExecutionState::Waiting(WaitReason::External),
            ExecutionState::Waiting(WaitReason::Checkpoint),
            ExecutionState::Waiting(WaitReason::CostThreshold),
            ExecutionState::Waiting(WaitReason::MemoryPressure),
            ExecutionState::Waiting(WaitReason::SubAgent),
            ExecutionState::Completed,
            ExecutionState::Failed,
            ExecutionState::Cancelled,
        ];
        for state in states {
            let json = serde_json::to_string(&state).unwrap();
            let parsed: ExecutionState = serde_json::from_str(&json).unwrap();
            assert_eq!(state, parsed);
        }
    }

    #[test]
    fn test_step_state_terminal() {
        assert!(!StepState::Pending.is_terminal());
        assert!(!StepState::Running.is_terminal());
        assert!(StepState::Completed.is_terminal());
        assert!(StepState::Failed.is_terminal());
        assert!(StepState::Skipped.is_terminal());
    }

    #[test]
    fn test_step_state_serde() {
        let states = vec![
            StepState::Pending,
            StepState::Running,
            StepState::Completed,
            StepState::Failed,
            StepState::Skipped,
        ];
        for state in states {
            let json = serde_json::to_string(&state).unwrap();
            let parsed: StepState = serde_json::from_str(&json).unwrap();
            assert_eq!(state, parsed);
        }
    }
}