返回文章列表

《Claude Code 源码解析系列》第9章|任务管理

说明 Claude Code 的工作流工具如何追踪 Todo、Task 与复杂任务状态。

《Claude Code 源码解析系列》第9章|任务管理

前面几篇在聊 Tools,聊的主要是”模型怎么接触外部世界”:

  • 文件类工具让它读写代码
  • 搜索类工具让它找到线索
  • 终端类工具让它运行命令
  • 网络和 MCP 工具让它接入外部能力

但还有一类工具,不碰文件、不跑命令、不发请求。它回答的是另一个问题:

一次工作变长以后,Claude Code 怎么知道自己做到哪了?

想象用户甩过来这么一句话:

帮我修复登录失败的问题,补测试,确认没有影响旧接口,最后总结一下修改点。

这明显不是一个单步动作。它至少得走这么几轮:

定位问题
-> 设计修复方案
-> 修改代码
-> 跑测试
-> 处理失败
-> 汇总结果

如果所有状态都放在对话文本里,模型很容易出状况:

  • 做到一半忘记之前答应过什么
  • 已经完成的事又重复做一遍
  • 依赖关系没理清楚,先做了后置任务
  • 后台命令还在跑,主对话却不知道去哪读输出
  • 多 Agent 协作时,谁负责什么很快变成一笔糊涂账

所以 Claude Code 需要一组”工作流类工具”。它们不代表某个外部资源,而是把 Agent 的工作过程本身变成可见、可更新、可恢复的状态。

这组工具可以按四层理解:

层次解决的问题典型形态
Todo当前对话里有哪些待办轻量 checklist
Task复杂工作如何被持久化、分配、跟踪依赖和停止任务清单 + 运行时任务
Plan动手前如何先进入只读规划和审批Plan Mode / 计划文件 / 权限切换
Worktree并行或高风险修改如何隔离工作区独立 git worktree

这篇重点拆 Task。因为它最容易被误解成”更复杂的 Todo”,但源码里它实际承担了两套完全不同的职责。

一、为什么 Todo 不够用

Todo 适合表达”我接下来要做哪几步”。直觉上很简单:

[ ] 复现登录失败
[ ] 找到失败原因
[ ] 修改 auth 逻辑
[ ] 补测试
[ ] 跑完整测试

这种清单对短任务挺好用。用户能看到 Agent 的进度,模型也能提醒自己别跑偏。

但 Todo 有一个天然边界:它描述的是”认知层面的计划”,不是”系统层面的任务对象”。

任务一复杂,清单就开始吃力。

“补测试”这一步可能依赖”先修 auth 逻辑”。“跑完整测试”可能是一个后台命令,执行几分钟才有结果。如果还有子 Agent 参与,某个 reviewer 认领了安全检查,另一个 worker 负责兼容性测试——这时候一个普通 checklist 很难回答这些问题:

  • 这项工作有没有唯一 ID?
  • 它的完整描述存在哪?
  • 谁认领了它?
  • 它被哪些任务阻塞?
  • 它完成后会解锁哪些任务?
  • 如果它对应一个后台进程,输出去哪读?
  • 如果后台进程卡住,怎么停掉?

Task 不是因为 Todo 写得不够漂亮才出现的。

Task 解决的是:

把”我要做的事”从聊天里的短句,升级成运行时可以管理的任务状态。

二、Task 的第一层:任务清单,管理”该做什么”

Claude Code 的 Task 最容易混淆,是因为它有两套同名机制。

第一层可以叫”任务清单 Task”。它关注的是:

这项工作是什么、谁负责、现在什么状态、和其他任务有什么依赖。

本地 wiki 里对这层的总结很清楚:TaskCreate / TaskGet / TaskUpdate / TaskList 管的是工作计划,而不是后台进程。相关材料见 Claude Code Task 源码设计:任务清单与运行时任务

把它想成一个比 Todo 更正式的任务数据库:

TaskCreate
-> 创建一条 pending 任务
-> 写入任务 ID、标题、描述、owner、依赖关系、metadata
-> 保存在本地任务目录中

这类任务通常会包含:

id:任务编号
subject:一句话标题
description:完整任务说明
activeForm:进行中时应该怎么描述
owner:谁负责
status:pending / in_progress / completed
blocks:它会阻塞哪些任务
blockedBy:它被哪些任务阻塞
metadata:额外上下文

它和 Todo 的区别在于:Todo 更像”给模型看的短期提醒”,Task 更像”给运行时和协作系统看的工作对象”。

(源码里用 JSON 文件存而不是内存数组,这个选择后面会细说,本质是为了解决多进程并发写入的冲突。)

