From ce6dce81f435b6595e62be3f489ba7620fdcf1b4 Mon Sep 17 00:00:00 2001 From: oudecheng <13802883547@139.com> Date: Fri, 12 Jun 2026 14:43:38 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E6=96=B0=E5=88=9B=E5=BB=BA=20todo=20?= =?UTF-8?q?=E9=A1=B9=E5=85=81=E8=AE=B8=E7=9B=B4=E6=8E=A5=E8=AE=BE=E4=B8=BA?= =?UTF-8?q?=20in=5Fprogress=EF=BC=8C=E6=97=A0=E9=9C=80=E5=85=88=20pending?= =?UTF-8?q?=20=E5=86=8D=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit agent 创建 todo 列表时可以将第一个任务直接标为 in_progress, 避免浪费一次工具调用。仍然禁止新项为 completed/cancelled。 Co-Authored-By: Claude Opus 4.8 --- src/tools/todo_write.rs | 61 ++++++++++++++++++++++++++++++++++++----- 1 file changed, 54 insertions(+), 7 deletions(-) diff --git a/src/tools/todo_write.rs b/src/tools/todo_write.rs index 3fc9b71..36e2abf 100644 --- a/src/tools/todo_write.rs +++ b/src/tools/todo_write.rs @@ -273,9 +273,9 @@ impl Tool for TodoWriteTool { }); } else { // 传入 id 但旧状态中没有 → 按新 item 处理 - if new_status != TodoStatus::Pending { + if !is_valid_new_status(&new_status) { validation_errors.push(format!( - "Item '{}': new items must start as 'pending', got '{}'", + "Item '{}': new items must start as 'pending' or 'in_progress', got '{}'", content, status_str )); continue; @@ -291,10 +291,10 @@ impl Tool for TodoWriteTool { }); } } else { - // 新 item(无 id)— 必须是 pending - if new_status != TodoStatus::Pending { + // 新 item(无 id)— 允许 pending 或 in_progress + if !is_valid_new_status(&new_status) { validation_errors.push(format!( - "Item '{}': new items must start as 'pending', got '{}'", + "Item '{}': new items must start as 'pending' or 'in_progress', got '{}'", content, status_str )); continue; @@ -423,6 +423,11 @@ pub(crate) fn scope_key_from_context(context: &ToolContext) -> Option { tid.or(sid).map(str::to_string) } +/// 新创建的 item 允许 pending 或 in_progress,不允许 completed/cancelled +fn is_valid_new_status(status: &TodoStatus) -> bool { + matches!(status, TodoStatus::Pending | TodoStatus::InProgress) +} + /// 校验状态转换合法性 fn validate_transition(old: &TodoStatus, new: &TodoStatus) -> Result<(), String> { if old.is_terminal() { @@ -694,7 +699,7 @@ mod tests { } #[tokio::test] - async fn test_new_item_must_be_pending() { + async fn test_new_item_cannot_be_completed_or_cancelled() { let tool = TodoWriteTool::new(test_state()); let context = test_context(); @@ -711,7 +716,49 @@ mod tests { .unwrap(); assert!(!result.success); - assert!(result.error.unwrap().contains("must start as 'pending'")); + assert!(result.error.unwrap().contains("new items must start as")); + + let result2 = tool + .execute_with_context( + &context, + json!({ + "todos": [ + {"content": "任务B", "status": "cancelled"} + ] + }), + ) + .await + .unwrap(); + + assert!(!result2.success); + assert!(result2.error.unwrap().contains("new items must start as")); + } + + #[tokio::test] + async fn test_new_item_can_start_as_in_progress() { + let tool = TodoWriteTool::new(test_state()); + let context = test_context(); + + // 直接创建第一个任务为 in_progress — 应该允许 + let result = tool + .execute_with_context( + &context, + json!({ + "todos": [ + {"content": "第一个任务", "status": "in_progress"}, + {"content": "第二个任务", "status": "pending"}, + {"content": "第三个任务", "status": "pending"} + ] + }), + ) + .await + .unwrap(); + + assert!(result.success); + let output: serde_json::Value = serde_json::from_str(&result.output).unwrap(); + let todos = output["current_todos"].as_array().unwrap(); + assert_eq!(todos.len(), 3); + assert_eq!(todos[0]["status"], "in_progress"); } #[tokio::test]