Skip to main content

forge_agent/workflow/
builder.rs

1//! Fluent builder API for workflow construction.
2//!
3//! Provides a convenient, chainable API for constructing workflows
4//! with multiple tasks and dependencies between them.
5
6use crate::workflow::dag::{Workflow, WorkflowError};
7use crate::workflow::task::{TaskId, WorkflowTask};
8use std::collections::HashMap;
9use std::sync::Arc;
10
11/// Fluent builder for constructing workflows.
12///
13/// WorkflowBuilder provides a chainable API for creating workflows
14/// with multiple tasks and dependencies between them.
15///
16/// # Example
17///
18/// ```ignore
19/// use forge_agent::workflow::{WorkflowBuilder, MockTask, TaskId};
20///
21/// let workflow = WorkflowBuilder::new()
22///     .add_task(Box::new(MockTask::new("a", "Task A")))
23///     .add_task(Box::new(MockTask::new("b", "Task B")))
24///     .add_task(Box::new(MockTask::new("c", "Task C")))
25///     .dependency(TaskId::new("a"), TaskId::new("b"))
26///     .dependency(TaskId::new("b"), TaskId::new("c"))
27///     .build()
28///     .unwrap();
29/// ```
30pub struct WorkflowBuilder {
31    /// Tasks to be added to the workflow
32    tasks: HashMap<TaskId, Box<dyn WorkflowTask>>,
33    /// Dependencies between tasks (from, to)
34    dependencies: Vec<(TaskId, TaskId)>,
35    /// Forge instance for auto-detection (optional)
36    forge: Option<Arc<forge_core::Forge>>,
37}
38
39impl WorkflowBuilder {
40    /// Creates a new WorkflowBuilder.
41    pub fn new() -> Self {
42        Self {
43            tasks: HashMap::new(),
44            dependencies: Vec::new(),
45            forge: None,
46        }
47    }
48
49    /// Configures the builder with a Forge instance for auto-detection.
50    ///
51    /// # Arguments
52    ///
53    /// * `forge` - Forge instance for graph-based dependency detection
54    ///
55    /// # Returns
56    ///
57    /// Self for method chaining
58    ///
59    /// # Example
60    ///
61    /// ```ignore
62    /// let builder = WorkflowBuilder::new()
63    ///     .with_auto_detect(&forge)
64    ///     .add_task(Box::new(GraphQueryTask::find_symbol("main")));
65    /// ```
66    pub fn with_auto_detect(mut self, forge: &forge_core::Forge) -> Self {
67        self.forge = Some(Arc::new(forge.clone()));
68        self
69    }
70
71    /// Adds a task to the workflow.
72    ///
73    /// # Arguments
74    ///
75    /// * `task` - Boxed trait object implementing WorkflowTask
76    ///
77    /// # Returns
78    ///
79    /// Self for method chaining
80    ///
81    /// # Example
82    ///
83    /// ```ignore
84    /// let builder = WorkflowBuilder::new()
85    ///     .add_task(Box::new(MockTask::new("task-1", "First Task")));
86    /// ```
87    pub fn add_task(mut self, task: Box<dyn WorkflowTask>) -> Self {
88        let id = task.id();
89        self.tasks.insert(id, task);
90        self
91    }
92
93    /// Adds a dependency between two tasks.
94    ///
95    /// Creates a directed edge from `from` to `to`, indicating that `to`
96    /// depends on `from` (from must execute first).
97    ///
98    /// # Arguments
99    ///
100    /// * `from` - Task ID of the prerequisite (executes first)
101    /// * `to` - Task ID of the dependent (executes after)
102    ///
103    /// # Returns
104    ///
105    /// Self for method chaining
106    ///
107    /// # Note
108    ///
109    /// Dependencies are validated when [`build`](Self::build) is called.
110    /// Invalid dependencies (cycles, missing tasks) will cause build to fail.
111    ///
112    /// # Example
113    ///
114    /// ```ignore
115    /// let builder = WorkflowBuilder::new()
116    ///     .add_task(Box::new(MockTask::new("a", "Task A")))
117    ///     .add_task(Box::new(MockTask::new("b", "Task B")))
118    ///     .dependency(TaskId::new("a"), TaskId::new("b"));
119    /// ```
120    pub fn dependency(mut self, from: TaskId, to: TaskId) -> Self {
121        self.dependencies.push((from, to));
122        self
123    }
124
125    /// Builds the workflow from configured tasks and dependencies.
126    ///
127    /// # Returns
128    ///
129    /// - `Ok(Workflow)` - If workflow is valid
130    /// - `Err(WorkflowError)` - If validation fails (cycles, missing tasks, empty)
131    ///
132    /// # Errors
133    ///
134    /// - `WorkflowError::EmptyWorkflow` - No tasks were added
135    /// - `WorkflowError::CycleDetected` - Dependencies contain a cycle
136    /// - `WorkflowError::TaskNotFound` - Dependency references non-existent task
137    ///
138    /// # Example
139    ///
140    /// ```ignore
141    /// let workflow = WorkflowBuilder::new()
142    ///     .add_task(Box::new(MockTask::new("task-1", "Task")))
143    ///     .build()
144    ///     .unwrap();
145    /// ```
146    pub fn build(self) -> Result<Workflow, WorkflowError> {
147        // Check for empty workflow
148        if self.tasks.is_empty() {
149            return Err(WorkflowError::EmptyWorkflow);
150        }
151
152        // Create workflow and add all tasks
153        let mut workflow = Workflow::new();
154        for (_id, task) in self.tasks {
155            workflow.add_task(task);
156        }
157
158        // Add all dependencies
159        for (from, to) in self.dependencies {
160            // Validate that both tasks exist
161            if !workflow.contains_task(&from) {
162                return Err(WorkflowError::TaskNotFound(from));
163            }
164            if !workflow.contains_task(&to) {
165                return Err(WorkflowError::TaskNotFound(to));
166            }
167
168            // Add dependency (will fail if cycle detected)
169            workflow.add_dependency(from, to)?;
170        }
171
172        Ok(workflow)
173    }
174
175    /// Builds the workflow with automatic dependency detection.
176    ///
177    /// This method builds the workflow, runs dependency detection using
178    /// the stored Forge instance, applies high-confidence suggestions,
179    /// and validates the completed workflow.
180    ///
181    /// # Returns
182    ///
183    /// - `Ok(Workflow)` - Valid workflow with auto-detected dependencies
184    /// - `Err(WorkflowError)` - If validation fails or no Forge configured
185    ///
186    /// # Errors
187    ///
188    /// - `WorkflowError::EmptyWorkflow` - No tasks were added
189    /// - `WorkflowError::CycleDetected` - Auto-detection created a cycle
190    /// - `WorkflowError::TaskNotFound` - Dependency references non-existent task
191    ///
192    /// # Example
193    ///
194    /// ```ignore
195    /// let workflow = WorkflowBuilder::new()
196    ///     .with_auto_detect(&forge)
197    ///     .add_task(Box::new(GraphQueryTask::find_symbol("process_data")))
198    ///     .add_task(Box::new(GraphQueryTask::references("process_data")))
199    ///     .build_auto_detect()
200    ///     .await?;
201    /// ```
202    pub async fn build_auto_detect(self) -> Result<Workflow, WorkflowError> {
203        use crate::workflow::auto_detect::DependencyAnalyzer;
204
205        // Extract forge reference before moving self
206        let forge_ref = self.forge.clone();
207
208        // Build workflow without validation first
209        let mut workflow = self.build_no_validate()?;
210
211        // Run dependency detection if Forge is configured
212        if let Some(forge) = forge_ref {
213            let analyzer = DependencyAnalyzer::new(forge.graph());
214            let suggestions = analyzer.detect_dependencies(&workflow).await?;
215
216            // Apply high-confidence suggestions
217            let high_confidence: Vec<_> = suggestions
218                .into_iter()
219                .filter(|s| s.is_high_confidence())
220                .collect();
221
222            let applied = workflow.apply_suggestions(high_confidence)?;
223            // Note: Auto-detected and applied {} dependencies
224            let _ = applied; // Suppress unused warning in Phase 8
225        }
226
227        Ok(workflow)
228    }
229
230    /// Builds workflow without validation (internal helper).
231    fn build_no_validate(self) -> Result<Workflow, WorkflowError> {
232        // Check for empty workflow
233        if self.tasks.is_empty() {
234            return Err(WorkflowError::EmptyWorkflow);
235        }
236
237        // Create workflow and add all tasks
238        let mut workflow = Workflow::new();
239        for (_id, task) in self.tasks {
240            workflow.add_task(task);
241        }
242
243        // Add all dependencies
244        for (from, to) in self.dependencies {
245            // Validate that both tasks exist
246            if !workflow.contains_task(&from) {
247                return Err(WorkflowError::TaskNotFound(from));
248            }
249            if !workflow.contains_task(&to) {
250                return Err(WorkflowError::TaskNotFound(to));
251            }
252
253            // Add dependency (will fail if cycle detected)
254            workflow.add_dependency(from, to)?;
255        }
256
257        Ok(workflow)
258    }
259
260    /// Creates a sequential workflow from a list of tasks.
261    ///
262    /// Tasks are executed in the order provided, with each task
263    /// depending on the previous task.
264    ///
265    /// # Arguments
266    ///
267    /// * `tasks` - Vector of boxed trait objects in execution order
268    ///
269    /// # Returns
270    ///
271    /// - `Ok(Workflow)` - If workflow is valid
272    /// - `Err(WorkflowError)` - If tasks vector is empty
273    ///
274    /// # Example
275    ///
276    /// ```ignore
277    /// let workflow = WorkflowBuilder::sequential(vec![
278    ///     Box::new(MockTask::new("step-1", "Step 1")),
279    ///     Box::new(MockTask::new("step-2", "Step 2")),
280    ///     Box::new(MockTask::new("step-3", "Step 3")),
281    /// ]).unwrap();
282    /// ```
283    pub fn sequential(tasks: Vec<Box<dyn WorkflowTask>>) -> Result<Workflow, WorkflowError> {
284        if tasks.is_empty() {
285            return Err(WorkflowError::EmptyWorkflow);
286        }
287
288        // Collect task IDs for dependency chaining
289        let mut builder = Self::new();
290        let mut prev_id: Option<TaskId> = None;
291
292        for task in tasks {
293            let id = task.id();
294            if let Some(prev) = prev_id {
295                builder = builder.dependency(prev, id.clone());
296            }
297            prev_id = Some(id);
298            builder = builder.add_task(task);
299        }
300
301        builder.build()
302    }
303}
304
305impl Default for WorkflowBuilder {
306    fn default() -> Self {
307        Self::new()
308    }
309}
310
311#[cfg(test)]
312mod tests {
313    use super::*;
314    use crate::workflow::task::{TaskContext, TaskError, TaskResult, WorkflowTask};
315    use async_trait::async_trait;
316
317    // Mock task for testing
318    struct MockTask {
319        id: TaskId,
320        name: String,
321        deps: Vec<TaskId>,
322    }
323
324    impl MockTask {
325        fn new(id: impl Into<TaskId>, name: &str) -> Self {
326            Self {
327                id: id.into(),
328                name: name.to_string(),
329                deps: Vec::new(),
330            }
331        }
332    }
333
334    #[async_trait]
335    impl WorkflowTask for MockTask {
336        async fn execute(&self, _context: &TaskContext) -> Result<TaskResult, TaskError> {
337            Ok(TaskResult::Success)
338        }
339
340        fn id(&self) -> TaskId {
341            self.id.clone()
342        }
343
344        fn name(&self) -> &str {
345            &self.name
346        }
347
348        fn dependencies(&self) -> Vec<TaskId> {
349            self.deps.clone()
350        }
351    }
352
353    #[test]
354    fn test_builder_fluent_api() {
355        let workflow = WorkflowBuilder::new()
356            .add_task(Box::new(MockTask::new("a", "Task A")))
357            .add_task(Box::new(MockTask::new("b", "Task B")))
358            .add_task(Box::new(MockTask::new("c", "Task C")))
359            .dependency(TaskId::new("a"), TaskId::new("b"))
360            .dependency(TaskId::new("b"), TaskId::new("c"))
361            .build()
362            .unwrap();
363
364        assert_eq!(workflow.task_count(), 3);
365        assert!(workflow.contains_task(&TaskId::new("a")));
366        assert!(workflow.contains_task(&TaskId::new("b")));
367        assert!(workflow.contains_task(&TaskId::new("c")));
368    }
369
370    #[test]
371    fn test_builder_with_dependencies() {
372        let workflow = WorkflowBuilder::new()
373            .add_task(Box::new(MockTask::new("a", "Task A")))
374            .add_task(Box::new(MockTask::new("b", "Task B")))
375            .add_task(Box::new(MockTask::new("c", "Task C")))
376            .dependency(TaskId::new("a"), TaskId::new("b"))
377            .dependency(TaskId::new("a"), TaskId::new("c"))
378            .build()
379            .unwrap();
380
381        let order = workflow.execution_order().unwrap();
382        assert_eq!(order.len(), 3);
383
384        // 'a' must come first (no dependencies, b and c depend on it)
385        assert_eq!(order[0], TaskId::new("a"));
386    }
387
388    #[test]
389    fn test_builder_sequential_helper() {
390        let workflow = WorkflowBuilder::sequential(vec![
391            Box::new(MockTask::new("step-1", "Step 1")),
392            Box::new(MockTask::new("step-2", "Step 2")),
393            Box::new(MockTask::new("step-3", "Step 3")),
394        ])
395        .unwrap();
396
397        assert_eq!(workflow.task_count(), 3);
398
399        let order = workflow.execution_order().unwrap();
400        assert_eq!(order.len(), 3);
401
402        // Verify sequential order
403        assert_eq!(order[0], TaskId::new("step-1"));
404        assert_eq!(order[1], TaskId::new("step-2"));
405        assert_eq!(order[2], TaskId::new("step-3"));
406    }
407
408    #[test]
409    fn test_builder_validation_failure() {
410        // Test empty workflow
411        let result = WorkflowBuilder::new().build();
412        assert!(matches!(result, Err(WorkflowError::EmptyWorkflow)));
413
414        // Test empty sequential
415        let result = WorkflowBuilder::sequential(vec![]);
416        assert!(matches!(result, Err(WorkflowError::EmptyWorkflow)));
417    }
418
419    #[test]
420    fn test_builder_cycle_detection() {
421        let result = WorkflowBuilder::new()
422            .add_task(Box::new(MockTask::new("a", "Task A")))
423            .add_task(Box::new(MockTask::new("b", "Task B")))
424            .add_task(Box::new(MockTask::new("c", "Task C")))
425            .dependency(TaskId::new("a"), TaskId::new("b"))
426            .dependency(TaskId::new("b"), TaskId::new("c"))
427            .dependency(TaskId::new("c"), TaskId::new("a")) // Creates cycle
428            .build();
429
430        assert!(matches!(result, Err(WorkflowError::CycleDetected(_))));
431    }
432
433    #[test]
434    fn test_builder_missing_task_dependency() {
435        let result = WorkflowBuilder::new()
436            .add_task(Box::new(MockTask::new("a", "Task A")))
437            .dependency(TaskId::new("a"), TaskId::new("nonexistent"))
438            .build();
439
440        assert!(matches!(result, Err(WorkflowError::TaskNotFound(_))));
441    }
442
443    #[test]
444    fn test_builder_default() {
445        let builder = WorkflowBuilder::default();
446        assert_eq!(builder.tasks.len(), 0);
447        assert_eq!(builder.dependencies.len(), 0);
448    }
449
450    #[tokio::test]
451    async fn test_builder_execute_workflow() {
452        use crate::workflow::executor::WorkflowExecutor;
453
454        let workflow = WorkflowBuilder::new()
455            .add_task(Box::new(MockTask::new("a", "Task A")))
456            .add_task(Box::new(MockTask::new("b", "Task B")))
457            .dependency(TaskId::new("a"), TaskId::new("b"))
458            .build()
459            .unwrap();
460
461        let mut executor = WorkflowExecutor::new(workflow);
462        let result = executor.execute().await.unwrap();
463
464        assert!(result.success);
465        assert_eq!(result.completed_tasks.len(), 2);
466    }
467
468    #[test]
469    fn test_builder_with_auto_detect() {
470        use forge_core::Forge;
471
472        // Create a forge instance (SQLite backend for testing)
473        let temp_dir = tempfile::tempdir().unwrap();
474        let rt = tokio::runtime::Runtime::new().unwrap();
475        let forge = rt.block_on(async {
476            Forge::open_with_backend(
477                temp_dir.path(),
478                forge_core::storage::BackendKind::SQLite,
479            )
480            .await
481            .unwrap()
482        });
483
484        let builder = WorkflowBuilder::new().with_auto_detect(&forge);
485
486        // Verify that forge is stored
487        assert!(builder.forge.is_some());
488    }
489
490    #[tokio::test]
491    async fn test_builder_auto_detect_no_forge() {
492        // Test that build_auto_detect works without Forge configured
493        let workflow = WorkflowBuilder::new()
494            .add_task(Box::new(MockTask::new("a", "Task A")))
495            .add_task(Box::new(MockTask::new("b", "Task B")))
496            .build_auto_detect()
497            .await
498            .unwrap();
499
500        assert_eq!(workflow.task_count(), 2);
501    }
502}