telos_agent/tools/builtin/
todo_write.rs1use async_trait::async_trait;
12use serde_json::{Value, json};
13use std::sync::{Arc, Mutex};
14
15use crate::error::AgentError;
16use crate::tools::api::{Tool, ToolContext, ToolDefinition, ToolOutput};
17
18#[derive(Debug, Clone, Default)]
20pub struct TodoList {
21 pub items: Vec<TodoItem>,
22}
23
24#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
25pub struct TodoItem {
26 pub content: String,
27 pub status: String,
28 #[serde(default, skip_serializing_if = "String::is_empty")]
29 pub active_form: String,
30}
31
32pub type SharedTodoList = Arc<Mutex<TodoList>>;
33
34pub struct TodoWriteTool {
36 todos: SharedTodoList,
37}
38
39impl TodoWriteTool {
40 pub fn new(todos: SharedTodoList) -> Self {
41 Self { todos }
42 }
43}
44
45#[async_trait]
46impl Tool for TodoWriteTool {
47 fn definition(&self) -> ToolDefinition {
48 ToolDefinition {
49 name: "TodoWrite".into(),
50 description: "Creates and manages a structured task list for tracking progress. Use for multi-step tasks (3+ steps), complex work, or when user provides multiple tasks. Skip for single straightforward tasks or conversational requests. \
51Rules: one item in_progress at a time; update status in real-time; mark in_progress before starting; always include active_form; mark completed only when fully done (no failing tests, no partial work)."
52 .into(),
53 input_schema: json!({
54 "type": "object",
55 "properties": {
56 "todos": {
57 "type": "array",
58 "description": "The complete todo list (replaces all existing items)",
59 "items": {
60 "type": "object",
61 "properties": {
62 "content": {
63 "type": "string",
64 "description": "The task content (imperative form, e.g. 'Implement login handler')"
65 },
66 "status": {
67 "type": "string",
68 "enum": ["pending", "in_progress", "completed"],
69 "description": "Current status of the task"
70 },
71 "active_form": {
72 "type": "string",
73 "description": "Present continuous form (e.g. 'Implementing login handler')"
74 }
75 },
76 "required": ["content", "status", "active_form"]
77 }
78 }
79 },
80 "required": ["todos"]
81 }),
82 }
83 }
84
85 fn prompt_text(&self) -> Option<&'static str> {
86 Some(TODO_WRITE_PROMPT)
87 }
88
89 fn is_concurrency_safe(&self, _: &Value) -> bool {
90 true
91 }
92
93 async fn invoke(
94 &self,
95 arguments: Value,
96 _context: ToolContext,
97 ) -> Result<ToolOutput, AgentError> {
98 let new_items: Vec<TodoItem> = {
99 let arr = arguments
100 .get("todos")
101 .and_then(|v| v.as_array())
102 .ok_or_else(|| AgentError::Validation("missing `todos` array".into()))?;
103
104 arr.iter()
105 .map(|item| TodoItem {
106 content: item.get("content").and_then(|v| v.as_str()).unwrap_or("").to_string(),
107 status: item
108 .get("status")
109 .and_then(|v| v.as_str())
110 .unwrap_or("pending")
111 .to_string(),
112 active_form: item
113 .get("active_form")
114 .and_then(|v| v.as_str())
115 .unwrap_or("")
116 .to_string(),
117 })
118 .collect()
119 };
120
121 let old_items = {
122 let mut todos = self.todos.lock().unwrap();
123 std::mem::replace(&mut todos.items, new_items).clone()
124 };
125
126 let new_items = self.todos.lock().unwrap().items.clone();
127
128 if !new_items.is_empty() && new_items.iter().all(|i| i.status == "completed") {
130 self.todos.lock().unwrap().items.clear();
131 }
132
133 let mut extra = Vec::new();
135 let completed_count = new_items.iter().filter(|i| i.status == "completed").count();
136 if completed_count >= 3 {
137 let has_verify_mention =
138 new_items.iter().any(|i| i.content.to_lowercase().contains("verif"));
139 if !has_verify_mention {
140 extra.push("Consider spawning a Verify subagent to validate the completed work.");
141 }
142 }
143
144 let mut result = json!({
145 "old_todos": old_items,
146 "new_todos": new_items,
147 "updated": true,
148 "message": format!("Todo list updated: {} items", new_items.len())
149 });
150
151 if let Some(obj) = result.as_object_mut()
152 && !extra.is_empty()
153 {
154 obj.insert("hints".into(), json!(extra));
155 }
156
157 Ok(ToolOutput::json(result))
158 }
159}
160
161const TODO_WRITE_PROMPT: &str = r#"TodoWrite manages your session task list.
162
163**Rules:**
1641. Exactly ONE item `in_progress` at a time.
1652. Update status immediately after finishing — mark new work `in_progress` before starting.
1663. Always include `active_form` (present continuous) for each item.
1674. Only mark `completed` when fully done (no failing tests, no partial work).
1685. Remove stale items promptly. Merge new user instructions into the full list."#;