Skip to main content

cascade_agent/planning/
mod.rs

1pub mod types;
2
3use crate::error::Result;
4use std::path::{Path, PathBuf};
5use types::{Plan, PlanData, PlanStatus, StepStatus};
6
7#[derive(Debug)]
8pub struct PlanManager {
9    plans_dir: PathBuf,
10}
11
12impl PlanManager {
13    pub fn new(plans_dir: PathBuf) -> Result<Self> {
14        std::fs::create_dir_all(&plans_dir)?;
15        Ok(Self { plans_dir })
16    }
17
18    pub fn create_plan(&self, task_id: &str, title: &str, steps: Vec<String>) -> Result<Plan> {
19        let mut plan = Plan::new(task_id, title, steps);
20        let filename = format!(
21            "{}_{}.toml",
22            plan.created_at.format("%Y%m%d_%H%M%S"),
23            &plan.id[..8]
24        );
25        plan.file_path = self.plans_dir.join(filename);
26        self.save_plan(&plan)?;
27        Ok(plan)
28    }
29
30    pub fn save_plan(&self, plan: &Plan) -> Result<()> {
31        let data = PlanData::from_plan(plan);
32        let content = toml::to_string_pretty(&data)?;
33        std::fs::write(&plan.file_path, content)?;
34        Ok(())
35    }
36
37    pub fn load_plan(&self, file_path: &Path) -> Result<Plan> {
38        let content = std::fs::read_to_string(file_path)?;
39        let data: PlanData = toml::from_str(&content)?;
40        data.to_plan(file_path)
41    }
42
43    pub fn list_plans(&self) -> Result<Vec<PathBuf>> {
44        let mut plans: Vec<PathBuf> = std::fs::read_dir(&self.plans_dir)?
45            .filter_map(|e| e.ok())
46            .map(|e| e.path())
47            .filter(|p| p.extension().is_some_and(|ext| ext == "toml"))
48            .collect();
49        plans.sort();
50        Ok(plans)
51    }
52
53    pub fn update_step(
54        &self,
55        plan: &mut Plan,
56        step_num: usize,
57        status: StepStatus,
58        result: Option<&str>,
59    ) -> Result<()> {
60        match status {
61            StepStatus::Completed => {
62                if !plan.mark_step_completed(step_num, result.map(String::from)) {
63                    return Err(crate::error::AgentError::ConfigError(format!(
64                        "Step {} not found in plan",
65                        step_num
66                    )));
67                }
68            }
69            StepStatus::Failed => {
70                if !plan.mark_step_failed(step_num, result.unwrap_or_default().to_string()) {
71                    return Err(crate::error::AgentError::ConfigError(format!(
72                        "Step {} not found in plan",
73                        step_num
74                    )));
75                }
76            }
77            StepStatus::InProgress => {
78                if !plan.mark_step_in_progress(step_num) {
79                    return Err(crate::error::AgentError::ConfigError(format!(
80                        "Step {} not found in plan",
81                        step_num
82                    )));
83                }
84            }
85            _ => {
86                if let Some(step) = plan.steps.iter_mut().find(|s| s.number == step_num) {
87                    step.status = status;
88                    step.result = result.map(String::from);
89                    plan.updated_at = chrono::Utc::now();
90                } else {
91                    return Err(crate::error::AgentError::ConfigError(format!(
92                        "Step {} not found in plan",
93                        step_num
94                    )));
95                }
96            }
97        }
98        self.save_plan(plan)?;
99        Ok(())
100    }
101
102    pub fn render_markdown(&self, plan: &Plan) -> String {
103        let status_emoji = match &plan.status {
104            PlanStatus::Draft => "📝",
105            PlanStatus::PendingApproval => "⏳",
106            PlanStatus::Approved => "✅",
107            PlanStatus::InProgress => "🔄",
108            PlanStatus::Completed => "🏁",
109            PlanStatus::Cancelled => "❌",
110        };
111
112        let step_emoji = |s: &StepStatus| match s {
113            StepStatus::Pending => "⬜",
114            StepStatus::InProgress => "🔄",
115            StepStatus::Completed => "✅",
116            StepStatus::Failed => "❌",
117            StepStatus::Skipped => "⏭️",
118        };
119
120        let mut md = format!("# {}\n\n", plan.title);
121
122        md.push_str(&format!("<!-- plan_id: {} -->\n", plan.id));
123        md.push_str(&format!("<!-- task_id: {} -->\n", plan.task_id));
124        md.push_str(&format!("<!-- plan_status: {:?} -->\n", plan.status));
125        md.push_str(&format!(
126            "<!-- created_at: {} -->\n",
127            plan.created_at.to_rfc3339()
128        ));
129        md.push_str(&format!(
130            "<!-- updated_at: {} -->\n",
131            plan.updated_at.to_rfc3339()
132        ));
133
134        md.push_str(&format!("**Status:** {} {:?}\n", status_emoji, plan.status));
135        md.push_str(&format!("**Task ID:** {}\n", plan.task_id));
136        md.push_str(&format!("**Plan ID:** {}\n", plan.id));
137        md.push_str(&format!(
138            "**Created:** {}\n\n",
139            plan.created_at.format("%Y-%m-%d %H:%M:%S UTC")
140        ));
141        md.push_str("---\n\n## Steps\n\n");
142
143        for step in &plan.steps {
144            md.push_str(&format!(
145                "<!-- step_status_{}: {:?} -->\n",
146                step.number, step.status
147            ));
148            md.push_str(&format!(
149                "{} **Step {}:** {}\n",
150                step_emoji(&step.status),
151                step.number,
152                step.description
153            ));
154            if let Some(result) = &step.result {
155                md.push_str(&format!("   - Result: {}\n", result));
156            }
157            md.push('\n');
158        }
159
160        md
161    }
162}