Learn Claude Code · Session 12

TASK SYSTEM

文件持久化任务图 + blockedBy 依赖关系 — 相比 s11 的新增内容解析
§0 整体功能 · Overview

解决的问题

s11 的 agent 是无状态的:每次对话结束后,完成了哪些工作、还有哪些待做,全部丢失。

s12 引入了 文件持久化的任务图:每个任务存为 .tasks/{id}.json,支持 blockedBy 依赖链,agent 可以创建、认领、完成任务,跨会话保持进度。

任务状态机

初始
pending
等待被认领
claim_task()
in_progress
已设置 owner
complete_task()
completed
解锁下游任务

关键新增 vs s11

  • from dataclasses import dataclass, asdict
  • TASKS_DIR = .tasks/ 持久化目录
  • @dataclass Task — 6 字段数据类
  • create_task / save_task / load_task / list_tasks / get_task — CRUD 层
  • can_start() — 依赖检查
  • claim_task() / complete_task() — 状态转移
  • 5 个新 tool runner 函数 + 8 个 TOOLS 定义
  • agent_loop 简化(移除了 s11 的完整错误恢复,聚焦任务系统)

§1 Task 数据类 — 结构定义

@dataclass Task

代码行语法注解
from dataclasses import dataclass, asdict dataclass:装饰器,自动生成 __init____repr____eq__asdict:将实例转换为 dict(用于 JSON 序列化)
@dataclass 装饰器语法;等价于在类定义后执行 Task = dataclass(Task);不需要手写 __init__
class Task: 普通类定义;装饰器负责生成样板代码
id: str 字段注解(PEP 526);dataclass 用注解顺序生成 __init__ 参数;类型为 str
subject: str 任务标题(简短描述);用于 list_tasks() 的显示
description: str 任务详细描述;可为空字符串
status: str # pending | in_progress | completed 行内注释说明允许的值;Python 未强制 Literal 类型,这里用字符串约定
owner: str | None # Agent name (multi-agent scenarios) str | None:联合类型(Python 3.10+);认领前为 None,认领后设为 agent 名
blockedBy: list[str] # Dependency task IDs list[str]:泛型类型注解(Python 3.9+);存储前置任务 ID 列表;空列表 = 无依赖

dataclass vs 手写类

手写 __init__ 相当于:

def __init__(self, id, subject, description, status, owner, blockedBy): self.id = id self.subject = subject ...

@dataclass 自动生成上述代码,减少重复。

asdict() 用于 JSON 序列化 task = Task( id="task_1234_0001", subject="Write tests", description="Unit tests for parser", status="pending", owner=None, blockedBy=[] ) import json from dataclasses import asdict json.dumps(asdict(task), indent=2) # → { # "id": "task_1234_0001", # "subject": "Write tests", # "description": "Unit tests for parser", # "status": "pending", # "owner": null, # "blockedBy": [] # }
Task(**json.loads(...)) 反序列化 data = '{"id":"task_1234","subject":"...",...}' task = Task(**json.loads(data)) # **dict 解包:将 dict 键值作为关键字参数传入 __init__

§2 TASKS_DIR — 持久化存储目录
代码行语法注解
TASKS_DIR = WORKDIR / ".tasks" Path / str:pathlib 运算符重载,拼接路径;等价于 Path(WORKDIR, ".tasks")
TASKS_DIR.mkdir(exist_ok=True) 模块级副作用:导入时立即创建目录;exist_ok=True = 目录已存在不报错;注意无 parents=True(WORKDIR 必须已存在)
def _task_path(task_id: str) -> Path: 私有辅助函数(_ 前缀约定);集中管理文件路径格式,便于统一修改
return TASKS_DIR / f"{task_id}.json" f-string 内嵌变量构造文件名;如 .tasks/task_1234_0001.json

文件布局示意

.tasks/ 目录结构 .tasks/ ├── task_1718700000_0001.json # pending ├── task_1718700001_0023.json # in_progress └── task_1718700002_0456.json # completed
为何用文件而非内存 dict?
  • 跨会话持久化:关闭进程后任务不丢失
  • 多 agent 协作:不同进程/子代理通过文件系统共享任务状态
  • 可检查性:文件是人类可读的 JSON,便于调试
生产 Claude Code 使用数据库(tasks.db)或内存 + 快照,此处用文件是教学简化。

§3 CRUD 函数 — create / save / load / list / get

create_task

