Skip to main content

opendev_tools_impl/
todo.rs

1//! Todo tool — list, update, and manage plan execution todos.
2//!
3//! Works with the `TodoManager` from `opendev-runtime` to let the agent
4//! query and update todo progress during plan execution.
5
6use std::collections::HashMap;
7use std::sync::{Arc, Mutex};
8
9use opendev_tools_core::{BaseTool, ToolContext, ToolDisplayMeta, ToolResult};
10
11/// Tool for managing plan execution todos.
12#[derive(Debug)]
13pub struct TodoTool {
14    /// Shared reference to the todo manager.
15    ///
16    /// Uses `Arc<Mutex<_>>` so the tool can be registered in the tool registry
17    /// while the manager is also accessed by the TUI and react loop.
18    manager: Arc<Mutex<opendev_runtime::TodoManager>>,
19}
20
21impl TodoTool {
22    /// Create a new todo tool with a shared manager.
23    pub fn new(manager: Arc<Mutex<opendev_runtime::TodoManager>>) -> Self {
24        Self { manager }
25    }
26}
27
28#[async_trait::async_trait]
29impl BaseTool for TodoTool {
30    fn name(&self) -> &str {
31        "todo"
32    }
33
34    fn description(&self) -> &str {
35        "Manage plan execution todos. List current todos, mark items as \
36         in-progress or completed, or add new items."
37    }
38
39    fn parameter_schema(&self) -> serde_json::Value {
40        serde_json::json!({
41            "type": "object",
42            "properties": {
43                "action": {
44                    "type": "string",
45                    "enum": ["list", "start", "complete", "add"],
46                    "description": "Action to perform on todos"
47                },
48                "id": {
49                    "type": "integer",
50                    "description": "Todo item ID (for start/complete)"
51                },
52                "title": {
53                    "type": "string",
54                    "description": "Title for a new todo item (for add)"
55                }
56            },
57            "required": ["action"]
58        })
59    }
60
61    async fn execute(
62        &self,
63        args: HashMap<String, serde_json::Value>,
64        _ctx: &ToolContext,
65    ) -> ToolResult {
66        let action = match args.get("action").and_then(|v| v.as_str()) {
67            Some(a) => a,
68            None => return ToolResult::fail("action is required"),
69        };
70
71        let mut mgr = match self.manager.lock() {
72            Ok(m) => m,
73            Err(e) => return ToolResult::fail(format!("Lock error: {e}")),
74        };
75
76        match action {
77            "list" => {
78                if !mgr.has_todos() {
79                    return ToolResult::ok("No todos.");
80                }
81                ToolResult::ok(mgr.format_status())
82            }
83            "start" => {
84                let id = match args.get("id").and_then(|v| v.as_u64()) {
85                    Some(id) => id as usize,
86                    None => return ToolResult::fail("id is required for start"),
87                };
88                if mgr.start(id) {
89                    ToolResult::ok(format!(
90                        "Todo {id} marked as in-progress.\n\n{}",
91                        mgr.format_status()
92                    ))
93                } else {
94                    ToolResult::fail(format!("Todo {id} not found"))
95                }
96            }
97            "complete" => {
98                let id = match args.get("id").and_then(|v| v.as_u64()) {
99                    Some(id) => id as usize,
100                    None => return ToolResult::fail("id is required for complete"),
101                };
102                if mgr.complete(id) {
103                    let status = mgr.format_status();
104                    if mgr.all_completed() {
105                        ToolResult::ok(format!(
106                            "Todo {id} completed. All todos are done!\n\n{status}"
107                        ))
108                    } else {
109                        ToolResult::ok(format!("Todo {id} completed.\n\n{status}"))
110                    }
111                } else {
112                    ToolResult::fail(format!("Todo {id} not found"))
113                }
114            }
115            "add" => {
116                let title = match args.get("title").and_then(|v| v.as_str()) {
117                    Some(t) if !t.is_empty() => t,
118                    _ => return ToolResult::fail("title is required for add"),
119                };
120                let id = mgr.add(title.to_string());
121                ToolResult::ok(format!(
122                    "Added todo {id}: {title}\n\n{}",
123                    mgr.format_status()
124                ))
125            }
126            _ => ToolResult::fail(format!(
127                "Unknown action: {action}. Available: list, start, complete, add"
128            )),
129        }
130    }
131
132    fn display_meta(&self) -> Option<ToolDisplayMeta> {
133        Some(ToolDisplayMeta {
134            verb: "Todo",
135            label: "task",
136            category: "Plan",
137            primary_arg_keys: &["action", "id", "title"],
138        })
139    }
140}
141
142#[cfg(test)]
143#[path = "todo_tests.rs"]
144mod tests;