agents_toolkit/builtin/
todos.rs

1//! Built-in todo list management tool
2//!
3//! Provides a tool for agents to manage their task lists.
4
5use 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
13/// Write todos tool - updates the agent's todo list
14pub 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        // Define the schema for TodoItem (matches the actual struct)
25        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        // Update mutable state if available
77        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        // Create state diff
85        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
96/// Read todos tool - retrieves the current todo list
97pub 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        // Read from current state
115        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            // Fallback to snapshot state
120            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
152/// Create the todos tool (write only)
153pub fn create_todos_tool() -> ToolBox {
154    std::sync::Arc::new(WriteTodosTool)
155}
156
157/// Create both read and write todos tools
158pub 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                // Verify state was updated
211                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}