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。