"""Session-scoped task plan manager."""
from __future__ import annotations
from dataclasses import dataclass
from .base import Tool, ToolResult, current_session_id
_STATUS_ICON: dict[str, str] = {
"pending": "○",
"in_progress": "◎",
"done": "✓",
"failed": "✗",
"skipped": "—",
}
@dataclass
class _Task:
text: str
status: str = "pending"
# In-memory store: session_id → task list (ephemeral, lives with the server process)
_plans: dict[str, list[_Task]] = {}
class TodoTool(Tool):
name = "todo"
description = (
"Manage your current task plan for multi-step work. "
"Use 'set' at the start of a complex task to record the steps, "
"'update' to mark each step done/failed/skipped as you progress, "
"'view' to check the current state."
)
parameters = {
"type": "object",
"properties": {
"op": {
"type": "string",
"enum": ["set", "view", "update", "clear"],
"description": (
"set — create/replace plan with a list of tasks; "
"view — show current plan; "
"update — change status of one task; "
"clear — reset plan"
),
},
"tasks": {
"type": "array",
"items": {"type": "string"},
"description": "Ordered list of task descriptions (required for 'set').",
},
"index": {
"type": "integer",
"description": "1-based task index (required for 'update').",
},
"status": {
"type": "string",
"enum": ["pending", "in_progress", "done", "failed", "skipped"],
"description": "New status for the task (required for 'update').",
},
},
"required": ["op"],
}
async def execute(self, params: dict) -> ToolResult:
sid = current_session_id.get() or "__default__"
op = params.get("op")
if op == "set":
raw = params.get("tasks") or []
if not raw:
return ToolResult(success=False, output="", error="'tasks' list is required for 'set'")
_plans[sid] = [_Task(text=str(t)) for t in raw]
return ToolResult(success=True, output=self._render(sid))
if op == "view":
if sid not in _plans or not _plans[sid]:
return ToolResult(success=True, output="No plan set for this session.")
return ToolResult(success=True, output=self._render(sid))
if op == "update":
tasks = _plans.get(sid)
if not tasks:
return ToolResult(success=False, output="", error="No plan set. Use 'set' first.")
idx = params.get("index")
status = params.get("status")
if not idx or not status:
return ToolResult(success=False, output="", error="'index' and 'status' are required for 'update'")
if idx < 1 or idx > len(tasks):
return ToolResult(success=False, output="", error=f"index {idx} is out of range (plan has {len(tasks)} tasks)")
tasks[idx - 1].status = status
return ToolResult(success=True, output=self._render(sid))
if op == "clear":
_plans.pop(sid, None)
return ToolResult(success=True, output="Plan cleared.")
return ToolResult(success=False, output="", error=f"Unknown op: {op!r}")
def _render(self, sid: str) -> str:
tasks = _plans.get(sid, [])
if not tasks:
return "Plan is empty."
n = len(tasks)
done = sum(1 for t in tasks if t.status == "done")
lines = [f"Plan — {done}/{n} done:"]
for i, t in enumerate(tasks, 1):
icon = _STATUS_ICON.get(t.status, "?")
suffix = f" ({t.status})" if t.status not in ("pending", "done") else ""
lines.append(f" {icon} {i}. {t.text}{suffix}")
return "\n".join(lines)