1use std::fmt::Write;
2use std::sync::Mutex;
3
4use async_trait::async_trait;
5use serde_json::{json, Value};
6
7use crate::engine::EventBus;
8use crate::tool::{FrameworkTool, Tool, ToolContext, ToolOutput};
9use crate::types::{AgentResult, RuntimeEvent, UpdatePlanArgs};
10
11pub struct UpdatePlanTool {
22 event_bus: Mutex<Option<EventBus>>,
23 last_objective: Mutex<Option<String>>,
24 custom_description: Option<String>,
25}
26
27fn normalize_step_text(raw: &str) -> String {
35 let text = raw.trim();
36
37 let text = if let Some(rest) = text.strip_prefix("Step").or_else(|| text.strip_prefix("step")) {
39 let rest = rest.trim_start_matches(|c: char| c.is_ascii_digit() || c == ' ');
40 rest.trim_start_matches(|c: char| c == ':' || c == '.' || c == ')' || c == ' ')
41 } else {
42 text
43 };
44
45 let text = if let Some(rest) = text.strip_prefix('第') {
47 let rest = rest.trim_start_matches(|c: char| {
49 c.is_ascii_digit()
50 || c == ' '
51 || matches!(c, '一' | '二' | '三' | '四' | '五' | '六' | '七' | '八' | '九' | '十')
52 });
53 rest.strip_prefix('步')
55 .map(|r| r.trim_start_matches(|c: char| matches!(c, ':' | ':' | '、' | '.' | ')' | ' ')))
56 .unwrap_or(text)
57 } else {
58 text
59 };
60
61 let text = text.trim_start_matches(|c: char| {
63 c.is_ascii_digit() || matches!(c, '.' | ')' | '(' | '、' | ' ' | '-' | '/')
64 });
65
66 let mut text = if text.chars().count() > 60 {
68 let truncated: String = text.chars().take(57).collect();
69 format!("{truncated}...")
70 } else {
71 text.to_string()
72 };
73
74 let trimmed = text.trim();
76 if trimmed.is_empty() {
77 raw.trim().to_string()
78 } else if trimmed.len() < text.len() {
79 trimmed.to_string()
80 } else {
81 text
82 }
83}
84
85impl UpdatePlanTool {
86 pub fn new() -> Self {
87 Self {
88 event_bus: Mutex::new(None),
89 last_objective: Mutex::new(None),
90 custom_description: None,
91 }
92 }
93
94 pub fn with_description(mut self, desc: String) -> Self {
100 self.custom_description = Some(desc);
101 self
102 }
103}
104
105impl Default for UpdatePlanTool {
106 fn default() -> Self {
107 Self::new()
108 }
109}
110
111impl FrameworkTool for UpdatePlanTool {
112 fn set_event_bus(&self, event_bus: EventBus) {
113 *self.event_bus.lock().unwrap() = Some(event_bus);
114 }
115}
116
117#[async_trait]
118impl Tool for UpdatePlanTool {
119 fn name(&self) -> &'static str {
120 "update_plan"
121 }
122
123 fn definition(&self) -> Value {
124 let description = self.custom_description.as_deref().unwrap_or(
125 "Record and display a structured plan / checklist to track progress on a complex task.\n\n\
126 Use this to show the user what steps you plan to take and update step statuses as you go.\n\n\
127 Rules:\n\
128 - Always include the user's goal as `objective`\n\
129 - Plan must have at least one step\n\
130 - At most one step may be in_progress at a time\n\
131 - Step descriptions should be concise and human-readable\n\
132 - Call this again whenever step statuses change\n\
133 - Skip this for simple/trivial tasks\n\n\
134 When creating a plan, follow these principles:\n\
135 1. 探查先行 — 第一步先确认相关组件和依赖是否就绪\n\
136 2. 依赖排序 — 被依赖的先执行,独立步骤可并行但不强制\n\
137 3. 每步闭环 — 一步做完可独立验证结果,不等下步才知道成败\n\
138 4. 标注风险 — 涉及 rm、kill、restart、改配置文件时注明\n\
139 5. 粒度适中 — 不过细也不过大,每步是独立可验证的最小逻辑单元\n\
140 6. 收敛止步 — 步骤过多说明任务需要拆分或先讨论再定"
141 );
142 json!({
143 "type": "function",
144 "function": {
145 "name": "update_plan",
146 "description": description,
147 "parameters": {
148 "type": "object",
149 "properties": {
150 "objective": {
151 "type": "string",
152 "description": "One-sentence summary of the user's goal. Example: \"安装 Casdoor 身份认证系统\". Must reflect the user's original intent, not just the current sub-task. Optional on subsequent calls — the tool remembers the last objective."
153 },
154 "explanation": {
155 "type": "string",
156 "description": "Optional explanation of why the plan is being created or changed."
157 },
158 "plan": {
159 "type": "array",
160 "description": "The complete plan checklist. Each item has a step description and status.",
161 "items": {
162 "type": "object",
163 "properties": {
164 "step": {
165 "type": "string",
166 "description": "Short description of this step (5-7 words). Example: '安装 Docker 引擎'"
167 },
168 "status": {
169 "type": "string",
170 "enum": ["pending", "in_progress", "completed"],
171 "description": "Current status of this step."
172 }
173 },
174 "required": ["step", "status"],
175 "additionalProperties": false
176 }
177 }
178 },
179 "required": ["plan"],
180 "additionalProperties": false
181 }
182 }
183 })
184 }
185
186 fn as_framework_tool(&self) -> Option<&dyn FrameworkTool> {
187 Some(self)
188 }
189
190 async fn call(&self, args: &Value, _ctx: &ToolContext) -> AgentResult<ToolOutput> {
191 let plan_args: UpdatePlanArgs = serde_json::from_value(args.clone())
192 .map_err(|e| crate::types::AgentError::ToolArgsInvalid {
193 name: "update_plan".to_string(),
194 raw: format!("deserialization error: {e}"),
195 })?;
196
197 if let Err(validation_err) = plan_args.validate() {
199 return Err(crate::types::AgentError::ToolArgsInvalid {
200 name: "update_plan".to_string(),
201 raw: validation_err,
202 });
203 }
204
205 let objective = match plan_args.objective {
207 Some(ref obj) => {
208 *self.last_objective.lock().unwrap() = Some(obj.clone());
209 obj.clone()
210 }
211 None => {
212 self.last_objective
213 .lock()
214 .unwrap()
215 .clone()
216 .unwrap_or_else(|| "(no objective specified)".to_string())
217 }
218 };
219
220 let normalized_plan: Vec<crate::types::PlanItem> = plan_args
222 .plan
223 .into_iter()
224 .map(|item| crate::types::PlanItem {
225 step: normalize_step_text(&item.step),
226 status: item.status,
227 })
228 .collect();
229
230 let total = normalized_plan.len();
232 let completed = normalized_plan
233 .iter()
234 .filter(|item| item.status == crate::types::PlanStepStatus::Completed)
235 .count();
236 let in_progress = normalized_plan
237 .iter()
238 .filter(|item| item.status == crate::types::PlanStepStatus::InProgress)
239 .count();
240
241 let raw = Some(serde_json::to_value(&normalized_plan).unwrap_or_default());
244
245 let mut summary = format!(
246 "📋 {}: {}/{} steps completed",
247 objective, completed, total
248 );
249 if in_progress > 0 {
250 let current = normalized_plan.iter().find(|item| {
251 item.status == crate::types::PlanStepStatus::InProgress
252 });
253 if let Some(item) = current {
254 write!(summary, ". Current: \"{}\"", item.step).unwrap();
255 }
256 }
257 if total == completed {
258 summary = format!("📋 {} — all steps completed!", objective);
259 }
260
261 {
263 let guard = self.event_bus.lock().unwrap();
264 if let Some(ref event_bus) = *guard {
265 event_bus.emit(RuntimeEvent::PlanUpdated {
266 session_id: _ctx.session_id.clone(),
267 objective: objective.clone(),
268 explanation: plan_args.explanation.clone(),
269 plan: normalized_plan,
270 });
271 }
272 }
273
274 Ok(ToolOutput {
275 summary,
276 raw,
277 control_flow: crate::tool::ToolControlFlow::Continue,
278 truncation: None,
279 })
280 }
281}
282
283#[cfg(test)]
284mod tests {
285 use super::*;
286 use crate::types::{PlanItem, PlanStepStatus};
287
288 #[test]
289 fn test_update_plan_args_validation() {
290 let args = UpdatePlanArgs {
292 objective: Some("安装 Docker".into()),
293 explanation: None,
294 plan: vec![
295 PlanItem { step: "Step 1".into(), status: PlanStepStatus::Completed },
296 PlanItem { step: "Step 2".into(), status: PlanStepStatus::InProgress },
297 PlanItem { step: "Step 3".into(), status: PlanStepStatus::Pending },
298 ],
299 };
300 assert!(args.validate().is_ok());
301
302 let args = UpdatePlanArgs {
304 objective: None,
305 explanation: None,
306 plan: vec![
307 PlanItem { step: "Step 1".into(), status: PlanStepStatus::Pending },
308 ],
309 };
310 assert!(args.validate().is_ok());
311
312 let args = UpdatePlanArgs {
314 objective: Some("".into()),
315 explanation: None,
316 plan: vec![
317 PlanItem { step: "Step 1".into(), status: PlanStepStatus::Pending },
318 ],
319 };
320 assert!(args.validate().is_err());
321
322 let args = UpdatePlanArgs {
324 objective: Some("安装 Docker".into()),
325 explanation: None,
326 plan: vec![],
327 };
328 assert!(args.validate().is_err());
329
330 let args = UpdatePlanArgs {
332 objective: Some("安装 Docker".into()),
333 explanation: None,
334 plan: vec![
335 PlanItem { step: "Step 1".into(), status: PlanStepStatus::InProgress },
336 PlanItem { step: "Step 2".into(), status: PlanStepStatus::InProgress },
337 ],
338 };
339 assert!(args.validate().is_err());
340
341 let args = UpdatePlanArgs {
343 objective: Some("安装 Docker".into()),
344 explanation: None,
345 plan: vec![
346 PlanItem { step: " ".into(), status: PlanStepStatus::Pending },
347 ],
348 };
349 assert!(args.validate().is_err());
350 }
351
352 #[test]
353 fn test_normalize_step_text() {
354 assert_eq!(normalize_step_text("1. 安装 Docker"), "安装 Docker");
356 assert_eq!(normalize_step_text("2) 添加 GPG 密钥"), "添加 GPG 密钥");
357 assert_eq!(normalize_step_text("(3) 更新 APT 包列表"), "更新 APT 包列表");
358 assert_eq!(normalize_step_text("Step 1: 安装 Docker"), "安装 Docker");
359 assert_eq!(normalize_step_text("step 2: 更新包列表"), "更新包列表");
360 assert_eq!(normalize_step_text("1、配置仓库"), "配置仓库");
361
362 assert_eq!(normalize_step_text("1-2) Install Docker"), "Install Docker");
364 assert_eq!(normalize_step_text("3/5) Verify config"), "Verify config");
365
366 assert_eq!(normalize_step_text("第一步:安装 Docker"), "安装 Docker");
368 assert_eq!(normalize_step_text("第1步:添加 GPG 密钥"), "添加 GPG 密钥");
369 assert_eq!(normalize_step_text("第 3 步: 更新包列表"), "更新包列表");
370 assert_eq!(normalize_step_text("第二步、配置仓库"), "配置仓库");
371
372 assert_eq!(normalize_step_text("第一个任务:安装"), "第一个任务:安装");
374
375 let long = "使用 apt install -y docker-ce docker-ce-cli containerd.io 命令来安装 Docker 引擎以及相关组件";
377 let result = normalize_step_text(long);
378 assert!(result.chars().count() <= 60);
379 assert!(result.ends_with("..."));
380
381 assert_eq!(normalize_step_text("安装 Docker 引擎"), "安装 Docker 引擎");
383
384 assert_eq!(normalize_step_text(" 安装 Docker "), "安装 Docker");
386
387 let result = normalize_step_text("123");
389 assert!(!result.is_empty());
390 }
391}