presentAssistantMessage: assistant メッセージのブロック別呈现器
役割
presentAssistantMessage は Task クラス内にある「ブロック別プッシャー」である。LLM のストリーミングレスポンスが parseAssistantMessageV2 で text / tool_use / reasoning の各 block に切り分けられた後、このメソッドが block を 1 つずつ UI とツール実行器に押し出す。自身でループを回すのではなく、ストリーミングコールバックや scheduleAssistantPresentation によって 1 回呼ばれるたびに 1 block を進め、その後再帰的に自分を呼んで次の block を進める。
このメソッドは agent loop 全体の最内層に位置する。recursivelyMakeClineRequests が attemptApiRequest を起動してストリームを取得し、ストリームコールバック内で蓄積したテキストを 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: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 はフォールバックで、実行中にストリームがさらに進んだ場合にもう 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