一个小例子:

用户目标:修复登录失败

#1 复现登录失败
#2 检查 auth middleware
#3 修复 token 刷新逻辑
#4 补回归测试
#5 跑完整测试

这里 #4 可能被 #3 阻塞,#5 又被 #4 阻塞。Todo 能让模型看懂这层关系,但系统很难稳定维护它。任务清单 Task 可以把依赖关系写进结构化字段里。

任务清单层的价值,不是”显示更多字”,而是把工作变成可以计算的状态:

哪些任务还没开始
哪些任务正在做
哪些任务完成了
哪些任务被阻塞
完成当前任务后会解锁什么
某个 owner 现在负责哪些任务

这也是多 Agent 协作里非常关键的一层。一旦有 teammate、worker、reviewer 参与,任务就不能只存在主 Agent 的脑子里,必须变成大家都能读写的共享协议。

三、任务清单为什么要落到文件系统

从源码设计看,Claude Code 没有把任务清单做成内存里的数组,而是每个任务存成一个独立 JSON 文件。

这看起来有点”重”,但背后是工程取舍。

(如果所有任务都存在一个全局数组里,多 Agent 或多进程并发更新时,细粒度锁很难做。一个 reviewer 更新了 #4 的状态,一个 worker 同时更新了 #3 的描述,没有单任务级别的隔离,最后写入的人很容易把前一个人的修改冲掉。)

一个任务一个文件,并发冲突就能缩小到单个任务级别:

tasks/
  task-list-id/
    .highwatermark
    1.json
    2.json
    3.json

.highwatermark 记录已经分配到的最大 ID,避免任务删除后复用旧编号。列表级锁管创建、重置、认领这类整体操作;任务级锁管单个任务的更新。

这说明 Task 系统不是在给 UI 加列表,而是在解决真实协作里的状态一致性问题。

一句话记住区别:

Todo 是对话里的工作记忆,任务清单 Task 是文件系统里的工作事实。

四、Task 的第二层:运行时任务,管理”谁正在跑”

到这里还只是 Task 的第一层。

Claude Code 里还有第二层,也叫 Task,但它关注的不是”该做什么”,而是:

当前有哪些后台执行体正在运行,它们的输出在哪,必要时怎么停掉。

这就是运行时 Task。

比如模型执行了一个测试命令:

npm test

如果这个命令很快结束,那只是一次普通 Bash 调用。但如果它变成后台任务,主对话就需要继续推进,同时保留对这个进程的控制权:

后台测试任务正在运行
-> 主 Agent 继续看代码或等待用户
-> 过一会儿读取测试输出
-> 如果卡住,就停止任务

运行时 Task 管的就是这种对象。它们存在当前会话的 AppState.tasks 中,类型包括:

local_bash
local_agent
remote_agent
in_process_teammate
local_workflow
monitor_mcp
dream

状态也和任务清单不同:

pending
running
completed
failed
killed

注意这里已经不是 pending / in_progress / completed 了。运行时 Task 更像一个进程生命周期,而不是工作计划状态。

所以 TaskStopTaskOutput 不属于任务清单那一层。

TaskOutput 关心的是:

这个后台任务有没有输出?
上次读到哪里了?
这次要不要阻塞等待?
输出文件有没有超限?

TaskStop 关心的是:

这个任务是否存在?
它是否仍处于 running?
它是哪种类型的后台任务?
应该调用哪种 kill 实现?

这和 TaskCreate 完全不是一回事。

五、同一件工作为什么会有两个 ID

两层 Task 最容易混的地方,是同一件现实工作可能同时对应两个 ID。

任务清单里有一项:

#5 跑完整测试

模型开始执行,调用 Bash 启动测试。如果测试进入后台,系统又生成一个运行时任务:

b9x4k2...  local_bash  running

同一件事,两个身份:

身份它代表什么谁使用
#5计划里的”跑完整测试”这项工作用户、主 Agent、teammate
b9x4k2...正在运行的测试进程运行时、TaskOutput、TaskStop

前者回答”工作有没有完成”,后者回答”进程现在怎么样”。

测试失败时,模型读取 b9x4k2... 的输出,然后把 #5 保持为 in_progress 或更新相关任务。测试成功,再把 #5 标记为 completed

两层 Task 不能合并的原因:

任务清单 Task:协作状态
运行时 Task:执行生命周期

一个描述”我们打算做什么、做到哪了”,一个描述”系统里谁正在运行、输出在哪、怎么停止”。

六、Task 工具分工

两层拆开以后,工具名字就清楚了。

任务清单层:

工具作用
TaskCreate创建计划任务,但不开始执行
TaskGet读取某个任务的完整说明、状态和依赖
TaskList列出任务摘要,帮助模型和队友扫描当前工作
TaskUpdate更新状态、owner、metadata、依赖,或删除任务

运行时层:

工具作用
TaskOutput读取后台任务输出,支持增量读取和等待
TaskStop停止仍在运行的后台任务

把它们混起来,就会踩坑:

  • TaskCreate 创建出来的 #1 不能被 TaskStop 停止
  • TaskOutput 不能读取 TaskCreate 里的 description
  • TaskUpdate 更新的是任务清单状态,不是后台进程状态
  • 后台进程失败了,不等于对应计划任务会自动完成或删除

这一点对读源码很重要。看到 Task 这个名字时,先问一句:

这里说的是任务清单,还是运行时执行体?

七、Task 和 Agent 协作的关系

Task 系统放在工作流类工具里,还有一个原因:它是多 Agent 协作的地基之一。

单 Agent 时代,任务状态可以粗糙一点。主 Agent 自己知道要做什么,顶多维护一个 Todo。

一旦进入多 Agent,情况就变了:

主 Agent:负责总目标和最终决策
worker:负责实现某个模块
reviewer:负责审查风险
tester:负责跑测试和复现

这时”任务”必须支持几件事:

  • 可以被某个 owner 认领
  • 可以展示给不同参与者
  • 可以表达依赖关系
  • 可以在完成时通知相关方
  • 可以在 teammate 退出时释放未完成任务
  • 可以把后台 agent 或 shell 纳入运行时管理

所以 Task 不是孤立工具。它和 Agent、Plan、Bash、权限、UI 都连在一起。

一张简图看全貌:

05.3工作流工具-任务管理 图 1

任务清单和运行时任务之间不是替代关系,是协作关系。

任务清单让团队知道”该做什么”;运行时任务让系统知道”谁正在跑”。

八、和 Plan、Worktree 的边界

Task 经常会和 Plan、Worktree 一起出现,但三者解决的问题不同。

Plan 解决的是动手前的边界:

先只读研究
-> 生成计划
-> 等用户批准
-> 再进入执行

Task 解决的是执行过程中的状态:

哪些工作项存在
-> 谁负责
-> 哪些完成了
-> 后台执行体如何读输出和停止

Worktree 解决的是文件修改的隔离:

并行修改或高风险修改
-> 创建独立工作区
-> 避免污染主工作树
-> 最后合并或丢弃

三者串起来:

Plan:先决定怎么做
Task:把要做的事变成可追踪状态
Worktree:给真正改代码的执行阶段提供隔离工作区

Todo 更轻,常常只是在当前对话里给用户和模型看的进度提示。

九、读源码时抓住这条线

读 Claude Code 的 Task 源码,不建议从工具名硬背。

更好的方式是带着两个问题看:

第一问:这段代码是在管理"工作计划",还是在管理"后台执行体"?
第二问:这个状态是给谁看的,模型、用户、teammate,还是运行时?

任务清单层重点看:

  • src/utils/tasks.ts
  • TaskCreateTool
  • TaskGetTool
  • TaskUpdateTool
  • TaskListTool
  • useTasksV2
  • TaskListV2

运行时层重点看:

  • src/Task.ts
  • src/tasks.ts
  • src/tasks/stopTask.ts
  • LocalShellTask
  • LocalAgentTask
  • TaskStopTool
  • TaskOutputTool
  • diskOutput
  • framework

如果只看 TaskCreate,会以为 Task 就是高级 Todo。

如果只看 TaskStop,又会以为 Task 就是后台进程。

把两层放在一起,才能看到 Claude Code 真正的设计:

它不是让模型”记得自己要做什么”,而是让宿主系统拥有一套可观察、可分配、可依赖、可停止、可恢复通知的工作状态。

小结

Task 是工作流类工具里最核心、也最容易误解的一环。

它不是 Todo 的加强版,是两套机制的组合:

  • 任务清单 Task:把计划项变成持久化、可分配、可依赖的工作对象。
  • 运行时 Task:把后台 shell、agent、remote session 等执行体变成可读取、可停止、可通知的生命周期对象。

理解这点以后,再看 Claude Code 的长任务能力就顺了:

Todo 让短期步骤可见
Plan 让执行前的理解和审批可控
Task 让执行过程可追踪
Worktree 让文件修改可隔离

这几层合起来,Claude Code 才不只是一个会说”我接下来要做什么”的聊天助手,而是一个能把工程工作组织起来的运行时。