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