Skip to content

presentAssistantMessage: the assistant-message block presenter

源码版本v4.0.10

Responsibilities

presentAssistantMessage is the "block advancer" inside the Task class. After the LLM stream is sliced into text / tool_use / reasoning blocks by parseAssistantMessageV2, this method is responsible for pushing those blocks to the UI and the tool executor one by one. It does not run in a loop of its own — each time it is called by a stream callback or by scheduleAssistantPresentation, it advances one block, then recursively calls itself to advance the next.

It sits at the innermost layer of the agent loop. recursivelyMakeClineRequests starts attemptApiRequest to grab the stream; inside the stream callback, the accumulated text is re-parsed into the assistantMessageContent array, and then presentAssistantMessage is called. So when you read this method, think of it as a state machine that is "woken repeatedly while the stream is still emitting, to see whether there is a new block to present".

The state it manages lives on taskState: currentStreamingContentIndex tracks which block it has advanced to, presentAssistantMessageLocked is a spin lock that prevents re-entry, presentAssistantMessageHasPendingUpdates flags "new content arrived while I was running", and userMessageContentReady is the signal to the outer pWaitFor that "all blocks for this round have been processed".

Design motivation

  • Lock + pending flag instead of a queue: There is no message queue; instead, a boolean lock plus a "if there is pending, run once more" tail-recursive re-entry control (lock check:2637). Simple, and it naturally coalesces multiple stream callbacks into one execution.
  • Present as it streams: It does not wait for the full response to finish; it advances as far as the stream has reached. A text block goes through say("text", content, ..., block.partial) incrementally; a tool_use block, once complete, is immediately handed to toolExecutor.executeTool.
  • Blocks are serial: When parallel tool calling is off, the didAlreadyUseTool flag makes subsequent blocks skip execution directly (parallel gate:2668). Serial execution ensures the user is not interrupted by a new tool while approving another.
  • cloneDeep against reference tampering: When taking a block, it deep-clones it before processing, because the stream is still updating properties on the objects in the original array — taking a reference directly would read a half-finished object (cloneDeep:2658).
  • Out-of-bounds is normal: An out-of-bounds index is not an error; it is the signal "the stream has not produced the next block yet, you came too early". Only if the stream has already ended (didCompleteReadingStream) does it set userMessageContentReady to let the outer layer continue (oob handling:2650).

Key files

  • presentAssistantMessage:2630 — the method body; the lock, block-type dispatch, and advance logic all live here.
  • lock + pending:2637 — re-entry protection: if already locked, set pending and return.
  • cloneDeep block:2658 — deep-clones the current block to avoid reading half-finished data the stream is writing.
  • switch block.type:2663 — dispatches by text / tool_use; reasoning goes through a separate path.
  • thinking tag strip:2685 — strips <thinking>, <function_calls>, and other tags so they do not pollute markdown rendering.
  • say text:2731 — sends the cleaned text content to the UI; block.partial controls incremental update vs final version.
  • checkpoint gate:2737 — when an initial checkpoint commit is running, non-read-only tools must wait for it to finish before executing.
  • executeTool:2743 — tool_use blocks go to ToolExecutor.executeTool; this method itself does not care about concrete tools.
  • userMessageContentReady:2769 — set to true when the last block completes, unblocking the outer pWaitFor.
  • tail recursion:2780 — if there are more blocks, it calls itself to advance to the next one, without waiting for a stream callback.
  • parseAssistantMessageV2 call:3506 — in the stream callback, re-parses the entire assistant text to produce the block array.
  • flush callback:685 — the flush entry registered with presentationScheduler; ultimately lands here.

Data flow

Every time presentAssistantMessage is woken, it first grabs the lock. Once it has the lock, it checks whether the current index is in bounds; if so, it takes the block and dispatches by type. The snippet below is the core logic for advancing to the next block after dispatch:

typescript
// 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();
}

This decides "after the current block is done, do we go straight to the next one?". If the block is partial (still streaming), it does not advance proactively; it waits for the next stream callback. If the block is complete, it advances the index; if the new index is still within the array, it calls itself to advance to the next block without waiting for a stream callback. The final presentAssistantMessageHasPendingUpdates is the fallback: if the stream moved forward during execution, it runs once more. userMessageContentReady is set only when "the last block is complete" (ready flag:2769).

Boundaries and failure modes

  • abort preempts: The very first thing at the method entry is checking taskState.abort; if cancelled, it throws "Cline instance aborted" directly (abort guard:2631). This ensures the cancel signal takes effect before any block executes.
  • Serial skip after a tool is rejected: Once didRejectTool is true, subsequent text blocks break directly; tool_use blocks are also caught by ToolExecutor's own rejection check when they go through it (reject gate:2667). The index still advances until it goes out of bounds, then userMessageContentReady is set so the outer layer takes back control.
  • Initial checkpoint blocking: If an initialCheckpointCommitPromise is running at task start, non-read-only tools (!READ_ONLY_TOOLS.includes(block.name)) must wait for it to finish (checkpoint wait:2737). Read-only tools can run in parallel.
  • Lock-leak protection: The lock is released before the dispatch switch (early unlock:2754). This looks odd but is intentional — later it calls presentAssistantMessage itself, and holding the lock would deadlock against itself.
  • Partial-block cleanup: A text block is sent to the UI even in partial state, but its tail may contain half an XML tag (e.g. <think not yet closed). The code checks whether what follows the last < is a valid tag name and trims it if so, to avoid UI jitter (partial tag trim:2695).
  • Stream ends first, block arrives later: When didCompleteReadingStream is already true and the index is out of bounds, userMessageContentReady is set directly so the outer pWaitFor continues (stream done oob:2650). It does not naively wait for a block that will never arrive.

Summary

presentAssistantMessage is the advancer at the innermost layer of the agent loop. It takes "present as far as the stream has reached" to the extreme: run when there is a new block, wait when there is not, and release when the stream ends. All UI presentation and tool-execution entries converge on this one method. To continue into how tools execute, go to /tools/coordinator and /tools/validator; to see the recursive driver one layer up, go to /agent-loop/attempt-api-request and /agent-loop/task-class.

See official docs: Cline docs · README