feat: 优化 todo_write 工具的 merge 参数描述,调整默认值逻辑并添加测试

This commit is contained in:
oudecheng 2026-06-29 09:24:05 +08:00
parent 4742a6aed9
commit bf1549b88b
4 changed files with 57 additions and 11 deletions

View File

@ -27,8 +27,8 @@ const TODO_WRITE_INSTRUCTIONS: &str = r#"
- todo - todo
### merge ### merge
- `merge: false` todo - `merge: true` **使**
- `merge: true` **使 merge=true** - `merge: false` todo
### ###
- `pending` - `pending`

View File

@ -26,6 +26,7 @@ pub(crate) fn ws_outbound_from_chat_message(message: &ChatMessage) -> Vec<WsOutb
topic_id: None, topic_id: None,
timestamp: None, timestamp: None,
reasoning_content: message.reasoning_content.clone(), reasoning_content: message.reasoning_content.clone(),
user_message_id: None,
}); });
} }
@ -42,6 +43,7 @@ pub(crate) fn ws_outbound_from_chat_message(message: &ChatMessage) -> Vec<WsOutb
topic_id: None, topic_id: None,
timestamp: None, timestamp: None,
reasoning_content: tc_reasoning.clone(), reasoning_content: tc_reasoning.clone(),
user_message_id: None,
})); }));
outbound outbound
} else { } else {
@ -54,6 +56,7 @@ pub(crate) fn ws_outbound_from_chat_message(message: &ChatMessage) -> Vec<WsOutb
topic_id: None, topic_id: None,
timestamp: None, timestamp: None,
reasoning_content: message.reasoning_content.clone(), reasoning_content: message.reasoning_content.clone(),
user_message_id: None,
}] }]
} }
} }

View File

@ -77,9 +77,9 @@ impl Tool for TodoWriteTool {
fn description(&self) -> &str { fn description(&self) -> &str {
"Manage a structured task list for tracking work within the current conversation. \ "Manage a structured task list for tracking work within the current conversation. \
Two modes: merge=false (default, full replacement omitted items are removed); \ Two modes: merge=true (default, incremental only send the items you want to add/update, \
merge=true (incremental only send the items you want to add/update, \ previously existing items are preserved); \
previously existing items are preserved). \ merge=false (full replacement omitted items are removed). \
Use when you have 3+ distinct steps to track. \ Use when you have 3+ distinct steps to track. \
Rules: only ONE in_progress at a time, complete work before marking completed, \ Rules: only ONE in_progress at a time, complete work before marking completed, \
every item requires an id (generate a short random string for new items)." every item requires an id (generate a short random string for new items)."
@ -91,7 +91,7 @@ impl Tool for TodoWriteTool {
"properties": { "properties": {
"merge": { "merge": {
"type": "boolean", "type": "boolean",
"description": "false (default): full replacement — todos not in the list are removed. true: incremental — only send items you want to add or update, existing items not mentioned are preserved." "description": "true (default): incremental — only send items you want to add or update, existing items not mentioned are preserved. false: full replacement — todos not in the list are removed."
}, },
"todos": { "todos": {
"type": "array", "type": "array",
@ -156,7 +156,7 @@ impl Tool for TodoWriteTool {
let merge_mode = args let merge_mode = args
.get("merge") .get("merge")
.and_then(|v| v.as_bool()) .and_then(|v| v.as_bool())
.unwrap_or(false); .unwrap_or(true);
// 3. 读锁获取旧状态 // 3. 读锁获取旧状态
let old_items = { let old_items = {
@ -754,11 +754,12 @@ mod tests {
.await .await
.unwrap(); .unwrap();
// 只传入一个任务任务B 被移除) // 全量替换:只传入一个任务任务B 被移除)
let result = tool let result = tool
.execute_with_context( .execute_with_context(
&context, &context,
json!({ json!({
"merge": false,
"todos": [ "todos": [
{"id": "i1", "content": "任务A", "status": "in_progress"} {"id": "i1", "content": "任务A", "status": "in_progress"}
] ]
@ -1054,11 +1055,12 @@ mod tests {
.await .await
.unwrap(); .unwrap();
// merge=false默认— 只传一个,另一个被删 // 显式指定 merge=false — 只传一个,另一个被删
let result = tool let result = tool
.execute_with_context( .execute_with_context(
&context, &context,
json!({ json!({
"merge": false,
"todos": [ "todos": [
{"id": "p1", "content": "任务A", "status": "in_progress"} {"id": "p1", "content": "任务A", "status": "in_progress"}
] ]
@ -1140,4 +1142,45 @@ mod tests {
assert!(!result.success); assert!(!result.success);
assert!(result.error.unwrap().contains("missing or empty 'id'")); assert!(result.error.unwrap().contains("missing or empty 'id'"));
} }
#[tokio::test]
async fn test_default_is_merge_mode() {
let state = test_state();
let tool = TodoWriteTool::new(state.clone());
let context = test_context();
// 先创建 2 个 todo
let _ = tool
.execute_with_context(
&context,
json!({
"todos": [
{"id": "x1", "content": "任务A", "status": "pending"},
{"id": "x2", "content": "任务B", "status": "pending"}
]
}),
)
.await
.unwrap();
// 不传 merge 参数,只更新一项 — 默认应为 merge=true旧项保留
let result = tool
.execute_with_context(
&context,
json!({
"todos": [
{"id": "x1", "content": "任务A", "status": "in_progress"}
]
}),
)
.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(), 2); // 默认 merge旧项保留
let task_a = todos.iter().find(|t| t["id"] == "x1").unwrap();
assert_eq!(task_a["status"], "in_progress");
}
} }

View File

@ -502,10 +502,10 @@ function App() {
} }
} }
// 过滤无实质内容的 merged_tool(无结果且非等待中) // 过滤无实质内容的 merged_toolresult 到达后才显示保留calling/pending 有 callContent 也保留
return result.filter(msg => { return result.filter(msg => {
if (msg.type !== 'merged_tool') return true if (msg.type !== 'merged_tool') return true
if (msg.status === 'pending') return true if (msg.status === 'calling' || msg.status === 'pending') return true
return !!(msg.resultContent && msg.resultContent.trim()) return !!(msg.resultContent && msg.resultContent.trim())
}) })
}, [messages]) }, [messages])