Skip to content

attemptApiRequest: ストリーミング LLM 呼び出しとリトライ

源码版本v4.0.10

役割

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 待機には上限: pWaitFormcpHub.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)。

主要ファイル

データフロー

リクエストは準備から yield まで「MCP 待機 → プロンプト組み立て → 履歴トランケート → ストリーム作成 → 生存確認 → 引き渡し」の主パスを通る。履歴トランケートのステップが鍵で、モデルが実際に何を見るかを決める:

typescript
// 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)。

トランケート完了後にストリームを作成し、生存確認を行う:

typescript
// 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 メッセージを探し、その streamingFailedMessageretryStatus を更新する (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