Skip to main content

pi_agent/tools/
todo.rs

1//! Lightweight in-memory todo list shared across one agent run. The model can
2//! `add`, `complete`, `list`, or `clear` items. Useful for long-horizon tasks.
3
4use 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}