learn-claude-code/s12_task_system/README.md
gui-yue 1baf1aca5a
Some checks are pending
CI / build (push) Waiting to run
Test / python-smoke (push) Waiting to run
Test / web-build (push) Waiting to run
Follow up PR #265: refine chapters, diagrams, and add S20 (#283)
* feat: s01-s14 docs quality overhaul — tool pipeline, single-agent, knowledge & resilience

Rewrite code.py and README (zh/en/ja) for s01-s14, each chapter building
incrementally on the previous. Key fixes across chapters:

- s01-s04: agent loop, tool dispatch, permission pipeline, hooks
- s05-s08: todo write, subagent, skill loading, context compact
- s09-s11: memory system, system prompt assembly, error recovery
- s12-s14: task graph, background tasks, cron scheduler

All chapters CC source-verified. Code inherits fixes forward (PROMPT_SECTIONS,
json.dumps cache, real-state context, can_start dep protection, etc.).

* feat: s15-s19 docs quality overhaul — multi-agent platform: teams, protocols, autonomy, worktree, MCP tools

Rewrite code.py and README (zh/en/ja) for s15-s19, the multi-agent platform
chapters. Each chapter inherits all previous fixes and adds one mechanism:

- s15: agent teams (TeamCreate, teammate threads, shared task list)
- s16: team protocols (plan approval, shutdown handshake, consume_inbox)
- s17: autonomous agents (idle polling, auto-claim, consume_lead_inbox)
- s18: worktree isolation (git worktree, bind_task, cwd switching, safety)
- s19: MCP tools (MCPClient, normalize_mcp_name, assemble_tool_pool, no cache)

All appendix source code references verified against CC source. Config priority
corrected: claude.ai < plugin < user < project < local.

* fix: 5 regressions across s05-s19 — glob safety, todo validation, memory extraction, protocol types, dep crash

- s05-s09: glob results now filter with is_relative_to(WORKDIR) (inherited from s02)
- s06-s08: todo_write validates content/status required fields (inherited from s05)
- s09: extract_memories uses pre-compression snapshot instead of compacted messages
- s16: submit_plan docstring clarifies protocol-only (not code-level gate)
- s17-s19: match_response restores type mismatch validation (from s16)
- s17-s19: claim_task deps list handles missing dep files without crashing

* fix: s12 Todo V2 logic reversal, s14/s15 cron range validation, s18/s19 worktree name validation

- s12 README (zh/en/ja): fix Todo V2 direction — interactive defaults to Task,
  non-interactive/SDK defaults to TodoWrite. Fix env var name to
  CLAUDE_CODE_ENABLE_TASKS (not TODO_V2).
- s14/s15: add _validate_cron_field with per-field range checks (minute 0-59,
  hour 0-23, dom 1-31, month 1-12, dow 0-6), step > 0, range lo <= hi.
  Replace old try/except validation that only caught exceptions.
- s18/s19: add validate_worktree_name() to remove_worktree and keep_worktree,
  not just create_worktree.

* fix: align s16-s19 teaching tool consistency

* fix pr265 chapter diagrams

* Add comprehensive s20 harness chapter

* Fix chapter smoke test regressions

* Clarify README tutorial track transition

---------

Co-authored-by: Haoran <bill-billion@outlook.com>
2026-05-20 21:45:38 +08:00

11 KiB
Raw Permalink Blame History

s12: Task System — 目标太大,拆成小任务

中文 · English · 日本語

s01 → ... → s10 → s11 → s12s13 → s14 → ... → s20

"大目标拆成小任务, 排好序, 持久化" — 文件持久化的任务图, 多 agent 协作的基础。

Harness 层: 任务 — 持久化的目标, 可恢复的进度。


问题

Agent 接到一个项目:搭数据库、写 API、加测试。它用 s05 的 TodoWrite 列了一张清单,然后开始写 API写到一半发现没数据库表回头补加测试时发现 API 接口签名又变了...

盖房子不能先盖屋顶再打地基。任务之间有先后。任务依赖应该形成有向无环图DAG教学版只演示 blockedBy 检查,没有实现环检测。

s05 的 TodoWrite 是一个列表。没有依赖关系、没有持久化、对话结束列表就没了。你需要的是任务系统:每个任务是一个 JSON 文件,任务之间有 blockedBy 依赖,跨会话持久化在磁盘上。


解决方案

Task System Overview

教学代码保留基础 agent loop为聚焦任务系统省略了 S11 的完整错误恢复RecoveryState、退避、升级、reactive compact、fallback model。新增 5 个任务工具 + .tasks/ 目录持久化 + blockedBy 依赖检查。任务系统与错误恢复是独立层CC 源码中 utils/tasks.ts 只管 CRUDquery.ts 的 with_retry/RecoveryState 管错误恢复,互不耦合。

