recursivelyMakeClineRequests: 再帰ドライバ
役割
recursivelyMakeClineRequests は Task クラス内にある「1 回の再帰 = 1 回の完全な LLM 呼び出し + ツール実行」を担うコアドライバで、apps/vscode/src/core/task/index.ts の Task クラスに定義されている。前ラウンドで蓄積した userContent (ツール結果、ユーザーフィードバック、noToolsUsed プロンプト等を含む) を受け取り、attemptApiRequest を呼んでストリームを取得し、そのストリームを StreamChunkCoordinator に振り分けさせる。ストリーム終了後、すべての block が presentAssistantMessage で処理し終わるのを待ち、そのラウンドで蓄積した taskState.userMessageContent を使って自分自身をもう一度呼ぶ。agent loop (エージェントループ) 全体の「ターン」という概念はこの再帰の中にある。
この関数は三重のネストされたループの中間に位置する。最外層は initiateTaskLoop で、while (!abort) によって「モデルがテキストだけでツールを呼ばない」状況をフォールバックし、noToolsUsed プロンプトを差し込んで再帰に戻す (initiateTaskLoop:1717)。中間層が recursivelyMakeClineRequests 自体で、1 回の呼び出しが 1 回の LLM API リクエストに対応する。最内層は presentAssistantMessage で、ストリームのコールバックや scheduler によって繰り返し起こされて block を進める。recursivelyMakeClineRequests は末尾で await this.recursivelyMakeClineRequests(this.taskState.userMessageContent) として自分自身を呼び (recurse:3830)、ツール結果は自然に次ラウンドの user content となるため、追加のオーケストレーションは不要である。
設計動機
- while ループではなく再帰: 各ラウンドの LLM レスポンス後、ツール結果がそのまま次ラウンドの user content になるため、関数末尾で自然に
await this.recursivelyMakeClineRequests(this.taskState.userMessageContent)と呼ぶ (recurse:3830)。この書き方により「1 ラウンドの API 呼び出し + ツール実行 + 状態リセット」を 1 箇所にまとめられ、コールスタックの深さがそのままターン数を反映するため、エラー時のスタックが読みやすい。 - mistake 上限の優先チェック: 関数先頭ですぐに
consecutiveMistakeCount >= maxConsecutiveMistakesをチェックし (mistake limit check:2826)、YOLO モードならそのままreturn trueでタスクを終了、そうでなければask("mistake_limit_reached")でユーザーに判断を委ねる。これによりモデルが無限ループでトークンを燃やすのを防ぐ。 - ストリーミング状態の全量リセット: 各ラウンド開始時に
currentStreamingContentIndex、assistantMessageContent、userMessageContent、didRejectTool、presentAssistantMessageLockedなど 10 個あまりのフィールドをゼロ化し (reset streaming state:3302)、前ラウンドの残滓が今回に影響しないようにする。リセットはstreamHandler.reset()とpresentationScheduler.reset()もカバーする。 - StreamChunkCoordinator による分流: 直接
for awaitでストリームを消費せず、StreamChunkCoordinatorで包み (stream coordinator:3368)、ストリームをreasoning/text/usageの 3 種類の chunk に分けてコールバックする。reasoning は reasoning handler、text はparseAssistantMessageV2で再解析、usage はトークンカウントを累積する。 - pWaitFor userMessageContentReady: ストリーム終了後に即座に再帰せず、
await pWaitFor(() => this.taskState.userMessageContentReady)で待つ (pWaitFor ready:3808)。presentAssistantMessageがすべての block を処理し終わるのを待ち、ツール結果がすべてuserMessageContentに蓄積されてから次ラウンドに入る。 - noToolsUsed で mistake を増やす: 1 ラウンド中に assistant が tool_use block を一つも出さなかった場合、
formatResponse.noToolsUsedのテキストを userMessageContent に差し込みconsecutiveMistakeCount++する (noToolsUsed:3818)。次ラウンドでは「ツールを呼ぶか attempt_completion するか」というプロンプトが模型に提示され、連続してツール不使用なら mistake 上限で打ち切られる。 - 空レスポンスはエラー経路へ: 1 ラウンドで assistant が text も tool_use も返さなかった場合、
empty_assistant_messageテレメトリを記録し、error を say したうえでask("api_req_failed")でユーザーにリトライ可否を問う (empty response:3834)。タスクが無言で継続するのを防ぐ。
主要ファイル
recursivelyMakeClineRequests:2790— 関数のエントリ。シグネチャは(userContent, includeFileDetails?) => Promise<boolean>で、didEndLoopを返す。abort check:2795— 関数に入ってすぐtaskState.abortをチェックし、キャンセル時はTask instance abortedを投げる。apiRequestCount++:2804— リクエストカウンタを自増し、focus chain list 管理に使う。mistake limit check:2826—consecutiveMistakeCount >= maxConsecutiveMistakesのとき mistake 処理分岐に入る。yolo fail:2841— YOLO モードでは say error + return true で即終了。ask mistake_limit_reached:2860— 非 YOLO モードでユーザーに ask し、ユーザーは新しい prompt で継続できる。reset streaming state:3302— ストリーミング状態の全量リセット。10 個あまりのフィールドをゼロ化 + handler/scheduler の reset。attemptApiRequest call:3319— ストリームを取得。最初の chunk の yield に失敗すると attemptApiRequest 内部の try/catch でapi_req_failedask に変換される。StreamChunkCoordinator:3368— ラップ層。ストリームを reasoning/text/usage の 3 種類の chunk に振り分ける。while true chunk loop:3387— 主消費ループ。coordinator から次の chunk を取り、switch で振り分ける。accumulate assistantMessage:3503— text chunk を assistantMessage + assistantTextOnly に累積し、再解析する。force partial false:3792— ストリーム終了後、残留する partial tool block を強制的にpartial = falseにして、presentAssistantMessage が finalize できるようにする。pWaitFor ready:3808— すべての block の処理完了を待ち、userMessageContentReady を真にする。noToolsUsed bump:3818— ツール呼び出しがないとき noToolsUsed プロンプトを差し込み + mistake++。recurse:3830— 蓄積した userMessageContent で自分自身を呼び、didEndLoop を返す。empty response:3834— 空レスポンス経路。say error + ask api_req_failed。outer catch:3935— フォールバック catch。理論上は attemptApiRequest 自身がすでに catch 済みだが、ここは二重安全策。initiateTaskLoop:1717— 外側の while。noToolsUsed プロンプト処理 + didEndLoop で抜ける。
データフロー
recursivelyMakeClineRequests に入るたび、まず mistake 上限チェックと remote workspace 検出を行い、その後ストリーミング状態をリセットしてストリームを起動する。以下はストリーミング状態リセット + ストリーム起動のコア部分である:
// apps/vscode/src/core/task/index.ts
// reset streaming state
this.taskState.currentStreamingContentIndex = 0;
this.taskState.assistantMessageContent = [];
this.taskState.didCompleteReadingStream = false;
this.taskState.userMessageContent = [];
this.taskState.userMessageContentReady = false;
this.taskState.didRejectTool = false;
this.taskState.didAlreadyUseTool = false;
this.taskState.presentAssistantMessageLocked = false;
this.taskState.presentAssistantMessageHasPendingUpdates = false;
this.taskState.didAutomaticallyRetryFailedApiRequest = false;
await this.diffViewProvider.reset();
this.streamHandler.reset();
this.presentationScheduler.reset();
this.taskState.toolUseIdMap.clear();
const { toolUseHandler, reasonsHandler } =
this.streamHandler.getHandlers();
const stream = this.attemptApiRequest(previousApiReqIndex);この部分は reset streaming state:3302 の付近にある。リセット完了後、StreamChunkCoordinator がストリームを引き取り (stream coordinator:3368)、while (true) ループが coordinator から chunk を取り出す。text chunk は assistantMessage += chunk.text; assistantTextOnly += chunk.text; に入り、すぐに parseAssistantMessageV2(assistantMessage) で全文を再解析し (parseAssistantMessageV2 call:3509)、長さが増えれば scheduleAssistantPresentation が presentAssistantMessage に新規 block を進めさせる。reasoning chunk は reasoning handler で thinking メッセージを差分更新する。usage chunk はトークンカウントと cost を累積する。ストリーム終了後、processNativeToolCalls がネイティブ tool call を処理し、flushAssistantPresentationOrThrow が残留 partial block を強制 finalize し、その後 await pWaitFor(() => userMessageContentReady) で presentAssistantMessage がすべての block を処理し終わるのを待つ (pWaitFor ready:3808)。その後 tool_use の有無を判定し、あれば checkpoint を取り userMessageContent で再帰、なければ noToolsUsed プロンプトを差し込んで再帰、完全に空のレスポンスならエラー経路に進む。再帰が返す didEndLoop は initiateTaskLoop まで伝わり、真なら while を抜け、偽なら外層が noToolsUsed プロンプトを足して次ラウンドへ行く。
境界と失敗
- abort はすべてに優先: 関数の最初の行で
taskState.abortをチェックし (abort check:2795)、キャンセル時は即座にTask instance abortedを投げ、ストリーミングリセットや API 呼び出しには入らない。これにより、キャンセル後に再帰が残っていても即座に抜ける。 - mistake 上限: YOLO モードでは
return trueでタスク終了 (yolo return:2847)。非 YOLO モードではユーザーに ask し、ユーザーが新 prompt を与えればそれを次ラウンドの userContent として再帰する (ask mistake_limit_reached:2860)。 - 空レスポンス: 1 ラウンドで assistant が text も tool_use も返さなかった場合、
empty_assistant_messageテレメトリを記録し (empty telemetry:3840)、request ID を含む error を say したうえでask("api_req_failed")でユーザーにリトライ判断を委ねる。 - ストリーム中段の失敗: 最初の chunk 以降のストリーム失敗は
recursivelyMakeClineRequestsの外層 try/catch で受け止める (outer catch:3935)。理論上はattemptApiRequest内部ですでに最初の chunk のエラーを catch 済みであり、ここの catch は unhandled rejection を防ぐ二重安全策である。 - partial block の残留: ストリーム終了時に partial block (閉じタグが見えていないもの) が残っていたら、強制的に
partial = falseにし (force partial false:3792)、presentAssistantMessageが進めて最終的にuserMessageContentReady = trueを置けるようにする。さもないとpWaitForが永遠に待ち続ける。 - remote workspace 検出の未完了:
await this.remoteWorkspaceDetectionPromiseを再帰開始時に待ち (remote workspace wait:2801)、presentation scheduler が最初の flush から正しい cadence を使えるようにする。 - apiRequestCount の自増: 各ラウンドで
apiRequestCount++(apiRequestCount++:2804)、apiRequestsSinceLastTodoUpdate++し、focus chain list 管理と todo 更新のテンポに使う。 - checkpoint は再帰の前: ツールがすべて実行され、
userMessageContentReadyになった後にcheckpointManager.saveCheckpointを呼び (saveCheckpoint:3811)、checkpoint がこのラウンドのすべてのファイル変更を反映するようにする。それから次ラウンドの再帰に入る。
まとめ
recursivelyMakeClineRequests は Cline agent loop の「1 ターン駆動器」である。「状態リセット → ストリーム取得 → 解析 → 呈現/実行 → 完了待ち → ツール結果で再帰」というチェーンを繋ぎ、mistake 上限、空レスポンス、abort といった境界をここに集中処理する。ストリーム取得とリトライ部分を見るには /agent-loop/attempt-api-request へ。block を UI やツールに進める最内層を見るには /agent-loop/present-assistant-message へ。全体の状態マシンの境界を見るには /agent-loop/task-class へ。
公式資料: Cline ドキュメント · README