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