Skip to content

Task 类:单轮 Agent 核心

源码版本v4.0.10

职责

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 交互都走 asksay 两个出口,这两个方法把消息塞进 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:188export 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 拿流:

typescript
// 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):

typescript
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 未连上:attemptApiRequestpWaitFor 等最多 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

对照官方资料:Cline 文档 · README