- 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
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
- 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)
- When a task is marked completed,
_clear_dependencyremoves its ID from all other tasks'blockedBylists.
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)
- The
updatemethod 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)
- 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:
Create 3 tasks: "Setup project", "Write code", "Write tests". Make them depend on each other in order.List all tasks and show the dependency graphComplete task 1 and then list tasks to see task 2 unblockedCreate a task board for refactoring: parse -> transform -> emit -> test