Skip to main content

ito_domain/schemas/
workflow_plan.rs

1//! Execution plan schema.
2//!
3//! A plan is derived from a workflow definition by choosing concrete models,
4//! context budgets, and prompt content for each task.
5
6use super::workflow::WorkflowDefinition;
7use serde::{Deserialize, Serialize};
8use std::collections::BTreeMap;
9
10#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
11/// A concrete execution plan for a workflow.
12pub struct ExecutionPlan {
13    /// The target tool/harness that will run the workflow.
14    pub tool: Tool,
15
16    /// The workflow definition this plan was generated from.
17    pub workflow: WorkflowDefinition,
18
19    /// Planned waves (in order).
20    pub waves: Vec<WavePlan>,
21}
22
23impl ExecutionPlan {
24    /// Validate semantic invariants for the execution plan.
25    pub fn validate(&self) -> Result<(), String> {
26        self.workflow.validate()?;
27
28        for wave in &self.waves {
29            wave.validate()?;
30        }
31
32        Ok(())
33    }
34}
35
36#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
37/// Planned execution for a single wave.
38pub struct WavePlan {
39    /// The referenced wave id from the workflow definition.
40    pub wave_id: String,
41
42    /// Tasks to run in this wave.
43    pub tasks: Vec<TaskPlan>,
44}
45
46impl WavePlan {
47    /// Validate semantic invariants for the wave plan.
48    pub fn validate(&self) -> Result<(), String> {
49        if self.wave_id.trim().is_empty() {
50            return Err("plan.wave_id must not be empty".to_string());
51        }
52        for task in &self.tasks {
53            task.validate()?;
54        }
55        Ok(())
56    }
57}
58
59#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
60/// Planned execution for a single task.
61pub struct TaskPlan {
62    /// The referenced task id from the workflow definition.
63    pub task_id: String,
64
65    /// Concrete model identifier (tool-specific).
66    pub model: String,
67
68    /// Context token budget.
69    pub context_budget: usize,
70
71    /// Fully-rendered prompt content.
72    pub prompt_content: String,
73
74    /// Optional input names expected by the prompt.
75    #[serde(default, skip_serializing_if = "Option::is_none")]
76    pub inputs: Option<Vec<String>>,
77
78    /// Optional output artifact name.
79    #[serde(default, skip_serializing_if = "Option::is_none")]
80    pub output: Option<String>,
81
82    /// Optional tool-specific context key/value pairs.
83    #[serde(default, skip_serializing_if = "Option::is_none")]
84    pub context: Option<BTreeMap<String, String>>,
85}
86
87impl TaskPlan {
88    /// Validate semantic invariants for the task plan.
89    pub fn validate(&self) -> Result<(), String> {
90        if self.task_id.trim().is_empty() {
91            return Err("plan.task_id must not be empty".to_string());
92        }
93        if self.model.trim().is_empty() {
94            return Err(format!(
95                "plan.model must not be empty (task {})",
96                self.task_id
97            ));
98        }
99        if self.prompt_content.trim().is_empty() {
100            return Err(format!(
101                "plan.prompt_content must not be empty (task {})",
102                self.task_id
103            ));
104        }
105        if let Some(inputs) = &self.inputs {
106            for i in inputs {
107                if i.trim().is_empty() {
108                    return Err(format!(
109                        "plan.inputs contains empty entry (task {})",
110                        self.task_id
111                    ));
112                }
113            }
114        }
115        if let Some(out) = &self.output
116            && out.trim().is_empty()
117        {
118            return Err(format!(
119                "plan.output must not be empty (task {})",
120                self.task_id
121            ));
122        }
123        if let Some(ctx) = &self.context {
124            for (k, v) in ctx {
125                if k.trim().is_empty() {
126                    return Err(format!(
127                        "plan.context has empty key (task {})",
128                        self.task_id
129                    ));
130                }
131                if v.trim().is_empty() {
132                    return Err(format!(
133                        "plan.context has empty value for '{k}' (task {})",
134                        self.task_id
135                    ));
136                }
137            }
138        }
139        Ok(())
140    }
141}
142
143#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
144/// Tool/harness selector for an execution plan.
145pub enum Tool {
146    /// OpenCode.
147    #[serde(rename = "opencode")]
148    OpenCode,
149    /// Claude Code.
150    #[serde(rename = "claude-code")]
151    ClaudeCode,
152    /// Codex.
153    #[serde(rename = "codex")]
154    Codex,
155}