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, 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    /// Insert or update one step while preserving the plan's insertion order.
241    ///
242    /// Flow replay can deliver the same durable step definition more than
243    /// once (for example when an observer is attached during a resumed run).
244    /// Keeping this operation on the canonical plan prevents each adapter from
245    /// implementing a subtly different deduplication and status policy.
246    pub fn upsert_step(&mut self, step: Task) {
247        let tool = step.tool.clone();
248        if let Some(existing) = self.steps.iter_mut().find(|item| item.id == step.id) {
249            // A terminal event is authoritative for the step. A duplicate
250            // StepCreated/Started notification must not regress a completed,
251            // failed, or cancelled task back to an active state.
252            existing.status = merge_status(existing.status, step.status);
253            existing.content = step.content;
254            existing.priority = step.priority;
255            existing.tool = step.tool.clone();
256            existing.dependencies = step.dependencies;
257            existing.success_criteria = step.success_criteria;
258        } else {
259            self.steps.push(step);
260        }
261        self.estimated_steps = self.steps.len();
262        if let Some(tool) = tool {
263            self.add_required_tool(tool);
264        }
265    }
266
267    pub fn add_required_tool(&mut self, tool: impl Into<String>) {
268        let tool_str = tool.into();
269        if !self.required_tools.contains(&tool_str) {
270            self.required_tools.push(tool_str);
271        }
272    }
273
274    /// Derive an identity for the immutable plan definition.
275    ///
276    /// Mutable status is deliberately excluded: restarting or retrying a Flow
277    /// must keep the same plan identity while its progress changes. The
278    /// identity contains only already-materialized task definitions and
279    /// exposes no task output or other runtime evidence. Dynamic Flow adapters
280    /// bound user-facing descriptions before they enter this projection.
281    pub fn definition_identity(
282        &self,
283    ) -> Result<
284        crate::execution_identity::ExecutionIdentityV1,
285        crate::execution_identity::ExecutionIdentityError,
286    > {
287        let steps = self
288            .steps
289            .iter()
290            .map(|step| {
291                serde_json::json!({
292                    "id": step.id,
293                    "content": step.content,
294                    "priority": step.priority,
295                    "tool": step.tool,
296                    "dependencies": step.dependencies,
297                    "success_criteria": step.success_criteria,
298                })
299            })
300            .collect::<Vec<_>>();
301        let mut required_tools = self.required_tools.clone();
302        required_tools.sort();
303        required_tools.dedup();
304        crate::execution_identity::ExecutionIdentityV1::derive(
305            crate::execution_identity::EXECUTION_PLAN_IDENTITY_DOMAIN_V1,
306            &serde_json::json!({
307                "goal": self.goal,
308                "complexity": self.complexity,
309                "required_tools": required_tools,
310                "steps": steps,
311            }),
312        )
313    }
314
315    /// Get steps that are ready to execute (dependencies met)
316    pub fn get_ready_steps(&self) -> Vec<&Task> {
317        self.steps
318            .iter()
319            .filter(|step| {
320                step.status == TaskStatus::Pending
321                    && step.dependencies.iter().all(|dep_id| {
322                        self.steps
323                            .iter()
324                            .find(|s| &s.id == dep_id)
325                            .map(|s| s.status == TaskStatus::Completed)
326                            .unwrap_or(false)
327                    })
328            })
329            .collect()
330    }
331
332    /// Update the status of a step by ID
333    pub fn mark_status(&mut self, step_id: &str, status: TaskStatus) {
334        if let Some(step) = self.steps.iter_mut().find(|s| s.id == step_id) {
335            step.status = merge_status(step.status, status);
336        }
337    }
338
339    /// Count remaining Pending steps
340    pub fn pending_count(&self) -> usize {
341        self.steps
342            .iter()
343            .filter(|s| s.status == TaskStatus::Pending)
344            .count()
345    }
346
347    /// Detect deadlock: Pending steps remain but none are ready to execute.
348    ///
349    /// This happens when all Pending steps have dependencies that are not
350    /// Completed (e.g., circular deps or all deps Failed/Skipped).
351    pub fn has_deadlock(&self) -> bool {
352        self.pending_count() > 0 && self.get_ready_steps().is_empty()
353    }
354
355    /// Get progress as a fraction (0.0 - 1.0)
356    pub fn progress(&self) -> f32 {
357        if self.steps.is_empty() {
358            return 0.0;
359        }
360        let completed = self
361            .steps
362            .iter()
363            .filter(|s| s.status == TaskStatus::Completed)
364            .count();
365        completed as f32 / self.steps.len() as f32
366    }
367}
368
369/// Merge a replay notification without allowing duplicate delivery to regress
370/// an already observed lifecycle transition.
371fn merge_status(current: TaskStatus, incoming: TaskStatus) -> TaskStatus {
372    if !current.is_active() {
373        return current;
374    }
375    if current == TaskStatus::InProgress && incoming == TaskStatus::Pending {
376        return current;
377    }
378    incoming
379}
380
381// ============================================================================
382// Goal Tracking Structures
383// ============================================================================
384
385/// Agent goal with success criteria
386#[derive(Debug, Clone, Serialize, Deserialize)]
387pub struct AgentGoal {
388    /// Goal description
389    pub description: String,
390    /// Success criteria (list of conditions)
391    pub success_criteria: Vec<String>,
392    /// Current progress (0.0 - 1.0)
393    pub progress: f32,
394    /// Is goal achieved?
395    pub achieved: bool,
396    /// Timestamp when goal was created
397    pub created_at: i64,
398    /// Timestamp when goal was achieved (if achieved)
399    pub achieved_at: Option<i64>,
400}
401
402impl AgentGoal {
403    pub fn new(description: impl Into<String>) -> Self {
404        Self {
405            description: description.into(),
406            success_criteria: Vec::new(),
407            progress: 0.0,
408            achieved: false,
409            created_at: chrono::Utc::now().timestamp(),
410            achieved_at: None,
411        }
412    }
413
414    pub fn with_criteria(mut self, criteria: Vec<String>) -> Self {
415        self.success_criteria = criteria;
416        self
417    }
418
419    pub fn update_progress(&mut self, progress: f32) {
420        self.progress = progress.clamp(0.0, 1.0);
421    }
422
423    pub fn mark_achieved(&mut self) {
424        self.achieved = true;
425        self.progress = 1.0;
426        self.achieved_at = Some(chrono::Utc::now().timestamp());
427    }
428}
429
430// ============================================================================
431// Tests
432// ============================================================================
433
434#[cfg(test)]
435mod tests {
436    use super::*;
437
438    // ========================================================================
439    // TaskStatus tests (merged from TodoStatus + StepStatus)
440    // ========================================================================
441
442    #[test]
443    fn test_task_status_display() {
444        assert_eq!(TaskStatus::Pending.to_string(), "pending");
445        assert_eq!(TaskStatus::InProgress.to_string(), "in_progress");
446        assert_eq!(TaskStatus::Completed.to_string(), "completed");
447        assert_eq!(TaskStatus::Failed.to_string(), "failed");
448        assert_eq!(TaskStatus::Skipped.to_string(), "skipped");
449        assert_eq!(TaskStatus::Cancelled.to_string(), "cancelled");
450    }
451
452    #[test]
453    fn test_task_status_from_str() {
454        assert_eq!(
455            TaskStatus::from_str("pending").unwrap(),
456            TaskStatus::Pending
457        );
458        assert_eq!(
459            TaskStatus::from_str("in_progress").unwrap(),
460            TaskStatus::InProgress
461        );
462        assert_eq!(
463            TaskStatus::from_str("inprogress").unwrap(),
464            TaskStatus::InProgress
465        );
466        assert_eq!(
467            TaskStatus::from_str("completed").unwrap(),
468            TaskStatus::Completed
469        );
470        assert_eq!(TaskStatus::from_str("done").unwrap(), TaskStatus::Completed);
471        assert_eq!(TaskStatus::from_str("failed").unwrap(), TaskStatus::Failed);
472        assert_eq!(
473            TaskStatus::from_str("skipped").unwrap(),
474            TaskStatus::Skipped
475        );
476        assert_eq!(
477            TaskStatus::from_str("cancelled").unwrap(),
478            TaskStatus::Cancelled
479        );
480        assert_eq!(
481            TaskStatus::from_str("canceled").unwrap(),
482            TaskStatus::Cancelled
483        );
484        assert_eq!(
485            TaskStatus::from_str("unknown").unwrap(),
486            TaskStatus::Pending
487        );
488    }
489
490    #[test]
491    fn test_task_status_is_active() {
492        assert!(TaskStatus::Pending.is_active());
493        assert!(TaskStatus::InProgress.is_active());
494        assert!(!TaskStatus::Completed.is_active());
495        assert!(!TaskStatus::Failed.is_active());
496        assert!(!TaskStatus::Skipped.is_active());
497        assert!(!TaskStatus::Cancelled.is_active());
498    }
499
500    #[test]
501    fn test_task_status_serialization() {
502        assert_eq!(
503            serde_json::to_string(&TaskStatus::InProgress).unwrap(),
504            "\"in_progress\""
505        );
506        assert_eq!(
507            serde_json::to_string(&TaskStatus::Failed).unwrap(),
508            "\"failed\""
509        );
510    }
511
512    // ========================================================================
513    // TaskPriority tests (from TodoPriority)
514    // ========================================================================
515
516    #[test]
517    fn test_task_priority_display() {
518        assert_eq!(TaskPriority::High.to_string(), "high");
519        assert_eq!(TaskPriority::Medium.to_string(), "medium");
520        assert_eq!(TaskPriority::Low.to_string(), "low");
521    }
522
523    #[test]
524    fn test_task_priority_from_str() {
525        assert_eq!(TaskPriority::from_str("high").unwrap(), TaskPriority::High);
526        assert_eq!(TaskPriority::from_str("h").unwrap(), TaskPriority::High);
527        assert_eq!(
528            TaskPriority::from_str("medium").unwrap(),
529            TaskPriority::Medium
530        );
531        assert_eq!(TaskPriority::from_str("med").unwrap(), TaskPriority::Medium);
532        assert_eq!(TaskPriority::from_str("low").unwrap(), TaskPriority::Low);
533        assert_eq!(TaskPriority::from_str("l").unwrap(), TaskPriority::Low);
534        assert_eq!(
535            TaskPriority::from_str("unknown").unwrap(),
536            TaskPriority::Medium
537        );
538    }
539
540    // ========================================================================
541    // Task tests (unified from PlanStep + Todo)
542    // ========================================================================
543
544    #[test]
545    fn test_task_new() {
546        let task = Task::new("1", "Test task");
547        assert_eq!(task.id, "1");
548        assert_eq!(task.content, "Test task");
549        assert_eq!(task.status, TaskStatus::Pending);
550        assert_eq!(task.priority, TaskPriority::Medium);
551        assert!(task.tool.is_none());
552        assert!(task.dependencies.is_empty());
553        assert!(task.success_criteria.is_none());
554    }
555
556    #[test]
557    fn test_task_builder() {
558        let task = Task::new("1", "Test task")
559            .with_priority(TaskPriority::High)
560            .with_status(TaskStatus::InProgress)
561            .with_tool("bash")
562            .with_dependencies(vec!["step-0".to_string()])
563            .with_success_criteria("Command exits with 0");
564
565        assert_eq!(task.priority, TaskPriority::High);
566        assert_eq!(task.status, TaskStatus::InProgress);
567        assert_eq!(task.tool, Some("bash".to_string()));
568        assert_eq!(task.dependencies, vec!["step-0".to_string()]);
569        assert_eq!(
570            task.success_criteria,
571            Some("Command exits with 0".to_string())
572        );
573    }
574
575    #[test]
576    fn test_task_is_active() {
577        let pending = Task::new("1", "Pending task");
578        let in_progress = Task::new("2", "In progress").with_status(TaskStatus::InProgress);
579        let completed = Task::new("3", "Completed").with_status(TaskStatus::Completed);
580        let failed = Task::new("4", "Failed").with_status(TaskStatus::Failed);
581        let cancelled = Task::new("5", "Cancelled").with_status(TaskStatus::Cancelled);
582
583        assert!(pending.is_active());
584        assert!(in_progress.is_active());
585        assert!(!completed.is_active());
586        assert!(!failed.is_active());
587        assert!(!cancelled.is_active());
588    }
589
590    #[test]
591    fn test_task_serialization() {
592        let task = Task::new("1", "Test task")
593            .with_priority(TaskPriority::High)
594            .with_status(TaskStatus::InProgress);
595
596        let json = serde_json::to_string(&task).unwrap();
597        let parsed: Task = serde_json::from_str(&json).unwrap();
598
599        assert_eq!(parsed.id, task.id);
600        assert_eq!(parsed.content, task.content);
601        assert_eq!(parsed.status, task.status);
602        assert_eq!(parsed.priority, task.priority);
603    }
604
605    // ========================================================================
606    // ExecutionPlan tests
607    // ========================================================================
608
609    #[test]
610    fn test_execution_plan() {
611        let mut plan = ExecutionPlan::new("Test goal", Complexity::Medium);
612
613        plan.add_step(Task::new("step-1", "First step"));
614        plan.add_step(
615            Task::new("step-2", "Second step").with_dependencies(vec!["step-1".to_string()]),
616        );
617
618        assert_eq!(plan.steps.len(), 2);
619        assert_eq!(plan.estimated_steps, 2);
620        assert_eq!(plan.progress(), 0.0);
621
622        // Mark first step as completed
623        plan.steps[0].status = TaskStatus::Completed;
624        assert_eq!(plan.progress(), 0.5);
625
626        // Check ready steps
627        let ready = plan.get_ready_steps();
628        assert_eq!(ready.len(), 1);
629        assert_eq!(ready[0].id, "step-2");
630    }
631
632    // ========================================================================
633    // ExecutionPlan helper method tests
634    // ========================================================================
635
636    #[test]
637    fn test_mark_status() {
638        let mut plan = ExecutionPlan::new("Test", Complexity::Simple);
639        plan.add_step(Task::new("s1", "Step 1"));
640        plan.add_step(Task::new("s2", "Step 2"));
641
642        assert_eq!(plan.steps[0].status, TaskStatus::Pending);
643        plan.mark_status("s1", TaskStatus::InProgress);
644        assert_eq!(plan.steps[0].status, TaskStatus::InProgress);
645        plan.mark_status("s1", TaskStatus::Completed);
646        assert_eq!(plan.steps[0].status, TaskStatus::Completed);
647        // Non-existent ID is a no-op
648        plan.mark_status("s999", TaskStatus::Failed);
649        assert_eq!(plan.steps[1].status, TaskStatus::Pending);
650    }
651
652    #[test]
653    fn test_pending_count() {
654        let mut plan = ExecutionPlan::new("Test", Complexity::Simple);
655        plan.add_step(Task::new("s1", "Step 1"));
656        plan.add_step(Task::new("s2", "Step 2"));
657        plan.add_step(Task::new("s3", "Step 3"));
658
659        assert_eq!(plan.pending_count(), 3);
660        plan.mark_status("s1", TaskStatus::Completed);
661        assert_eq!(plan.pending_count(), 2);
662        plan.mark_status("s2", TaskStatus::Failed);
663        assert_eq!(plan.pending_count(), 1);
664        plan.mark_status("s3", TaskStatus::InProgress);
665        assert_eq!(plan.pending_count(), 0);
666    }
667
668    #[test]
669    fn test_has_deadlock() {
670        // Circular dependency: s1 depends on s2, s2 depends on s1
671        let mut plan = ExecutionPlan::new("Test", Complexity::Simple);
672        plan.add_step(Task::new("s1", "Step 1").with_dependencies(vec!["s2".to_string()]));
673        plan.add_step(Task::new("s2", "Step 2").with_dependencies(vec!["s1".to_string()]));
674
675        assert!(plan.has_deadlock());
676
677        // No deadlock when steps have no deps
678        let mut plan2 = ExecutionPlan::new("Test", Complexity::Simple);
679        plan2.add_step(Task::new("s1", "Step 1"));
680        assert!(!plan2.has_deadlock());
681
682        // Deadlock when dependency failed
683        let mut plan3 = ExecutionPlan::new("Test", Complexity::Simple);
684        plan3.add_step(Task::new("s1", "Step 1"));
685        plan3.add_step(Task::new("s2", "Step 2").with_dependencies(vec!["s1".to_string()]));
686        plan3.mark_status("s1", TaskStatus::Failed);
687        assert!(plan3.has_deadlock()); // s2 depends on s1 which failed, not completed
688    }
689
690    #[test]
691    fn test_get_ready_steps_parallel() {
692        // Three independent steps — all should be ready simultaneously
693        let mut plan = ExecutionPlan::new("Test", Complexity::Medium);
694        plan.add_step(Task::new("s1", "Step 1"));
695        plan.add_step(Task::new("s2", "Step 2"));
696        plan.add_step(Task::new("s3", "Step 3"));
697
698        let ready = plan.get_ready_steps();
699        assert_eq!(ready.len(), 3);
700    }
701
702    #[test]
703    fn test_get_ready_steps_wave() {
704        // s1 and s2 are independent; s3 depends on both
705        let mut plan = ExecutionPlan::new("Test", Complexity::Medium);
706        plan.add_step(Task::new("s1", "Step 1"));
707        plan.add_step(Task::new("s2", "Step 2"));
708        plan.add_step(
709            Task::new("s3", "Step 3").with_dependencies(vec!["s1".to_string(), "s2".to_string()]),
710        );
711
712        // Wave 1: s1 and s2
713        let ready = plan.get_ready_steps();
714        assert_eq!(ready.len(), 2);
715        let ids: Vec<&str> = ready.iter().map(|s| s.id.as_str()).collect();
716        assert!(ids.contains(&"s1"));
717        assert!(ids.contains(&"s2"));
718
719        // Complete wave 1
720        plan.mark_status("s1", TaskStatus::Completed);
721        plan.mark_status("s2", TaskStatus::Completed);
722
723        // Wave 2: s3
724        let ready = plan.get_ready_steps();
725        assert_eq!(ready.len(), 1);
726        assert_eq!(ready[0].id, "s3");
727    }
728
729    #[test]
730    fn upsert_preserves_order_and_does_not_regress_status() {
731        let mut plan = ExecutionPlan::new("Test", Complexity::Simple);
732        plan.upsert_step(
733            Task::new("step", "First")
734                .with_tool("read")
735                .with_status(TaskStatus::InProgress),
736        );
737        plan.upsert_step(
738            Task::new("step", "Updated")
739                .with_tool("read")
740                .with_status(TaskStatus::Pending),
741        );
742        assert_eq!(plan.steps.len(), 1);
743        assert_eq!(plan.steps[0].content, "Updated");
744        assert_eq!(plan.steps[0].status, TaskStatus::InProgress);
745        assert_eq!(plan.required_tools, vec!["read"]);
746    }
747
748    #[test]
749    fn definition_identity_ignores_progress_status() {
750        let mut plan = ExecutionPlan::new("Test", Complexity::Simple);
751        plan.add_step(Task::new("step", "First").with_tool("read"));
752        let before = plan.definition_identity().unwrap();
753        plan.mark_status("step", TaskStatus::Completed);
754        let after = plan.definition_identity().unwrap();
755        assert_eq!(before, after);
756    }
757
758    // ========================================================================
759    // AgentGoal tests
760    // ========================================================================
761
762    #[test]
763    fn test_agent_goal() {
764        let mut goal = AgentGoal::new("Complete task")
765            .with_criteria(vec!["Criterion 1".to_string(), "Criterion 2".to_string()]);
766
767        assert_eq!(goal.description, "Complete task");
768        assert_eq!(goal.success_criteria.len(), 2);
769        assert_eq!(goal.progress, 0.0);
770        assert!(!goal.achieved);
771
772        goal.update_progress(0.5);
773        assert_eq!(goal.progress, 0.5);
774
775        goal.mark_achieved();
776        assert!(goal.achieved);
777        assert_eq!(goal.progress, 1.0);
778        assert!(goal.achieved_at.is_some());
779    }
780
781    #[test]
782    fn test_complexity_levels() {
783        assert_eq!(
784            serde_json::to_string(&Complexity::Simple).unwrap(),
785            "\"Simple\""
786        );
787        assert_eq!(
788            serde_json::to_string(&Complexity::Complex).unwrap(),
789            "\"Complex\""
790        );
791    }
792}