claude_agent/tools/
plan.rs1use async_trait::async_trait;
4use schemars::JsonSchema;
5use serde::{Deserialize, Serialize};
6
7use super::SchemaTool;
8use super::context::ExecutionContext;
9use crate::session::session_state::ToolState;
10use crate::types::ToolResult;
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
13#[serde(rename_all = "snake_case")]
14pub enum PlanAction {
15 Start,
16 Complete,
17 Cancel,
18 Update,
19 Status,
20}
21
22#[derive(Debug, Deserialize, JsonSchema)]
23#[schemars(deny_unknown_fields)]
24pub struct PlanInput {
25 pub action: PlanAction,
27 #[serde(default)]
29 pub name: Option<String>,
30 #[serde(default)]
32 pub content: Option<String>,
33}
34
35pub struct PlanTool {
36 state: ToolState,
37}
38
39impl PlanTool {
40 pub fn new(state: ToolState) -> Self {
41 Self { state }
42 }
43}
44
45#[async_trait]
46impl SchemaTool for PlanTool {
47 type Input = PlanInput;
48
49 const NAME: &'static str = "Plan";
50 const DESCRIPTION: &'static str = r#"Manage structured planning workflow for complex implementation tasks.
51
52## Actions
53
54- **start**: Begin plan mode for complex tasks (creates Draft plan)
55- **complete**: Finalize plan and proceed to implementation (approves plan)
56- **cancel**: Abort current plan
57- **update**: Update plan content while in plan mode
58- **status**: Check current plan state
59
60## When to Use Plan Mode
61
62Use `action: "start"` for implementation tasks unless they're simple:
63
641. **New Feature Implementation**: Adding meaningful new functionality
652. **Multiple Valid Approaches**: Task can be solved in several ways
663. **Code Modifications**: Changes affecting existing behavior
674. **Architectural Decisions**: Choosing between patterns or technologies
685. **Multi-File Changes**: Task touching more than 2-3 files
696. **Unclear Requirements**: Need exploration before understanding scope
70
71## When NOT to Use
72
73Skip for simple tasks:
74- Single-line fixes (typos, obvious bugs)
75- Adding a single function with clear requirements
76- Tasks with specific, detailed instructions
77- Pure research (use Task tool with Explore agent)
78
79## Workflow
80
811. Call with `action: "start"` to enter plan mode
822. Explore codebase using Glob, Grep, Read tools
833. Call with `action: "update"` to record your plan
844. Call with `action: "complete"` to finalize and proceed
85
86## Examples
87
88```json
89// Start planning
90{"action": "start", "name": "Add user authentication"}
91
92// Update plan content
93{"action": "update", "content": "1. Add JWT middleware\n2. Create auth routes"}
94
95// Complete and proceed
96{"action": "complete"}
97
98// Check status
99{"action": "status"}
100
101// Cancel if needed
102{"action": "cancel"}
103```
104
105## Integration
106
107- Use Plan for high-level approach and exploration
108- Use TodoWrite for granular task tracking during execution
109- Plan content persists across session compaction"#;
110
111 async fn handle(&self, input: PlanInput, _context: &ExecutionContext) -> ToolResult {
112 match input.action {
113 PlanAction::Start => self.start(input.name).await,
114 PlanAction::Complete => self.complete().await,
115 PlanAction::Cancel => self.cancel().await,
116 PlanAction::Update => self.update(input.content).await,
117 PlanAction::Status => self.status().await,
118 }
119 }
120}
121
122impl PlanTool {
123 async fn start(&self, name: Option<String>) -> ToolResult {
124 if self.state.is_in_plan_mode().await {
125 return ToolResult::error(
126 "Already in plan mode. Complete or cancel the current plan first.",
127 );
128 }
129
130 let plan = self.state.enter_plan_mode(name).await;
131 ToolResult::success(format!(
132 "Plan mode started.\n\
133 Plan ID: {}\n\
134 Status: {:?}\n\n\
135 Explore the codebase and design your approach.\n\
136 Use action: \"update\" to record your plan.\n\
137 Use action: \"complete\" when ready to proceed.",
138 plan.id, plan.status
139 ))
140 }
141
142 async fn complete(&self) -> ToolResult {
143 if !self.state.is_in_plan_mode().await {
144 return ToolResult::error("No active plan. Use action: \"start\" first.");
145 }
146
147 match self.state.exit_plan_mode().await {
148 Some(plan) => {
149 let content = if plan.content.is_empty() {
150 "No plan content recorded.".to_string()
151 } else {
152 plan.content.clone()
153 };
154
155 ToolResult::success(format!(
156 "Plan completed.\n\
157 Plan ID: {}\n\
158 Name: {}\n\
159 Status: {:?}\n\n\
160 ## Content\n\n{}\n\n\
161 Proceed with implementation.",
162 plan.id,
163 plan.name.as_deref().unwrap_or("Unnamed"),
164 plan.status,
165 content
166 ))
167 }
168 None => ToolResult::error("No active plan found."),
169 }
170 }
171
172 async fn cancel(&self) -> ToolResult {
173 if !self.state.is_in_plan_mode().await {
174 return ToolResult::error("No active plan to cancel.");
175 }
176
177 match self.state.cancel_plan().await {
178 Some(plan) => ToolResult::success(format!(
179 "Plan cancelled.\n\
180 Plan ID: {}\n\
181 Status: {:?}",
182 plan.id, plan.status
183 )),
184 None => ToolResult::error("No active plan found."),
185 }
186 }
187
188 async fn update(&self, content: Option<String>) -> ToolResult {
189 if !self.state.is_in_plan_mode().await {
190 return ToolResult::error("No active plan. Use action: \"start\" first.");
191 }
192
193 let content = match content {
194 Some(c) if !c.is_empty() => c,
195 _ => return ToolResult::error("Content is required for update action."),
196 };
197
198 self.state.update_plan_content(content.clone()).await;
199 ToolResult::success(format!(
200 "Plan content updated.\n\n## Content\n\n{}",
201 content
202 ))
203 }
204
205 async fn status(&self) -> ToolResult {
206 match self.state.current_plan().await {
207 Some(plan) => {
208 let content_preview = if plan.content.is_empty() {
209 "No content recorded.".to_string()
210 } else if plan.content.len() > 500 {
211 let mut end = 500;
213 while !plan.content.is_char_boundary(end) && end > 0 {
214 end -= 1;
215 }
216 format!("{}...", &plan.content[..end])
217 } else {
218 plan.content.clone()
219 };
220
221 ToolResult::success(format!(
222 "Plan Status\n\
223 Plan ID: {}\n\
224 Name: {}\n\
225 Status: {:?}\n\
226 Created: {}\n\n\
227 ## Content Preview\n\n{}",
228 plan.id,
229 plan.name.as_deref().unwrap_or("Unnamed"),
230 plan.status,
231 plan.created_at.format("%Y-%m-%d %H:%M:%S UTC"),
232 content_preview
233 ))
234 }
235 None => ToolResult::success("No active plan."),
236 }
237 }
238}
239
240#[cfg(test)]
241mod tests {
242 use super::*;
243 use crate::session::SessionId;
244 use crate::tools::Tool;
245
246 fn test_context() -> ExecutionContext {
247 ExecutionContext::default()
248 }
249
250 #[tokio::test]
251 async fn test_plan_lifecycle() {
252 let tool_state = ToolState::new(SessionId::new());
253 let tool = PlanTool::new(tool_state);
254 let context = test_context();
255
256 let result = tool
258 .execute(
259 serde_json::json!({"action": "start", "name": "Test Plan"}),
260 &context,
261 )
262 .await;
263 assert!(!result.is_error());
264 assert!(result.text().contains("Plan mode started"));
265
266 let result = tool
268 .execute(
269 serde_json::json!({"action": "update", "content": "Step 1\nStep 2"}),
270 &context,
271 )
272 .await;
273 assert!(!result.is_error());
274 assert!(result.text().contains("Plan content updated"));
275
276 let result = tool
278 .execute(serde_json::json!({"action": "status"}), &context)
279 .await;
280 assert!(!result.is_error());
281 assert!(result.text().contains("Step 1"));
282
283 let result = tool
285 .execute(serde_json::json!({"action": "complete"}), &context)
286 .await;
287 assert!(!result.is_error());
288 assert!(result.text().contains("Plan completed"));
289 }
290
291 #[tokio::test]
292 async fn test_plan_cancel() {
293 let tool_state = ToolState::new(SessionId::new());
294 let tool = PlanTool::new(tool_state);
295 let context = test_context();
296
297 let result = tool
299 .execute(serde_json::json!({"action": "start"}), &context)
300 .await;
301 assert!(!result.is_error());
302
303 let result = tool
305 .execute(serde_json::json!({"action": "cancel"}), &context)
306 .await;
307 assert!(!result.is_error());
308 assert!(result.text().contains("Plan cancelled"));
309
310 let result = tool
312 .execute(serde_json::json!({"action": "status"}), &context)
313 .await;
314 assert!(result.text().contains("No active plan"));
315 }
316
317 #[tokio::test]
318 async fn test_double_start_rejected() {
319 let tool_state = ToolState::new(SessionId::new());
320 let tool = PlanTool::new(tool_state);
321 let context = test_context();
322
323 let _ = tool
324 .execute(serde_json::json!({"action": "start"}), &context)
325 .await;
326
327 let result = tool
328 .execute(serde_json::json!({"action": "start"}), &context)
329 .await;
330 assert!(result.is_error());
331 assert!(result.text().contains("Already in plan mode"));
332 }
333
334 #[tokio::test]
335 async fn test_complete_without_start() {
336 let tool_state = ToolState::new(SessionId::new());
337 let tool = PlanTool::new(tool_state);
338 let context = test_context();
339
340 let result = tool
341 .execute(serde_json::json!({"action": "complete"}), &context)
342 .await;
343 assert!(result.is_error());
344 assert!(result.text().contains("No active plan"));
345 }
346
347 #[tokio::test]
348 async fn test_update_requires_content() {
349 let tool_state = ToolState::new(SessionId::new());
350 let tool = PlanTool::new(tool_state);
351 let context = test_context();
352
353 let _ = tool
354 .execute(serde_json::json!({"action": "start"}), &context)
355 .await;
356
357 let result = tool
358 .execute(serde_json::json!({"action": "update"}), &context)
359 .await;
360 assert!(result.is_error());
361 assert!(result.text().contains("Content is required"));
362 }
363}