Skip to main content

brainwires_agents/
plan_executor.rs

1//! Plan Executor Agent - Executes plans by orchestrating task execution
2//!
3//! Runs through a plan's tasks, respecting dependencies and approval modes.
4//! Integrates with completion detection to auto-progress tasks.
5
6use anyhow::Result;
7use std::sync::Arc;
8use tokio::sync::RwLock;
9
10use brainwires_core::{PlanMetadata, Task};
11
12use crate::task_manager::TaskManager;
13
14/// Approval mode for plan execution
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
16pub enum ExecutionApprovalMode {
17    /// Suggest mode - ask user before each task (safest)
18    Suggest,
19    /// Auto-edit mode - auto-approve file edits, ask for shell commands
20    AutoEdit,
21    /// Full-auto mode - auto-approve everything (default for plan execution)
22    #[default]
23    FullAuto,
24}
25
26impl std::fmt::Display for ExecutionApprovalMode {
27    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
28        match self {
29            Self::Suggest => write!(f, "suggest"),
30            Self::AutoEdit => write!(f, "auto-edit"),
31            Self::FullAuto => write!(f, "full-auto"),
32        }
33    }
34}
35
36impl std::str::FromStr for ExecutionApprovalMode {
37    type Err = String;
38
39    fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
40        match s.to_lowercase().as_str() {
41            "suggest" => Ok(Self::Suggest),
42            "auto-edit" | "autoedit" => Ok(Self::AutoEdit),
43            "full-auto" | "fullauto" | "auto" => Ok(Self::FullAuto),
44            _ => Err(format!("Unknown approval mode: {}", s)),
45        }
46    }
47}
48
49/// Status of plan execution
50#[derive(Debug, Clone, PartialEq, Eq)]
51pub enum PlanExecutionStatus {
52    /// Not started
53    Idle,
54    /// Currently executing
55    Running,
56    /// Waiting for user approval
57    WaitingForApproval(String),
58    /// Paused by user
59    Paused,
60    /// Completed successfully
61    Completed,
62    /// Failed with error
63    Failed(String),
64}
65
66impl std::fmt::Display for PlanExecutionStatus {
67    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
68        match self {
69            Self::Idle => write!(f, "Idle"),
70            Self::Running => write!(f, "Running"),
71            Self::WaitingForApproval(task) => write!(f, "Waiting for approval: {}", task),
72            Self::Paused => write!(f, "Paused"),
73            Self::Completed => write!(f, "Completed"),
74            Self::Failed(err) => write!(f, "Failed: {}", err),
75        }
76    }
77}
78
79/// Configuration for plan execution
80#[derive(Debug, Clone)]
81pub struct PlanExecutionConfig {
82    /// Approval mode
83    pub approval_mode: ExecutionApprovalMode,
84    /// Maximum iterations per task
85    pub max_iterations_per_task: u32,
86    /// Whether to auto-start next task after completion
87    pub auto_advance: bool,
88    /// Stop on first error
89    pub stop_on_error: bool,
90}
91
92impl Default for PlanExecutionConfig {
93    fn default() -> Self {
94        Self {
95            approval_mode: ExecutionApprovalMode::FullAuto,
96            max_iterations_per_task: 15,
97            auto_advance: true,
98            stop_on_error: true,
99        }
100    }
101}
102
103/// Plan Executor Agent - coordinates execution of a plan's tasks
104pub struct PlanExecutorAgent {
105    /// The plan being executed
106    plan: Arc<RwLock<PlanMetadata>>,
107    /// Task manager
108    task_manager: Arc<RwLock<TaskManager>>,
109    /// Execution configuration
110    config: PlanExecutionConfig,
111    /// Current execution status
112    status: Arc<RwLock<PlanExecutionStatus>>,
113    /// Current task being executed (if any)
114    current_task_id: Arc<RwLock<Option<String>>>,
115}
116
117impl PlanExecutorAgent {
118    /// Create a new plan executor
119    pub fn new(
120        plan: PlanMetadata,
121        task_manager: Arc<RwLock<TaskManager>>,
122        config: PlanExecutionConfig,
123    ) -> Self {
124        Self {
125            plan: Arc::new(RwLock::new(plan)),
126            task_manager,
127            config,
128            status: Arc::new(RwLock::new(PlanExecutionStatus::Idle)),
129            current_task_id: Arc::new(RwLock::new(None)),
130        }
131    }
132
133    /// Get the plan
134    #[tracing::instrument(name = "agent.plan.get", skip(self))]
135    pub async fn plan(&self) -> PlanMetadata {
136        self.plan.read().await.clone()
137    }
138
139    /// Get the execution status
140    pub async fn status(&self) -> PlanExecutionStatus {
141        self.status.read().await.clone()
142    }
143
144    /// Get the current task ID
145    pub async fn current_task_id(&self) -> Option<String> {
146        self.current_task_id.read().await.clone()
147    }
148
149    /// Get the approval mode
150    pub fn approval_mode(&self) -> ExecutionApprovalMode {
151        self.config.approval_mode
152    }
153
154    /// Set the approval mode
155    pub fn set_approval_mode(&mut self, mode: ExecutionApprovalMode) {
156        self.config.approval_mode = mode;
157    }
158
159    /// Check if a task needs approval based on current mode
160    pub fn needs_approval(&self, _task: &Task) -> bool {
161        match self.config.approval_mode {
162            ExecutionApprovalMode::Suggest => true,   // Always ask
163            ExecutionApprovalMode::AutoEdit => false, // Auto-approve (shell commands need separate handling)
164            ExecutionApprovalMode::FullAuto => false, // Never ask
165        }
166    }
167
168    /// Get the next task to execute
169    #[tracing::instrument(name = "agent.plan.next_task", skip(self))]
170    pub async fn get_next_task(&self) -> Option<Task> {
171        let task_mgr = self.task_manager.read().await;
172        let ready_tasks = task_mgr.get_ready_tasks().await;
173        ready_tasks.into_iter().next()
174    }
175
176    /// Start executing a specific task
177    #[tracing::instrument(name = "agent.plan.start_task", skip(self))]
178    pub async fn start_task(&self, task_id: &str) -> Result<()> {
179        let task_mgr = self.task_manager.write().await;
180
181        // Check if task can start
182        match task_mgr.can_start(task_id).await {
183            Ok(true) => {}
184            Ok(false) => {
185                anyhow::bail!(
186                    "Task '{}' cannot be started (may already be completed)",
187                    task_id
188                );
189            }
190            Err(blocking_tasks) => {
191                anyhow::bail!(
192                    "Task '{}' is blocked by incomplete dependencies: {}",
193                    task_id,
194                    blocking_tasks.join(", ")
195                );
196            }
197        }
198
199        // Start the task
200        task_mgr.start_task(task_id).await?;
201
202        // Update current task
203        *self.current_task_id.write().await = Some(task_id.to_string());
204
205        // Update status
206        *self.status.write().await = PlanExecutionStatus::Running;
207
208        Ok(())
209    }
210
211    /// Complete the current task
212    #[tracing::instrument(name = "agent.plan.complete_task", skip(self, summary))]
213    pub async fn complete_current_task(&self, summary: String) -> Result<Option<Task>> {
214        let task_id = {
215            let current = self.current_task_id.read().await;
216            current.clone()
217        };
218
219        if let Some(task_id) = task_id {
220            let task_mgr = self.task_manager.write().await;
221            task_mgr.complete_task(&task_id, summary).await?;
222
223            // Clear current task
224            *self.current_task_id.write().await = None;
225
226            // Check if plan is complete
227            let stats = task_mgr.get_stats().await;
228            if stats.completed == stats.total {
229                *self.status.write().await = PlanExecutionStatus::Completed;
230            }
231
232            // Get and return the next task if auto-advance is enabled
233            if self.config.auto_advance {
234                let ready_tasks = task_mgr.get_ready_tasks().await;
235                return Ok(ready_tasks.into_iter().next());
236            }
237        }
238
239        Ok(None)
240    }
241
242    /// Skip the current task
243    pub async fn skip_current_task(&self, reason: Option<String>) -> Result<Option<Task>> {
244        let task_id = {
245            let current = self.current_task_id.read().await;
246            current.clone()
247        };
248
249        if let Some(task_id) = task_id {
250            let task_mgr = self.task_manager.write().await;
251            task_mgr.skip_task(&task_id, reason).await?;
252
253            // Clear current task
254            *self.current_task_id.write().await = None;
255
256            // Get next task if auto-advance
257            if self.config.auto_advance {
258                let ready_tasks = task_mgr.get_ready_tasks().await;
259                return Ok(ready_tasks.into_iter().next());
260            }
261        }
262
263        Ok(None)
264    }
265
266    /// Fail the current task
267    pub async fn fail_current_task(&self, error: String) -> Result<()> {
268        let task_id = {
269            let current = self.current_task_id.read().await;
270            current.clone()
271        };
272
273        if let Some(task_id) = task_id {
274            let task_mgr = self.task_manager.write().await;
275            task_mgr.fail_task(&task_id, error.clone()).await?;
276
277            // Clear current task
278            *self.current_task_id.write().await = None;
279
280            if self.config.stop_on_error {
281                *self.status.write().await = PlanExecutionStatus::Failed(error);
282            }
283        }
284
285        Ok(())
286    }
287
288    /// Pause execution
289    pub async fn pause(&self) {
290        *self.status.write().await = PlanExecutionStatus::Paused;
291    }
292
293    /// Resume execution
294    pub async fn resume(&self) -> Option<Task> {
295        *self.status.write().await = PlanExecutionStatus::Running;
296
297        // Return current task or get next ready task
298        let current = self.current_task_id.read().await.clone();
299        if current.is_some() {
300            let task_mgr = self.task_manager.read().await;
301            if let Some(id) = current {
302                return task_mgr.get_task(&id).await;
303            }
304        }
305
306        self.get_next_task().await
307    }
308
309    /// Request approval for a task (in Suggest mode)
310    pub async fn request_approval(&self, task: &Task) {
311        *self.status.write().await =
312            PlanExecutionStatus::WaitingForApproval(task.description.clone());
313    }
314
315    /// Approve and start a task
316    pub async fn approve_and_start(&self, task_id: &str) -> Result<()> {
317        self.start_task(task_id).await
318    }
319
320    /// Get execution progress
321    pub async fn get_progress(&self) -> ExecutionProgress {
322        let task_mgr = self.task_manager.read().await;
323        let stats = task_mgr.get_stats().await;
324        let time_stats = task_mgr.get_time_stats().await;
325
326        ExecutionProgress {
327            total_tasks: stats.total,
328            completed_tasks: stats.completed,
329            in_progress_tasks: stats.in_progress,
330            pending_tasks: stats.pending,
331            blocked_tasks: stats.blocked,
332            skipped_tasks: stats.skipped,
333            failed_tasks: stats.failed,
334            total_duration_secs: time_stats.total_duration_secs,
335            average_task_duration_secs: time_stats.average_duration_secs,
336            estimated_remaining_secs: task_mgr.estimate_remaining_time().await,
337        }
338    }
339
340    /// Format progress as a string
341    pub async fn format_progress(&self) -> String {
342        let progress = self.get_progress().await;
343        let status = self.status().await;
344
345        let mut output = format!(
346            "Plan Execution Status: {}\n\
347             Progress: {}/{} tasks completed\n",
348            status, progress.completed_tasks, progress.total_tasks
349        );
350
351        if progress.in_progress_tasks > 0 {
352            output.push_str(&format!("  In Progress: {}\n", progress.in_progress_tasks));
353        }
354        if progress.blocked_tasks > 0 {
355            output.push_str(&format!("  Blocked: {}\n", progress.blocked_tasks));
356        }
357        if progress.skipped_tasks > 0 {
358            output.push_str(&format!("  Skipped: {}\n", progress.skipped_tasks));
359        }
360        if progress.failed_tasks > 0 {
361            output.push_str(&format!("  Failed: {}\n", progress.failed_tasks));
362        }
363
364        if progress.total_duration_secs > 0 {
365            output.push_str(&format!(
366                "Time: {} elapsed",
367                format_duration(progress.total_duration_secs)
368            ));
369
370            if let Some(remaining) = progress.estimated_remaining_secs {
371                output.push_str(&format!(", ~{} remaining", format_duration(remaining)));
372            }
373            output.push('\n');
374        }
375
376        output
377    }
378}
379
380/// Execution progress information
381#[derive(Debug, Clone)]
382pub struct ExecutionProgress {
383    /// Total number of tasks in the plan.
384    pub total_tasks: usize,
385    /// Number of completed tasks.
386    pub completed_tasks: usize,
387    /// Number of tasks currently in progress.
388    pub in_progress_tasks: usize,
389    /// Number of pending tasks.
390    pub pending_tasks: usize,
391    /// Number of blocked tasks.
392    pub blocked_tasks: usize,
393    /// Number of skipped tasks.
394    pub skipped_tasks: usize,
395    /// Number of failed tasks.
396    pub failed_tasks: usize,
397    /// Total elapsed duration in seconds.
398    pub total_duration_secs: i64,
399    /// Average task duration in seconds.
400    pub average_task_duration_secs: Option<i64>,
401    /// Estimated remaining time in seconds.
402    pub estimated_remaining_secs: Option<i64>,
403}
404
405/// Format duration in human readable form
406fn format_duration(secs: i64) -> String {
407    if secs < 60 {
408        format!("{}s", secs)
409    } else if secs < 3600 {
410        format!("{}m {}s", secs / 60, secs % 60)
411    } else {
412        format!("{}h {}m", secs / 3600, (secs % 3600) / 60)
413    }
414}
415
416#[cfg(test)]
417mod tests {
418    use super::*;
419    use brainwires_core::{PlanStatus, TaskPriority, TaskStatus};
420
421    fn create_test_plan() -> PlanMetadata {
422        PlanMetadata {
423            plan_id: "test-plan-1".to_string(),
424            conversation_id: "conv-1".to_string(),
425            title: "Test Plan".to_string(),
426            task_description: "Test the plan executor".to_string(),
427            plan_content: "1. First task\n2. Second task".to_string(),
428            model_id: None,
429            status: PlanStatus::Active,
430            executed: false,
431            iterations_used: 0,
432            created_at: 0,
433            updated_at: 0,
434            file_path: None,
435            embedding: None,
436            // Branching fields
437            parent_plan_id: None,
438            child_plan_ids: Vec::new(),
439            branch_name: None,
440            merged: false,
441            depth: 0,
442        }
443    }
444
445    async fn create_test_task_manager() -> Arc<RwLock<TaskManager>> {
446        let task_mgr = TaskManager::new();
447
448        // Add test tasks
449        task_mgr
450            .create_task("First task".to_string(), None, TaskPriority::Normal)
451            .await
452            .unwrap();
453        task_mgr
454            .create_task("Second task".to_string(), None, TaskPriority::Normal)
455            .await
456            .unwrap();
457
458        Arc::new(RwLock::new(task_mgr))
459    }
460
461    #[tokio::test]
462    async fn test_executor_creation() {
463        let plan = create_test_plan();
464        let task_mgr = create_test_task_manager().await;
465        let config = PlanExecutionConfig::default();
466
467        let executor = PlanExecutorAgent::new(plan, task_mgr, config);
468
469        assert_eq!(executor.status().await, PlanExecutionStatus::Idle);
470        assert!(executor.current_task_id().await.is_none());
471    }
472
473    #[tokio::test]
474    async fn test_approval_modes() {
475        let plan = create_test_plan();
476        let task_mgr = create_test_task_manager().await;
477        let config = PlanExecutionConfig::default();
478
479        let mut executor = PlanExecutorAgent::new(plan, task_mgr, config);
480
481        // Default is FullAuto
482        assert_eq!(executor.approval_mode(), ExecutionApprovalMode::FullAuto);
483
484        // Change mode
485        executor.set_approval_mode(ExecutionApprovalMode::Suggest);
486        assert_eq!(executor.approval_mode(), ExecutionApprovalMode::Suggest);
487    }
488
489    #[tokio::test]
490    async fn test_get_next_task() {
491        let plan = create_test_plan();
492        let task_mgr = create_test_task_manager().await;
493        let config = PlanExecutionConfig::default();
494
495        let executor = PlanExecutorAgent::new(plan, task_mgr, config);
496
497        let next = executor.get_next_task().await;
498        assert!(next.is_some());
499        // Don't check specific task - order is non-deterministic
500        let desc = next.unwrap().description;
501        assert!(desc == "First task" || desc == "Second task");
502    }
503
504    #[tokio::test]
505    async fn test_start_task() {
506        let plan = create_test_plan();
507        let task_mgr = create_test_task_manager().await;
508        let config = PlanExecutionConfig::default();
509
510        // Get the first task ID
511        let task_id = {
512            let mgr = task_mgr.read().await;
513            let tasks = mgr.get_all_tasks().await;
514            tasks[0].id.clone()
515        };
516
517        let executor = PlanExecutorAgent::new(plan, task_mgr.clone(), config);
518
519        // Start the task
520        executor.start_task(&task_id).await.unwrap();
521
522        assert_eq!(executor.status().await, PlanExecutionStatus::Running);
523        assert_eq!(executor.current_task_id().await, Some(task_id.clone()));
524
525        // Verify task status in manager
526        let mgr = task_mgr.read().await;
527        let task = mgr.get_task(&task_id).await.unwrap();
528        assert_eq!(task.status, TaskStatus::InProgress);
529    }
530
531    #[tokio::test]
532    async fn test_complete_task() {
533        let plan = create_test_plan();
534        let task_mgr = create_test_task_manager().await;
535        let config = PlanExecutionConfig::default();
536
537        let task_id = {
538            let mgr = task_mgr.read().await;
539            let tasks = mgr.get_all_tasks().await;
540            tasks[0].id.clone()
541        };
542
543        let executor = PlanExecutorAgent::new(plan, task_mgr.clone(), config);
544
545        // Start and complete task
546        executor.start_task(&task_id).await.unwrap();
547        let next = executor
548            .complete_current_task("Done".to_string())
549            .await
550            .unwrap();
551
552        // Should get next task due to auto-advance
553        assert!(next.is_some());
554        // Don't check specific task - the other task should be returned
555        let next_desc = next.unwrap().description;
556        let started_desc = {
557            let mgr = task_mgr.read().await;
558            mgr.get_task(&task_id).await.unwrap().description.clone()
559        };
560        // Next task should be different from the one we completed
561        assert_ne!(next_desc, started_desc);
562
563        // Current task should be cleared
564        assert!(executor.current_task_id().await.is_none());
565    }
566
567    #[tokio::test]
568    async fn test_pause_resume() {
569        let plan = create_test_plan();
570        let task_mgr = create_test_task_manager().await;
571        let config = PlanExecutionConfig::default();
572
573        let executor = PlanExecutorAgent::new(plan, task_mgr, config);
574
575        executor.pause().await;
576        assert_eq!(executor.status().await, PlanExecutionStatus::Paused);
577
578        let next = executor.resume().await;
579        assert_eq!(executor.status().await, PlanExecutionStatus::Running);
580        assert!(next.is_some());
581    }
582
583    #[tokio::test]
584    async fn test_progress() {
585        let plan = create_test_plan();
586        let task_mgr = create_test_task_manager().await;
587        let config = PlanExecutionConfig::default();
588
589        let task_id = {
590            let mgr = task_mgr.read().await;
591            let tasks = mgr.get_all_tasks().await;
592            tasks[0].id.clone()
593        };
594
595        let executor = PlanExecutorAgent::new(plan, task_mgr, config);
596
597        // Start task
598        executor.start_task(&task_id).await.unwrap();
599
600        let progress = executor.get_progress().await;
601        assert_eq!(progress.total_tasks, 2);
602        assert_eq!(progress.in_progress_tasks, 1);
603        assert_eq!(progress.pending_tasks, 1);
604        assert_eq!(progress.completed_tasks, 0);
605    }
606
607    #[test]
608    fn test_approval_mode_parsing() {
609        assert_eq!(
610            "suggest".parse::<ExecutionApprovalMode>().unwrap(),
611            ExecutionApprovalMode::Suggest
612        );
613        assert_eq!(
614            "auto-edit".parse::<ExecutionApprovalMode>().unwrap(),
615            ExecutionApprovalMode::AutoEdit
616        );
617        assert_eq!(
618            "full-auto".parse::<ExecutionApprovalMode>().unwrap(),
619            ExecutionApprovalMode::FullAuto
620        );
621        assert_eq!(
622            "auto".parse::<ExecutionApprovalMode>().unwrap(),
623            ExecutionApprovalMode::FullAuto
624        );
625    }
626
627    #[test]
628    fn test_format_duration() {
629        assert_eq!(format_duration(30), "30s");
630        assert_eq!(format_duration(90), "1m 30s");
631        assert_eq!(format_duration(3661), "1h 1m");
632    }
633}