1use std::sync::Mutex;
5
6use async_trait::async_trait;
7use serde_json::{json, Value};
8
9use crate::types::{AgentTool, AgentToolResult};
10
11#[derive(Default)]
12pub struct TodoTool {
13 items: Mutex<Vec<TodoItem>>,
14}
15
16#[derive(Clone)]
17struct TodoItem {
18 text: String,
19 done: bool,
20}
21
22impl TodoTool {
23 pub fn new() -> Self {
24 Self::default()
25 }
26}
27
28#[async_trait]
29impl AgentTool for TodoTool {
30 fn name(&self) -> &str {
31 "todo"
32 }
33 fn description(&self) -> &str {
34 "Track tasks across a single agent run. action ∈ {add, complete, list, clear}. add expects 'text'; complete expects 'index' (1-based)."
35 }
36 fn parameters(&self) -> Value {
37 json!({
38 "type": "object",
39 "properties": {
40 "action": {"type": "string", "enum": ["add", "complete", "list", "clear"]},
41 "text": {"type": "string"},
42 "index": {"type": "integer"}
43 },
44 "required": ["action"]
45 })
46 }
47 async fn execute(&self, _id: &str, args: Value) -> Result<AgentToolResult, String> {
48 let action = args
49 .get("action")
50 .and_then(|v| v.as_str())
51 .ok_or("missing 'action'")?;
52 let mut items = self.items.lock().map_err(|e| e.to_string())?;
53 match action {
54 "add" => {
55 let text = args
56 .get("text")
57 .and_then(|v| v.as_str())
58 .ok_or("missing 'text'")?
59 .to_string();
60 items.push(TodoItem { text, done: false });
61 Ok(AgentToolResult::text(format_list(&items)))
62 }
63 "complete" => {
64 let idx = args
65 .get("index")
66 .and_then(|v| v.as_u64())
67 .ok_or("missing 'index'")? as usize;
68 if idx == 0 || idx > items.len() {
69 return Err(format!("index {idx} out of range (1..={})", items.len()));
70 }
71 items[idx - 1].done = true;
72 Ok(AgentToolResult::text(format_list(&items)))
73 }
74 "list" => Ok(AgentToolResult::text(format_list(&items))),
75 "clear" => {
76 items.clear();
77 Ok(AgentToolResult::text("(cleared)".to_string()))
78 }
79 other => Err(format!("unknown action: {other}")),
80 }
81 }
82}
83
84fn format_list(items: &[TodoItem]) -> String {
85 if items.is_empty() {
86 return "(empty)".to_string();
87 }
88 let mut out = String::new();
89 for (i, item) in items.iter().enumerate() {
90 let mark = if item.done { "[x]" } else { "[ ]" };
91 out.push_str(&format!("{} {} {}\n", i + 1, mark, item.text));
92 }
93 out
94}