matrixcode_core/tools/
todo_write.rs1use anyhow::Result;
2use async_trait::async_trait;
3use serde_json::{Value, json};
4
5use super::{Tool, ToolDefinition};
6use crate::approval::RiskLevel;
7
8pub struct TodoWriteTool;
9
10#[async_trait]
11impl Tool for TodoWriteTool {
12 fn definition(&self) -> ToolDefinition {
13 ToolDefinition {
14 name: "todo_write".to_string(),
15 description:
16 "Maintain a structured todo list to plan and track multi-step work. \
17 Each call REPLACES the full list. Use for non-trivial tasks (3+ \
18 steps) to show progress. Keep exactly one task 'in_progress' at \
19 a time and mark tasks 'completed' as soon as they finish."
20 .to_string(),
21 parameters: json!({
22 "type": "object",
23 "properties": {
24 "todos": {
25 "type": "array",
26 "description": "Full replacement list of todo items",
27 "items": {
28 "type": "object",
29 "properties": {
30 "content": {
31 "type": "string",
32 "description": "Imperative form (e.g. 'Run tests')"
33 },
34 "activeForm": {
35 "type": "string",
36 "description": "Present-continuous form (e.g. 'Running tests')"
37 },
38 "status": {
39 "type": "string",
40 "enum": ["pending", "in_progress", "completed"]
41 }
42 },
43 "required": ["content", "activeForm", "status"]
44 }
45 }
46 },
47 "required": ["todos"]
48 }),
49 }
50 }
51
52 async fn execute(&self, params: Value) -> Result<String> {
53 let todos = params["todos"]
54 .as_array()
55 .ok_or_else(|| anyhow::anyhow!("missing 'todos' array"))?;
56
57 let mut in_progress_count = 0;
61 let _completed_count = 0;
62 let _pending_count = 0;
63 let mut lines = Vec::with_capacity(todos.len() + 1);
64
65 for (i, todo) in todos.iter().enumerate() {
66 let content = todo["content"]
67 .as_str()
68 .ok_or_else(|| anyhow::anyhow!("todo {}: missing 'content'", i))?;
69 let active = todo["activeForm"]
70 .as_str()
71 .ok_or_else(|| anyhow::anyhow!("todo {}: missing 'activeForm'", i))?;
72 let status = todo["status"]
73 .as_str()
74 .ok_or_else(|| anyhow::anyhow!("todo {}: missing 'status'", i))?;
75
76 let (marker, display) = match status {
77 "pending" => ("[ ]", content),
78 "in_progress" => {
79 in_progress_count += 1;
80 ("[~]", active)
81 }
82 "completed" => ("[x]", content),
83 other => anyhow::bail!("todo {}: invalid status '{}'", i, other),
84 };
85
86 lines.push(format!(" {} {}", marker, display));
87 }
88
89 if in_progress_count > 1 {
90 anyhow::bail!(
92 "at most one task may be 'in_progress' at a time (found {})",
93 in_progress_count
94 );
95 }
96
97 if lines.is_empty() {
98 return Ok("Todo list cleared.".to_string());
100 }
101
102 let header = format!("Todos ({}):", lines.len());
103 let mut out = String::with_capacity(header.len() + lines.iter().map(|l| l.len() + 1).sum::<usize>());
104 out.push_str(&header);
105 for l in &lines {
106 out.push('\n');
107 out.push_str(l);
108 }
109
110 Ok(out)
115 }
116
117 fn risk_level(&self) -> RiskLevel {
118 RiskLevel::Mutating
119 }
120}