Skip to main content

agent_code_lib/tools/
plan_mode.rs

1//! Plan mode tools: switch between execution and planning modes.
2//!
3//! Plan mode restricts the agent to read-only tools, preventing
4//! mutations while the user reviews and approves a plan.
5//! The LLM decides when to enter plan mode based on task complexity.
6
7use async_trait::async_trait;
8use serde_json::json;
9
10use super::{Tool, ToolContext, ToolResult};
11use crate::error::ToolError;
12
13/// Enter plan mode (read-only operations only).
14pub struct EnterPlanModeTool;
15
16#[async_trait]
17impl Tool for EnterPlanModeTool {
18    fn name(&self) -> &'static str {
19        "EnterPlanMode"
20    }
21
22    fn description(&self) -> &'static str {
23        "Switch to plan mode for safe exploration before making changes."
24    }
25
26    fn prompt(&self) -> String {
27        "Use this tool when you need to plan an approach before making changes. \
28         In plan mode, only read-only tools are available (FileRead, Grep, Glob, Bash). \
29         Write tools are blocked until ExitPlanMode is called.\n\n\
30         When to enter plan mode:\n\
31         - Complex tasks requiring multiple file changes\n\
32         - Unclear requirements that need investigation first\n\
33         - Multiple possible approaches to evaluate\n\
34         - Large refactors where the plan should be reviewed\n\
35         - When the user asks to \"plan\", \"think through\", or \"design\"\n\n\
36         You should write your plan to a file before exiting plan mode."
37            .to_string()
38    }
39
40    fn input_schema(&self) -> serde_json::Value {
41        json!({
42            "type": "object",
43            "properties": {}
44        })
45    }
46
47    fn is_read_only(&self) -> bool {
48        true
49    }
50
51    fn is_concurrency_safe(&self) -> bool {
52        true
53    }
54
55    async fn call(
56        &self,
57        _input: serde_json::Value,
58        _ctx: &ToolContext,
59    ) -> Result<ToolResult, ToolError> {
60        // Generate a plan file path.
61        let plan_dir = dirs::data_dir()
62            .unwrap_or_else(|| std::path::PathBuf::from("."))
63            .join("agent-code")
64            .join("plans");
65        let _ = std::fs::create_dir_all(&plan_dir);
66
67        let slug = generate_slug();
68        let plan_path = plan_dir.join(format!("{slug}.md"));
69
70        // Create the plan file with a template.
71        let template = format!(
72            "# Plan\n\n\
73             Created: {}\n\n\
74             ## Goal\n\n\
75             (describe what needs to be accomplished)\n\n\
76             ## Approach\n\n\
77             (outline the steps)\n\n\
78             ## Files to modify\n\n\
79             (list files and what changes each needs)\n\n\
80             ## Risks / open questions\n\n\
81             (anything uncertain)\n",
82            chrono::Utc::now().format("%Y-%m-%d %H:%M UTC"),
83        );
84        let _ = std::fs::write(&plan_path, &template);
85
86        Ok(ToolResult::success(format!(
87            "Entered plan mode. Only read-only tools are available.\n\
88             Plan file created: {}\n\
89             Write your plan to this file, then call ExitPlanMode when ready.",
90            plan_path.display()
91        )))
92    }
93}
94
95/// Exit plan mode (re-enable all tools).
96pub struct ExitPlanModeTool;
97
98#[async_trait]
99impl Tool for ExitPlanModeTool {
100    fn name(&self) -> &'static str {
101        "ExitPlanMode"
102    }
103
104    fn description(&self) -> &'static str {
105        "Exit plan mode and re-enable all tools for execution. \
106         Call this after your plan is complete and ready to implement."
107    }
108
109    fn input_schema(&self) -> serde_json::Value {
110        json!({
111            "type": "object",
112            "properties": {}
113        })
114    }
115
116    fn is_read_only(&self) -> bool {
117        true
118    }
119
120    fn is_concurrency_safe(&self) -> bool {
121        true
122    }
123
124    async fn call(
125        &self,
126        _input: serde_json::Value,
127        _ctx: &ToolContext,
128    ) -> Result<ToolResult, ToolError> {
129        Ok(ToolResult::success(
130            "Exited plan mode. All tools are now available for execution.",
131        ))
132    }
133}
134
135/// Generate a memorable slug for plan files (adjective-noun).
136fn generate_slug() -> String {
137    let adjectives = [
138        "brave", "calm", "dark", "eager", "fair", "golden", "hidden", "iron", "jade", "keen",
139        "light", "mystic", "noble", "ocean", "proud", "quick", "rapid", "silent", "true", "vivid",
140    ];
141    let nouns = [
142        "anchor", "beacon", "cedar", "dawn", "ember", "falcon", "grove", "harbor", "island",
143        "jewel", "kernel", "lantern", "meadow", "nexus", "orbit", "peak", "quill", "river",
144        "spark", "tower",
145    ];
146
147    let now = std::time::SystemTime::now()
148        .duration_since(std::time::UNIX_EPOCH)
149        .unwrap_or_default()
150        .subsec_nanos();
151
152    let adj = adjectives[(now as usize) % adjectives.len()];
153    let noun = nouns[((now as usize) / adjectives.len()) % nouns.len()];
154
155    format!("{adj}-{noun}")
156}