Task 类:单轮 Agent 核心
职责
Task 类是 Cline 的「单回合 agent」状态机。一个 Task 实例对应一次任务 (task),从用户敲下回车那一刻生,到任务被取消或正常收尾那一刻死。它不是常驻服务,而是被 Controller 创建出来跑完一轮就丢的一次性对象。这种「每轮一个实例」的模型让并发控制变得直白:同一个 Task 永远不会同时处理两轮 LLM 请求,因为只有它自己在动。
它做的事可以列成三段:先把当前用户输入、历史消息、系统提示、MCP 工具列表、规则文件等拼成一次完整的 API 请求;然后驱动 LLM 流式响应,边流边用 parseAssistantMessageV2 把文本切成 text / tool_use / reasoning 三种 block;最后把这些 block 交给 presentAssistantMessage 一个一个呈现给 UI 并执行其中的工具。工具结果回流为下一轮的 user content,触发递归调用 (recursion),直到模型不再发 tool_use 或者用户中断。
把它理解成一个三层嵌套的循环就够了:最外层 initiateTaskLoop 兜底「模型没调工具就再问一次」,中间层 recursivelyMakeClineRequests 每递归一次就是一次完整的 API 调用 + 工具执行,内层 presentAssistantMessage 在流里增量推进 block。所有「问问题」「请用户确认」「报告进度」的 UI 交互都走 ask 和 say 两个出口,这两个方法把消息塞进 messageStateHandler 再 post 到 webview。
设计动机
- 一次任务一个实例:把状态隔离在每个 Task 实例里,取消、回滚、checkpoint 都能以实例为边界处理。
abortTask只要置上taskState.abort标志,所有递归路径都会读到并自行退出 (abortTask:1801)。 - 递归而非循环:每一轮 LLM 响应后,工具结果天然就是下一轮的 user content,所以
recursivelyMakeClineRequests在结尾把自己再调一遍 (recurse:3830)。这种写法把「单轮 API 调用 + 工具执行」的代码只写一遍,调用栈深度自然反映回合数,出错时堆栈也好读。 - 流式解析与呈现分离:LLM 返回是边吐边解析的,
parseAssistantMessageV2每收到一段文本就重解析整个 assistant 文本 (parseAssistantMessageV2 call:3509),presentAssistantMessage按 block 边界推进,这样工具不会等整条响应才开跑。 - 单锁防状态竞争:所有 Task 状态修改都过
withStateLock拿同一把 Mutex (withStateLock:205),避免流式回调、工具执行、UI 交互三路并发改坏状态。 - YOLO 模式与 mistake 限额:连错次数到顶就直接终止 (
mistake limit:2826),避免模型在死循环里烧 token。
关键文件
Task class definition:188—export class Task,声明所有核心字段:taskId、taskState、api、controller、messageStateHandler 等。constructor:310— 接收TaskParams,初始化 clineIgnore、toolExecutor、streamHandler、presentationScheduler 等依赖。startTask:1253— 入口,准备初始 user content、跑 TaskStart hook,然后调用initiateTaskLoop。initiateTaskLoop:1717— 外层 while 循环,模型只回文本不调工具时用noToolsUsed提示再问一遍。recursivelyMakeClineRequests:2790— 中层递归驱动器,负责 mistake 限额检查、checkpoint 初始化、调attemptApiRequest拉流。attemptApiRequest:2175— 等待 MCP 连接、读取规则文件、拼系统提示,最后yield*出 LLM 流。presentAssistantMessage:2630— 内层 block 推进器,按 type 分发:text 去掉 thinking 标签后 say,tool_use 交给toolExecutor.executeTool。ask:789— 让用户回答的出口,管理 partial 消息更新和 webview 响应回调。say:969— 单向报告进度的出口,partial 模式用于流式增量更新同一条消息。abortTask:1801— 分阶段取消:先决定是否跑 TaskCancel hook,再置 abort 标志,再取消 hook / 后台命令,最后跑 hook。ToolExecutor.executeTool:212— Task 把工具执行委托出去,自己不直接处理任何具体工具。
数据流
一次 LLM 请求的核心路径是「递归 → 拉流 → 解析 → 呈现 → 回流 → 再递归」。recursivelyMakeClineRequests 一进来先重置这一轮的流式状态,然后启动 attemptApiRequest 拿流:
// apps/vscode/src/core/task/index.ts
// reset streaming state
this.taskState.currentStreamingContentIndex = 0;
this.taskState.assistantMessageContent = [];
this.taskState.didCompleteReadingStream = false;
this.taskState.userMessageContent = [];
this.taskState.userMessageContentReady = false;
this.taskState.didRejectTool = false;
this.taskState.didAlreadyUseTool = false;
this.taskState.presentAssistantMessageLocked = false;
// ...
const stream = this.attemptApiRequest(previousApiReqIndex); // yields only if the first chunk is successful这段在 reset streaming state:3302 附近。重置完后,StreamChunkCoordinator 把流分成 text / usage / reasoning 几种 chunk 分别回调处理。每收到一段 text,就重跑一次 parseAssistantMessageV2 把整个 assistant 文本拆成 block 数组 (parseAssistantMessageV2 call:3509):
assistantMessage += chunk.text;
assistantTextOnly += chunk.text; // Accumulate text separately
// parse raw assistant message into content blocks
const prevLength = this.taskState.assistantMessageContent.length;
this.taskState.assistantMessageContent =
parseAssistantMessageV2(assistantMessage);block 数组变了就 scheduleAssistantPresentation 触发 presentAssistantMessage。后者按 block.type 走分支:text 走 say("text", ...),tool_use 走 toolExecutor.executeTool(block) (executeTool call:2743)。工具结果被塞进 taskState.userMessageContent,等整条流走完、userMessageContentReady 被置上,recursivelyMakeClineRequests 用这份 user content 再调自己一次 (recurse:3830),直到模型不再带 tool_use,外层 initiateTaskLoop 就会用 noToolsUsed 提示再问一遍,或由用户结束。
边界与失败
- mistake 限额触发:连错次数达到
maxConsecutiveMistakes时,YOLO 模式直接return true终止任务;否则ask("mistake_limit_reached")让用户决定 (mistake limit:2826)。 - 空响应:整轮 assistant 没有任何 text 或 tool_use block,记一条
empty_assistant_message遥测并提示用户重试 (empty response:3834)。 - 用户中途取消:
abortTask先 capture 是否要跑 TaskCancel hook,再置abort标志,避免置完标志后 hook 判定漏判 (abortTask:1801)。 - 工具被拒绝:
didRejectTool置上后,后续 text block 直接 skip,流被[Response interrupted by user feedback]截断 (didRejectTool:3536)。 - MCP 未连上:
attemptApiRequest用pWaitFor等最多 10 秒,超时只记日志不阻塞,系统提示照常生成 (mcp wait:2177)。 - initial checkpoint 未完成:第一个 checkpoint commit 在跑时,非只读工具会被
await this.initialCheckpointCommitPromise阻塞,只读工具可并发 (initialCheckpoint gate:2737)。 - partial block 收尾:流结束时还残留的 partial block 被强制
partial = false,让presentAssistantMessage能正常推进并最终置上userMessageContentReady(finalize partial blocks:3783)。
小结
Task 类是 Cline 在「单回合」维度上的全部状态机:它把一次任务的生命周期封装成「构造 → 启动 → 递归拉流 → 工具执行 → 收尾」的清晰链路,所有 UI 交互收敛到 ask / say 两个出口,所有状态变更收敛到一把 Mutex。这种「每轮一个实例」的取舍换来的是取消、checkpoint、mistake 限额等边界处理都能以实例为单元简单落地。
想往里钻可以接着看:
- 递归本身:
/agent-loop/recursion - LLM 调用与系统提示拼装:
/agent-loop/attempt-api-request - 助手文本如何被切成 block:
/agent-loop/parse-assistant-message