| |
| """ |
| Todo Tool Module - Planning & Task Management |
| |
| Provides an in-memory task list the agent uses to decompose complex tasks, |
| track progress, and maintain focus across long conversations. The state |
| lives on the AIAgent instance (one per session) and is re-injected into |
| the conversation after context compression events. |
| |
| Design: |
| - Single `todo` tool: provide `todos` param to write, omit to read |
| - Every call returns the full current list |
| - No system prompt mutation, no tool response modification |
| - Behavioral guidance lives entirely in the tool schema description |
| """ |
|
|
| import json |
| from typing import Dict, Any, List, Optional |
|
|
|
|
| |
| VALID_STATUSES = {"pending", "in_progress", "completed", "cancelled"} |
|
|
|
|
| class TodoStore: |
| """ |
| In-memory todo list. One instance per AIAgent (one per session). |
| |
| Items are ordered -- list position is priority. Each item has: |
| - id: unique string identifier (agent-chosen) |
| - content: task description |
| - status: pending | in_progress | completed | cancelled |
| """ |
|
|
| def __init__(self): |
| self._items: List[Dict[str, str]] = [] |
|
|
| def write(self, todos: List[Dict[str, Any]], merge: bool = False) -> List[Dict[str, str]]: |
| """ |
| Write todos. Returns the full current list after writing. |
| |
| Args: |
| todos: list of {id, content, status} dicts |
| merge: if False, replace the entire list. If True, update |
| existing items by id and append new ones. |
| """ |
| if not merge: |
| |
| self._items = [self._validate(t) for t in todos] |
| else: |
| |
| existing = {item["id"]: item for item in self._items} |
| for t in todos: |
| item_id = str(t.get("id", "")).strip() |
| if not item_id: |
| continue |
|
|
| if item_id in existing: |
| |
| if "content" in t and t["content"]: |
| existing[item_id]["content"] = str(t["content"]).strip() |
| if "status" in t and t["status"]: |
| status = str(t["status"]).strip().lower() |
| if status in VALID_STATUSES: |
| existing[item_id]["status"] = status |
| else: |
| |
| validated = self._validate(t) |
| existing[validated["id"]] = validated |
| self._items.append(validated) |
| |
| seen = set() |
| rebuilt = [] |
| for item in self._items: |
| current = existing.get(item["id"], item) |
| if current["id"] not in seen: |
| rebuilt.append(current) |
| seen.add(current["id"]) |
| self._items = rebuilt |
| return self.read() |
|
|
| def read(self) -> List[Dict[str, str]]: |
| """Return a copy of the current list.""" |
| return [item.copy() for item in self._items] |
|
|
| def has_items(self) -> bool: |
| """Check if there are any items in the list.""" |
| return len(self._items) > 0 |
|
|
| def format_for_injection(self) -> Optional[str]: |
| """ |
| Render the todo list for post-compression injection. |
| |
| Returns a human-readable string to append to the compressed |
| message history, or None if the list is empty. |
| """ |
| if not self._items: |
| return None |
|
|
| |
| markers = { |
| "completed": "[x]", |
| "in_progress": "[>]", |
| "pending": "[ ]", |
| "cancelled": "[~]", |
| } |
|
|
| |
| |
| active_items = [ |
| item for item in self._items |
| if item["status"] in ("pending", "in_progress") |
| ] |
| if not active_items: |
| return None |
|
|
| lines = ["[Your active task list was preserved across context compression]"] |
| for item in active_items: |
| marker = markers.get(item["status"], "[?]") |
| lines.append(f"- {marker} {item['id']}. {item['content']} ({item['status']})") |
|
|
| return "\n".join(lines) |
|
|
| @staticmethod |
| def _validate(item: Dict[str, Any]) -> Dict[str, str]: |
| """ |
| Validate and normalize a todo item. |
| |
| Ensures required fields exist and status is valid. |
| Returns a clean dict with only {id, content, status}. |
| """ |
| item_id = str(item.get("id", "")).strip() |
| if not item_id: |
| item_id = "?" |
|
|
| content = str(item.get("content", "")).strip() |
| if not content: |
| content = "(no description)" |
|
|
| status = str(item.get("status", "pending")).strip().lower() |
| if status not in VALID_STATUSES: |
| status = "pending" |
|
|
| return {"id": item_id, "content": content, "status": status} |
|
|
|
|
| def todo_tool( |
| todos: Optional[List[Dict[str, Any]]] = None, |
| merge: bool = False, |
| store: Optional[TodoStore] = None, |
| ) -> str: |
| """ |
| Single entry point for the todo tool. Reads or writes depending on params. |
| |
| Args: |
| todos: if provided, write these items. If None, read current list. |
| merge: if True, update by id. If False (default), replace entire list. |
| store: the TodoStore instance from the AIAgent. |
| |
| Returns: |
| JSON string with the full current list and summary metadata. |
| """ |
| if store is None: |
| return json.dumps({"error": "TodoStore not initialized"}, ensure_ascii=False) |
|
|
| if todos is not None: |
| items = store.write(todos, merge) |
| else: |
| items = store.read() |
|
|
| |
| pending = sum(1 for i in items if i["status"] == "pending") |
| in_progress = sum(1 for i in items if i["status"] == "in_progress") |
| completed = sum(1 for i in items if i["status"] == "completed") |
| cancelled = sum(1 for i in items if i["status"] == "cancelled") |
|
|
| return json.dumps({ |
| "todos": items, |
| "summary": { |
| "total": len(items), |
| "pending": pending, |
| "in_progress": in_progress, |
| "completed": completed, |
| "cancelled": cancelled, |
| }, |
| }, ensure_ascii=False) |
|
|
|
|
| def check_todo_requirements() -> bool: |
| """Todo tool has no external requirements -- always available.""" |
| return True |
|
|
|
|
| |
| |
| |
| |
| |
|
|
| TODO_SCHEMA = { |
| "name": "todo", |
| "description": ( |
| "Manage your task list for the current session. Use for complex tasks " |
| "with 3+ steps or when the user provides multiple tasks. " |
| "Call with no parameters to read the current list.\n\n" |
| "Writing:\n" |
| "- Provide 'todos' array to create/update items\n" |
| "- merge=false (default): replace the entire list with a fresh plan\n" |
| "- merge=true: update existing items by id, add any new ones\n\n" |
| "Each item: {id: string, content: string, " |
| "status: pending|in_progress|completed|cancelled}\n" |
| "List order is priority. Only ONE item in_progress at a time.\n" |
| "Mark items completed immediately when done. If something fails, " |
| "cancel it and add a revised item.\n\n" |
| "Always returns the full current list." |
| ), |
| "parameters": { |
| "type": "object", |
| "properties": { |
| "todos": { |
| "type": "array", |
| "description": "Task items to write. Omit to read current list.", |
| "items": { |
| "type": "object", |
| "properties": { |
| "id": { |
| "type": "string", |
| "description": "Unique item identifier" |
| }, |
| "content": { |
| "type": "string", |
| "description": "Task description" |
| }, |
| "status": { |
| "type": "string", |
| "enum": ["pending", "in_progress", "completed", "cancelled"], |
| "description": "Current status" |
| } |
| }, |
| "required": ["id", "content", "status"] |
| } |
| }, |
| "merge": { |
| "type": "boolean", |
| "description": ( |
| "true: update existing items by id, add new ones. " |
| "false (default): replace the entire list." |
| ), |
| "default": False |
| } |
| }, |
| "required": [] |
| } |
| } |
|
|
|
|
| |
| from tools.registry import registry |
|
|
| registry.register( |
| name="todo", |
| toolset="todo", |
| schema=TODO_SCHEMA, |
| handler=lambda args, **kw: todo_tool( |
| todos=args.get("todos"), merge=args.get("merge", False), store=kw.get("store")), |
| check_fn=check_todo_requirements, |
| emoji="📋", |
| ) |
|
|