agents_toolkit/builtin/
todos.rs1use agents_core::command::StateDiff;
6use agents_core::state::TodoItem;
7use agents_core::tools::{Tool, ToolBox, ToolContext, ToolParameterSchema, ToolResult, ToolSchema};
8use async_trait::async_trait;
9use serde::Deserialize;
10use serde_json::Value;
11use std::collections::HashMap;
12
13pub struct WriteTodosTool;
15
16#[derive(Deserialize)]
17struct WriteTodosArgs {
18 todos: Vec<TodoItem>,
19}
20
21#[async_trait]
22impl Tool for WriteTodosTool {
23 fn schema(&self) -> ToolSchema {
24 let mut todo_item_props = HashMap::new();
26 todo_item_props.insert(
27 "content".to_string(),
28 ToolParameterSchema::string("The todo item description"),
29 );
30 todo_item_props.insert(
31 "status".to_string(),
32 ToolParameterSchema {
33 schema_type: "string".to_string(),
34 description: Some(
35 "Status of the todo (pending, in_progress, completed)".to_string(),
36 ),
37 enum_values: Some(vec![
38 serde_json::json!("pending"),
39 serde_json::json!("in_progress"),
40 serde_json::json!("completed"),
41 ]),
42 properties: None,
43 required: None,
44 items: None,
45 default: None,
46 additional: HashMap::new(),
47 },
48 );
49
50 let todo_item_schema = ToolParameterSchema::object(
51 "A single todo item",
52 todo_item_props,
53 vec!["content".to_string(), "status".to_string()],
54 );
55
56 let mut properties = HashMap::new();
57 properties.insert(
58 "todos".to_string(),
59 ToolParameterSchema::array("List of todo items", todo_item_schema),
60 );
61
62 ToolSchema::new(
63 "write_todos",
64 "Update the agent's todo list to track task progress",
65 ToolParameterSchema::object(
66 "Write todos parameters",
67 properties,
68 vec!["todos".to_string()],
69 ),
70 )
71 }
72
73 async fn execute(&self, args: Value, ctx: ToolContext) -> anyhow::Result<ToolResult> {
74 let args: WriteTodosArgs = serde_json::from_value(args)?;
75
76 if let Some(state_handle) = &ctx.state_handle {
78 let mut state = state_handle
79 .write()
80 .expect("todo state write lock poisoned");
81 state.todos = args.todos.clone();
82 }
83
84 let diff = StateDiff {
86 todos: Some(args.todos.clone()),
87 ..StateDiff::default()
88 };
89
90 let message =
91 ctx.text_response(format!("Updated todo list with {} items", args.todos.len()));
92 Ok(ToolResult::with_state(message, diff))
93 }
94}
95
96pub struct ReadTodosTool;
98
99#[async_trait]
100impl Tool for ReadTodosTool {
101 fn schema(&self) -> ToolSchema {
102 ToolSchema::new(
103 "read_todos",
104 "Read the current todo list to check task progress",
105 ToolParameterSchema::object(
106 "Read todos parameters (no parameters needed)",
107 HashMap::new(),
108 vec![],
109 ),
110 )
111 }
112
113 async fn execute(&self, _args: Value, ctx: ToolContext) -> anyhow::Result<ToolResult> {
114 let todos = if let Some(state_handle) = &ctx.state_handle {
116 let state = state_handle.read().expect("todo state read lock poisoned");
117 state.todos.clone()
118 } else {
119 ctx.state.todos.clone()
121 };
122
123 if todos.is_empty() {
124 return Ok(ToolResult::text(&ctx, "No todos found."));
125 }
126
127 let todo_list = todos
128 .iter()
129 .enumerate()
130 .map(|(i, todo)| {
131 let (status_emoji, status_text) = match todo.status {
132 agents_core::state::TodoStatus::Completed => ("β
", "COMPLETED"),
133 agents_core::state::TodoStatus::InProgress => ("π", "IN_PROGRESS"),
134 agents_core::state::TodoStatus::Pending => ("βΈοΈ", "PENDING"),
135 };
136 format!(
137 "{}. {} {} - {}",
138 i + 1,
139 status_emoji,
140 status_text,
141 todo.content
142 )
143 })
144 .collect::<Vec<_>>()
145 .join("\n");
146
147 let response = format!("Current TODO list ({} items):\n{}", todos.len(), todo_list);
148 Ok(ToolResult::text(&ctx, response))
149 }
150}
151
152pub fn create_todos_tool() -> ToolBox {
154 std::sync::Arc::new(WriteTodosTool)
155}
156
157pub fn create_todos_tools() -> Vec<ToolBox> {
159 vec![
160 std::sync::Arc::new(WriteTodosTool),
161 std::sync::Arc::new(ReadTodosTool),
162 ]
163}
164
165#[cfg(test)]
166mod tests {
167 use super::*;
168 use agents_core::state::AgentStateSnapshot;
169 use serde_json::json;
170 use std::sync::{Arc, RwLock};
171
172 #[tokio::test]
173 async fn write_todos_updates_state() {
174 let state = Arc::new(AgentStateSnapshot::default());
175 let state_handle = Arc::new(RwLock::new(AgentStateSnapshot::default()));
176 let ctx = ToolContext::with_mutable_state(state, state_handle.clone());
177
178 let tool = WriteTodosTool;
179 let result = tool
180 .execute(
181 json!({
182 "todos": [
183 {
184 "content": "Do task",
185 "status": "pending"
186 },
187 {
188 "content": "Ship feature",
189 "status": "completed"
190 }
191 ]
192 }),
193 ctx,
194 )
195 .await
196 .unwrap();
197
198 match result {
199 ToolResult::WithStateUpdate {
200 message,
201 state_diff,
202 } => {
203 assert!(message
204 .content
205 .as_text()
206 .unwrap()
207 .contains("Updated todo list"));
208 assert_eq!(state_diff.todos.as_ref().unwrap().len(), 2);
209
210 let final_state = state_handle.read().unwrap();
212 assert_eq!(final_state.todos.len(), 2);
213 assert_eq!(final_state.todos[0].content, "Do task");
214 }
215 _ => panic!("Expected state update result"),
216 }
217 }
218}