Skip to main content

sidebyside_core/
execution.rs

1//! Execution state machine
2//!
3//! This module defines the execution state and result types.
4
5use serde::{Deserialize, Serialize};
6use std::collections::HashMap;
7use chrono::{DateTime, Utc};
8
9use crate::ids::{DecisionId, ExecutionId, PlanId, StepId};
10use crate::plan::Plan;
11
12/// The execution state of a plan
13#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct PlanExecution {
15    /// Unique identifier for this execution
16    pub id: ExecutionId,
17    /// The plan being executed
18    pub plan_id: PlanId,
19    /// Current state of the execution
20    pub state: ExecutionState,
21    /// Results of completed steps
22    pub step_results: HashMap<StepId, StepResult>,
23    /// Decisions made during execution
24    pub decisions: HashMap<DecisionId, RecordedDecision>,
25    /// When this execution started
26    pub started_at: DateTime<Utc>,
27    /// When this execution completed (if completed)
28    pub completed_at: Option<DateTime<Utc>>,
29}
30
31impl PlanExecution {
32    /// Create a new plan execution
33    #[must_use]
34    pub fn new(id: ExecutionId, plan: &Plan) -> Self {
35        Self {
36            id,
37            plan_id: plan.id.clone(),
38            state: ExecutionState::NotStarted,
39            step_results: HashMap::new(),
40            decisions: HashMap::new(),
41            started_at: Utc::now(),
42            completed_at: None,
43        }
44    }
45
46    /// Check if the execution is complete
47    #[must_use]
48    pub fn is_complete(&self) -> bool {
49        matches!(
50            self.state,
51            ExecutionState::Completed
52                | ExecutionState::Failed { .. }
53                | ExecutionState::Cancelled { .. }
54        )
55    }
56
57    /// Check if the execution is successful
58    #[must_use]
59    pub fn is_success(&self) -> bool {
60        matches!(self.state, ExecutionState::Completed)
61    }
62
63    /// Get the result for a specific step
64    #[must_use]
65    pub fn get_step_result(&self, step_id: &StepId) -> Option<&StepResult> {
66        self.step_results.get(step_id)
67    }
68
69    /// Record a step result
70    pub fn record_step_result(&mut self, step_id: StepId, result: StepResult) {
71        self.step_results.insert(step_id, result);
72    }
73
74    /// Record a decision
75    pub fn record_decision(&mut self, decision_id: DecisionId, decision: RecordedDecision) {
76        self.decisions.insert(decision_id, decision);
77    }
78}
79
80/// Current state of a plan execution
81#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
82pub enum ExecutionState {
83    /// Execution has not yet started
84    NotStarted,
85    /// Execution is currently running
86    Running {
87        /// ID of the currently executing step
88        current_step: StepId,
89    },
90    /// Execution is waiting for a decision from Claude
91    WaitingForDecision {
92        /// ID of the decision point we're waiting on
93        decision_id: DecisionId,
94    },
95    /// Execution completed successfully
96    Completed,
97    /// Execution failed
98    Failed {
99        /// Reason for failure
100        reason: String,
101        /// ID of the step that failed (if applicable)
102        failed_step: Option<StepId>,
103    },
104    /// Execution was cancelled
105    Cancelled {
106        /// Reason for cancellation
107        reason: String,
108    },
109}
110
111/// Result of executing a single step
112#[derive(Debug, Clone, Serialize, Deserialize)]
113pub struct StepResult {
114    /// The outcome of the step
115    pub outcome: StepOutcome,
116    /// Output data from the step
117    pub outputs: HashMap<String, serde_json::Value>,
118    /// When the step started
119    pub started_at: DateTime<Utc>,
120    /// When the step completed
121    pub completed_at: DateTime<Utc>,
122    /// Number of retry attempts used
123    pub retry_count: u32,
124}
125
126impl StepResult {
127    /// Create a successful step result
128    #[must_use]
129    pub fn success(outputs: HashMap<String, serde_json::Value>) -> Self {
130        let now = Utc::now();
131        Self {
132            outcome: StepOutcome::Success,
133            outputs,
134            started_at: now,
135            completed_at: now,
136            retry_count: 0,
137        }
138    }
139
140    /// Create a failed step result
141    #[must_use]
142    pub fn failure(error: impl Into<String>) -> Self {
143        let now = Utc::now();
144        Self {
145            outcome: StepOutcome::Failure {
146                error: error.into(),
147            },
148            outputs: HashMap::new(),
149            started_at: now,
150            completed_at: now,
151            retry_count: 0,
152        }
153    }
154
155    /// Create a skipped step result
156    #[must_use]
157    pub fn skipped(reason: impl Into<String>) -> Self {
158        let now = Utc::now();
159        Self {
160            outcome: StepOutcome::Skipped {
161                reason: reason.into(),
162            },
163            outputs: HashMap::new(),
164            started_at: now,
165            completed_at: now,
166            retry_count: 0,
167        }
168    }
169}
170
171/// Outcome of a step execution
172#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
173pub enum StepOutcome {
174    /// Step completed successfully
175    Success,
176    /// Step failed
177    Failure {
178        /// Error message
179        error: String,
180    },
181    /// Step is still pending
182    Pending,
183    /// Step was skipped
184    Skipped {
185        /// Reason for skipping
186        reason: String,
187    },
188}
189
190/// A recorded decision for replay determinism
191#[derive(Debug, Clone, Serialize, Deserialize)]
192pub struct RecordedDecision {
193    /// Hash of the context that led to this decision
194    pub context_hash: String,
195    /// The decision that was made
196    pub action: String,
197    /// Reasoning provided by Claude
198    pub reasoning: String,
199    /// Confidence level (0.0 to 1.0)
200    pub confidence: f64,
201    /// When the decision was made
202    pub timestamp: DateTime<Utc>,
203    /// Version of the model used for this decision
204    pub model_version: String,
205}
206
207#[cfg(test)]
208mod tests {
209    use super::*;
210    use crate::ids::PlanId;
211    use crate::plan::Plan;
212
213    #[test]
214    fn test_execution_creation() {
215        let plan = Plan::new(PlanId::generate(), "test-plan");
216        let execution = PlanExecution::new(ExecutionId::generate(), &plan);
217        assert_eq!(execution.state, ExecutionState::NotStarted);
218        assert!(!execution.is_complete());
219    }
220
221    #[test]
222    fn test_step_result_success() {
223        let result = StepResult::success(HashMap::new());
224        assert_eq!(result.outcome, StepOutcome::Success);
225    }
226
227    #[test]
228    fn test_step_result_failure() {
229        let result = StepResult::failure("test error");
230        assert!(matches!(result.outcome, StepOutcome::Failure { .. }));
231    }
232
233    #[test]
234    fn test_is_complete_for_all_terminal_states() {
235        let plan = Plan::new(PlanId::generate(), "test-plan");
236        let mut execution = PlanExecution::new(ExecutionId::generate(), &plan);
237
238        // NotStarted is not complete
239        assert!(!execution.is_complete());
240
241        // Completed is complete
242        execution.state = ExecutionState::Completed;
243        assert!(execution.is_complete());
244
245        // Failed is complete
246        execution.state = ExecutionState::Failed {
247            reason: "test".to_string(),
248            failed_step: None,
249        };
250        assert!(execution.is_complete());
251
252        // Cancelled is complete
253        execution.state = ExecutionState::Cancelled {
254            reason: "test".to_string(),
255        };
256        assert!(execution.is_complete());
257    }
258}