Skip to content

recursivelyMakeClineRequests: 再帰ドライバ

源码版本v4.0.10

役割

recursivelyMakeClineRequests は Task クラス内にある「1 回の再帰 = 1 回の完全な LLM 呼び出し + ツール実行」を担うコアドライバで、apps/vscode/src/core/task/index.tsTask クラスに定義されている。前ラウンドで蓄積した 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") でユーザーに判断を委ねる。これによりモデルが無限ループでトークンを燃やすのを防ぐ。
  • ストリーミング状態の全量リセット: 各ラウンド開始時に currentStreamingContentIndexassistantMessageContentuserMessageContentdidRejectToolpresentAssistantMessageLocked など 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:2826consecutiveMistakeCount >= 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_failed ask に変換される。
  • 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 検出を行い、その後ストリーミング状態をリセットしてストリームを起動する。以下はストリーミング状態リセット + ストリーム起動のコア部分である:

typescript
// 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)、長さが増えれば scheduleAssistantPresentationpresentAssistantMessage に新規 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 プロンプトを差し込んで再帰、完全に空のレスポンスならエラー経路に進む。再帰が返す didEndLoopinitiateTaskLoop まで伝わり、真なら 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