1use serde::{Deserialize, Serialize};
7use std::collections::BTreeMap;
8
9#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
10pub struct WorkflowDefinition {
15 pub version: String,
17
18 pub id: String,
20
21 pub name: String,
23
24 #[serde(default, skip_serializing_if = "String::is_empty")]
26 pub description: String,
27
28 #[serde(default, skip_serializing_if = "Option::is_none")]
30 pub requires: Option<WorkflowRequires>,
31
32 #[serde(
34 rename = "context_files",
35 default,
36 skip_serializing_if = "Option::is_none"
37 )]
38 pub context_files: Option<Vec<String>>,
39
40 pub waves: Vec<WaveDefinition>,
42
43 #[serde(
45 rename = "on_complete",
46 default,
47 skip_serializing_if = "Option::is_none"
48 )]
49 pub on_complete: Option<OnComplete>,
50}
51
52impl WorkflowDefinition {
53 pub fn validate(&self) -> Result<(), String> {
55 if self.version.trim().is_empty() {
56 return Err("workflow.version must not be empty".to_string());
57 }
58 if self.id.trim().is_empty() {
59 return Err("workflow.id must not be empty".to_string());
60 }
61 if self.name.trim().is_empty() {
62 return Err("workflow.name must not be empty".to_string());
63 }
64 if self.waves.is_empty() {
65 return Err("workflow.waves must not be empty".to_string());
66 }
67
68 if let Some(requires) = &self.requires {
69 if let Some(vars) = &requires.variables {
70 for v in vars {
71 if v.trim().is_empty() {
72 return Err("workflow.requires.variables contains empty entry".to_string());
73 }
74 }
75 }
76 if let Some(files) = &requires.files {
77 for f in files {
78 if f.trim().is_empty() {
79 return Err("workflow.requires.files contains empty entry".to_string());
80 }
81 }
82 }
83 }
84
85 if let Some(files) = &self.context_files {
86 for f in files {
87 if f.trim().is_empty() {
88 return Err("workflow.context_files contains empty entry".to_string());
89 }
90 }
91 }
92
93 let mut seen_waves: Vec<&str> = Vec::new();
94 for wave in &self.waves {
95 wave.validate()?;
96 if seen_waves.contains(&wave.id.as_str()) {
97 return Err(format!("workflow.waves has duplicate id: {}", wave.id));
98 }
99 seen_waves.push(wave.id.as_str());
100 }
101
102 Ok(())
103 }
104}
105
106#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
107pub struct WorkflowRequires {
109 #[serde(default, skip_serializing_if = "Option::is_none")]
111 pub files: Option<Vec<String>>,
112
113 #[serde(default, skip_serializing_if = "Option::is_none")]
115 pub variables: Option<Vec<String>>,
116}
117
118#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
119pub struct OnComplete {
121 #[serde(
123 rename = "update_state",
124 default,
125 skip_serializing_if = "Option::is_none"
126 )]
127 pub update_state: Option<bool>,
128
129 #[serde(
131 rename = "update_roadmap",
132 default,
133 skip_serializing_if = "Option::is_none"
134 )]
135 pub update_roadmap: Option<bool>,
136
137 #[serde(default, skip_serializing_if = "Option::is_none")]
139 pub notify: Option<Vec<String>>,
140}
141
142#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
143pub struct WaveDefinition {
145 pub id: String,
147
148 #[serde(default, skip_serializing_if = "Option::is_none")]
150 pub name: Option<String>,
151
152 pub tasks: Vec<TaskDefinition>,
154
155 #[serde(default, skip_serializing_if = "Option::is_none")]
157 pub checkpoint: Option<bool>,
158}
159
160impl WaveDefinition {
161 pub fn validate(&self) -> Result<(), String> {
163 if self.id.trim().is_empty() {
164 return Err("wave.id must not be empty".to_string());
165 }
166 if let Some(name) = &self.name
167 && name.trim().is_empty()
168 {
169 return Err(format!("wave.name must not be empty (wave {})", self.id));
170 }
171 if self.tasks.is_empty() {
172 return Err(format!("wave.tasks must not be empty (wave {})", self.id));
173 }
174
175 let mut seen_tasks: Vec<&str> = Vec::new();
176 for task in &self.tasks {
177 task.validate()?;
178 if seen_tasks.contains(&task.id.as_str()) {
179 return Err(format!(
180 "wave.tasks has duplicate id: {} (wave {})",
181 task.id, self.id
182 ));
183 }
184 seen_tasks.push(task.id.as_str());
185 }
186 Ok(())
187 }
188}
189
190#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
191pub struct TaskDefinition {
193 pub id: String,
195
196 pub name: String,
198
199 pub agent: AgentType,
201
202 pub prompt: String,
204
205 #[serde(default, skip_serializing_if = "Option::is_none")]
207 pub inputs: Option<Vec<String>>,
208
209 #[serde(default, skip_serializing_if = "Option::is_none")]
211 pub output: Option<String>,
212
213 #[serde(rename = "type", default, skip_serializing_if = "Option::is_none")]
215 pub task_type: Option<TaskType>,
216
217 #[serde(default, skip_serializing_if = "Option::is_none")]
219 pub context: Option<BTreeMap<String, String>>,
220}
221
222impl TaskDefinition {
223 pub fn validate(&self) -> Result<(), String> {
225 if self.id.trim().is_empty() {
226 return Err("task.id must not be empty".to_string());
227 }
228 if self.name.trim().is_empty() {
229 return Err(format!("task.name must not be empty (task {})", self.id));
230 }
231 if self.prompt.trim().is_empty() {
232 return Err(format!("task.prompt must not be empty (task {})", self.id));
233 }
234 if let Some(inputs) = &self.inputs {
235 for i in inputs {
236 if i.trim().is_empty() {
237 return Err(format!(
238 "task.inputs contains empty entry (task {})",
239 self.id
240 ));
241 }
242 }
243 }
244 if let Some(out) = &self.output
245 && out.trim().is_empty()
246 {
247 return Err(format!("task.output must not be empty (task {})", self.id));
248 }
249 if let Some(ctx) = &self.context {
250 for (k, v) in ctx {
251 if k.trim().is_empty() {
252 return Err(format!("task.context has empty key (task {})", self.id));
253 }
254 if v.trim().is_empty() {
255 return Err(format!(
256 "task.context has empty value for '{k}' (task {})",
257 self.id
258 ));
259 }
260 }
261 }
262
263 Ok(())
264 }
265}
266
267#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
268#[serde(rename_all = "snake_case")]
269pub enum AgentType {
271 Research,
273 Execution,
275 Review,
277 Planning,
279}
280
281#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
282#[serde(rename_all = "snake_case")]
283pub enum TaskType {
285 Auto,
287 Checkpoint,
289 Decision,
291}