Skip to main content

codetether_agent/tool/
plan.rs

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