代码行语法注解
def create_task(subject: str, description: str = "", 默认参数 description="":调用时可省略;Python 默认参数在函数定义时求值
blockedBy: list[str] | None = None) -> Task: blockedBy=None(非 []):避免 Python 可变默认参数陷阱(多次调用共享同一列表)
task = Task( dataclass __init__ 调用;所有字段必须提供(无默认值的字段)
id=f"task_{int(time.time())}_{random.randint(0, 9999):04d}", int(time.time()):Unix 时间戳(秒);random.randint(0,9999):4 位随机数;:04d:格式化为至少 4 位,不足补零;组合避免 ID 碰撞
status="pending", 所有新任务初始状态为 "pending";由状态机控制后续转移
owner=None, 未认领;claim_task() 调用后赋值
blockedBy=blockedBy or [], or []:若 blockedBy 为 None(或其他 falsy 值),使用新建空列表;正确处理可变默认参数
) Task 实例构造完毕
save_task(task) 立即持久化到文件;create 操作原子性(Python 进程内)
return task 返回 Task 实例,让调用方获取 id

save_task / load_task

代码行语法注解
def save_task(task: Task): 无返回值(隐式 return None);副作用:写文件
_task_path(task.id).write_text(json.dumps(asdict(task), indent=2)) asdict(task):dataclass → dict;json.dumps(..., indent=2):格式化 JSON;Path.write_text():原子写入(在大多数 OS 上)
def load_task(task_id: str) -> Task: 返回 Task 实例;若文件不存在抛 FileNotFoundError(调用方处理)
return Task(**json.loads(_task_path(task_id).read_text())) 链式调用:read_text()json.loads()**dict 解包 → Task() 构造;一行完成反序列化

list_tasks

代码行语法注解
def list_tasks() -> list[Task]: 返回所有任务列表,按文件名排序(时间戳前缀保证创建时间顺序)
return [Task(**json.loads(p.read_text())) 列表推导式(list comprehension):对每个文件 p 执行加载
for p in sorted(TASKS_DIR.glob("task_*.json"))] Path.glob("task_*.json"):仅匹配 task_ 前缀的 JSON;sorted():按 Path 的字典序排序(等价于时间序)

可变默认参数陷阱

❌ 错误写法(共享列表) def create_task(subject, blockedBy=[]): blockedBy.append("bug") # 修改了默认列表! # 下次调用 create_task("x") 时 # blockedBy 已经是 ["bug"] # ✅ 正确写法 def create_task(subject, blockedBy=None): blockedBy = blockedBy or [] # 每次调用都创建新列表

ID 生成示例

create_task 调用时序 time.time() = 1718700000.42 int(...) = 1718700000 random.randint(0, 9999) = 23 f"{23:04d}" = "0023" id = "task_1718700000_0023" # 文件: .tasks/task_1718700000_0023.json

list_tasks 完整示例

输入 → 输出 # .tasks/ 目录中有 3 个文件 tasks = list_tasks() # → [ # Task(id="task_1718700000_0001", subject="设计 API", # status="completed", blockedBy=[], ...), # Task(id="task_1718700001_0023", subject="写测试", # status="in_progress", blockedBy=["task_..._0001"], ...), # Task(id="task_1718700002_0456", subject="部署", # status="pending", blockedBy=["task_..._0023"], ...) # ]

§4 can_start(task_id) — 依赖检查

逐行注解

代码行语法注解
def can_start(task_id: str) -> bool: 返回布尔值;用于 claim_task() 调用前的守卫检查
"""Check if all blockedBy dependencies are completed. docstring 第一行:单句总结
Missing dependencies are treated as blocked.""" 关键设计决策文档:依赖 ID 不存在时视为阻塞(保守策略)
task = load_task(task_id) 从文件加载最新状态(不使用缓存)
for dep_id in task.blockedBy: 遍历依赖 ID 列表;若列表为空,循环不执行,直接 return True
if not _task_path(dep_id).exists(): 先检查文件存在性:依赖文件不存在 = 依赖 ID 无效 = 视为阻塞
return False 早返回(fail-fast):发现任一阻塞即返回 False,无需检查后续依赖
if load_task(dep_id).status != "completed": 加载依赖任务检查状态;"pending" 或 "in_progress" 均视为阻塞
return False 依赖未完成 → 当前任务不可开始
return True 所有依赖均已完成(或无依赖)→ 可以开始

依赖图示例

任务依赖链 Task A (completed) ↓ Task B (in_progress, blockedBy=[A]) ↓ Task C (pending, blockedBy=[B]) ↓ Task D (pending, blockedBy=[B, C]) can_start("C") → False # B 未 completed can_start("B") → True # A 已 completed can_start("A") → True # 无依赖 can_start("D") → False # B、C 均未完成
为何缺失依赖视为阻塞?
保守策略:如果依赖 ID 被误写或依赖文件被删除,宁可阻塞而非贸然开始,防止在不完整前提下执行后续任务,产生难以追溯的错误。

