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};
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    /// Get progress as a fraction (0.0 - 1.0)
266    pub fn progress(&self) -> f32 {
267        if self.steps.is_empty() {
268            return 0.0;
269        }
270        let completed = self
271            .steps
272            .iter()
273            .filter(|s| s.status == TaskStatus::Completed)
274            .count();
275        completed as f32 / self.steps.len() as f32
276    }
277}
278
279// ============================================================================
280// Goal Tracking Structures
281// ============================================================================
282
283/// Agent goal with success criteria
284#[derive(Debug, Clone, Serialize, Deserialize)]
285pub struct AgentGoal {
286    /// Goal description
287    pub description: String,
288    /// Success criteria (list of conditions)
289    pub success_criteria: Vec<String>,
290    /// Current progress (0.0 - 1.0)
291    pub progress: f32,
292    /// Is goal achieved?
293    pub achieved: bool,
294    /// Timestamp when goal was created
295    pub created_at: i64,
296    /// Timestamp when goal was achieved (if achieved)
297    pub achieved_at: Option<i64>,
298}
299
300impl AgentGoal {
301    pub fn new(description: impl Into<String>) -> Self {
302        Self {
303            description: description.into(),
304            success_criteria: Vec::new(),
305            progress: 0.0,
306            achieved: false,
307            created_at: chrono::Utc::now().timestamp(),
308            achieved_at: None,
309        }
310    }
311
312    pub fn with_criteria(mut self, criteria: Vec<String>) -> Self {
313        self.success_criteria = criteria;
314        self
315    }
316
317    pub fn update_progress(&mut self, progress: f32) {
318        self.progress = progress.clamp(0.0, 1.0);
319    }
320
321    pub fn mark_achieved(&mut self) {
322        self.achieved = true;
323        self.progress = 1.0;
324        self.achieved_at = Some(chrono::Utc::now().timestamp());
325    }
326}
327
328// ============================================================================
329// Tests
330// ============================================================================
331
332#[cfg(test)]
333mod tests {
334    use super::*;
335
336    // ========================================================================
337    // TaskStatus tests (merged from TodoStatus + StepStatus)
338    // ========================================================================
339
340    #[test]
341    fn test_task_status_display() {
342        assert_eq!(TaskStatus::Pending.to_string(), "pending");
343        assert_eq!(TaskStatus::InProgress.to_string(), "in_progress");
344        assert_eq!(TaskStatus::Completed.to_string(), "completed");
345        assert_eq!(TaskStatus::Failed.to_string(), "failed");
346        assert_eq!(TaskStatus::Skipped.to_string(), "skipped");
347        assert_eq!(TaskStatus::Cancelled.to_string(), "cancelled");
348    }
349
350    #[test]
351    fn test_task_status_from_str() {
352        assert_eq!(
353            TaskStatus::from_str("pending").unwrap(),
354            TaskStatus::Pending
355        );
356        assert_eq!(
357            TaskStatus::from_str("in_progress").unwrap(),
358            TaskStatus::InProgress
359        );
360        assert_eq!(
361            TaskStatus::from_str("inprogress").unwrap(),
362            TaskStatus::InProgress
363        );
364        assert_eq!(
365            TaskStatus::from_str("completed").unwrap(),
366            TaskStatus::Completed
367        );
368        assert_eq!(TaskStatus::from_str("done").unwrap(), TaskStatus::Completed);
369        assert_eq!(TaskStatus::from_str("failed").unwrap(), TaskStatus::Failed);
370        assert_eq!(
371            TaskStatus::from_str("skipped").unwrap(),
372            TaskStatus::Skipped
373        );
374        assert_eq!(
375            TaskStatus::from_str("cancelled").unwrap(),
376            TaskStatus::Cancelled
377        );
378        assert_eq!(
379            TaskStatus::from_str("canceled").unwrap(),
380            TaskStatus::Cancelled
381        );
382        assert_eq!(
383            TaskStatus::from_str("unknown").unwrap(),
384            TaskStatus::Pending
385        );
386    }
387
388    #[test]
389    fn test_task_status_is_active() {
390        assert!(TaskStatus::Pending.is_active());
391        assert!(TaskStatus::InProgress.is_active());
392        assert!(!TaskStatus::Completed.is_active());
393        assert!(!TaskStatus::Failed.is_active());
394        assert!(!TaskStatus::Skipped.is_active());
395        assert!(!TaskStatus::Cancelled.is_active());
396    }
397
398    #[test]
399    fn test_task_status_serialization() {
400        assert_eq!(
401            serde_json::to_string(&TaskStatus::InProgress).unwrap(),
402            "\"in_progress\""
403        );
404        assert_eq!(
405            serde_json::to_string(&TaskStatus::Failed).unwrap(),
406            "\"failed\""
407        );
408    }
409
410    // ========================================================================
411    // TaskPriority tests (from TodoPriority)
412    // ========================================================================
413
414    #[test]
415    fn test_task_priority_display() {
416        assert_eq!(TaskPriority::High.to_string(), "high");
417        assert_eq!(TaskPriority::Medium.to_string(), "medium");
418        assert_eq!(TaskPriority::Low.to_string(), "low");
419    }
420
421    #[test]
422    fn test_task_priority_from_str() {
423        assert_eq!(TaskPriority::from_str("high").unwrap(), TaskPriority::High);
424        assert_eq!(TaskPriority::from_str("h").unwrap(), TaskPriority::High);
425        assert_eq!(
426            TaskPriority::from_str("medium").unwrap(),
427            TaskPriority::Medium
428        );
429        assert_eq!(TaskPriority::from_str("med").unwrap(), TaskPriority::Medium);
430        assert_eq!(TaskPriority::from_str("low").unwrap(), TaskPriority::Low);
431        assert_eq!(TaskPriority::from_str("l").unwrap(), TaskPriority::Low);
432        assert_eq!(
433            TaskPriority::from_str("unknown").unwrap(),
434            TaskPriority::Medium
435        );
436    }
437
438    // ========================================================================
439    // Task tests (unified from PlanStep + Todo)
440    // ========================================================================
441
442    #[test]
443    fn test_task_new() {
444        let task = Task::new("1", "Test task");
445        assert_eq!(task.id, "1");
446        assert_eq!(task.content, "Test task");
447        assert_eq!(task.status, TaskStatus::Pending);
448        assert_eq!(task.priority, TaskPriority::Medium);
449        assert!(task.tool.is_none());
450        assert!(task.dependencies.is_empty());
451        assert!(task.success_criteria.is_none());
452    }
453
454    #[test]
455    fn test_task_builder() {
456        let task = Task::new("1", "Test task")
457            .with_priority(TaskPriority::High)
458            .with_status(TaskStatus::InProgress)
459            .with_tool("bash")
460            .with_dependencies(vec!["step-0".to_string()])
461            .with_success_criteria("Command exits with 0");
462
463        assert_eq!(task.priority, TaskPriority::High);
464        assert_eq!(task.status, TaskStatus::InProgress);
465        assert_eq!(task.tool, Some("bash".to_string()));
466        assert_eq!(task.dependencies, vec!["step-0".to_string()]);
467        assert_eq!(
468            task.success_criteria,
469            Some("Command exits with 0".to_string())
470        );
471    }
472
473    #[test]
474    fn test_task_is_active() {
475        let pending = Task::new("1", "Pending task");
476        let in_progress = Task::new("2", "In progress").with_status(TaskStatus::InProgress);
477        let completed = Task::new("3", "Completed").with_status(TaskStatus::Completed);
478        let failed = Task::new("4", "Failed").with_status(TaskStatus::Failed);
479        let cancelled = Task::new("5", "Cancelled").with_status(TaskStatus::Cancelled);
480
481        assert!(pending.is_active());
482        assert!(in_progress.is_active());
483        assert!(!completed.is_active());
484        assert!(!failed.is_active());
485        assert!(!cancelled.is_active());
486    }
487
488    #[test]
489    fn test_task_serialization() {
490        let task = Task::new("1", "Test task")
491            .with_priority(TaskPriority::High)
492            .with_status(TaskStatus::InProgress);
493
494        let json = serde_json::to_string(&task).unwrap();
495        let parsed: Task = serde_json::from_str(&json).unwrap();
496
497        assert_eq!(parsed.id, task.id);
498        assert_eq!(parsed.content, task.content);
499        assert_eq!(parsed.status, task.status);
500        assert_eq!(parsed.priority, task.priority);
501    }
502
503    #[test]
504    fn test_task_deserialize_description_alias() {
505        // Backward compat: "description" field alias works
506        let json = r#"{"id": "step-1", "description": "Test step", "status": "pending"}"#;
507        let task: Task = serde_json::from_str(json).unwrap();
508        assert_eq!(task.content, "Test step");
509    }
510
511    // ========================================================================
512    // ExecutionPlan tests
513    // ========================================================================
514
515    #[test]
516    fn test_execution_plan() {
517        let mut plan = ExecutionPlan::new("Test goal", Complexity::Medium);
518
519        plan.add_step(Task::new("step-1", "First step"));
520        plan.add_step(
521            Task::new("step-2", "Second step").with_dependencies(vec!["step-1".to_string()]),
522        );
523
524        assert_eq!(plan.steps.len(), 2);
525        assert_eq!(plan.estimated_steps, 2);
526        assert_eq!(plan.progress(), 0.0);
527
528        // Mark first step as completed
529        plan.steps[0].status = TaskStatus::Completed;
530        assert_eq!(plan.progress(), 0.5);
531
532        // Check ready steps
533        let ready = plan.get_ready_steps();
534        assert_eq!(ready.len(), 1);
535        assert_eq!(ready[0].id, "step-2");
536    }
537
538    // ========================================================================
539    // AgentGoal tests
540    // ========================================================================
541
542    #[test]
543    fn test_agent_goal() {
544        let mut goal = AgentGoal::new("Complete task")
545            .with_criteria(vec!["Criterion 1".to_string(), "Criterion 2".to_string()]);
546
547        assert_eq!(goal.description, "Complete task");
548        assert_eq!(goal.success_criteria.len(), 2);
549        assert_eq!(goal.progress, 0.0);
550        assert!(!goal.achieved);
551
552        goal.update_progress(0.5);
553        assert_eq!(goal.progress, 0.5);
554
555        goal.mark_achieved();
556        assert!(goal.achieved);
557        assert_eq!(goal.progress, 1.0);
558        assert!(goal.achieved_at.is_some());
559    }
560
561    #[test]
562    fn test_complexity_levels() {
563        assert_eq!(
564            serde_json::to_string(&Complexity::Simple).unwrap(),
565            "\"Simple\""
566        );
567        assert_eq!(
568            serde_json::to_string(&Complexity::Complex).unwrap(),
569            "\"Complex\""
570        );
571    }
572}