1use crate::tool::{Tool, ToolResult, ToolSchema, parse_tool_args};
2use async_trait::async_trait;
3use serde::{Deserialize, Serialize};
4use serde_json::{Value, json};
5
6pub struct TodoWriteTool;
7pub struct TodoReadTool;
8
9#[derive(Debug, Clone, Serialize, Deserialize)]
10#[serde(rename_all = "snake_case")]
11enum TodoStatus {
12 Pending,
13 InProgress,
14 Completed,
15 Cancelled,
16}
17
18#[derive(Debug, Clone, Serialize, Deserialize)]
19#[serde(rename_all = "lowercase")]
20enum TodoPriority {
21 High,
22 Medium,
23 Low,
24}
25
26#[derive(Debug, Clone, Serialize, Deserialize)]
27struct TodoItem {
28 content: String,
29 status: TodoStatus,
30 priority: TodoPriority,
31}
32
33#[derive(Debug, Deserialize)]
34struct TodoWriteArgs {
35 todos: Vec<TodoItem>,
36}
37
38#[derive(Debug, Serialize)]
39struct TodoCounts {
40 total: usize,
41 pending: usize,
42 in_progress: usize,
43 completed: usize,
44 cancelled: usize,
45}
46
47#[derive(Debug, Serialize)]
48struct TodoWriteOutput {
49 todos: Vec<TodoItem>,
50 counts: TodoCounts,
51}
52
53impl TodoCounts {
54 fn from_todos(todos: &[TodoItem]) -> Self {
55 let mut counts = Self {
56 total: todos.len(),
57 pending: 0,
58 in_progress: 0,
59 completed: 0,
60 cancelled: 0,
61 };
62
63 for item in todos {
64 match item.status {
65 TodoStatus::Pending => counts.pending += 1,
66 TodoStatus::InProgress => counts.in_progress += 1,
67 TodoStatus::Completed => counts.completed += 1,
68 TodoStatus::Cancelled => counts.cancelled += 1,
69 }
70 }
71
72 counts
73 }
74}
75
76#[async_trait]
77impl Tool for TodoReadTool {
78 fn schema(&self) -> ToolSchema {
79 ToolSchema {
80 name: "todo_read".to_string(),
81 description: "Read canonical todo list state".to_string(),
82 capability: Some("todo_read".to_string()),
83 mutating: Some(false),
84 parameters: json!({
85 "type": "object",
86 "properties": {},
87 "additionalProperties": false
88 }),
89 }
90 }
91
92 async fn execute(&self, args: Value) -> ToolResult {
93 if !args.is_object() {
94 return ToolResult::error("invalid todo_read args: expected object");
95 }
96
97 let output = TodoWriteOutput {
98 todos: Vec::new(),
99 counts: TodoCounts::from_todos(&[]),
100 };
101
102 ToolResult::ok_json_typed_serializable(
103 "todo list snapshot",
104 "application/vnd.hh.todo+json",
105 &output,
106 )
107 }
108}
109
110#[async_trait]
111impl Tool for TodoWriteTool {
112 fn schema(&self) -> ToolSchema {
113 ToolSchema {
114 name: "todo_write".to_string(),
115 description: "Set canonical todo list state".to_string(),
116 capability: Some("todo_write".to_string()),
117 mutating: Some(true),
118 parameters: json!({
119 "type": "object",
120 "properties": {
121 "todos": {
122 "type": "array",
123 "items": {
124 "type": "object",
125 "properties": {
126 "content": {"type": "string"},
127 "status": {
128 "type": "string",
129 "enum": ["pending", "in_progress", "completed", "cancelled"]
130 },
131 "priority": {
132 "type": "string",
133 "enum": ["high", "medium", "low"]
134 }
135 },
136 "required": ["content", "status", "priority"]
137 }
138 }
139 },
140 "required": ["todos"]
141 }),
142 }
143 }
144
145 async fn execute(&self, args: Value) -> ToolResult {
146 let parsed: TodoWriteArgs = match parse_tool_args(args, "todo_write") {
147 Ok(value) => value,
148 Err(err) => return err,
149 };
150
151 for item in &parsed.todos {
152 if item.content.trim().is_empty() {
153 return ToolResult::error("todo content must not be empty");
154 }
155 }
156
157 let todos = parsed.todos;
158 let output = TodoWriteOutput {
159 counts: TodoCounts::from_todos(&todos),
160 todos,
161 };
162
163 ToolResult::ok_json_typed_serializable(
164 "todo list updated",
165 "application/vnd.hh.todo+json",
166 &output,
167 )
168 }
169}