Task 類別:單輪 Agent 核心
職責
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 互動都走 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