§5 claim_task(task_id, owner) — 状态转移 pending → in_progress

逐行注解

代码行语法注解
def claim_task(task_id: str, owner: str = "agent") -> str: 返回字符串(成功消息或错误描述);owner 默认 "agent",多代理时传入唯一名称
task = load_task(task_id) 从文件读取最新状态(防止竞态条件:另一代理已认领)
if task.status != "pending": 状态守卫:只有 pending 状态可被认领;已 in_progress 或 completed 的任务不可重复认领
return f"Task {task_id} is {task.status}, cannot claim" 返回描述性错误字符串(不抛异常);调用方(tool runner)将此作为工具结果返回给 LLM
if not can_start(task_id): 依赖守卫:调用 can_start() 检查所有 blockedBy
deps = [d for d in task.blockedBy 列表推导式:筛选出尚未完成的依赖 ID
if not _task_path(d).exists() or load_task(d).status != "completed"] 条件过滤:文件不存在或状态非 completed
return f"Blocked by: {deps}" 告知 LLM 被哪些任务阻塞,LLM 可据此决定先完成前置任务
task.owner = owner 直接修改 dataclass 实例的属性(dataclass 默认可变)
task.status = "in_progress" 状态转移:pending → in_progress
save_task(task) 持久化修改后的状态到文件
print(f" \033[36m[claim] {task.subject} → in_progress (owner: {owner})\033[0m") \033[36m = 青色;调试输出,不影响返回值
return f"Claimed {task.id} ({task.subject})" 成功消息;LLM 收到后知道任务已认领,可开始执行
完整调用示例 # 场景: 任务 B blockedBy=[A], A 已 completed claim_task("task_B_id") # → load_task → status=pending ✓ # → can_start → A.status="completed" ✓ # → task.owner = "agent", task.status = "in_progress" # → save_task(task) # → "Claimed task_B_id (Write tests)" # 场景: 任务 C blockedBy=[B], B 仍 in_progress claim_task("task_C_id") # → load_task → status=pending ✓ # → can_start → B.status="in_progress" ✗ # → deps = ["task_B_id"] # → "Blocked by: ['task_B_id']"
设计:返回字符串而非抛异常
工具函数的输出直接传回给 LLM 作为 tool_result。返回描述性字符串让 LLM 理解发生了什么,并做出下一步决策(如先完成前置任务)。异常会被 agent_loop 捕获并转为通用错误消息,信息量更少。

§6 complete_task(task_id) — 状态转移 in_progress → completed + 解锁下游

逐行注解

代码行语法注解
def complete_task(task_id: str) -> str: 返回字符串;包含完成消息和解锁的下游任务列表
task = load_task(task_id) 从文件加载最新状态
if task.status != "in_progress": 只有 in_progress 的任务可完成;防止直接从 pending 跳到 completed
return f"Task {task_id} is {task.status}, cannot complete" 错误消息;LLM 需先 claim_task 再 complete_task
task.status = "completed" 状态转移:in_progress → completed
save_task(task) 先保存完成状态,再查询下游(确保下游查询到的是最新状态)
unblocked = [t.subject for t in list_tasks() 列表推导:遍历所有任务,筛选出刚被解锁的任务
if t.status == "pending" and t.blockedBy and can_start(t.id)] 条件:仍是 pending(未被认领) 有依赖(t.blockedBy 非空) 现在可以开始
print(f" \033[32m[complete] {task.subject} ✓\033[0m") \033[32m = 绿色;✓ Unicode 对勾
msg = f"Completed {task.id} ({task.subject})" 基础完成消息
if unblocked: 有任务被解锁时追加信息;LLM 可据此主动认领下游任务
msg += f"\nUnblocked: {', '.join(unblocked)}" +=:字符串原地拼接(字符串不可变,实际创建新字符串)
print(f" \033[33m[unblocked] {', '.join(unblocked)}\033[0m") \033[33m = 黄色;提示有任务解锁
return msg 返回完整消息字符串(包含解锁信息)
complete_task 完整场景 # 任务图: A(completed) → B(in_progress) → C(pending) # → D(pending) complete_task("task_B_id") # 1. load: B.status="in_progress" ✓ # 2. B.status = "completed" # 3. save_task(B) # 4. list_tasks() = [A, B, C, D] # C: status=pending, blockedBy=[B], can_start? → B.completed ✓ → True # D: status=pending, blockedBy=[B], can_start? → B.completed ✓ → True # 5. unblocked = ["Write tests", "Deploy"] # 返回: # "Completed task_B_id (Code review) # Unblocked: Write tests, Deploy"
为何先 save 再查 unblocked?
save_task(task) 必须在 list_tasks() 之前执行,因为 can_start() 通过读文件检查依赖状态。若先查后保存,当前任务仍显示为 in_progress,下游任务不会被标记为可启动。

§7 Tool Runner 函数 — 工具包装层

5 个新 runner 函数

代码行语法注解
def run_create_task(subject: str, description: str = "", 包装 create_task();参数签名与工具定义中的 input_schema 对应
blockedBy: list[str] | None = None) -> str: 返回字符串(tool result);LLM 通过字符串了解操作结果
task = create_task(subject, description, blockedBy) 调用业务逻辑层;分离 tool 协议层和业务层
deps = f" (blockedBy: {', '.join(blockedBy)})" if blockedBy else "" 三元条件表达式:value_if_true if condition else value_if_false
return f"Created {task.id}: {task.subject}{deps}" 返回包含新任务 ID 的字符串;LLM 后续操作需要此 ID
def run_list_tasks() -> str: 无参数;对应工具定义 input_schema: {properties: {}}
icon = {"pending": "○", "in_progress": "●", 字典查找替代 if-elif 链;Unicode 圆圈表示状态
"completed": "✓"}.get(t.status, "?") .get(key, default):未知状态显示 "?"(防御性编程)
deps = f" (blockedBy: {', '.join(t.blockedBy)})" if t.blockedBy else "" 条件格式化:有依赖才显示 blockedBy 信息
owner = f" [{t.owner}]" if t.owner else "" 条件格式化:有 owner 才显示
def run_get_task(task_id: str) -> str: 包装 get_task();增加 try/except 处理文件不存在
except FileNotFoundError: 捕获特定异常类型;比 except Exception 更精确
def run_claim_task(task_id: str) -> str: 包装 claim_task(),固定 owner="agent";工具层不暴露 owner 参数
def run_complete_task(task_id: str) -> str: 最薄包装层;直接转发给业务逻辑

run_list_tasks 输出示例

终端输出格式 run_list_tasks() # → " ○ task_1718700000_0001: 设计 API [pending] ● task_1718700001_0023: 写测试 [in_progress] [agent] (blockedBy: task_1718700000_0001) ✓ task_1718700002_0456: 部署 [completed] (blockedBy: task_1718700001_0023)"

为何要有 runner 层?

业务函数(claim_task)返回字符串,而 TOOL_HANDLERS 的 value 必须是接受 **block.input 的函数。

Runner 层将业务函数的签名适配为 agent_loop 期望的接口,同时可以在此添加:格式化输出、错误包装、参数验证、调试打印。


§8 工具定义 — 5 个新增工具的 JSON Schema

关键新工具注解

代码行语法注解
{"name": "create_task", 工具名 = TOOL_HANDLERS 字典的键 = LLM 在 tool_use 中引用的名称
"description": "Create a new task with optional blockedBy dependencies.", 描述供 LLM 理解工具功能;应说明可选参数行为
"input_schema": {"type": "object", JSON Schema 格式;Anthropic API 用此描述参数结构
"properties": { 参数字段定义
"subject": {"type": "string"}, 必填参数(在 required 列表中)
"description": {"type": "string"}, 可选参数(不在 required 中);LLM 可省略
"blockedBy": {"type": "array", JSON Schema 数组类型
"items": {"type": "string"}}}, items:数组元素类型约束;每个元素必须是字符串(task ID)
"required": ["subject"]}}, 只有 subject 必填;description 和 blockedBy 可选
{"name": "list_tasks", 无参数工具
"input_schema": {"type": "object", "properties": {}, properties: {}:空属性字典 = 无参数
"required": []}}, required: []:空必填列表;LLM 调用时不需传入任何参数

LLM 调用工具的完整数据流

create_task 工具调用链 # 1. LLM 返回 tool_use block: { "type": "tool_use", "id": "toolu_01abc", "name": "create_task", "input": { "subject": "写单元测试", "blockedBy": ["task_1718700000_0001"] } } # 2. agent_loop 执行: handler = TOOL_HANDLERS["create_task"] # → run_create_task output = handler(**block.input) # → handler(subject="写单元测试", blockedBy=["task_..."]) # → create_task(...) # → save_task(...) # → "Created task_1718700001_0023: 写单元测试 (blockedBy: task_1718700000_0001)" # 3. 追加 tool_result: {"type": "tool_result", "tool_use_id": "toolu_01abc", "content": "Created task_1718700001_0023: ..."}
s12 对 agent_loop 的简化
s12 移除了 s11 的完整错误恢复(RecoveryState、指数退避、Path 1/2/3),只保留基础的 try/except 追加错误消息。这是刻意的教学分层:任务系统与错误恢复是正交的独立层,真实 Claude Code 中它们自然组合,教学代码分开展示各自核心逻辑。