use std::sync::Arc;
use tokio::sync::Mutex;
use async_trait::async_trait;
use serde::{Deserialize, Serialize};
use serde_json::json;
use crate::tools::spec::{
ApprovalRequirement, ToolCapability, ToolContext, ToolError, ToolResult, ToolSpec,
};
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum TodoStatus {
Pending,
InProgress,
Completed,
}
impl TodoStatus {
#[allow(dead_code)]
pub fn as_str(self) -> &'static str {
match self {
TodoStatus::Pending => "pending",
TodoStatus::InProgress => "in_progress",
TodoStatus::Completed => "completed",
}
}
#[must_use]
pub fn from_str(value: &str) -> Option<Self> {
match value.trim().to_lowercase().as_str() {
"pending" => Some(TodoStatus::Pending),
"in_progress" | "inprogress" => Some(TodoStatus::InProgress),
"completed" | "done" => Some(TodoStatus::Completed),
_ => None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TodoItem {
pub id: u32,
pub content: String,
pub status: TodoStatus,
}
#[derive(Debug, Clone, Serialize)]
pub struct TodoListSnapshot {
pub items: Vec<TodoItem>,
pub completion_pct: u8,
pub in_progress_id: Option<u32>,
}
#[derive(Debug, Clone, Default)]
pub struct TodoList {
items: Vec<TodoItem>,
next_id: u32,
}
impl TodoList {
#[must_use]
pub fn new() -> Self {
Self {
items: Vec::new(),
next_id: 1,
}
}
#[must_use]
pub fn snapshot(&self) -> TodoListSnapshot {
TodoListSnapshot {
items: self.items.clone(),
completion_pct: self.completion_percentage(),
in_progress_id: self.in_progress_id(),
}
}
pub fn add(&mut self, content: String, status: TodoStatus) -> TodoItem {
let status = match status {
TodoStatus::InProgress => {
self.set_single_in_progress(None);
TodoStatus::InProgress
}
other => other,
};
let item = TodoItem {
id: self.next_id,
content,
status,
};
self.next_id += 1;
self.items.push(item.clone());
item
}
pub fn update_status(&mut self, id: u32, status: TodoStatus) -> Option<TodoItem> {
let mut updated: Option<TodoItem> = None;
if status == TodoStatus::InProgress {
self.set_single_in_progress(Some(id));
}
for item in &mut self.items {
if item.id == id {
item.status = status;
updated = Some(item.clone());
break;
}
}
updated
}
#[must_use]
pub fn completion_percentage(&self) -> u8 {
if self.items.is_empty() {
return 0;
}
let total = self.items.len();
let completed = self
.items
.iter()
.filter(|item| item.status == TodoStatus::Completed)
.count();
let percent = completed.saturating_mul(100);
let percent = (percent + total / 2) / total;
u8::try_from(percent).unwrap_or(u8::MAX)
}
#[must_use]
pub fn in_progress_id(&self) -> Option<u32> {
self.items
.iter()
.find(|item| item.status == TodoStatus::InProgress)
.map(|item| item.id)
}
pub fn clear(&mut self) {
self.items.clear();
self.next_id = 1;
}
fn set_single_in_progress(&mut self, allow_id: Option<u32>) {
for item in &mut self.items {
if Some(item.id) != allow_id && item.status == TodoStatus::InProgress {
item.status = TodoStatus::Pending;
}
}
}
}
pub type SharedTodoList = Arc<Mutex<TodoList>>;
pub fn new_shared_todo_list() -> SharedTodoList {
Arc::new(Mutex::new(TodoList::new()))
}
pub struct TodoWriteTool {
todo_list: SharedTodoList,
}
impl TodoWriteTool {
pub fn new(todo_list: SharedTodoList) -> Self {
Self { todo_list }
}
}
pub struct TodoAddTool {
todo_list: SharedTodoList,
}
impl TodoAddTool {
pub fn new(todo_list: SharedTodoList) -> Self {
Self { todo_list }
}
}
#[async_trait]
impl ToolSpec for TodoAddTool {
fn name(&self) -> &'static str {
"todo_add"
}
fn description(&self) -> &'static str {
"Add a single todo item (legacy compatibility)."
}
fn input_schema(&self) -> serde_json::Value {
json!({
"type": "object",
"properties": {
"content": {
"type": "string",
"description": "The task description"
},
"status": {
"type": "string",
"enum": ["pending", "in_progress", "completed"],
"description": "Task status (default: pending)"
}
},
"required": ["content"]
})
}
fn capabilities(&self) -> Vec<ToolCapability> {
vec![ToolCapability::WritesFiles]
}
fn approval_requirement(&self) -> ApprovalRequirement {
ApprovalRequirement::Auto
}
async fn execute(
&self,
input: serde_json::Value,
_context: &ToolContext,
) -> Result<ToolResult, ToolError> {
let content = input
.get("content")
.and_then(|v| v.as_str())
.ok_or_else(|| ToolError::invalid_input("Missing 'content'"))?;
let status = input
.get("status")
.and_then(|v| v.as_str())
.and_then(TodoStatus::from_str)
.unwrap_or(TodoStatus::Pending);
let mut list = self.todo_list.lock().await;
let item = list.add(content.to_string(), status);
let snapshot = list.snapshot();
let result = serde_json::to_string_pretty(&snapshot).unwrap_or_else(|_| "{}".to_string());
Ok(ToolResult::success(format!(
"Added todo #{} ({})\n{}",
item.id,
item.status.as_str(),
result
)))
}
}
pub struct TodoUpdateTool {
todo_list: SharedTodoList,
}
impl TodoUpdateTool {
pub fn new(todo_list: SharedTodoList) -> Self {
Self { todo_list }
}
}
#[async_trait]
impl ToolSpec for TodoUpdateTool {
fn name(&self) -> &'static str {
"todo_update"
}
fn description(&self) -> &'static str {
"Update a todo item's status by id (legacy compatibility)."
}
fn input_schema(&self) -> serde_json::Value {
json!({
"type": "object",
"properties": {
"id": {
"type": "integer",
"description": "Todo item id"
},
"status": {
"type": "string",
"enum": ["pending", "in_progress", "completed"],
"description": "New status"
}
},
"required": ["id", "status"]
})
}
fn capabilities(&self) -> Vec<ToolCapability> {
vec![ToolCapability::WritesFiles]
}
fn approval_requirement(&self) -> ApprovalRequirement {
ApprovalRequirement::Auto
}
async fn execute(
&self,
input: serde_json::Value,
_context: &ToolContext,
) -> Result<ToolResult, ToolError> {
let id = input
.get("id")
.and_then(|v| v.as_u64())
.and_then(|v| u32::try_from(v).ok())
.ok_or_else(|| ToolError::invalid_input("Missing or invalid 'id'"))?;
let status = input
.get("status")
.and_then(|v| v.as_str())
.and_then(TodoStatus::from_str)
.ok_or_else(|| ToolError::invalid_input("Missing or invalid 'status'"))?;
let mut list = self.todo_list.lock().await;
let updated = list.update_status(id, status);
let snapshot = list.snapshot();
let result = serde_json::to_string_pretty(&snapshot).unwrap_or_else(|_| "{}".to_string());
match updated {
Some(item) => Ok(ToolResult::success(format!(
"Updated todo #{} to {}\n{}",
item.id,
item.status.as_str(),
result
))),
None => Ok(ToolResult::error(format!("Todo id {id} not found"))),
}
}
}
pub struct TodoListTool {
todo_list: SharedTodoList,
}
impl TodoListTool {
pub fn new(todo_list: SharedTodoList) -> Self {
Self { todo_list }
}
}
#[async_trait]
impl ToolSpec for TodoListTool {
fn name(&self) -> &'static str {
"todo_list"
}
fn description(&self) -> &'static str {
"List current todo items (legacy compatibility)."
}
fn input_schema(&self) -> serde_json::Value {
json!({
"type": "object",
"properties": {}
})
}
fn capabilities(&self) -> Vec<ToolCapability> {
vec![ToolCapability::ReadOnly]
}
fn approval_requirement(&self) -> ApprovalRequirement {
ApprovalRequirement::Auto
}
async fn execute(
&self,
_input: serde_json::Value,
_context: &ToolContext,
) -> Result<ToolResult, ToolError> {
let list = self.todo_list.lock().await;
let snapshot = list.snapshot();
let result = serde_json::to_string_pretty(&snapshot).unwrap_or_else(|_| "{}".to_string());
Ok(ToolResult::success(format!(
"Todo list ({} items, {}% complete)\n{}",
snapshot.items.len(),
snapshot.completion_pct,
result
)))
}
}
#[async_trait]
impl ToolSpec for TodoWriteTool {
fn name(&self) -> &'static str {
"todo_write"
}
fn description(&self) -> &'static str {
"Write or update the todo list for tracking tasks. Use this to plan and track progress on multi-step tasks. Each todo item has a content string and a status (pending, in_progress, completed). Only one item should be in_progress at a time."
}
fn input_schema(&self) -> serde_json::Value {
json!({
"type": "object",
"properties": {
"todos": {
"type": "array",
"description": "The complete list of todo items. This replaces the existing list.",
"items": {
"type": "object",
"properties": {
"content": {
"type": "string",
"description": "The task description"
},
"status": {
"type": "string",
"enum": ["pending", "in_progress", "completed"],
"description": "Task status"
}
},
"required": ["content", "status"]
}
}
},
"required": ["todos"]
})
}
fn capabilities(&self) -> Vec<ToolCapability> {
vec![ToolCapability::WritesFiles]
}
fn approval_requirement(&self) -> ApprovalRequirement {
ApprovalRequirement::Auto
}
async fn execute(
&self,
input: serde_json::Value,
_context: &ToolContext,
) -> Result<ToolResult, ToolError> {
let todos = input
.get("todos")
.and_then(|v| v.as_array())
.ok_or_else(|| ToolError::invalid_input("Missing or invalid 'todos' array"))?;
let mut list = self.todo_list.lock().await;
list.clear();
for item in todos {
let content = item
.get("content")
.and_then(|v| v.as_str())
.ok_or_else(|| ToolError::invalid_input("Todo item missing 'content'"))?;
let status_str = item
.get("status")
.and_then(|v| v.as_str())
.unwrap_or("pending");
let status = TodoStatus::from_str(status_str).unwrap_or(TodoStatus::Pending);
list.add(content.to_string(), status);
}
let snapshot = list.snapshot();
let result = serde_json::to_string_pretty(&snapshot).unwrap_or_else(|_| "{}".to_string());
Ok(ToolResult::success(format!(
"Todo list updated ({} items, {}% complete)\n{}",
snapshot.items.len(),
snapshot.completion_pct,
result
)))
}
}