analysis_claude_code/docs/en/s07-task-system.md
CrazyBoyM c6a27ef1d7 feat: build an AI agent from 0 to 1 -- 11 progressive sessions
- 11 sessions from basic agent loop to autonomous teams
- Python MVP implementations for each session
- Mental-model-first docs in en/zh/ja
- Interactive web platform with step-through visualizations
- Incremental architecture: each session adds one mechanism
2026-02-21 17:02:43 +08:00

6.0 KiB

s07: Tasks

Tasks persist as JSON files on the filesystem with a dependency graph, so they survive context compression and can be shared across agents.

The Problem

In-memory state like TodoManager (s03) is lost when the context is compressed (s06). After auto_compact replaces messages with a summary, the todo list is gone. The agent has to reconstruct it from the summary text, which is lossy and error-prone.

This is the critical s06-to-s07 bridge: TodoManager items die with compression; file-based tasks don't. Moving state to the filesystem makes it compression-proof.

More fundamentally, in-memory state is invisible to other agents. When we eventually build teams (s09+), teammates need a shared task board. In-memory data structures are process-local.

The solution is to persist tasks as JSON files in .tasks/. Each task is a separate file with an ID, subject, status, and dependency graph. Completing task 1 automatically unblocks task 2 if task 2 has blockedBy: [1]. The file system becomes the source of truth.

The Solution

.tasks/
  task_1.json  {"id":1, "status":"completed", ...}
  task_2.json  {"id":2, "blockedBy":[1], "status":"pending"}
  task_3.json  {"id":3, "blockedBy":[2], "status":"pending"}

Dependency resolution:
+----------+     +----------+     +----------+
| task 1   | --> | task 2   | --> | task 3   |
| complete |     | blocked  |     | blocked  |
+----------+     +----------+     +----------+
     |                ^
     +--- completing task 1 removes it from
          task 2's blockedBy list

How It Works

  1. The TaskManager provides CRUD operations. Each task is a JSON file.
class TaskManager:
    def create(self, subject: str, description: str = "") -> str:
        task = {
            "id": self._next_id,
            "subject": subject,
            "description": description,
            "status": "pending",
            "blockedBy": [],
            "blocks": [],
            "owner": "",
        }
        self._save(task)
        self._next_id += 1
        return json.dumps(task, indent=2)
  1. When a task is marked completed, _clear_dependency removes its ID from all other tasks' blockedBy lists.
def _clear_dependency(self, completed_id: int):
    for f in self.dir.glob("task_*.json"):
        task = json.loads(f.read_text())
        if completed_id in task.get("blockedBy", []):
            task["blockedBy"].remove(completed_id)
            self._save(task)
  1. The update method handles status changes and bidirectional dependency wiring.
def update(self, task_id, status=None,
           add_blocked_by=None, add_blocks=None):
    task = self._load(task_id)
    if status:
        task["status"] = status
        if status == "completed":
            self._clear_dependency(task_id)
    if add_blocks:
        task["blocks"] = list(set(task["blocks"] + add_blocks))
        for blocked_id in add_blocks:
            blocked = self._load(blocked_id)
            if task_id not in blocked["blockedBy"]:
                blocked["blockedBy"].append(task_id)
                self._save(blocked)
    self._save(task)
  1. Four task tools are added to the dispatch map.
TOOL_HANDLERS = {
    # ...base tools...
    "task_create": lambda **kw: TASKS.create(kw["subject"]),
    "task_update": lambda **kw: TASKS.update(kw["task_id"],
                       kw.get("status")),
    "task_list":   lambda **kw: TASKS.list_all(),
    "task_get":    lambda **kw: TASKS.get(kw["task_id"]),
}

Key Code

The TaskManager with dependency graph (from agents/s07_task_system.py, lines 46-123):

class TaskManager:
    def __init__(self, tasks_dir: Path):
        self.dir = tasks_dir
        self.dir.mkdir(exist_ok=True)
        self._next_id = self._max_id() + 1

    def _load(self, task_id: int) -> dict:
        path = self.dir / f"task_{task_id}.json"
        return json.loads(path.read_text())

    def _save(self, task: dict):
        path = self.dir / f"task_{task['id']}.json"
        path.write_text(json.dumps(task, indent=2))

    def create(self, subject, description=""):
        task = {"id": self._next_id, "subject": subject,
                "status": "pending", "blockedBy": [],
                "blocks": [], "owner": ""}
        self._save(task)
        self._next_id += 1
        return json.dumps(task, indent=2)

    def _clear_dependency(self, completed_id):
        for f in self.dir.glob("task_*.json"):
            task = json.loads(f.read_text())
            if completed_id in task.get("blockedBy", []):
                task["blockedBy"].remove(completed_id)
                self._save(task)

What Changed From s06

Component Before (s06) After (s07)
Tools 5 8 (+task_create/update/list/get)
State storage In-memory only JSON files in .tasks/
Dependencies None blockedBy + blocks graph
Compression Three-layer Removed (different focus)
Persistence Lost on compact Survives compression

Design Rationale

File-based state survives context compression. When the agent's conversation is compacted, in-memory state is lost, but tasks written to disk persist. The dependency graph ensures correct execution order even after context loss. This is the bridge between ephemeral conversation and persistent work -- the agent can forget conversation details but always has the task board to remind it what needs doing. The filesystem as source of truth also enables future multi-agent sharing, since any process can read the same JSON files.

Try It

cd learn-claude-code
python agents/s07_task_system.py

Example prompts to try:

  1. Create 3 tasks: "Setup project", "Write code", "Write tests". Make them depend on each other in order.
  2. List all tasks and show the dependency graph
  3. Complete task 1 and then list tasks to see task 2 unblocked
  4. Create a task board for refactoring: parse -> transform -> emit -> test