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}