Skip to main content

ito_domain/schemas/
workflow_state.rs

1//! Workflow execution state schema.
2//!
3//! This module contains serde models for tracking runtime workflow progress.
4
5use super::workflow::WorkflowDefinition;
6use serde::{Deserialize, Serialize};
7use std::collections::BTreeMap;
8
9#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
10/// Runtime state for a workflow execution.
11pub struct WorkflowExecution {
12    /// The workflow definition being executed.
13    pub workflow: WorkflowDefinition,
14
15    /// High-level execution status.
16    pub status: ExecutionStatus,
17
18    /// Start timestamp (tool-defined format).
19    pub started_at: String,
20
21    /// Completion timestamp, if complete.
22    #[serde(default, skip_serializing_if = "Option::is_none")]
23    pub completed_at: Option<String>,
24
25    /// Index of the currently active wave.
26    pub current_wave_index: usize,
27
28    /// Per-wave execution state.
29    pub waves: Vec<WaveExecution>,
30
31    /// Runtime variables captured during execution.
32    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
33    pub variables: BTreeMap<String, String>,
34}
35
36impl WorkflowExecution {
37    /// Validate semantic invariants for the execution state.
38    pub fn validate(&self) -> Result<(), String> {
39        self.workflow.validate()?;
40
41        if self.started_at.trim().is_empty() {
42            return Err("execution.started_at must not be empty".to_string());
43        }
44        if let Some(ts) = &self.completed_at
45            && ts.trim().is_empty()
46        {
47            return Err("execution.completed_at must not be empty".to_string());
48        }
49        if !self.waves.is_empty() && self.current_wave_index >= self.waves.len() {
50            return Err(format!(
51                "execution.current_wave_index out of bounds: {} (len {})",
52                self.current_wave_index,
53                self.waves.len()
54            ));
55        }
56
57        for wave in &self.waves {
58            wave.validate()?;
59        }
60        for (k, v) in &self.variables {
61            if k.trim().is_empty() {
62                return Err("execution.variables has empty key".to_string());
63            }
64            if v.trim().is_empty() {
65                return Err(format!("execution.variables has empty value for '{k}'"));
66            }
67        }
68
69        Ok(())
70    }
71}
72
73#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
74/// Execution state for a single wave.
75pub struct WaveExecution {
76    /// Wave definition.
77    pub wave: super::workflow::WaveDefinition,
78
79    /// Wave execution status.
80    pub status: ExecutionStatus,
81
82    /// Per-task execution state.
83    pub tasks: Vec<TaskExecution>,
84}
85
86impl WaveExecution {
87    /// Validate semantic invariants for the wave execution.
88    pub fn validate(&self) -> Result<(), String> {
89        self.wave.validate()?;
90        for task in &self.tasks {
91            task.validate()?;
92        }
93        Ok(())
94    }
95}
96
97#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
98/// Execution state for a single task.
99pub struct TaskExecution {
100    /// Task definition.
101    pub task: super::workflow::TaskDefinition,
102
103    /// Task execution status.
104    pub status: ExecutionStatus,
105
106    /// Task start timestamp.
107    #[serde(default, skip_serializing_if = "Option::is_none")]
108    pub started_at: Option<String>,
109
110    /// Task completion timestamp.
111    #[serde(default, skip_serializing_if = "Option::is_none")]
112    pub completed_at: Option<String>,
113
114    /// Error message, if failed.
115    #[serde(default, skip_serializing_if = "Option::is_none")]
116    pub error: Option<String>,
117
118    /// Captured task output.
119    #[serde(default, skip_serializing_if = "Option::is_none")]
120    pub output_content: Option<String>,
121}
122
123impl TaskExecution {
124    /// Validate semantic invariants for the task execution.
125    pub fn validate(&self) -> Result<(), String> {
126        self.task.validate()?;
127        if let Some(ts) = &self.started_at
128            && ts.trim().is_empty()
129        {
130            return Err(format!(
131                "execution.task.started_at must not be empty ({})",
132                self.task.id
133            ));
134        }
135
136        if let Some(ts) = &self.completed_at
137            && ts.trim().is_empty()
138        {
139            return Err(format!(
140                "execution.task.completed_at must not be empty ({})",
141                self.task.id
142            ));
143        }
144
145        if let Some(e) = &self.error
146            && e.trim().is_empty()
147        {
148            return Err(format!(
149                "execution.task.error must not be empty ({})",
150                self.task.id
151            ));
152        }
153
154        if let Some(out) = &self.output_content
155            && out.trim().is_empty()
156        {
157            return Err(format!(
158                "execution.task.output_content must not be empty ({})",
159                self.task.id
160            ));
161        }
162        Ok(())
163    }
164}
165
166#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
167#[serde(rename_all = "snake_case")]
168/// High-level execution status.
169pub enum ExecutionStatus {
170    /// Waiting to start.
171    Pending,
172    /// Currently running.
173    Running,
174    /// Completed successfully.
175    Complete,
176    /// Completed with failure.
177    Failed,
178    /// Skipped by the runner.
179    Skipped,
180}