sidebyside-core 0.1.0

Core domain types for Sidebyside SDK
Documentation
//! Execution state machine
//!
//! This module defines the execution state and result types.

use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use chrono::{DateTime, Utc};

use crate::ids::{DecisionId, ExecutionId, PlanId, StepId};
use crate::plan::Plan;

/// The execution state of a plan
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PlanExecution {
    /// Unique identifier for this execution
    pub id: ExecutionId,
    /// The plan being executed
    pub plan_id: PlanId,
    /// Current state of the execution
    pub state: ExecutionState,
    /// Results of completed steps
    pub step_results: HashMap<StepId, StepResult>,
    /// Decisions made during execution
    pub decisions: HashMap<DecisionId, RecordedDecision>,
    /// When this execution started
    pub started_at: DateTime<Utc>,
    /// When this execution completed (if completed)
    pub completed_at: Option<DateTime<Utc>>,
}

impl PlanExecution {
    /// Create a new plan execution
    #[must_use]
    pub fn new(id: ExecutionId, plan: &Plan) -> Self {
        Self {
            id,
            plan_id: plan.id.clone(),
            state: ExecutionState::NotStarted,
            step_results: HashMap::new(),
            decisions: HashMap::new(),
            started_at: Utc::now(),
            completed_at: None,
        }
    }

    /// Check if the execution is complete
    #[must_use]
    pub fn is_complete(&self) -> bool {
        matches!(
            self.state,
            ExecutionState::Completed
                | ExecutionState::Failed { .. }
                | ExecutionState::Cancelled { .. }
        )
    }

    /// Check if the execution is successful
    #[must_use]
    pub fn is_success(&self) -> bool {
        matches!(self.state, ExecutionState::Completed)
    }

    /// Get the result for a specific step
    #[must_use]
    pub fn get_step_result(&self, step_id: &StepId) -> Option<&StepResult> {
        self.step_results.get(step_id)
    }

    /// Record a step result
    pub fn record_step_result(&mut self, step_id: StepId, result: StepResult) {
        self.step_results.insert(step_id, result);
    }

    /// Record a decision
    pub fn record_decision(&mut self, decision_id: DecisionId, decision: RecordedDecision) {
        self.decisions.insert(decision_id, decision);
    }
}

/// Current state of a plan execution
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum ExecutionState {
    /// Execution has not yet started
    NotStarted,
    /// Execution is currently running
    Running {
        /// ID of the currently executing step
        current_step: StepId,
    },
    /// Execution is waiting for a decision from Claude
    WaitingForDecision {
        /// ID of the decision point we're waiting on
        decision_id: DecisionId,
    },
    /// Execution completed successfully
    Completed,
    /// Execution failed
    Failed {
        /// Reason for failure
        reason: String,
        /// ID of the step that failed (if applicable)
        failed_step: Option<StepId>,
    },
    /// Execution was cancelled
    Cancelled {
        /// Reason for cancellation
        reason: String,
    },
}

/// Result of executing a single step
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StepResult {
    /// The outcome of the step
    pub outcome: StepOutcome,
    /// Output data from the step
    pub outputs: HashMap<String, serde_json::Value>,
    /// When the step started
    pub started_at: DateTime<Utc>,
    /// When the step completed
    pub completed_at: DateTime<Utc>,
    /// Number of retry attempts used
    pub retry_count: u32,
}

impl StepResult {
    /// Create a successful step result
    #[must_use]
    pub fn success(outputs: HashMap<String, serde_json::Value>) -> Self {
        let now = Utc::now();
        Self {
            outcome: StepOutcome::Success,
            outputs,
            started_at: now,
            completed_at: now,
            retry_count: 0,
        }
    }

    /// Create a failed step result
    #[must_use]
    pub fn failure(error: impl Into<String>) -> Self {
        let now = Utc::now();
        Self {
            outcome: StepOutcome::Failure {
                error: error.into(),
            },
            outputs: HashMap::new(),
            started_at: now,
            completed_at: now,
            retry_count: 0,
        }
    }

    /// Create a skipped step result
    #[must_use]
    pub fn skipped(reason: impl Into<String>) -> Self {
        let now = Utc::now();
        Self {
            outcome: StepOutcome::Skipped {
                reason: reason.into(),
            },
            outputs: HashMap::new(),
            started_at: now,
            completed_at: now,
            retry_count: 0,
        }
    }
}

/// Outcome of a step execution
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum StepOutcome {
    /// Step completed successfully
    Success,
    /// Step failed
    Failure {
        /// Error message
        error: String,
    },
    /// Step is still pending
    Pending,
    /// Step was skipped
    Skipped {
        /// Reason for skipping
        reason: String,
    },
}

/// A recorded decision for replay determinism
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RecordedDecision {
    /// Hash of the context that led to this decision
    pub context_hash: String,
    /// The decision that was made
    pub action: String,
    /// Reasoning provided by Claude
    pub reasoning: String,
    /// Confidence level (0.0 to 1.0)
    pub confidence: f64,
    /// When the decision was made
    pub timestamp: DateTime<Utc>,
    /// Version of the model used for this decision
    pub model_version: String,
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::ids::PlanId;
    use crate::plan::Plan;

    #[test]
    fn test_execution_creation() {
        let plan = Plan::new(PlanId::generate(), "test-plan");
        let execution = PlanExecution::new(ExecutionId::generate(), &plan);
        assert_eq!(execution.state, ExecutionState::NotStarted);
        assert!(!execution.is_complete());
    }

    #[test]
    fn test_step_result_success() {
        let result = StepResult::success(HashMap::new());
        assert_eq!(result.outcome, StepOutcome::Success);
    }

    #[test]
    fn test_step_result_failure() {
        let result = StepResult::failure("test error");
        assert!(matches!(result.outcome, StepOutcome::Failure { .. }));
    }

    #[test]
    fn test_is_complete_for_all_terminal_states() {
        let plan = Plan::new(PlanId::generate(), "test-plan");
        let mut execution = PlanExecution::new(ExecutionId::generate(), &plan);

        // NotStarted is not complete
        assert!(!execution.is_complete());

        // Completed is complete
        execution.state = ExecutionState::Completed;
        assert!(execution.is_complete());

        // Failed is complete
        execution.state = ExecutionState::Failed {
            reason: "test".to_string(),
            failed_step: None,
        };
        assert!(execution.is_complete());

        // Cancelled is complete
        execution.state = ExecutionState::Cancelled {
            reason: "test".to_string(),
        };
        assert!(execution.is_complete());
    }
}