miyabi_workflow/
builder.rs

1//! WorkflowBuilder - Fluent API for defining agent workflows
2
3use 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/// Builder for constructing agent workflows
13#[derive(Clone)]
14pub struct WorkflowBuilder {
15    name: String,
16    steps: Vec<Step>,
17    current_step: Option<String>,
18}
19
20/// A single step in a workflow
21#[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/// Conditional branch definition
31#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
32pub struct ConditionalBranch {
33    /// Branch name
34    pub name: String,
35    /// Condition to evaluate
36    pub condition: Condition,
37    /// ID of next step if condition is true
38    pub next_step: String,
39}
40
41/// Type of workflow step
42#[derive(Clone, Debug, PartialEq)]
43pub enum StepType {
44    /// Sequential step (default)
45    Sequential,
46    /// Parallel step (executes concurrently with siblings)
47    Parallel,
48    /// Conditional branching with multiple possible paths
49    Conditional { branches: Vec<ConditionalBranch> },
50}
51
52impl WorkflowBuilder {
53    /// Create a new workflow with the given name
54    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    /// Add a step to the workflow (no dependencies)
63    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    /// Add a sequential step that depends on the previous step
79    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    /// Add a conditional branch with custom conditions
98    ///
99    /// # Arguments
100    /// * `name` - Name of the decision point
101    /// * `branches` - Vec of (branch_name, condition, next_step_id) tuples
102    ///
103    /// # Example
104    /// ```ignore
105    /// workflow.branch_on("quality-gate", vec![
106    ///     ("high", Condition::FieldGreaterThan { field: "score".into(), value: 0.9 }, "deploy"),
107    ///     ("low", Condition::Always, "reject"),
108    /// ]);
109    /// ```
110    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    /// Add a simple pass/fail conditional branch
140    ///
141    /// This is a convenience method for the common case of branching on success/failure.
142    ///
143    /// # Arguments
144    /// * `name` - Name of the decision point
145    /// * `pass_step` - Step ID to execute if condition passes
146    /// * `fail_step` - Step ID to execute if condition fails
147    ///
148    /// # Example
149    /// ```ignore
150    /// workflow
151    ///     .step("test", AgentType::ReviewAgent)
152    ///     .branch("deploy-decision", "deploy", "rollback");
153    /// ```
154    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    /// Add parallel steps that execute concurrently
188    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    /// Build the DAG representation of this workflow
211    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            // Add metadata for conditional steps
221            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            // Add dependency edges (incoming edges to this step)
252            for dep in &step.dependencies {
253                edges.push(Edge {
254                    from: dep.clone(),
255                    to: step.id.clone(),
256                });
257            }
258
259            // Add conditional branch edges (outgoing edges from this step)
260            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 &current_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}