Files
hermes-agent/tools/todo_tool.py
Teknium d0ffb111c2 refactor: codebase-wide lint cleanup — unused imports, dead code, and inefficient patterns (#5821)
Comprehensive cleanup across 80 files based on automated (ruff, pyflakes, vulture)
and manual analysis of the entire codebase.

Changes by category:

Unused imports removed (~95 across 55 files):
- Removed genuinely unused imports from all major subsystems
- agent/, hermes_cli/, tools/, gateway/, plugins/, cron/
- Includes imports in try/except blocks that were truly unused
  (vs availability checks which were left alone)

Unused variables removed (~25):
- Removed dead variables: connected, inner, channels, last_exc,
  source, new_server_names, verify, pconfig, default_terminal,
  result, pending_handled, temperature, loop
- Dropped unused argparse subparser assignments in hermes_cli/main.py
  (12 instances of add_parser() where result was never used)

Dead code removed:
- run_agent.py: Removed dead ternary (None if False else None) and
  surrounding unreachable branch in identity fallback
- run_agent.py: Removed write-only attribute _last_reported_tool
- hermes_cli/providers.py: Removed dead @property decorator on
  module-level function (decorator has no effect outside a class)
- gateway/run.py: Removed unused MCP config load before reconnect
- gateway/platforms/slack.py: Removed dead SessionSource construction

Undefined name bugs fixed (would cause NameError at runtime):
- batch_runner.py: Added missing logger = logging.getLogger(__name__)
- tools/environments/daytona.py: Added missing Dict and Path imports

Unnecessary global statements removed (14):
- tools/terminal_tool.py: 5 functions declared global for dicts
  they only mutated via .pop()/[key]=value (no rebinding)
- tools/browser_tool.py: cleanup thread loop only reads flag
- tools/rl_training_tool.py: 4 functions only do dict mutations
- tools/mcp_oauth.py: only reads the global
- hermes_time.py: only reads cached values

Inefficient patterns fixed:
- startswith/endswith tuple form: 15 instances of
  x.startswith('a') or x.startswith('b') consolidated to
  x.startswith(('a', 'b'))
- len(x)==0 / len(x)>0: 13 instances replaced with pythonic
  truthiness checks (not x / bool(x))
- in dict.keys(): 5 instances simplified to in dict
- Redefined unused name: removed duplicate _strip_mdv2 import in
  send_message_tool.py

Other fixes:
- hermes_cli/doctor.py: Replaced undefined logger.debug() with pass
- hermes_cli/config.py: Consolidated chained .endswith() calls

Test results: 3934 passed, 17 failed (all pre-existing on main),
19 skipped. Zero regressions.
2026-04-07 10:25:31 -07:00

269 lines
9.4 KiB
Python

#!/usr/bin/env python3
"""
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 status values for todo items
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:
# Replace mode: new list entirely
self._items = [self._validate(t) for t in todos]
else:
# Merge mode: update existing items by id, append new ones
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 # Can't merge without an id
if item_id in existing:
# Update only the fields the LLM actually provided
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:
# New item -- validate fully and append to end
validated = self._validate(t)
existing[validated["id"]] = validated
self._items.append(validated)
# Rebuild _items preserving order for existing items
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 bool(self._items)
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
# Status markers for compact display
markers = {
"completed": "[x]",
"in_progress": "[>]",
"pending": "[ ]",
"cancelled": "[~]",
}
# Only inject pending/in_progress items — completed/cancelled ones
# cause the model to re-do finished work after compression.
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()
# Build summary counts
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
# =============================================================================
# OpenAI Function-Calling Schema
# =============================================================================
# Behavioral guidance is baked into the description so it's part of the
# static tool schema (cached, never changes mid-conversation).
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": []
}
}
# --- Registry ---
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="📋",
)