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
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        todo_item_props.insert(
50            "activeForm".to_string(),
51            ToolParameterSchema::string("Present continuous form (e.g., 'Running tests')"),
52        );
53
54        let todo_item_schema = ToolParameterSchema::object(
55            "A single todo item",
56            todo_item_props,
57            vec![
58                "content".to_string(),
59                "status".to_string(),
60                "activeForm".to_string(),
61            ],
62        );
63
64        let mut properties = HashMap::new();
65        properties.insert(
66            "todos".to_string(),
67            ToolParameterSchema::array("List of todo items", todo_item_schema),
68        );
69
70        ToolSchema::new(
71            "write_todos",
72            "Update the agent's todo list to track task progress",
73            ToolParameterSchema::object(
74                "Write todos parameters",
75                properties,
76                vec!["todos".to_string()],
77            ),
78        )
79    }
80
81    async fn execute(&self, args: Value, ctx: ToolContext) -> anyhow::Result<ToolResult> {
82        let args: WriteTodosArgs = serde_json::from_value(args)?;
83
84        // Update mutable state if available
85        if let Some(state_handle) = &ctx.state_handle {
86            let mut state = state_handle
87                .write()
88                .expect("todo state write lock poisoned");
89            state.todos = args.todos.clone();
90        }
91
92        // Create state diff
93        let diff = StateDiff {
94            todos: Some(args.todos.clone()),
95            ..StateDiff::default()
96        };
97
98        let message =
99            ctx.text_response(format!("Updated todo list with {} items", args.todos.len()));
100        Ok(ToolResult::with_state(message, diff))
101    }
102}
103
104/// Create the todos tool
105pub fn create_todos_tool() -> ToolBox {
106    std::sync::Arc::new(WriteTodosTool)
107}
108
109#[cfg(test)]
110mod tests {
111    use super::*;
112    use agents_core::state::AgentStateSnapshot;
113    use serde_json::json;
114    use std::sync::{Arc, RwLock};
115
116    #[tokio::test]
117    async fn write_todos_updates_state() {
118        let state = Arc::new(AgentStateSnapshot::default());
119        let state_handle = Arc::new(RwLock::new(AgentStateSnapshot::default()));
120        let ctx = ToolContext::with_mutable_state(state, state_handle.clone());
121
122        let tool = WriteTodosTool;
123        let result = tool
124            .execute(
125                json!({
126                    "todos": [
127                        {
128                            "content": "Do task",
129                            "status": "pending",
130                            "activeForm": "Doing task"
131                        },
132                        {
133                            "content": "Ship feature",
134                            "status": "completed",
135                            "activeForm": "Shipping feature"
136                        }
137                    ]
138                }),
139                ctx,
140            )
141            .await
142            .unwrap();
143
144        match result {
145            ToolResult::WithStateUpdate {
146                message,
147                state_diff,
148            } => {
149                assert!(message
150                    .content
151                    .as_text()
152                    .unwrap()
153                    .contains("Updated todo list"));
154                assert_eq!(state_diff.todos.as_ref().unwrap().len(), 2);
155
156                // Verify state was updated
157                let final_state = state_handle.read().unwrap();
158                assert_eq!(final_state.todos.len(), 2);
159                assert_eq!(final_state.todos[0].content, "Do task");
160            }
161            _ => panic!("Expected state update result"),
162        }
163    }
164}