Source code for atloop.tools.interaction.todo_write
"""Todo write tool for managing task lists."""
import json
from typing import Any, Dict, List, Optional
from atloop.runtime.sandbox_adapter import SandboxAdapter
from atloop.tools.base import BaseTool, ToolResult
[docs]
class TodoWriteTool(BaseTool):
"""
Tool for creating and managing task lists in TODO.md format.
**Use cases:**
- Tracking progress on complex, multi-step tasks
- Managing task status (pending, in_progress, completed)
- Organizing work into manageable chunks
- Providing visibility into current work status
**Task statuses:**
- `pending`: Task not yet started
- `in_progress`: Task currently being worked on
- `completed`: Task finished
**Best practices:**
- At least one task should be in_progress at any time
- Use activeForm to describe what's happening (e.g., "Running tests")
- Update status as work progresses
"""
[docs]
def __init__(self, sandbox: SandboxAdapter):
"""
Initialize todo write tool.
Args:
sandbox: Sandbox adapter instance
"""
self.sandbox = sandbox
self.todo_file = "TODO.md"
@property
def name(self) -> str:
"""Tool name."""
return "todo_write"
@property
def description(self) -> str:
"""Tool description."""
return "创建和管理任务列表(TODO.md 格式)\n 参数: todos (array): 任务数组,每个任务包含:\n - content (string): 任务内容(必需,命令式,如 'Run tests')\n - activeForm (string): 进行时形式(必需,如 'Running tests')\n - status (string): 任务状态 - 'pending'(未开始)、'in_progress'(进行中)、'completed'(已完成)\n 示例: todo_write(todos=[{'content': 'Run tests', 'activeForm': 'Running tests', 'status': 'in_progress'}])\n 说明: 用于跟踪复杂任务的进度。至少应有一个任务处于 in_progress 状态。"
[docs]
def validate_args(self, args: Dict[str, Any]) -> tuple[bool, Optional[str]]:
"""Validate arguments."""
if "todos" not in args:
return False, "Missing required argument: 'todos'"
if not isinstance(args["todos"], list):
return False, "Argument 'todos' must be a list"
for i, todo in enumerate(args["todos"]):
if not isinstance(todo, dict):
return False, f"Todo[{i}] must be a dictionary"
if "content" not in todo:
return False, f"Todo[{i}] missing required field: 'content'"
if "activeForm" not in todo:
return False, f"Todo[{i}] missing required field: 'activeForm'"
if "status" not in todo:
return False, f"Todo[{i}] missing required field: 'status'"
if todo["status"] not in ["pending", "in_progress", "completed"]:
return (
False,
f"Todo[{i}] invalid status: must be 'pending', 'in_progress', or 'completed'",
)
return True, None
[docs]
def execute(self, args: Dict[str, Any]) -> ToolResult:
"""
Execute todo write tool to create or update TODO.md.
**Args:**
args: Tool arguments dictionary
- todos (list, required): List of todo items. Each item is a dictionary with:
- content (str, required): Task content in imperative form.
Example: "Run tests", "Fix bug", "Write documentation"
- activeForm (str, required): Present continuous form describing the action.
Example: "Running tests", "Fixing bug", "Writing documentation"
- status (str, required): Task status. Must be one of:
- "pending": Task not yet started
- "in_progress": Task currently being worked on
- "completed": Task finished
**Returns:**
ToolResult with:
- ok (bool): True if TODO.md was written successfully (no errors in stderr)
- stdout (str): Summary of updated TODO list with counts by status
- stderr (str): Error messages if any (empty string means success)
- meta (dict): Contains todo_file, todo_count, pending, in_progress, completed
**Examples:**
# Create initial TODO list
todo_write(todos=[
{
"content": "Set up project structure",
"activeForm": "Setting up project structure",
"status": "in_progress"
},
{
"content": "Write unit tests",
"activeForm": "Writing unit tests",
"status": "pending"
}
])
# Update TODO list (replaces entire list)
todo_write(todos=[
{
"content": "Set up project structure",
"activeForm": "Setting up project structure",
"status": "completed"
},
{
"content": "Write unit tests",
"activeForm": "Writing unit tests",
"status": "in_progress"
}
])
**Important Notes:**
- This tool **replaces** the entire TODO.md file with the new todos list
- To update a single task, read current todos, modify, then write back
- At least one task should be in_progress (warning shown if none)
- TODO.md is created in the workspace root directory
**File Format:**
The tool generates markdown format:
```markdown
# TODO
## In Progress
- [ ] Task 1 (Running task 1)
## Pending
- [ ] Task 2 (Will run task 2)
## Completed
- [x] Task 3
```
**Error Handling:**
- Success is determined by stderr content, not exit code
- If stderr is empty, the operation succeeded
- Check stderr for specific error messages if ok=False
"""
todos = args["todos"]
# Read existing todos if file exists
todo_file_path = self.todo_file
# Check if TODO.md exists
check_cmd = f"test -f {todo_file_path} && echo 'exists' || echo 'not_found'"
check_result = self._run_command(check_cmd, timeout_sec=5)
if "exists" in check_result.get("stdout", ""):
# Read existing todos
read_cmd = f"cat {todo_file_path} 2>/dev/null"
read_result = self._run_command(read_cmd, timeout_sec=5)
if not read_result.get("stderr", "").strip():
try:
# Try to parse as JSON first (for structured format)
content = read_result.get("stdout", "").strip()
if content.startswith("{"):
json.loads(content).get("todos", [])
else:
# Parse markdown format
self._parse_markdown_todos(content)
except (json.JSONDecodeError, ValueError):
# If parsing fails, start fresh
pass
# Merge with existing todos (simple merge strategy)
# For now, replace all todos with new ones
# In a more sophisticated implementation, we could merge by content/activeForm
# Generate TODO.md content
todo_content = self._generate_todo_markdown(todos)
# Write TODO.md
write_cmd = f"cat > {todo_file_path} <<'TODO_EOF'\n{todo_content}\nTODO_EOF"
write_result = self._run_command(write_cmd, timeout_sec=10)
# Check for write errors in stderr, not exit code
if write_result.get("stderr", "").strip():
return ToolResult(
ok=False,
stdout="",
stderr=f"Failed to write TODO.md: {write_result.get('stderr', 'Unknown error')}",
meta={"todo_file": todo_file_path},
)
# Generate summary
pending_count = sum(1 for t in todos if t["status"] == "pending")
in_progress_count = sum(1 for t in todos if t["status"] == "in_progress")
completed_count = sum(1 for t in todos if t["status"] == "completed")
summary = f"Updated TODO.md with {len(todos)} task(s):\n"
summary += f" - Pending: {pending_count}\n"
summary += f" - In Progress: {in_progress_count}\n"
summary += f" - Completed: {completed_count}\n"
if in_progress_count == 0 and len(todos) > 0:
summary += "\n⚠️ Warning: No tasks are in_progress. Consider marking at least one task as in_progress."
return ToolResult(
ok=True,
stdout=summary,
stderr="",
meta={
"todo_file": todo_file_path,
"todo_count": len(todos),
"pending": pending_count,
"in_progress": in_progress_count,
"completed": completed_count,
},
)
def _parse_markdown_todos(self, content: str) -> List[Dict[str, Any]]:
"""Parse markdown TODO format."""
todos = []
lines = content.split("\n")
current_todo = None
for line in lines:
line = line.strip()
if line.startswith("- [ ]") or line.startswith("- [x]"):
# New todo item
if current_todo:
todos.append(current_todo)
status = "completed" if line.startswith("- [x]") else "pending"
content_text = line[5:].strip()
current_todo = {
"content": content_text,
"activeForm": content_text, # Default to same as content
"status": status,
}
elif current_todo and line:
# Continuation of current todo
current_todo["content"] += " " + line
if current_todo:
todos.append(current_todo)
return todos
def _generate_todo_markdown(self, todos: List[Dict[str, Any]]) -> str:
"""Generate markdown TODO format."""
lines = ["# TODO\n", ""]
# Group by status
pending = [t for t in todos if t["status"] == "pending"]
in_progress = [t for t in todos if t["status"] == "in_progress"]
completed = [t for t in todos if t["status"] == "completed"]
if in_progress:
lines.append("## In Progress\n")
for todo in in_progress:
lines.append(f"- [ ] {todo['content']} ({todo['activeForm']})")
lines.append("")
if pending:
lines.append("## Pending\n")
for todo in pending:
lines.append(f"- [ ] {todo['content']} ({todo['activeForm']})")
lines.append("")
if completed:
lines.append("## Completed\n")
for todo in completed:
lines.append(f"- [x] {todo['content']}")
lines.append("")
return "\n".join(lines)
def _run_command(self, cmd: str, timeout_sec: int = 600) -> Dict[str, Any]:
"""Run a shell command in sandbox."""
return self.sandbox.exec_shell(
command=cmd,
workdir="/workspace",
timeout_seconds=timeout_sec,
)