Skip to content

presentAssistantMessage: assistant メッセージのブロック別呈现器

源码版本v4.0.10

役割

presentAssistantMessage は Task クラス内にある「ブロック別プッシャー」である。LLM のストリーミングレスポンスが parseAssistantMessageV2 で text / tool_use / reasoning の各 block に切り分けられた後、このメソッドが block を 1 つずつ UI とツール実行器に押し出す。自身でループを回すのではなく、ストリーミングコールバックや scheduleAssistantPresentation によって 1 回呼ばれるたびに 1 block を進め、その後再帰的に自分を呼んで次の block を進める。

このメソッドは agent loop 全体の最内層に位置する。recursivelyMakeClineRequestsattemptApiRequest を起動してストリームを取得し、ストリームコールバック内で蓄積したテキストを assistantMessageContent 配列に再解析したうえで presentAssistantMessage を呼ぶ。したがってこのメソッドを読むときは「ストリームがまだ文字を吐き出している最中に、繰り返し起こされては新規 block を呈现できるかを見る状態機械」として捉える必要がある。

このメソッドが管轄する状態はすべて taskState 上にある: currentStreamingContentIndex が現在どこまで block を進めたかを示し、presentAssistantMessageLocked は再入を防ぐスピンロック、presentAssistantMessageHasPendingUpdates は「実行中に新規内容が到着した」を示すフラグ、userMessageContentReady は外層の pWaitFor に渡す「このラウンドのすべての block が処理完了した」シグナルである。

設計動機

  • キューの代わりにロック + pending フラグ: メッセージキューを使わず、boolean ロックと「pending があればもう 1 回走る」という末尾再帰型の再入制御で済ませる (lock check:2637)。シンプルであり、複数回のストリームコールバックを自然に 1 回の実行にマージできる。
  • ストリームと並行して呈现: レスポンス全体の終了を待たず、ストリームが届いたところから順に呈现する。text block は say("text", content, ..., block.partial) で差分更新し、tool_use block は完全揃い次第すぐ toolExecutor.executeTool に渡す。
  • block 間は直列: parallel tool calling が無効のとき、didAlreadyUseTool フラグにより後続 block は実行をスキップする (parallel gate:2668)。直列実行により、ユーザーが 1 つのツールを承認中に新たなツールで割り込まれるのを防ぐ。
  • 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:2663text / 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:685presentationScheduler が登録した flush エントリ。最終的にはここに行き着く。

データフロー

presentAssistantMessage は呼び起こされるたびにまずロックを取得する。取得後、現在のインデックスが範囲内かを確認し、範囲内なら block を取り出して型に応じて振り分ける。以下は振り分け後に次の block へ進めるかを決めるコアロジックである:

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

この部分は「現在の block 処理完了後にすぐ次へ繋ぐか」を決める。block が partial (ストリーム中) なら自らは進めず次のストリームコールバックを待つ。block が complete ならインデックスを進め、新しいインデックスが配列範囲内なら直接自分を呼んで次の block を進め、ストリームコールバックを待たない。末尾の presentAssistantMessageHasPendingUpdates はフォールバックで、実行中にストリームがさらに進んだ場合にもう 1 回走らせる。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 があれば走らせ、なければ待ち、ストリーム終了すれば通す。すべての UI 呈現とツール実行のエントリがこの 1 つのメソッドに集約される。ツール実行の続きを見るには /tools/coordinator/tools/validator へ。一つ上の再帰ドライバを見るには /agent-loop/attempt-api-request/agent-loop/task-class へ。

公式資料: Cline ドキュメント · README