ito_domain/schemas/
workflow_plan.rs1use super::workflow::WorkflowDefinition;
7use serde::{Deserialize, Serialize};
8use std::collections::BTreeMap;
9
10#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
11pub struct ExecutionPlan {
13 pub tool: Tool,
15
16 pub workflow: WorkflowDefinition,
18
19 pub waves: Vec<WavePlan>,
21}
22
23impl ExecutionPlan {
24 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)]
37pub struct WavePlan {
39 pub wave_id: String,
41
42 pub tasks: Vec<TaskPlan>,
44}
45
46impl WavePlan {
47 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)]
60pub struct TaskPlan {
62 pub task_id: String,
64
65 pub model: String,
67
68 pub context_budget: usize,
70
71 pub prompt_content: String,
73
74 #[serde(default, skip_serializing_if = "Option::is_none")]
76 pub inputs: Option<Vec<String>>,
77
78 #[serde(default, skip_serializing_if = "Option::is_none")]
80 pub output: Option<String>,
81
82 #[serde(default, skip_serializing_if = "Option::is_none")]
84 pub context: Option<BTreeMap<String, String>>,
85}
86
87impl TaskPlan {
88 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)]
144pub enum Tool {
146 #[serde(rename = "opencode")]
148 OpenCode,
149 #[serde(rename = "claude-code")]
151 ClaudeCode,
152 #[serde(rename = "codex")]
154 Codex,
155}