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