fix: 新创建 todo 项允许直接设为 in_progress,无需先 pending 再更新

agent 创建 todo 列表时可以将第一个任务直接标为 in_progress,
避免浪费一次工具调用。仍然禁止新项为 completed/cancelled。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
oudecheng 2026-06-12 14:43:38 +08:00
parent 881fcace47
commit ce6dce81f4

View File

@ -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<String> {
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]