cascade_agent/planning/
mod.rs1pub 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}