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