attemptApiRequest: ストリーミング LLM 呼び出しとリトライ
役割
attemptApiRequest は Task の中で「LLM provider と通信する」部分を担うジェネレータ (generator) メソッドである。async * 関数で、ストリームの chunk を上層の recursivelyMakeClineRequests に yield していく。yield する前には多くの準備作業がある: MCP サーバーの接続完了を待つ、ルールファイルを読む、システムプロンプトを組み立てる、コンテキストトランケーション (context truncation) を行う。最初の chunk でエラーが出たときは、自動リトライするか、ユーザーに問うか、諦めるかを判断する。
このメソッドの位置は agent loop の中層である。recursivelyMakeClineRequests は再帰のたびに attemptApiRequest を 1 回呼び、ストリームを取得した後は外側の while ループが chunk を消費し、解析し、presentAssistantMessage を呼ぶ。したがってこのメソッド自身はストリームを消費せず、「ストリームを準備して安全に引き渡す」ことだけを担当する。
出力型は ApiStream で、本質は非同期イテレータである。上層はまず iterator.next() で最初の chunk を試し取りする (first chunk probe:2389)。最初の chunk が成功すれば yield* iterator で残りをすべて引き渡し、失敗すればエラー分類とリトライロジックに進む。この「最初の 1 chunk で生存確認」設計により、「ストリーム前失敗」と「ストリーム中失敗」を分けて処理できる。前者は状態がクリーンなので副作用なくリトライ可能だが、後者はすでに一部ツールが実行されている可能性があり、単純なリトライはできない。
設計動機
- MCP 待機には上限:
pWaitForでmcpHub.isConnectingが偽になるのを待ち、10 秒でタイムアウトしたら error を記録して先へ進む (mcp wait:2177)。MCP でリクエスト全体が止まるのを防ぐ。 - SystemPromptContext を一箇所に集約: cwd、IDE、provider 情報、ルールファイル、clineignore、スキル、editor tabs、parallel tool calling などをすべて 1 つの context オブジェクトに詰め込む (
promptContext:2300)。getSystemPromptが一度に消費し、プロンプト組み立てロジックが散らばるのを防ぐ。 - コンテキストトランケーションは呼び出しの前:
contextManager.getNewContextMessagesAndMetadataが対話履歴をモデルのコンテキストウィンドウ内に収まるよう裁断する (context truncate:2354)。裁断過程で削除範囲の更新が必要と分かった場合は、即座に clineMessages に書き戻して永続化する。 - 最初の chunk の生存確認戦略:
isWaitingForFirstChunkフラグ + 個別の try/catch でiterator.next()を包み、最初の chunk 失敗時は状態がまだクリーンなため安全にリトライできる (first chunk try:2388)。 - エラー分類でリトライ判断: auth、spend limit、quota、entitlement、ClinePass 上限、insufficient credits など「リトライしても無駄」なエラーは自動リトライをスキップし (
shouldRetry gate:2495)、その他のエラーは最大 3 回まで自動リトライし、2s/4s/8s の指数バックオフで遅延する (backoff:2509)。
主要ファイル
attemptApiRequest:2175— メソッド本体。MCP 待機からyield*までがすべてここにある。pWaitFor mcpHub:2177— MCP サーバーの接続を待ち、10 秒でタイムアウト。SystemPromptContext:2300— すべてのプロンプト入力を 1 つのオブジェクトに集め、getSystemPromptに渡す。getSystemPrompt:2350— 最終的な systemPrompt と tools 配列を生成。getNewContextMessagesAndMetadata:2354— コンテキストマネージャが対話履歴を裁断し、トランケート後のメッセージとメタデータを返す。deleted range update:2366— 裁断で削除範囲が更新された場合、即座に clineMessages に永続化。createMessage:2378— api handler でストリームを作り、systemPrompt、truncated history、tools を渡す。first chunk probe:2388— 個別の try/catch で最初の chunk を取り、失敗時はエラー分類へ。context window check:2393—checkContextWindowExceededErrorでコンテキスト超過かを判定。handleContextWindowExceededError:2409— コンテキスト超過時に自動でトランケートして 1 回リトライ。shouldRetry gate:2495— リトライ無意味なエラー型を除外。exponential backoff:2509— 2s/4s/8s の 3 回自動リトライ、遅延漸増。yield* recurse:2620— リトライ成功後に自身を再帰呼び出しし、新しいストリームの chunk を上層に引き渡す。
データフロー
リクエストは準備から yield まで「MCP 待機 → プロンプト組み立て → 履歴トランケート → ストリーム作成 → 生存確認 → 引き渡し」の主パスを通る。履歴トランケートのステップが鍵で、モデルが実際に何を見るかを決める:
// apps/vscode/src/core/task/index.ts
const contextManagementMetadata =
await this.contextManager.getNewContextMessagesAndMetadata(
this.messageStateHandler.getApiConversationHistory(),
this.messageStateHandler.getClineMessages(),
this.api,
this.taskState.conversationHistoryDeletedRange,
previousApiReqIndex,
await ensureTaskDirectoryExists(this.taskId),
this.stateManager.getGlobalSettingsKey("useAutoCondense") &&
isNextGenModelFamily(this.api.getModel().id),
);
if (contextManagementMetadata.updatedConversationHistoryDeletedRange) {
this.taskState.conversationHistoryDeletedRange =
contextManagementMetadata.conversationHistoryDeletedRange;
await this.messageStateHandler.saveClineMessagesAndUpdateHistory();
// saves task history item which we use to keep track of conversation history deleted range
}ContextManager は完全な対話履歴 + 前回記録された削除範囲 + 前回リクエストのトークン使用量を受け取り、新しいトランケート範囲を算出する。範囲に変化があれば、即座に clineMessages をディスクに書き込む——次のリクエストがこの新しい範囲に依存するためである。useAutoCondense は新モデル (next-gen) 専用の自動圧縮スイッチで、有効化するとモデル自身が生成した要約で初期メッセージを置き換える (autocondense flag:2362)。
トランケート完了後にストリームを作成し、生存確認を行う:
// apps/vscode/src/core/task/index.ts
const stream = this.api.createMessage(
systemPrompt,
truncatedConversationHistory,
tools,
);
const iterator = stream[Symbol.asyncIterator]();
try {
// awaiting first chunk to see if it will throw an error
this.taskState.isWaitingForFirstChunk = true;
const firstChunk = await iterator.next();
yield firstChunk.value;
this.taskState.isWaitingForFirstChunk = false;
} catch (error) {
// ... エラー分類、自動リトライ、またはユーザーに問う
yield* this.attemptApiRequest(previousApiReqIndex);
return;
}isWaitingForFirstChunk フラグは外層に「いま最初の chunk を待っている、ストリーム中状態として扱わないで」と伝える。最初の chunk 失敗時の catch 分岐はエラー型で切り分ける: コンテキストウィンドウ超過かつ自動リトライ未実施なら handleContextWindowExceededError で自動トランケートしてリトライ、その他のエラーは shouldRetry で自動バックオフリトライまたは ask("api_req_failed") でユーザーに判断を委ねる。
境界と失敗
- MCP タイムアウトは致命的でない: MCP が 10 秒で繋がらない場合は error 記録だけで先へ進む (
mcp timeout catch:2179)。以降の system prompt では MCP ツールが欠落する可能性があるが、リクエスト自体はそれで失敗しない。 - コンテキスト超過の自動リトライは 1 回だけ:
didAutomaticallyRetryFailedApiRequestフラグがhandleContextWindowExceededErrorの呼び出しを 1 回だけに制限する (auto retry flag:2407)。2 回目も超過したらask("api_req_failed")に切り替え、判断をユーザーに委ねる。 - 対話がすでに短いのに超過: トランケート後のメッセージ数が 3 以下の場合は「context window exceeded, retry to truncate」をユーザーに伝えるだけでするが、自動トランケートは行わない (
conversation bricked:2423)。この状況は対話が基本的に駄目になっており、ユーザーの介入が必要である。 - api_req_started 状態の更新: リトライのたびに最後の
api_req_startedメッセージを探し、そのstreamingFailedMessageとretryStatusを更新する (update api_req_started:2433)。UI はこのフィールドで「リトライ中」「リトライ枯渇」状態を表示する。 - error_retry 二重表示の重複排除: 自動リトライ時はまず
say("error_retry", ...)で完全な情報を出し、その後api_req_startedからstreamingFailedMessageを削除して、同一エラーが ErrorRow と error_retry の両方に表示されるのを防ぐ (dedupe error display:2548)。 - リトライカウンタの手動ゼロ化: ユーザーが yes でリトライするとき
autoRetryAttemptsをゼロに戻し (reset counter:2586)、以降の失敗に 3 回の自動リトライ枠を再確保する。 - 最初の chunk 以降の失敗はここでは扱わない: 最初の chunk 成功後のストリーム中失敗は
recursivelyMakeClineRequestsの外層 try/catch が受け持つ (stream-mid failure:3935)。ここが管轄するのは「ストリーム開始前」の失敗だけである。
まとめ
attemptApiRequest は「準備 + 生存確認 + リトライ」の 3 つを一体化したものである。準備段階ではルール、スキル、MCP、コンテキスト裁断を一通り走らせ、生存確認段階では最初の chunk でリトライ可否を決め、リトライ段階ではエラー分類 + 指数バックオフ + ユーザーフォールバックの三層で受ける。ストリームが一度始まると yield* で引き渡され、以降はこのメソッドの管轄外である。ストリームが引き渡された後の消費や block への切り分けを見るには /agent-loop/present-assistant-message へ。さらに外側の再帰ドライバを見るには /agent-loop/task-class へ。
公式資料: Cline ドキュメント · README