Skip to main content

cascade_agent/tools/
planning_tools.rs

1//! Planning tools: create_plan, update_plan_step, list_plans, get_plan.
2
3use std::sync::Arc;
4
5use async_trait::async_trait;
6use serde_json::{json, Value};
7
8use super::{Tool, ToolResult};
9use crate::planning::types::StepStatus;
10use crate::planning::PlanManager;
11
12// ---------------------------------------------------------------------------
13// CreatePlanTool
14// ---------------------------------------------------------------------------
15
16#[derive(Debug)]
17pub struct CreatePlanTool {
18    plan_manager: Arc<std::sync::Mutex<PlanManager>>,
19    task_id: String,
20}
21
22impl CreatePlanTool {
23    pub fn new(plan_manager: Arc<std::sync::Mutex<PlanManager>>, task_id: String) -> Self {
24        Self {
25            plan_manager,
26            task_id,
27        }
28    }
29}
30
31#[async_trait]
32impl Tool for CreatePlanTool {
33    fn name(&self) -> &str {
34        "create_plan"
35    }
36
37    fn description(&self) -> &str {
38        "Create a new execution plan with a list of steps. Returns the plan ID and rendered markdown."
39    }
40
41    fn parameters_schema(&self) -> Value {
42        json!({
43            "type": "object",
44            "properties": {
45                "title": {
46                    "type": "string",
47                    "description": "The plan title."
48                },
49                "steps": {
50                    "type": "array",
51                    "items": { "type": "string" },
52                    "description": "Ordered list of step descriptions."
53                }
54            },
55            "required": ["title", "steps"]
56        })
57    }
58
59    async fn execute(&self, args: Value) -> ToolResult {
60        let title = match args.get("title").and_then(|v| v.as_str()) {
61            Some(t) => t,
62            None => return ToolResult::err("Missing required parameter 'title'"),
63        };
64
65        let steps: Vec<String> = match args.get("steps").and_then(|v| v.as_array()) {
66            Some(arr) => arr
67                .iter()
68                .filter_map(|v| v.as_str().map(String::from))
69                .collect(),
70            None => {
71                return ToolResult::err("Missing required parameter 'steps' (must be an array)")
72            }
73        };
74
75        if steps.is_empty() {
76            return ToolResult::err("Plan must have at least one step");
77        }
78
79        let pm = self.plan_manager.lock().unwrap();
80        match pm.create_plan(&self.task_id, title, steps) {
81            Ok(plan) => {
82                let rendered = pm.render_markdown(&plan);
83                ToolResult::ok(json!({
84                    "plan_id": plan.id,
85                    "title": plan.title,
86                    "step_count": plan.steps.len(),
87                    "file_path": plan.file_path.to_string_lossy(),
88                    "rendered": rendered,
89                }))
90            }
91            Err(e) => ToolResult::err(format!("Failed to create plan: {}", e)),
92        }
93    }
94}
95
96// ---------------------------------------------------------------------------
97// UpdatePlanStepTool
98// ---------------------------------------------------------------------------
99
100#[derive(Debug)]
101pub struct UpdatePlanStepTool {
102    plan_manager: Arc<std::sync::Mutex<PlanManager>>,
103}
104
105impl UpdatePlanStepTool {
106    pub fn new(plan_manager: Arc<std::sync::Mutex<PlanManager>>) -> Self {
107        Self { plan_manager }
108    }
109}
110
111#[async_trait]
112impl Tool for UpdatePlanStepTool {
113    fn name(&self) -> &str {
114        "update_plan_step"
115    }
116
117    fn description(&self) -> &str {
118        "Update the status of a step in an existing plan. Status can be: in_progress, completed, failed, skipped."
119    }
120
121    fn parameters_schema(&self) -> Value {
122        json!({
123            "type": "object",
124            "properties": {
125                "plan_file_path": {
126                    "type": "string",
127                    "description": "The file path of the plan to update."
128                },
129                "step_number": {
130                    "type": "integer",
131                    "description": "The step number (1-indexed)."
132                },
133                "status": {
134                    "type": "string",
135                    "enum": ["in_progress", "completed", "failed", "skipped"],
136                    "description": "The new status for the step."
137                },
138                "result": {
139                    "type": "string",
140                    "description": "Optional result description for completed/failed steps."
141                }
142            },
143            "required": ["plan_file_path", "step_number", "status"]
144        })
145    }
146
147    async fn execute(&self, args: Value) -> ToolResult {
148        let plan_file_path = match args.get("plan_file_path").and_then(|v| v.as_str()) {
149            Some(p) => p,
150            None => return ToolResult::err("Missing required parameter 'plan_file_path'"),
151        };
152
153        let step_number = match args.get("step_number").and_then(|v| v.as_u64()) {
154            Some(n) => n as usize,
155            None => return ToolResult::err("Missing required parameter 'step_number'"),
156        };
157
158        let status_str = match args.get("status").and_then(|v| v.as_str()) {
159            Some(s) => s,
160            None => return ToolResult::err("Missing required parameter 'status'"),
161        };
162
163        let status = match status_str {
164            "in_progress" => StepStatus::InProgress,
165            "completed" => StepStatus::Completed,
166            "failed" => StepStatus::Failed,
167            "skipped" => StepStatus::Skipped,
168            _ => {
169                return ToolResult::err(format!(
170                    "Unknown status: '{}'. Use: in_progress, completed, failed, skipped",
171                    status_str
172                ))
173            }
174        };
175
176        let result = args.get("result").and_then(|v| v.as_str());
177
178        let pm = self.plan_manager.lock().unwrap();
179        let path = std::path::Path::new(plan_file_path);
180
181        let mut plan = match pm.load_plan(path) {
182            Ok(p) => p,
183            Err(e) => return ToolResult::err(format!("Failed to load plan: {}", e)),
184        };
185
186        match pm.update_step(&mut plan, step_number, status, result) {
187            Ok(()) => {
188                let rendered = pm.render_markdown(&plan);
189                ToolResult::ok(json!({
190                    "plan_id": plan.id,
191                    "step_number": step_number,
192                    "status": status_str,
193                    "plan_status": format!("{:?}", plan.status),
194                    "rendered": rendered,
195                }))
196            }
197            Err(e) => ToolResult::err(format!("Failed to update step: {}", e)),
198        }
199    }
200}
201
202// ---------------------------------------------------------------------------
203// ListPlansTool
204// ---------------------------------------------------------------------------
205
206#[derive(Debug)]
207pub struct ListPlansTool {
208    plan_manager: Arc<std::sync::Mutex<PlanManager>>,
209}
210
211impl ListPlansTool {
212    pub fn new(plan_manager: Arc<std::sync::Mutex<PlanManager>>) -> Self {
213        Self { plan_manager }
214    }
215}
216
217#[async_trait]
218impl Tool for ListPlansTool {
219    fn name(&self) -> &str {
220        "list_plans"
221    }
222
223    fn description(&self) -> &str {
224        "List all existing plans in the plans directory."
225    }
226
227    fn parameters_schema(&self) -> Value {
228        json!({
229            "type": "object",
230            "properties": {}
231        })
232    }
233
234    async fn execute(&self, _args: Value) -> ToolResult {
235        let pm = self.plan_manager.lock().unwrap();
236        match pm.list_plans() {
237            Ok(paths) => {
238                let plans: Vec<Value> = paths
239                    .iter()
240                    .filter_map(|p| {
241                        let plan = pm.load_plan(p).ok()?;
242                        Some(json!({
243                            "plan_id": plan.id,
244                            "title": plan.title,
245                            "status": format!("{:?}", plan.status),
246                            "steps": plan.steps.len(),
247                            "file_path": p.to_string_lossy(),
248                        }))
249                    })
250                    .collect();
251
252                ToolResult::ok(json!({
253                    "count": plans.len(),
254                    "plans": plans,
255                }))
256            }
257            Err(e) => ToolResult::err(format!("Failed to list plans: {}", e)),
258        }
259    }
260}
261
262// ---------------------------------------------------------------------------
263// GetPlanTool
264// ---------------------------------------------------------------------------
265
266#[derive(Debug)]
267pub struct GetPlanTool {
268    plan_manager: Arc<std::sync::Mutex<PlanManager>>,
269}
270
271impl GetPlanTool {
272    pub fn new(plan_manager: Arc<std::sync::Mutex<PlanManager>>) -> Self {
273        Self { plan_manager }
274    }
275}
276
277#[async_trait]
278impl Tool for GetPlanTool {
279    fn name(&self) -> &str {
280        "get_plan"
281    }
282
283    fn description(&self) -> &str {
284        "Load and render a specific plan by its file path."
285    }
286
287    fn parameters_schema(&self) -> Value {
288        json!({
289            "type": "object",
290            "properties": {
291                "plan_file_path": {
292                    "type": "string",
293                    "description": "The file path of the plan to load."
294                }
295            },
296            "required": ["plan_file_path"]
297        })
298    }
299
300    async fn execute(&self, args: Value) -> ToolResult {
301        let plan_file_path = match args.get("plan_file_path").and_then(|v| v.as_str()) {
302            Some(p) => p,
303            None => return ToolResult::err("Missing required parameter 'plan_file_path'"),
304        };
305
306        let pm = self.plan_manager.lock().unwrap();
307        match pm.load_plan(std::path::Path::new(plan_file_path)) {
308            Ok(plan) => {
309                let rendered = pm.render_markdown(&plan);
310                ToolResult::ok(json!({
311                    "plan_id": plan.id,
312                    "title": plan.title,
313                    "status": format!("{:?}", plan.status),
314                    "task_id": plan.task_id,
315                    "steps": plan.steps.iter().map(|s| json!({
316                        "number": s.number,
317                        "description": s.description,
318                        "status": format!("{:?}", s.status),
319                        "result": s.result,
320                    })).collect::<Vec<_>>(),
321                    "rendered": rendered,
322                }))
323            }
324            Err(e) => ToolResult::err(format!("Failed to load plan: {}", e)),
325        }
326    }
327}