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