TodoWrite vs Task System

TodoWrite (s05) Task System (s12)
存储 内存列表 .tasks/ JSON 文件
依赖 blockedBy 依赖图
持久性 对话结束即丢 跨会话
多 Agent owner 字段
状态 checked / unchecked pending → in_progress → completed

工作原理

Task DAG

Task: 数据结构

每个任务是一个 JSON 文件,存于 .tasks/ 目录:

@dataclass
class Task:
    id: str
    subject: str
    description: str
    status: str          # pending | in_progress | completed
    owner: str | None    # Agent 名(多 Agent 场景)
    blockedBy: list[str] # 依赖的任务 ID 列表

ID 用 timestamp + random hex 生成简单但够用。CC 用顺序 ID + highwatermark 文件防止 ID 重用,是更严谨的设计。

create_task: 创建任务

def create_task(subject: str, description: str = "",
                blockedBy: list[str] | None = None) -> Task:
    task = Task(
        id=f"task_{int(time.time())}_{random_hex(4)}",
        subject=subject, description=description,
        status="pending", owner=None,
        blockedBy=blockedBy or [],
    )
    save_task(task)
    return task

创建时自动 save_task.tasks/{id}.jsonblockedBy 声明依赖,比如 "写 API" 的 blockedBy["task_schema"]

can_start: 依赖检查

一个任务只能在它的 blockedBy 全部 completed 之后才能开始:

def can_start(task_id: str) -> bool:
    task = load_task(task_id)
    for dep_id in task.blockedBy:
        if not _task_path(dep_id).exists():
            return False  # missing dependency = blocked
        dep = load_task(dep_id)
        if dep.status != "completed":
            return False
    return True

can_startclaim_task 的前置检查:blockedBy 里有任何一个不是 completed就不能认领。不存在的依赖视为 blocked避免引用错误 ID 时崩溃。

claim_task: 认领任务

Agent 开始做一个任务时,调用 claim_task:设置 owner,状态从 pendingin_progressowner 字段记录谁在做这个任务,多 Agent 场景下防止重复认领:

def claim_task(task_id: str, owner: str = "agent") -> str:
    task = load_task(task_id)
    if task.status != "pending":
        return f"Task {task_id} is {task.status}, cannot claim"
    if not can_start(task_id):
        deps = [d for d in task.blockedBy
                if load_task(d).status != "completed"]
        return f"Blocked by: {deps}"
    task.owner = owner
    task.status = "in_progress"
    save_task(task)
    return f"Claimed {task_id} ({task.subject})"

如果任务已被别人认领(status != "pending"),或者依赖没完成(can_start 返回 False拒绝认领。

complete_task: 完成与解锁

任务做完后,设为 completed。同时扫描所有其他任务,找出刚刚被解锁的下游任务:

def complete_task(task_id: str) -> str:
    task = load_task(task_id)
    task.status = "completed"
    save_task(task)
    # 找出被解锁的下游任务
    unblocked = [t.subject for t in list_tasks()
                 if t.status == "pending" and t.blockedBy
                 and can_start(t.id)]
    msg = f"Completed {task_id} ({task.subject})"
    if unblocked:
        msg += f"\nUnblocked: {', '.join(unblocked)}"
    return msg

完成 "schema" 后,"endpoints" 和 "docs" 的 can_start 返回 True它们可以开始。

get_task: 查看完整细节

list_tasks 只显示一行摘要。get_task 返回完整的任务 JSON包括 description 和依赖细节。跨会话恢复时Agent 需要读取完整描述才能继续工作:

def get_task(task_id: str) -> str:
    task = load_task(task_id)
    return json.dumps(asdict(task), indent=2)

状态机: 两个动作,三个状态

pending ──claim──→ in_progress ──complete──→ completed

这里的 claim / complete 是动作,pending / in_progress / completed 是状态:

  • claim_task: pendingin_progress。设置 owner开始工作。
  • complete_task: in_progresscompleted。把任务标记为完成,并解锁下游。

CC 没有 in_progress → pending 的 release 路径。如果 teammate 终止或 shutdownCC 会把它未完成的任务 unassign清除 owner并将 status 重置为 pending,方便其他 agent 重新认领。教学版省略了这一恢复路径。

合起来跑

# 创建有依赖的任务
schema = create_task("setup database schema")
endpoints = create_task("create API endpoints", blockedBy=[schema.id])
tests = create_task("write tests", blockedBy=[endpoints.id])
docs = create_task("write docs", blockedBy=[schema.id])

# Agent 认领第一个可做的任务
claim_task(schema.id)       # ✓ Claimed (无依赖)
complete_task(schema.id)    # ✓ Completed → 解锁 endpoints, docs

claim_task(endpoints.id)    # ✓ Claimed (schema 已完成)
complete_task(endpoints.id) # ✓ Completed → 解锁 tests

claim_task(docs.id)         # ✓ Claimed (schema 已完成)
complete_task(docs.id)      # ✓ Completed

claim_task(tests.id)        # ✓ Claimed (endpoints 已完成)
complete_task(tests.id)     # ✓ Completed

每个 create_task 写一个 JSON 文件,每个 claim_task / complete_task 更新文件。跨会话时,.tasks/ 目录还在Agent 读文件就能恢复进度。


相对 s11 的变更

组件 之前 (s11) 之后 (s12)
任务管理 Task dataclass + 5 个工具
新类型 Taskid, subject, description, status, owner, blockedBy
存储 无持久化 .tasks/{id}.json 跨会话
依赖 blockedBy 图 + can_start 检查
工具 bash, read_file, write_file (3) + create_task, list_tasks, get_task, claim_task, complete_task (8)
生命周期 pending → in_progress → completed无 release 回退)

