use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use chrono::{DateTime, Utc};
use crate::ids::{DecisionId, ExecutionId, PlanId, StepId};
use crate::plan::Plan;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PlanExecution {
pub id: ExecutionId,
pub plan_id: PlanId,
pub state: ExecutionState,
pub step_results: HashMap<StepId, StepResult>,
pub decisions: HashMap<DecisionId, RecordedDecision>,
pub started_at: DateTime<Utc>,
pub completed_at: Option<DateTime<Utc>>,
}
impl PlanExecution {
#[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,
}
}
#[must_use]
pub fn is_complete(&self) -> bool {
matches!(
self.state,
ExecutionState::Completed
| ExecutionState::Failed { .. }
| ExecutionState::Cancelled { .. }
)
}
#[must_use]
pub fn is_success(&self) -> bool {
matches!(self.state, ExecutionState::Completed)
}
#[must_use]
pub fn get_step_result(&self, step_id: &StepId) -> Option<&StepResult> {
self.step_results.get(step_id)
}
pub fn record_step_result(&mut self, step_id: StepId, result: StepResult) {
self.step_results.insert(step_id, result);
}
pub fn record_decision(&mut self, decision_id: DecisionId, decision: RecordedDecision) {
self.decisions.insert(decision_id, decision);
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum ExecutionState {
NotStarted,
Running {
current_step: StepId,
},
WaitingForDecision {
decision_id: DecisionId,
},
Completed,
Failed {
reason: String,
failed_step: Option<StepId>,
},
Cancelled {
reason: String,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StepResult {
pub outcome: StepOutcome,
pub outputs: HashMap<String, serde_json::Value>,
pub started_at: DateTime<Utc>,
pub completed_at: DateTime<Utc>,
pub retry_count: u32,
}
impl StepResult {
#[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,
}
}
#[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,
}
}
#[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,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum StepOutcome {
Success,
Failure {
error: String,
},
Pending,
Skipped {
reason: String,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RecordedDecision {
pub context_hash: String,
pub action: String,
pub reasoning: String,
pub confidence: f64,
pub timestamp: DateTime<Utc>,
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);
assert!(!execution.is_complete());
execution.state = ExecutionState::Completed;
assert!(execution.is_complete());
execution.state = ExecutionState::Failed {
reason: "test".to_string(),
failed_step: None,
};
assert!(execution.is_complete());
execution.state = ExecutionState::Cancelled {
reason: "test".to_string(),
};
assert!(execution.is_complete());
}
}