1use crate::condition::Condition;
4use crate::error::{Result, WorkflowError};
5use miyabi_types::{
6 agent::AgentType,
7 task::{Task, TaskType},
8 workflow::{Edge, DAG},
9};
10use std::collections::HashSet;
11
12#[derive(Clone)]
14pub struct WorkflowBuilder {
15 name: String,
16 steps: Vec<Step>,
17 current_step: Option<String>,
18}
19
20#[derive(Clone, Debug)]
22pub struct Step {
23 pub id: String,
24 pub name: String,
25 pub agent_type: AgentType,
26 pub dependencies: Vec<String>,
27 pub step_type: StepType,
28}
29
30#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
32pub struct ConditionalBranch {
33 pub name: String,
35 pub condition: Condition,
37 pub next_step: String,
39}
40
41#[derive(Clone, Debug, PartialEq)]
43pub enum StepType {
44 Sequential,
46 Parallel,
48 Conditional { branches: Vec<ConditionalBranch> },
50}
51
52impl WorkflowBuilder {
53 pub fn new(name: &str) -> Self {
55 Self {
56 name: name.to_string(),
57 steps: Vec::new(),
58 current_step: None,
59 }
60 }
61
62 pub fn step(mut self, name: &str, agent: AgentType) -> Self {
64 let step_id = format!("step-{}", self.steps.len());
65 let step = Step {
66 id: step_id.clone(),
67 name: name.to_string(),
68 agent_type: agent,
69 dependencies: vec![],
70 step_type: StepType::Sequential,
71 };
72
73 self.steps.push(step);
74 self.current_step = Some(step_id);
75 self
76 }
77
78 pub fn then(mut self, name: &str, agent: AgentType) -> Self {
80 let step_id = format!("step-{}", self.steps.len());
81 let dependencies =
82 self.current_step.as_ref().map(|id| vec![id.clone()]).unwrap_or_default();
83
84 let step = Step {
85 id: step_id.clone(),
86 name: name.to_string(),
87 agent_type: agent,
88 dependencies,
89 step_type: StepType::Sequential,
90 };
91
92 self.steps.push(step);
93 self.current_step = Some(step_id);
94 self
95 }
96
97 pub fn branch_on(mut self, name: &str, branches: Vec<(&str, Condition, &str)>) -> Self {
111 let step_id = format!("step-{}", self.steps.len());
112 let dependencies =
113 self.current_step.as_ref().map(|id| vec![id.clone()]).unwrap_or_default();
114
115 let conditional_branches: Vec<ConditionalBranch> = branches
116 .into_iter()
117 .map(|(branch_name, condition, next)| ConditionalBranch {
118 name: branch_name.to_string(),
119 condition,
120 next_step: next.to_string(),
121 })
122 .collect();
123
124 let step = Step {
125 id: step_id.clone(),
126 name: name.to_string(),
127 agent_type: AgentType::CoordinatorAgent,
128 dependencies,
129 step_type: StepType::Conditional {
130 branches: conditional_branches,
131 },
132 };
133
134 self.steps.push(step);
135 self.current_step = Some(step_id);
136 self
137 }
138
139 pub fn branch(mut self, name: &str, pass_step: &str, fail_step: &str) -> Self {
155 let step_id = format!("step-{}", self.steps.len());
156 let dependencies =
157 self.current_step.as_ref().map(|id| vec![id.clone()]).unwrap_or_default();
158
159 let conditional_branches = vec![
160 ConditionalBranch {
161 name: "pass".to_string(),
162 condition: Condition::success("success"),
163 next_step: pass_step.to_string(),
164 },
165 ConditionalBranch {
166 name: "fail".to_string(),
167 condition: Condition::Always,
168 next_step: fail_step.to_string(),
169 },
170 ];
171
172 let step = Step {
173 id: step_id.clone(),
174 name: name.to_string(),
175 agent_type: AgentType::CoordinatorAgent,
176 dependencies,
177 step_type: StepType::Conditional {
178 branches: conditional_branches,
179 },
180 };
181
182 self.steps.push(step);
183 self.current_step = Some(step_id);
184 self
185 }
186
187 pub fn parallel(mut self, steps: Vec<(&str, AgentType)>) -> Self {
189 let parent_id = self.current_step.clone();
190
191 for (name, agent) in steps {
192 let step_id = format!("step-{}", self.steps.len());
193 let dependencies = parent_id.as_ref().map(|id| vec![id.clone()]).unwrap_or_default();
194
195 let step = Step {
196 id: step_id.clone(),
197 name: name.to_string(),
198 agent_type: agent,
199 dependencies,
200 step_type: StepType::Parallel,
201 };
202
203 self.steps.push(step);
204 }
205
206 self.current_step = None;
207 self
208 }
209
210 pub fn build_dag(self) -> Result<DAG> {
212 if self.steps.is_empty() {
213 return Err(WorkflowError::EmptyWorkflow);
214 }
215
216 let mut nodes = Vec::new();
217 let mut edges = Vec::new();
218
219 for step in &self.steps {
220 let metadata = if let StepType::Conditional { branches } = &step.step_type {
222 let mut meta = std::collections::HashMap::new();
223 meta.insert("is_conditional".to_string(), serde_json::json!(true));
224 meta.insert(
225 "conditional_branches".to_string(),
226 serde_json::to_value(branches).unwrap_or(serde_json::Value::Null),
227 );
228 Some(meta)
229 } else {
230 None
231 };
232
233 let task = Task {
234 id: step.id.clone(),
235 title: step.name.clone(),
236 description: format!("Workflow step: {}", step.name),
237 task_type: TaskType::Feature,
238 priority: 1,
239 assigned_agent: Some(step.agent_type),
240 dependencies: step.dependencies.clone(),
241 estimated_duration: None,
242 status: None,
243 start_time: None,
244 end_time: None,
245 metadata,
246 severity: None,
247 impact: None,
248 };
249 nodes.push(task);
250
251 for dep in &step.dependencies {
253 edges.push(Edge {
254 from: dep.clone(),
255 to: step.id.clone(),
256 });
257 }
258
259 if let StepType::Conditional { branches } = &step.step_type {
261 for branch in branches {
262 edges.push(Edge {
263 from: step.id.clone(),
264 to: branch.next_step.clone(),
265 });
266 }
267 }
268 }
269
270 let levels = self.compute_levels(&nodes, &edges)?;
271
272 Ok(DAG {
273 nodes,
274 edges,
275 levels,
276 })
277 }
278
279 fn compute_levels(&self, nodes: &[Task], edges: &[Edge]) -> Result<Vec<Vec<String>>> {
280 let mut levels: Vec<Vec<String>> = Vec::new();
281 let mut remaining: HashSet<String> = nodes.iter().map(|n| n.id.clone()).collect();
282
283 while !remaining.is_empty() {
284 let mut current_level = Vec::new();
285
286 for node_id in &remaining {
287 let has_unresolved_deps =
288 edges.iter().any(|e| e.to == *node_id && remaining.contains(&e.from));
289
290 if !has_unresolved_deps {
291 current_level.push(node_id.clone());
292 }
293 }
294
295 if current_level.is_empty() {
296 return Err(WorkflowError::CircularDependency);
297 }
298
299 for id in ¤t_level {
300 remaining.remove(id);
301 }
302
303 levels.push(current_level);
304 }
305
306 Ok(levels)
307 }
308
309 pub fn name(&self) -> &str {
310 &self.name
311 }
312
313 pub fn steps(&self) -> &[Step] {
314 &self.steps
315 }
316}
317
318#[cfg(test)]
319mod tests {
320 use super::*;
321 use miyabi_types::agent::AgentType;
322
323 #[test]
324 fn test_empty_workflow() {
325 let workflow = WorkflowBuilder::new("empty");
326 let result = workflow.build_dag();
327 assert!(result.is_err());
328 assert!(matches!(result.unwrap_err(), WorkflowError::EmptyWorkflow));
329 }
330
331 #[test]
332 fn test_single_step() {
333 let workflow = WorkflowBuilder::new("single").step("analyze", AgentType::IssueAgent);
334
335 let dag = workflow.build_dag().unwrap();
336 assert_eq!(dag.nodes.len(), 1);
337 assert_eq!(dag.edges.len(), 0);
338 assert_eq!(dag.levels.len(), 1);
339 assert_eq!(dag.levels[0].len(), 1);
340 }
341
342 #[test]
343 fn test_sequential_workflow() {
344 let workflow = WorkflowBuilder::new("sequential")
345 .step("analyze", AgentType::IssueAgent)
346 .then("implement", AgentType::CodeGenAgent);
347
348 let dag = workflow.build_dag().unwrap();
349 assert_eq!(dag.nodes.len(), 2);
350 assert_eq!(dag.edges.len(), 1);
351 assert_eq!(dag.levels.len(), 2);
352 }
353
354 #[test]
355 fn test_parallel_workflow() {
356 let workflow = WorkflowBuilder::new("parallel")
357 .step("start", AgentType::IssueAgent)
358 .parallel(vec![
359 ("task1", AgentType::CodeGenAgent),
360 ("task2", AgentType::ReviewAgent),
361 ]);
362
363 let dag = workflow.build_dag().unwrap();
364 assert_eq!(dag.nodes.len(), 3);
365 assert_eq!(dag.levels.len(), 2);
366 assert_eq!(dag.levels[1].len(), 2);
367 }
368
369 #[test]
370 fn test_complex_workflow() {
371 let workflow = WorkflowBuilder::new("complex")
372 .step("analyze", AgentType::IssueAgent)
373 .then("implement", AgentType::CodeGenAgent)
374 .parallel(vec![
375 ("test", AgentType::ReviewAgent),
376 ("lint", AgentType::CodeGenAgent),
377 ]);
378
379 let dag = workflow.build_dag().unwrap();
380 assert_eq!(dag.nodes.len(), 4);
381 assert!(dag.levels.len() >= 2);
382 }
383}