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 && step_num > 0
189 && step_num <= plan.steps.len()
190 {
191 let step = &mut plan.steps[step_num - 1];
192 step.completed = true;
193 step.notes = p.notes.clone();
194 plan.current_step = step_num;
195 }
196
197 let completed_count = plan.steps.iter().filter(|s| s.completed).count();
199 let total_steps = plan.steps.len();
200 let status = plan
201 .steps
202 .iter()
203 .enumerate()
204 .map(|(i, s)| {
205 let icon = if s.completed { "✓" } else { "○" };
206 let notes = s
207 .notes
208 .as_ref()
209 .map(|n| format!(" [{}]", n))
210 .unwrap_or_default();
211 format!(" {} {}. {}{}", icon, i + 1, s.description, notes)
212 })
213 .collect::<Vec<_>>()
214 .join("\n");
215
216 let output = format!(
217 "📋 Plan Status\n\nGoal: {}\n\nProgress: {}/{} steps\n\n{}\n\n{}",
218 plan.goal,
219 completed_count,
220 total_steps,
221 status,
222 p.summary
223 .as_ref()
224 .map(|s| format!("Summary: {}", s))
225 .unwrap_or_default()
226 );
227
228 let should_exit = completed_count == total_steps || p.summary.is_some();
229 (output, completed_count, total_steps, should_exit)
230 };
231
232 if should_exit {
234 IN_PLAN_MODE.store(false, Ordering::SeqCst);
235 *CURRENT_PLAN.write() = None;
236 }
237
238 Ok(ToolResult::success(output)
239 .with_metadata("completed", json!(completed_count))
240 .with_metadata("total", json!(total_steps)))
241 }
242}