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 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 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 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
104pub 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 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}