codetether_agent/tool/
plan.rs1use 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 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 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 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}