Skip to main content

codetether_agent/tool/
plan.rs

1//! Plan Tool - Enter/exit planning mode for multi-step reasoning.
2
3use super::{Tool, ToolResult};
4use anyhow::{Context, Result};
5use async_trait::async_trait;
6use parking_lot::RwLock;
7use serde::Deserialize;
8use serde_json::{Value, json};
9use std::sync::atomic::{AtomicBool, Ordering};
10
11static IN_PLAN_MODE: AtomicBool = AtomicBool::new(false);
12
13lazy_static::lazy_static! {
14    static ref CURRENT_PLAN: RwLock<Option<Plan>> = RwLock::new(None);
15}
16
17#[derive(Debug, Clone)]
18struct Plan {
19    goal: String,
20    steps: Vec<PlanStep>,
21    current_step: usize,
22}
23
24#[derive(Debug, Clone)]
25struct PlanStep {
26    description: String,
27    completed: bool,
28    notes: Option<String>,
29}
30
31pub struct PlanEnterTool;
32pub struct PlanExitTool;
33
34impl Default for PlanEnterTool {
35    fn default() -> Self {
36        Self::new()
37    }
38}
39
40impl Default for PlanExitTool {
41    fn default() -> Self {
42        Self::new()
43    }
44}
45
46impl PlanEnterTool {
47    pub fn new() -> Self {
48        Self
49    }
50}
51
52impl PlanExitTool {
53    pub fn new() -> Self {
54        Self
55    }
56}
57
58#[derive(Deserialize)]
59struct EnterParams {
60    goal: String,
61    steps: Vec<String>,
62}
63
64#[derive(Deserialize)]
65struct ExitParams {
66    #[serde(default)]
67    summary: Option<String>,
68    #[serde(default)]
69    step_complete: Option<usize>,
70    #[serde(default)]
71    notes: Option<String>,
72}
73
74#[async_trait]
75impl Tool for PlanEnterTool {
76    fn id(&self) -> &str {
77        "plan_enter"
78    }
79    fn name(&self) -> &str {
80        "Enter Plan Mode"
81    }
82    fn description(&self) -> &str {
83        "Enter planning mode with a goal and list of steps. Use before complex multi-step tasks."
84    }
85    fn parameters(&self) -> Value {
86        json!({
87            "type": "object",
88            "properties": {
89                "goal": {"type": "string", "description": "The overall goal to achieve"},
90                "steps": {
91                    "type": "array",
92                    "items": {"type": "string"},
93                    "description": "Ordered list of steps to complete the goal"
94                }
95            },
96            "required": ["goal", "steps"]
97        })
98    }
99
100    async fn execute(&self, params: Value) -> Result<ToolResult> {
101        let p: EnterParams = serde_json::from_value(params).context("Invalid params")?;
102
103        if IN_PLAN_MODE.load(Ordering::SeqCst) {
104            return Ok(ToolResult::error(
105                "Already in plan mode. Exit current plan first.",
106            ));
107        }
108
109        if p.steps.is_empty() {
110            return Ok(ToolResult::error("At least one step is required"));
111        }
112
113        let plan = Plan {
114            goal: p.goal.clone(),
115            steps: p
116                .steps
117                .iter()
118                .map(|s| PlanStep {
119                    description: s.clone(),
120                    completed: false,
121                    notes: None,
122                })
123                .collect(),
124            current_step: 0,
125        };
126
127        *CURRENT_PLAN.write() = Some(plan.clone());
128        IN_PLAN_MODE.store(true, Ordering::SeqCst);
129
130        let output = format!(
131            "📋 Plan Mode Activated\n\nGoal: {}\n\nSteps:\n{}",
132            p.goal,
133            p.steps
134                .iter()
135                .enumerate()
136                .map(|(i, s)| format!("  {}. {}", i + 1, s))
137                .collect::<Vec<_>>()
138                .join("\n")
139        );
140
141        Ok(ToolResult::success(output)
142            .with_metadata("step_count", json!(p.steps.len()))
143            .with_metadata("current_step", json!(1)))
144    }
145}
146
147#[async_trait]
148impl Tool for PlanExitTool {
149    fn id(&self) -> &str {
150        "plan_exit"
151    }
152    fn name(&self) -> &str {
153        "Exit Plan Mode"
154    }
155    fn description(&self) -> &str {
156        "Exit planning mode. Optionally mark a step as complete or provide a summary."
157    }
158    fn parameters(&self) -> Value {
159        json!({
160            "type": "object",
161            "properties": {
162                "summary": {"type": "string", "description": "Summary of what was accomplished"},
163                "step_complete": {"type": "integer", "description": "Mark step number as complete (1-indexed)"},
164                "notes": {"type": "string", "description": "Notes for the completed step"}
165            }
166        })
167    }
168
169    async fn execute(&self, params: Value) -> Result<ToolResult> {
170        let p: ExitParams = serde_json::from_value(params).unwrap_or(ExitParams {
171            summary: None,
172            step_complete: None,
173            notes: None,
174        });
175
176        if !IN_PLAN_MODE.load(Ordering::SeqCst) {
177            return Ok(ToolResult::error("Not in plan mode"));
178        }
179
180        let (output, completed_count, total_steps, should_exit) = {
181            let mut plan_guard = CURRENT_PLAN.write();
182            let plan = plan_guard
183                .as_mut()
184                .ok_or_else(|| anyhow::anyhow!("No active plan"))?;
185
186            // Mark step complete if specified
187            if let Some(step_num) = p.step_complete {
188                if step_num > 0 && step_num <= plan.steps.len() {
189                    let step = &mut plan.steps[step_num - 1];
190                    step.completed = true;
191                    step.notes = p.notes.clone();
192                    plan.current_step = step_num;
193                }
194            }
195
196            // Build status report
197            let completed_count = plan.steps.iter().filter(|s| s.completed).count();
198            let total_steps = plan.steps.len();
199            let status = plan
200                .steps
201                .iter()
202                .enumerate()
203                .map(|(i, s)| {
204                    let icon = if s.completed { "✓" } else { "○" };
205                    let notes = s
206                        .notes
207                        .as_ref()
208                        .map(|n| format!(" [{}]", n))
209                        .unwrap_or_default();
210                    format!("  {} {}. {}{}", icon, i + 1, s.description, notes)
211                })
212                .collect::<Vec<_>>()
213                .join("\n");
214
215            let output = format!(
216                "📋 Plan Status\n\nGoal: {}\n\nProgress: {}/{} steps\n\n{}\n\n{}",
217                plan.goal,
218                completed_count,
219                total_steps,
220                status,
221                p.summary
222                    .as_ref()
223                    .map(|s| format!("Summary: {}", s))
224                    .unwrap_or_default()
225            );
226
227            let should_exit = completed_count == total_steps || p.summary.is_some();
228            (output, completed_count, total_steps, should_exit)
229        };
230
231        // If all steps complete or explicit exit, leave plan mode
232        if should_exit {
233            IN_PLAN_MODE.store(false, Ordering::SeqCst);
234            *CURRENT_PLAN.write() = None;
235        }
236
237        Ok(ToolResult::success(output)
238            .with_metadata("completed", json!(completed_count))
239            .with_metadata("total", json!(total_steps)))
240    }
241}