Skip to main content

a3s_code_core/planning/
mod.rs

1//! Planning, Goal Tracking, and Task Management
2//!
3//! Unified task tracking for both execution planning (decomposed steps with
4//! dependencies) and user-facing task lists (priority, manual tracking).
5//!
6//! The [`Task`] struct replaces the former separate `PlanStep` and `Todo` types.
7
8pub mod llm_planner;
9
10pub use llm_planner::{AchievementResult, LlmPlanner, Planner};
11
12use serde::{Deserialize, Serialize};
13use std::fmt;
14use std::str::FromStr;
15
16// ============================================================================
17// Task Status (unified from StepStatus + TodoStatus)
18// ============================================================================
19
20/// Task status — covers both execution steps and manual tasks
21#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
22#[serde(rename_all = "snake_case")]
23pub enum TaskStatus {
24    /// Task is waiting to be started
25    #[default]
26    Pending,
27    /// Task is currently being worked on
28    InProgress,
29    /// Task completed successfully
30    Completed,
31    /// Task failed during execution
32    Failed,
33    /// Task was skipped (dependency resolution, no longer needed)
34    Skipped,
35    /// Task was cancelled by user
36    Cancelled,
37}
38
39impl fmt::Display for TaskStatus {
40    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
41        match self {
42            TaskStatus::Pending => write!(f, "pending"),
43            TaskStatus::InProgress => write!(f, "in_progress"),
44            TaskStatus::Completed => write!(f, "completed"),
45            TaskStatus::Failed => write!(f, "failed"),
46            TaskStatus::Skipped => write!(f, "skipped"),
47            TaskStatus::Cancelled => write!(f, "cancelled"),
48        }
49    }
50}
51
52impl FromStr for TaskStatus {
53    type Err = std::convert::Infallible;
54
55    fn from_str(s: &str) -> Result<Self, Self::Err> {
56        Ok(match s.to_lowercase().as_str() {
57            "pending" => TaskStatus::Pending,
58            "in_progress" | "inprogress" => TaskStatus::InProgress,
59            "completed" | "done" => TaskStatus::Completed,
60            "failed" => TaskStatus::Failed,
61            "skipped" => TaskStatus::Skipped,
62            "cancelled" | "canceled" => TaskStatus::Cancelled,
63            _ => TaskStatus::Pending,
64        })
65    }
66}
67
68impl TaskStatus {
69    /// Check if task is still active (not completed, failed, skipped, or cancelled)
70    pub fn is_active(&self) -> bool {
71        matches!(self, TaskStatus::Pending | TaskStatus::InProgress)
72    }
73}
74
75// ============================================================================
76// Task Priority
77// ============================================================================
78
79/// Task priority level
80#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
81#[serde(rename_all = "snake_case")]
82pub enum TaskPriority {
83    /// High priority — should be done first
84    High,
85    /// Medium priority — normal importance
86    #[default]
87    Medium,
88    /// Low priority — can be deferred
89    Low,
90}
91
92impl fmt::Display for TaskPriority {
93    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
94        match self {
95            TaskPriority::High => write!(f, "high"),
96            TaskPriority::Medium => write!(f, "medium"),
97            TaskPriority::Low => write!(f, "low"),
98        }
99    }
100}
101
102impl FromStr for TaskPriority {
103    type Err = std::convert::Infallible;
104
105    fn from_str(s: &str) -> Result<Self, Self::Err> {
106        Ok(match s.to_lowercase().as_str() {
107            "high" | "h" | "1" => TaskPriority::High,
108            "medium" | "med" | "m" | "2" => TaskPriority::Medium,
109            "low" | "l" | "3" => TaskPriority::Low,
110            _ => TaskPriority::Medium,
111        })
112    }
113}
114
115// ============================================================================
116// Task (unified from PlanStep + Todo)
117// ============================================================================
118
119/// A task item — used for both execution plan steps and user-facing task tracking
120#[derive(Debug, Clone, Serialize, Deserialize)]
121pub struct Task {
122    /// Unique identifier
123    pub id: String,
124    /// Brief description of the task
125    #[serde(alias = "description")]
126    pub content: String,
127    /// Current status
128    pub status: TaskStatus,
129    /// Priority level (for user-facing ordering)
130    #[serde(default)]
131    pub priority: TaskPriority,
132    /// Tool to use for this step (execution plans)
133    #[serde(default, skip_serializing_if = "Option::is_none")]
134    pub tool: Option<String>,
135    /// IDs of tasks that must complete before this one
136    #[serde(default, skip_serializing_if = "Vec::is_empty")]
137    pub dependencies: Vec<String>,
138    /// Expected output or success criteria
139    #[serde(default, skip_serializing_if = "Option::is_none")]
140    pub success_criteria: Option<String>,
141}
142
143impl Task {
144    /// Create a new task with pending status and medium priority
145    pub fn new(id: impl Into<String>, content: impl Into<String>) -> Self {
146        Self {
147            id: id.into(),
148            content: content.into(),
149            status: TaskStatus::Pending,
150            priority: TaskPriority::Medium,
151            tool: None,
152            dependencies: Vec::new(),
153            success_criteria: None,
154        }
155    }
156
157    /// Set priority
158    pub fn with_priority(mut self, priority: TaskPriority) -> Self {
159        self.priority = priority;
160        self
161    }
162
163    /// Set status
164    pub fn with_status(mut self, status: TaskStatus) -> Self {
165        self.status = status;
166        self
167    }
168
169    /// Set tool for execution
170    pub fn with_tool(mut self, tool: impl Into<String>) -> Self {
171        self.tool = Some(tool.into());
172        self
173    }
174
175    /// Set dependency IDs
176    pub fn with_dependencies(mut self, deps: Vec<String>) -> Self {
177        self.dependencies = deps;
178        self
179    }
180
181    /// Set success criteria
182    pub fn with_success_criteria(mut self, criteria: impl Into<String>) -> Self {
183        self.success_criteria = Some(criteria.into());
184        self
185    }
186
187    /// Check if task is still active
188    pub fn is_active(&self) -> bool {
189        self.status.is_active()
190    }
191}
192
193// ============================================================================
194// Planning Structures
195// ============================================================================
196
197/// Task complexity level
198#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
199pub enum Complexity {
200    /// Simple task (1-2 steps)
201    Simple,
202    /// Medium complexity (3-5 steps)
203    Medium,
204    /// Complex task (6-10 steps)
205    Complex,
206    /// Very complex task (10+ steps)
207    VeryComplex,
208}
209
210/// Execution plan for a task
211#[derive(Debug, Clone, Serialize, Deserialize)]
212pub struct ExecutionPlan {
213    /// High-level goal
214    pub goal: String,
215    /// Decomposed steps
216    pub steps: Vec<Task>,
217    /// Estimated complexity
218    pub complexity: Complexity,
219    /// Required tools
220    pub required_tools: Vec<String>,
221    /// Estimated total steps
222    pub estimated_steps: usize,
223}
224
225impl ExecutionPlan {
226    pub fn new(goal: impl Into<String>, complexity: Complexity) -> Self {
227        Self {
228            goal: goal.into(),
229            steps: Vec::new(),
230            complexity,
231            required_tools: Vec::new(),
232            estimated_steps: 0,
233        }
234    }
235
236    pub fn add_step(&mut self, step: Task) {
237        self.steps.push(step);
238        self.estimated_steps = self.steps.len();
239    }
240
241    pub fn add_required_tool(&mut self, tool: impl Into<String>) {
242        let tool_str = tool.into();
243        if !self.required_tools.contains(&tool_str) {
244            self.required_tools.push(tool_str);
245        }
246    }
247
248    /// Get steps that are ready to execute (dependencies met)
249    pub fn get_ready_steps(&self) -> Vec<&Task> {
250        self.steps
251            .iter()
252            .filter(|step| {
253                step.status == TaskStatus::Pending
254                    && step.dependencies.iter().all(|dep_id| {
255                        self.steps
256                            .iter()
257                            .find(|s| &s.id == dep_id)
258                            .map(|s| s.status == TaskStatus::Completed)
259                            .unwrap_or(false)
260                    })
261            })
262            .collect()
263    }
264
265    /// Update the status of a step by ID
266    pub fn mark_status(&mut self, step_id: &str, status: TaskStatus) {
267        if let Some(step) = self.steps.iter_mut().find(|s| s.id == step_id) {
268            step.status = status;
269        }
270    }
271
272    /// Count remaining Pending steps
273    pub fn pending_count(&self) -> usize {
274        self.steps
275            .iter()
276            .filter(|s| s.status == TaskStatus::Pending)
277            .count()
278    }
279
280    /// Detect deadlock: Pending steps remain but none are ready to execute.
281    ///
282    /// This happens when all Pending steps have dependencies that are not
283    /// Completed (e.g., circular deps or all deps Failed/Skipped).
284    pub fn has_deadlock(&self) -> bool {
285        self.pending_count() > 0 && self.get_ready_steps().is_empty()
286    }
287
288    /// Get progress as a fraction (0.0 - 1.0)
289    pub fn progress(&self) -> f32 {
290        if self.steps.is_empty() {
291            return 0.0;
292        }
293        let completed = self
294            .steps
295            .iter()
296            .filter(|s| s.status == TaskStatus::Completed)
297            .count();
298        completed as f32 / self.steps.len() as f32
299    }
300}
301
302// ============================================================================
303// Goal Tracking Structures
304// ============================================================================
305
306/// Agent goal with success criteria
307#[derive(Debug, Clone, Serialize, Deserialize)]
308pub struct AgentGoal {
309    /// Goal description
310    pub description: String,
311    /// Success criteria (list of conditions)
312    pub success_criteria: Vec<String>,
313    /// Current progress (0.0 - 1.0)
314    pub progress: f32,
315    /// Is goal achieved?
316    pub achieved: bool,
317    /// Timestamp when goal was created
318    pub created_at: i64,
319    /// Timestamp when goal was achieved (if achieved)
320    pub achieved_at: Option<i64>,
321}
322
323impl AgentGoal {
324    pub fn new(description: impl Into<String>) -> Self {
325        Self {
326            description: description.into(),
327            success_criteria: Vec::new(),
328            progress: 0.0,
329            achieved: false,
330            created_at: chrono::Utc::now().timestamp(),
331            achieved_at: None,
332        }
333    }
334
335    pub fn with_criteria(mut self, criteria: Vec<String>) -> Self {
336        self.success_criteria = criteria;
337        self
338    }
339
340    pub fn update_progress(&mut self, progress: f32) {
341        self.progress = progress.clamp(0.0, 1.0);
342    }
343
344    pub fn mark_achieved(&mut self) {
345        self.achieved = true;
346        self.progress = 1.0;
347        self.achieved_at = Some(chrono::Utc::now().timestamp());
348    }
349}
350
351// ============================================================================
352// Tests
353// ============================================================================
354
355#[cfg(test)]
356mod tests {
357    use super::*;
358
359    // ========================================================================
360    // TaskStatus tests (merged from TodoStatus + StepStatus)
361    // ========================================================================
362
363    #[test]
364    fn test_task_status_display() {
365        assert_eq!(TaskStatus::Pending.to_string(), "pending");
366        assert_eq!(TaskStatus::InProgress.to_string(), "in_progress");
367        assert_eq!(TaskStatus::Completed.to_string(), "completed");
368        assert_eq!(TaskStatus::Failed.to_string(), "failed");
369        assert_eq!(TaskStatus::Skipped.to_string(), "skipped");
370        assert_eq!(TaskStatus::Cancelled.to_string(), "cancelled");
371    }
372
373    #[test]
374    fn test_task_status_from_str() {
375        assert_eq!(
376            TaskStatus::from_str("pending").unwrap(),
377            TaskStatus::Pending
378        );
379        assert_eq!(
380            TaskStatus::from_str("in_progress").unwrap(),
381            TaskStatus::InProgress
382        );
383        assert_eq!(
384            TaskStatus::from_str("inprogress").unwrap(),
385            TaskStatus::InProgress
386        );
387        assert_eq!(
388            TaskStatus::from_str("completed").unwrap(),
389            TaskStatus::Completed
390        );
391        assert_eq!(TaskStatus::from_str("done").unwrap(), TaskStatus::Completed);
392        assert_eq!(TaskStatus::from_str("failed").unwrap(), TaskStatus::Failed);
393        assert_eq!(
394            TaskStatus::from_str("skipped").unwrap(),
395            TaskStatus::Skipped
396        );
397        assert_eq!(
398            TaskStatus::from_str("cancelled").unwrap(),
399            TaskStatus::Cancelled
400        );
401        assert_eq!(
402            TaskStatus::from_str("canceled").unwrap(),
403            TaskStatus::Cancelled
404        );
405        assert_eq!(
406            TaskStatus::from_str("unknown").unwrap(),
407            TaskStatus::Pending
408        );
409    }
410
411    #[test]
412    fn test_task_status_is_active() {
413        assert!(TaskStatus::Pending.is_active());
414        assert!(TaskStatus::InProgress.is_active());
415        assert!(!TaskStatus::Completed.is_active());
416        assert!(!TaskStatus::Failed.is_active());
417        assert!(!TaskStatus::Skipped.is_active());
418        assert!(!TaskStatus::Cancelled.is_active());
419    }
420
421    #[test]
422    fn test_task_status_serialization() {
423        assert_eq!(
424            serde_json::to_string(&TaskStatus::InProgress).unwrap(),
425            "\"in_progress\""
426        );
427        assert_eq!(
428            serde_json::to_string(&TaskStatus::Failed).unwrap(),
429            "\"failed\""
430        );
431    }
432
433    // ========================================================================
434    // TaskPriority tests (from TodoPriority)
435    // ========================================================================
436
437    #[test]
438    fn test_task_priority_display() {
439        assert_eq!(TaskPriority::High.to_string(), "high");
440        assert_eq!(TaskPriority::Medium.to_string(), "medium");
441        assert_eq!(TaskPriority::Low.to_string(), "low");
442    }
443
444    #[test]
445    fn test_task_priority_from_str() {
446        assert_eq!(TaskPriority::from_str("high").unwrap(), TaskPriority::High);
447        assert_eq!(TaskPriority::from_str("h").unwrap(), TaskPriority::High);
448        assert_eq!(
449            TaskPriority::from_str("medium").unwrap(),
450            TaskPriority::Medium
451        );
452        assert_eq!(TaskPriority::from_str("med").unwrap(), TaskPriority::Medium);
453        assert_eq!(TaskPriority::from_str("low").unwrap(), TaskPriority::Low);
454        assert_eq!(TaskPriority::from_str("l").unwrap(), TaskPriority::Low);
455        assert_eq!(
456            TaskPriority::from_str("unknown").unwrap(),
457            TaskPriority::Medium
458        );
459    }
460
461    // ========================================================================
462    // Task tests (unified from PlanStep + Todo)
463    // ========================================================================
464
465    #[test]
466    fn test_task_new() {
467        let task = Task::new("1", "Test task");
468        assert_eq!(task.id, "1");
469        assert_eq!(task.content, "Test task");
470        assert_eq!(task.status, TaskStatus::Pending);
471        assert_eq!(task.priority, TaskPriority::Medium);
472        assert!(task.tool.is_none());
473        assert!(task.dependencies.is_empty());
474        assert!(task.success_criteria.is_none());
475    }
476
477    #[test]
478    fn test_task_builder() {
479        let task = Task::new("1", "Test task")
480            .with_priority(TaskPriority::High)
481            .with_status(TaskStatus::InProgress)
482            .with_tool("bash")
483            .with_dependencies(vec!["step-0".to_string()])
484            .with_success_criteria("Command exits with 0");
485
486        assert_eq!(task.priority, TaskPriority::High);
487        assert_eq!(task.status, TaskStatus::InProgress);
488        assert_eq!(task.tool, Some("bash".to_string()));
489        assert_eq!(task.dependencies, vec!["step-0".to_string()]);
490        assert_eq!(
491            task.success_criteria,
492            Some("Command exits with 0".to_string())
493        );
494    }
495
496    #[test]
497    fn test_task_is_active() {
498        let pending = Task::new("1", "Pending task");
499        let in_progress = Task::new("2", "In progress").with_status(TaskStatus::InProgress);
500        let completed = Task::new("3", "Completed").with_status(TaskStatus::Completed);
501        let failed = Task::new("4", "Failed").with_status(TaskStatus::Failed);
502        let cancelled = Task::new("5", "Cancelled").with_status(TaskStatus::Cancelled);
503
504        assert!(pending.is_active());
505        assert!(in_progress.is_active());
506        assert!(!completed.is_active());
507        assert!(!failed.is_active());
508        assert!(!cancelled.is_active());
509    }
510
511    #[test]
512    fn test_task_serialization() {
513        let task = Task::new("1", "Test task")
514            .with_priority(TaskPriority::High)
515            .with_status(TaskStatus::InProgress);
516
517        let json = serde_json::to_string(&task).unwrap();
518        let parsed: Task = serde_json::from_str(&json).unwrap();
519
520        assert_eq!(parsed.id, task.id);
521        assert_eq!(parsed.content, task.content);
522        assert_eq!(parsed.status, task.status);
523        assert_eq!(parsed.priority, task.priority);
524    }
525
526    #[test]
527    fn test_task_deserialize_description_alias() {
528        // Backward compat: "description" field alias works
529        let json = r#"{"id": "step-1", "description": "Test step", "status": "pending"}"#;
530        let task: Task = serde_json::from_str(json).unwrap();
531        assert_eq!(task.content, "Test step");
532    }
533
534    // ========================================================================
535    // ExecutionPlan tests
536    // ========================================================================
537
538    #[test]
539    fn test_execution_plan() {
540        let mut plan = ExecutionPlan::new("Test goal", Complexity::Medium);
541
542        plan.add_step(Task::new("step-1", "First step"));
543        plan.add_step(
544            Task::new("step-2", "Second step").with_dependencies(vec!["step-1".to_string()]),
545        );
546
547        assert_eq!(plan.steps.len(), 2);
548        assert_eq!(plan.estimated_steps, 2);
549        assert_eq!(plan.progress(), 0.0);
550
551        // Mark first step as completed
552        plan.steps[0].status = TaskStatus::Completed;
553        assert_eq!(plan.progress(), 0.5);
554
555        // Check ready steps
556        let ready = plan.get_ready_steps();
557        assert_eq!(ready.len(), 1);
558        assert_eq!(ready[0].id, "step-2");
559    }
560
561    // ========================================================================
562    // ExecutionPlan helper method tests
563    // ========================================================================
564
565    #[test]
566    fn test_mark_status() {
567        let mut plan = ExecutionPlan::new("Test", Complexity::Simple);
568        plan.add_step(Task::new("s1", "Step 1"));
569        plan.add_step(Task::new("s2", "Step 2"));
570
571        assert_eq!(plan.steps[0].status, TaskStatus::Pending);
572        plan.mark_status("s1", TaskStatus::InProgress);
573        assert_eq!(plan.steps[0].status, TaskStatus::InProgress);
574        plan.mark_status("s1", TaskStatus::Completed);
575        assert_eq!(plan.steps[0].status, TaskStatus::Completed);
576        // Non-existent ID is a no-op
577        plan.mark_status("s999", TaskStatus::Failed);
578        assert_eq!(plan.steps[1].status, TaskStatus::Pending);
579    }
580
581    #[test]
582    fn test_pending_count() {
583        let mut plan = ExecutionPlan::new("Test", Complexity::Simple);
584        plan.add_step(Task::new("s1", "Step 1"));
585        plan.add_step(Task::new("s2", "Step 2"));
586        plan.add_step(Task::new("s3", "Step 3"));
587
588        assert_eq!(plan.pending_count(), 3);
589        plan.mark_status("s1", TaskStatus::Completed);
590        assert_eq!(plan.pending_count(), 2);
591        plan.mark_status("s2", TaskStatus::Failed);
592        assert_eq!(plan.pending_count(), 1);
593        plan.mark_status("s3", TaskStatus::InProgress);
594        assert_eq!(plan.pending_count(), 0);
595    }
596
597    #[test]
598    fn test_has_deadlock() {
599        // Circular dependency: s1 depends on s2, s2 depends on s1
600        let mut plan = ExecutionPlan::new("Test", Complexity::Simple);
601        plan.add_step(Task::new("s1", "Step 1").with_dependencies(vec!["s2".to_string()]));
602        plan.add_step(Task::new("s2", "Step 2").with_dependencies(vec!["s1".to_string()]));
603
604        assert!(plan.has_deadlock());
605
606        // No deadlock when steps have no deps
607        let mut plan2 = ExecutionPlan::new("Test", Complexity::Simple);
608        plan2.add_step(Task::new("s1", "Step 1"));
609        assert!(!plan2.has_deadlock());
610
611        // Deadlock when dependency failed
612        let mut plan3 = ExecutionPlan::new("Test", Complexity::Simple);
613        plan3.add_step(Task::new("s1", "Step 1"));
614        plan3.add_step(Task::new("s2", "Step 2").with_dependencies(vec!["s1".to_string()]));
615        plan3.mark_status("s1", TaskStatus::Failed);
616        assert!(plan3.has_deadlock()); // s2 depends on s1 which failed, not completed
617    }
618
619    #[test]
620    fn test_get_ready_steps_parallel() {
621        // Three independent steps — all should be ready simultaneously
622        let mut plan = ExecutionPlan::new("Test", Complexity::Medium);
623        plan.add_step(Task::new("s1", "Step 1"));
624        plan.add_step(Task::new("s2", "Step 2"));
625        plan.add_step(Task::new("s3", "Step 3"));
626
627        let ready = plan.get_ready_steps();
628        assert_eq!(ready.len(), 3);
629    }
630
631    #[test]
632    fn test_get_ready_steps_wave() {
633        // s1 and s2 are independent; s3 depends on both
634        let mut plan = ExecutionPlan::new("Test", Complexity::Medium);
635        plan.add_step(Task::new("s1", "Step 1"));
636        plan.add_step(Task::new("s2", "Step 2"));
637        plan.add_step(
638            Task::new("s3", "Step 3").with_dependencies(vec!["s1".to_string(), "s2".to_string()]),
639        );
640
641        // Wave 1: s1 and s2
642        let ready = plan.get_ready_steps();
643        assert_eq!(ready.len(), 2);
644        let ids: Vec<&str> = ready.iter().map(|s| s.id.as_str()).collect();
645        assert!(ids.contains(&"s1"));
646        assert!(ids.contains(&"s2"));
647
648        // Complete wave 1
649        plan.mark_status("s1", TaskStatus::Completed);
650        plan.mark_status("s2", TaskStatus::Completed);
651
652        // Wave 2: s3
653        let ready = plan.get_ready_steps();
654        assert_eq!(ready.len(), 1);
655        assert_eq!(ready[0].id, "s3");
656    }
657
658    // ========================================================================
659    // AgentGoal tests
660    // ========================================================================
661
662    #[test]
663    fn test_agent_goal() {
664        let mut goal = AgentGoal::new("Complete task")
665            .with_criteria(vec!["Criterion 1".to_string(), "Criterion 2".to_string()]);
666
667        assert_eq!(goal.description, "Complete task");
668        assert_eq!(goal.success_criteria.len(), 2);
669        assert_eq!(goal.progress, 0.0);
670        assert!(!goal.achieved);
671
672        goal.update_progress(0.5);
673        assert_eq!(goal.progress, 0.5);
674
675        goal.mark_achieved();
676        assert!(goal.achieved);
677        assert_eq!(goal.progress, 1.0);
678        assert!(goal.achieved_at.is_some());
679    }
680
681    #[test]
682    fn test_complexity_levels() {
683        assert_eq!(
684            serde_json::to_string(&Complexity::Simple).unwrap(),
685            "\"Simple\""
686        );
687        assert_eq!(
688            serde_json::to_string(&Complexity::Complex).unwrap(),
689            "\"Complex\""
690        );
691    }
692}