presentAssistantMessage:assistant 訊息分塊呈現器
職責
presentAssistantMessage 是 Task 類別裡那台「分塊推進器」。LLM 流式回應被 parseAssistantMessageV2 切成 text / tool_use / reasoning 幾種 block 後,這個方法負責一個一個把 block 推到 UI 和工具執行器。它不是自己迴圈跑,而是每次被流式回呼或者 scheduleAssistantPresentation 呼一次,推進一個 block,然後遞迴呼叫自己推進下一個。
它的位置在整個 agent loop 的最內層。recursivelyMakeClineRequests 拉起 attemptApiRequest 拿到流,流回呼裡把累積的文本重解析成 assistantMessageContent 陣列,然後呼叫 presentAssistantMessage。所以你看這個方法時,要把它當成「在流仍在吐字的過程中,被反覆叫醒,看有沒有新 block 可以呈現」的狀態機。
它管的狀態都在 taskState 上:currentStreamingContentIndex 指當前推到第幾個 block,presentAssistantMessageLocked 是個自旋鎖防止重入,presentAssistantMessageHasPendingUpdates 標記「在我執行期間又有新內容進來了」,userMessageContentReady 是給外層 pWaitFor 看的「這一輪所有 block 都處理完了」訊號。
設計動機
- 鎖 + pending 標記替代佇列:不用訊息佇列,而是一個布林鎖加上「有 pending 就再跑一遍」的尾遞迴式重入控制 (
lock check:2637)。簡單,且天然把多次流回呼合併成一次執行。 - 邊流邊呈現:不等整條回應結束,流到哪推到哪。text block 增量
say("text", content, ..., block.partial),tool_use block 一旦完整就立刻toolExecutor.executeTool。 - block 之間串行:並行 tool calling 關掉時,
didAlreadyUseTool標誌讓後續 block 直接跳過執行 (parallel gate:2668)。串行執行保證使用者在審批一個工具時不會被新工具打斷。 - cloneDeep 防引用篡改:取 block 時深拷貝一份再處理,因為流仍在更新原陣列裡的物件屬性,直接拿引用會讀到半成品 (
cloneDeep:2658)。 - out-of-bounds 是常態:索引越界不是錯誤,是「流還沒出下一個 block,你來得太早」的訊號;如果流已經結束 (
didCompleteReadingStream),才把userMessageContentReady置真讓外層繼續 (oob handling:2650)。
關鍵檔案
presentAssistantMessage:2630— 方法主體,鎖、block 類型分發、推進邏輯都在這。lock + pending:2637— 重入保護:已鎖就把 pending 置真並 return。cloneDeep block:2658— 深拷貝當前 block,避免讀到流正在寫的半成品。switch block.type:2663— 按text/tool_use分發,reasoning 走另外路徑。thinking tag strip:2685— 去掉<thinking>、<function_calls>等標籤,避免它們污染 markdown 渲染。say text:2731— 把清理後的 text 內容發給 UI,block.partial控制是增量更新還是最終版。checkpoint gate:2737— 有初始 checkpoint commit 在跑時,非唯讀工具必須等它結束才能執行。executeTool:2743— tool_use block 交給ToolExecutor.executeTool,自身不關心具體工具。userMessageContentReady:2769— 最後一個 block 完成時置真,讓外層pWaitFor解除阻塞。tail recursion:2780— 還有後續 block 就自己呼叫自己推進下一個,不必等流回呼。parseAssistantMessageV2 call:3506— 流回呼裡重解析整個 assistant 文本,生成 block 陣列。flush callback:685—presentationScheduler註冊的 flush 入口,最終還是落到這裡。
資料流
每次 presentAssistantMessage 被叫醒,先搶鎖。搶到後看當前索引有沒有越界,沒越界就取出 block、按類型分發。下面這段是分發後推進到下一個 block 的核心邏輯:
// apps/vscode/src/core/task/index.ts
if (
!block.partial ||
this.taskState.didRejectTool ||
(!this.isParallelToolCallingEnabled() && this.taskState.didAlreadyUseTool)
) {
// block is finished streaming and executing
if (
this.taskState.currentStreamingContentIndex ===
this.taskState.assistantMessageContent.length - 1
) {
// last block is complete and it is finished executing
this.taskState.userMessageContentReady = true; // will allow pwaitfor to continue
}
// call next block if it exists (if not then read stream will call it when its ready)
this.taskState.currentStreamingContentIndex++; // need to increment regardless, so when read stream calls this function again it will be streaming the next block
if (
this.taskState.currentStreamingContentIndex <
this.taskState.assistantMessageContent.length
) {
// there are already more content blocks to stream, so we'll call this function ourselves
await this.presentAssistantMessage();
return;
}
}
// block is partial, but the read stream may have finished
if (this.taskState.presentAssistantMessageHasPendingUpdates) {
await this.presentAssistantMessage();
}這段決定了「當前 block 處理完之後要不要立刻接下一個」。block 是 partial (還在流) 就不主動推進,等下一次流回呼;block 是 complete 就推進索引,如果新索引還在陣列範圍內,直接自呼叫推進下一個 block,不用等流回呼。最後那個 presentAssistantMessageHasPendingUpdates 是兜底:如果執行期間流又往前推了,就再跑一遍。userMessageContentReady 只在「最後一個 block 完成」時置真 (ready flag:2769)。
邊界與失敗
- abort 搶先:方法入口第一件事就是看
taskState.abort,取消了就直接拋 "Cline instance aborted" (abort guard:2631)。這保證取消訊號能在任何 block 執行前生效。 - 工具被拒絕後串行跳過:
didRejectTool一旦為真,後續 text block 直接break,tool_use block 走 ToolExecutor 時也會被它內部的拒絕檢查攔下 (reject gate:2667)。索引仍然推進,直到越界後置userMessageContentReady,讓外層把控制權交還。 - 初始 checkpoint 阻塞:任務開始時如果有個
initialCheckpointCommitPromise在跑,非唯讀工具 (!READ_ONLY_TOOLS.includes(block.name)) 必須等它結束 (checkpoint wait:2737)。唯讀工具可以並行跑。 - 鎖洩漏防護:鎖釋放在分發 switch 之前 (
early unlock:2754)。這看起來奇怪,但故意如此——後面要呼叫presentAssistantMessage自身,如果還拿著鎖會撞上自己。 - partial block 的清理:text block 在 partial 狀態下也要發到 UI,但末尾可能出現半個 XML 標籤 (比如
<think還沒閉合)。程式碼會偵測最後一個<之後是不是合法標籤名,是的話就切掉,避免 UI 跳動 (partial tag trim:2695)。 - 流先結束、block 後到:
didCompleteReadingStream已真但索引越界時,直接置userMessageContentReady讓外層 pWaitFor 繼續 (stream done oob:2650)。不會傻等一個永遠不會來的 block。
小結
presentAssistantMessage 是 agent loop 最內層那台推進機。它把「流到哪、推到哪」做到極致:有新 block 就跑、沒新 block 就等、流結束就放行。所有 UI 呈現和工具執行入口都匯聚到這一個方法。要繼續看工具怎麼執行,轉 /tools/coordinator 和 /tools/validator;要看它上一層的遞迴驅動,轉 /agent-loop/attempt-api-request 和 /agent-loop/task-class。