"""Session-scoped task plan manager."""
from __future__ import annotations
from dataclasses import dataclass, field
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"
validation: str = "" # how the result was verified (required for done, encouraged for failed)
# In-memory store: session_id → task list (ephemeral, lives with the server process)
_plans: dict[str, list[_Task]] = {}
class TodoTool(Tool):
name = "todo"
description = (
"Task plan tracker. Your todo list is automatically populated from the plan at the start of each task — "
"you do NOT need to call 'set'. "
"Indexes are 1-based. Call 'update' with status='in_progress' when you start a step. "
"Call 'update' immediately after completing or failing each step — before moving to the next. "
"When marking a step 'done', you MUST provide a 'validation' field describing how you verified the result. "
"When marking a step 'failed', provide 'validation' explaining what went wrong and what you tried. "
"Before final response, make sure every completed step, including the final step, is marked done with validation. "
"Call 'view' to re-orient yourself after sub-agent execution or long tool chains. "
"Use 'set' only when you need to replace the plan mid-task (rare). "
"Statuses: pending → in_progress → done / failed / skipped."
)
parameters = {
"type": "object",
"properties": {
"op": {
"type": "string",
"enum": ["set", "view", "update", "clear"],
"description": (
"set — create/replace the Master Plan with a list of task milestones; "
"view — show the current state of the plan; "
"update — change the status of a specific task; "
"clear — reset the 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').",
},
"validation": {
"type": "string",
"description": (
"Required when status='done': briefly describe how you verified the result "
"(e.g. 'ran test_tool — output matched expected', 'read the file and confirmed content'). "
"Encouraged when status='failed': describe what went wrong and what you tried."
),
},
},
"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")
validation = (params.get("validation") or "").strip()
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)")
if status == "done" and not validation:
return ToolResult(
success=False,
output=(
f"Cannot mark step {idx} as done without validation.\n"
"Provide a 'validation' field describing how you verified the result before marking it done.\n"
"Example: \"ran test_tool — all assertions passed\" or \"read the output file and confirmed expected content\".\n"
"If you haven't verified yet — verify first, then update."
),
error="validation_required",
)
if status == "failed" and not validation:
# Soft prompt — don't block, but encourage explanation
tasks[idx - 1].status = status
return ToolResult(
success=True,
output=(
self._render(sid) + "\n\n"
f"[Tip: next time add a 'validation' field when marking a step failed — "
"describe what went wrong and what you tried. "
"This helps with re-planning.]"
),
)
tasks[idx - 1].status = status
tasks[idx - 1].validation = validation
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 ""
validation_note = f" [verified: {t.validation}]" if t.validation else ""
lines.append(f" {icon} {i}. {t.text}{suffix}{validation_note}")
return "\n".join(lines)