1use anyhow::Result;
7use std::sync::Arc;
8use tokio::sync::RwLock;
9
10use brainwires_core::{PlanMetadata, Task};
11
12use crate::task_manager::TaskManager;
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
16pub enum ExecutionApprovalMode {
17 Suggest,
19 AutoEdit,
21 #[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#[derive(Debug, Clone, PartialEq, Eq)]
51pub enum PlanExecutionStatus {
52 Idle,
54 Running,
56 WaitingForApproval(String),
58 Paused,
60 Completed,
62 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#[derive(Debug, Clone)]
81pub struct PlanExecutionConfig {
82 pub approval_mode: ExecutionApprovalMode,
84 pub max_iterations_per_task: u32,
86 pub auto_advance: bool,
88 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
103pub struct PlanExecutorAgent {
105 plan: Arc<RwLock<PlanMetadata>>,
107 task_manager: Arc<RwLock<TaskManager>>,
109 config: PlanExecutionConfig,
111 status: Arc<RwLock<PlanExecutionStatus>>,
113 current_task_id: Arc<RwLock<Option<String>>>,
115}
116
117impl PlanExecutorAgent {
118 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 #[tracing::instrument(name = "agent.plan.get", skip(self))]
135 pub async fn plan(&self) -> PlanMetadata {
136 self.plan.read().await.clone()
137 }
138
139 pub async fn status(&self) -> PlanExecutionStatus {
141 self.status.read().await.clone()
142 }
143
144 pub async fn current_task_id(&self) -> Option<String> {
146 self.current_task_id.read().await.clone()
147 }
148
149 pub fn approval_mode(&self) -> ExecutionApprovalMode {
151 self.config.approval_mode
152 }
153
154 pub fn set_approval_mode(&mut self, mode: ExecutionApprovalMode) {
156 self.config.approval_mode = mode;
157 }
158
159 pub fn needs_approval(&self, _task: &Task) -> bool {
161 match self.config.approval_mode {
162 ExecutionApprovalMode::Suggest => true, ExecutionApprovalMode::AutoEdit => false, ExecutionApprovalMode::FullAuto => false, }
166 }
167
168 #[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 #[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 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 task_mgr.start_task(task_id).await?;
201
202 *self.current_task_id.write().await = Some(task_id.to_string());
204
205 *self.status.write().await = PlanExecutionStatus::Running;
207
208 Ok(())
209 }
210
211 #[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 *self.current_task_id.write().await = None;
225
226 let stats = task_mgr.get_stats().await;
228 if stats.completed == stats.total {
229 *self.status.write().await = PlanExecutionStatus::Completed;
230 }
231
232 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 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 *self.current_task_id.write().await = None;
255
256 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 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 *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 pub async fn pause(&self) {
290 *self.status.write().await = PlanExecutionStatus::Paused;
291 }
292
293 pub async fn resume(&self) -> Option<Task> {
295 *self.status.write().await = PlanExecutionStatus::Running;
296
297 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 pub async fn request_approval(&self, task: &Task) {
311 *self.status.write().await =
312 PlanExecutionStatus::WaitingForApproval(task.description.clone());
313 }
314
315 pub async fn approve_and_start(&self, task_id: &str) -> Result<()> {
317 self.start_task(task_id).await
318 }
319
320 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 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#[derive(Debug, Clone)]
382pub struct ExecutionProgress {
383 pub total_tasks: usize,
385 pub completed_tasks: usize,
387 pub in_progress_tasks: usize,
389 pub pending_tasks: usize,
391 pub blocked_tasks: usize,
393 pub skipped_tasks: usize,
395 pub failed_tasks: usize,
397 pub total_duration_secs: i64,
399 pub average_task_duration_secs: Option<i64>,
401 pub estimated_remaining_secs: Option<i64>,
403}
404
405fn 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 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 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 assert_eq!(executor.approval_mode(), ExecutionApprovalMode::FullAuto);
483
484 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 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 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 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 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 executor.start_task(&task_id).await.unwrap();
547 let next = executor
548 .complete_current_task("Done".to_string())
549 .await
550 .unwrap();
551
552 assert!(next.is_some());
554 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 assert_ne!(next_desc, started_desc);
562
563 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 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}