Skip to content

Task 類別:單輪 Agent 核心

源码版本v4.0.10

職責

Task 類別是 Cline 的「單回合 agent」狀態機。一個 Task 實例對應一次任務 (task),從使用者敲下 Enter 那一刻生,到任務被取消或正常收尾那一刻死。它不是常駐服務,而是被 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