试一下

cd learn-claude-code
python s12_task_system/code.py

试试这些 prompt

  1. Create tasks: setup database schema, create API endpoints (depends on schema), write tests (depends on endpoints), write docs (depends on schema)
  2. List all tasks and their statuses
  3. Claim the first unblocked task and complete it
  4. List tasks again — which ones are now unblocked?

观察重点:.tasks/ 目录下是否生成了 JSON 文件?完成任务后,被阻塞的任务是否解锁?


接下来

任务图有了。但有些任务要跑很久——比如全量测试、部署到服务器。Agent 调 LLM 按量计费,不能干等一个慢操作。

s13 Background Tasks → 慢操作放后台。Agent 继续处理其他任务,后台跑完了通知它。

深入 CC 源码

以下基于 CC 源码 utils/tasks.ts862 行)、tools/TaskCreateTool/TaskCreateTool.ts138 行)、tools/TaskUpdateTool/TaskUpdateTool.ts406 行)、tools/TaskGetTool/TaskGetTool.ts128 行)、tools/TaskListTool/TaskListTool.ts116 行)、hooks/useTaskListWatcher.ts221 行)的分析。

一、TaskRecord 的完整字段

教学版只讲了 id、subject、status、owner、blockedBy。CC 实际有 9 个字段(utils/tasks.ts:76-89

字段 类型 用途
id string 递增整数 ID
subject string 简短标题
description string 自由格式描述
activeForm string? 进行时态in_progress 时在 spinner 显示
owner string? 分配的 agent ID
status pending/in_progress/completed 生命周期
blocks string[] 此任务阻塞的任务 ID下游
blockedBy string[] 阻塞此任务的任务 ID上游
metadata Record? 任意扩展键值对

存储位置:~/.claude/tasks/{taskListId}/{id}.json。每个任务一个文件。

二、不是 TodoWrite 的升级,是两个独立系统

CC 中 Task System 和 TodoWrite 同时存在,通过 isTodoV2Enabled() 切换(utils/tasks.ts:133)——交互式会话默认启用 TaskV2非交互式/SDK 默认用 TodoWrite。环境变量 CLAUDE_CODE_ENABLE_TASKS 可强制启用 Task。Task 有 TodoWrite 没有的文件锁并发保护、依赖强制执行、ownership、fs.watch 响应式监听、生命周期 hooks。

三、并发认领的锁机制

claimTask()utils/tasks.ts:541-612)用双重锁防竞争:

任务文件锁proper-lockfile 锁住 {taskId}.json(最多重试 30 次,指数退避 5-100ms。锁内

  1. 重新读取任务(防 TOCTOU
  2. 检查已被他人认领 → already_claimed
  3. 检查已完成 → already_resolved
  4. 检查上游未完成 → blocked
  5. 设置 owner

列表级锁agent busy 检查时):.lock 文件,原子性扫描所有任务并检查该 agent 是否已有其他 open task。

注意:教学版把 claim 和开始工作合成一步claim = set owner + in_progress真实 CC 的 claimTask 主要解决 owner 竞争,只设 owner 不改 status状态更新由 TaskUpdate 完成。

四、高水位标防 ID 重用

.highwatermark 文件记录曾分配过的最高任务 ID。即使任务被删除ID 也不会被重用。

五、四个 Task 工具

CC 的任务系统有四个工具(不是教学版的一个通用 Task 工具):TaskCreateTaskGetTaskUpdateTaskList。全部设置 isConcurrencySafe: trueshouldDefer: true(工具 schema 不在初始 prompt 中,需 ToolSearch 后才可见)。

教学版的 create_task(blockedBy=...) 在创建时直接声明依赖,是合理简化。真实 CC 的 TaskCreate 只接受 subject/description/activeForm/metadata依赖关系由 TaskUpdateaddBlocks/addBlockedBy 维护。