attemptApiRequest: Streaming-LLM-Aufruf und Retry
Verantwortung
attemptApiRequest ist die Generator-Methode in der Task-Klasse, die für «die Kommunikation mit dem LLM-Provider» zuständig ist. Sie ist eine async *-Funktion, die einzelne Stream-Chunks an den aufrufenden recursivelyMakeClineRequests yieldet. Vor dem yielden erledigt sie eine ganze Reihe von Vorbereitungen: auf MCP-Server warten, Regeldateien lesen, Systemprompt zusammenstellen, Kontextabschnitt (context truncation) vornehmen. Tritt beim ersten Chunk ein Fehler auf, entscheidet sie, ob automatisch erneut versucht wird, den Nutzer gefragt wird oder direkt abgebrochen wird.
Sie sitzt in der mittleren Schicht des Agent-Loops. recursivelyMakeClineRequests ruft bei jeder Rekursion einmal attemptApiRequest auf, übergibt den Strom danach aber an die äußere while-Schleife, die Chunks konsumiert, parst und presentAssistantMessage aufruft. Die Methode selbst konsumiert den Stream also nicht, sondern ist ausschließlich dafür zuständig, «den Stream vorzubereiten und sicher herauszugeben».
Ihr Rückgabetyp ist ApiStream, im Wesentlichen ein asynchroner Iterator. Die aufrufende Schicht holt sich mit iterator.next() zuerst den ersten Chunk zum Prüfversuch (first chunk probe:2389). Gelingt der erste Chunk, wird per yield* iterator der gesamte Rest weitergereicht; schlägt er fehl, geht es in die Fehlerklassifikation und Retry-Logik. Dieses «erst einen Chunk testen»-Design trennt das «Scheitern vor Stream-Beginn» vom «Scheitern während des Streams», weil ersteres einen sauberen Zustand hinterlässt, der retry-geeignet ist, während letzteres eventuell schon teilweise Werkzeuge ausgeführt hat und nicht einfach wiederholt werden darf.
Entwurfsmotivation
- MCP-Wartezeit ist begrenzt: Mit
pWaitForwird gewartet, bismcpHub.isConnectingfalse wird; nach 10 Sekunden Timeout wird der Fehler nur protokolliert und weitergemacht (mcp wait:2177). MCP darf nicht die gesamte Anfrage blockieren. - SystemPromptContext als Sammelbecken: cwd, IDE, Provider-Informationen, Regeldateien, clineignore, Skills, Editor-Tabs, Parallel Tool Calling usw. werden in einem einzigen Context-Objekt gesammelt (
promptContext:2300).getSystemPromptkonsumiert sie in einem Rutsch, damit die Prompt-Zusammensetzungslogik nicht verstreut. - Kontextabschnitt vor dem Aufruf:
contextManager.getNewContextMessagesAndMetadataschneidet die Dialoghistorie auf das Kontextfenster des Modells zurecht (context truncate:2354). Ergibt sich beim Abschneiden eine aktualisierte Löschspanne, wird sie sofort in clineMessages zurückgeschrieben und persistent gespeichert. - First-Chunk-Prüfstrategie: Das
isWaitingForFirstChunk-Flag plus ein separater try/catch umiterator.next()stellen sicher, dass der Zustand beim Fehlschlagen des ersten Chunks noch sauber und ein Retry sicher ist (first chunk try:2388). - Fehlerklassifikation entscheidet über Retry: auth, spend limit, quota, entitlement, ClinePass-Limit, insufficient credits – Fehler, bei denen ein Retry nichts bringt – überspringen das automatische Retry (
shouldRetry gate:2495); andere Fehler werden bis zu 3 Mal automatisch erneut versucht, mit 2s/4s/8s exponentiellem Backoff (backoff:2509).
Schlüsseldateien
attemptApiRequest:2175— Methodenrumpf, vom MCP-Warten bisyield*ist hier alles drin.pWaitFor mcpHub:2177— Wartet auf MCP-Server-Verbindung, 10 Sekunden Timeout.SystemPromptContext:2300— Sammelt alle Prompt-Eingaben in einem Objekt, bereit fürgetSystemPrompt.getSystemPrompt:2350— Erzeugt den endgültigen systemPrompt und das Tools-Array.getNewContextMessagesAndMetadata:2354— Der Kontext-Manager schneidet die Dialoghistorie zurecht und liefert abgeschnittene Nachrichten und Metadaten.deleted range update:2366— Wenn der Abschnitt die Löschspanne aktualisiert, sofort in clineMessages persistent speichern.createMessage:2378— Erzeugt mit dem API-Handler den Stream, übergibt systemPrompt, abgeschnittene Historie, tools.first chunk probe:2388— Separater try/catch für den ersten Chunk, bei Fehlschlag Fehlerklassifikation.context window check:2393—checkContextWindowExceededErrorprüft, ob der Kontext zu lang ist.handleContextWindowExceededError:2409— Bei Kontextüberlänge automatisch abschneiden und einmal erneut versuchen.shouldRetry gate:2495— Schließt Fehlerarten aus, bei denen ein Retry sinnlos ist.exponential backoff:2509— 2s/4s/8s drei automatische Retrys mit steigender Verzögerung.yield* recurse:2620— Nach erfolgreichem Retry rekursiver Selbstaufruf, reicht Chunks des neuen Streams nach oben weiter.
Datenfluss
Der Hauptpfad von der Vorbereitung bis zum yielden ist «MCP warten → Prompt bauen → Historie abschneiden → Stream erzeugen → Probe → weiterreichen». Der Abschnitt der Historie ist der entscheidende Schritt, weil er festlegt, was das Modell tatsächlich sieht:
// 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 nimmt die vollständige Dialoghistorie + die zuletzt notierte Löschspanne + den Token-Verbrauch der letzten Anfrage und berechnet die neue Abschnittsspanne. Ändert sich die Spanne, werden clineMessages sofort auf die Platte geschrieben – die nächste Anfrage ist auf diesen neuen Bereich angewiesen. useAutoCondense ist ein automatischer Kondensierungs-Schalter ausschließlich für Next-Generation-Modelle; wenn aktiv, generiert das Modell selbst Zusammenfassungen, die frühe Nachrichten ersetzen (autocondense flag:2362).
Nach dem Abschneiden wird erst der Stream erzeugt und geprüft:
// 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) {
// ... Fehlerklassifikation, automatisches Retry oder Nutzer fragen
yield* this.attemptApiRequest(previousApiReqIndex);
return;
}Das isWaitingForFirstChunk-Flag sagt der Außenschicht: «Ich warte gerade auf den ersten Chunk, behandle mich nicht als Mid-Stream-Zustand». Der catch-Zweig beim ersten Chunk verzweigt nach Fehlertyp: Bei Kontextfenster-Überlänge, sofern noch kein Auto-Retry stattfand, ruft er handleContextWindowExceededError auf und schneidet automatisch ab; andere Fehler gehen je nach shouldRetry entweder den Backoff-Retry-Pfad oder ask("api_req_failed"), um die Entscheidung an den Nutzer zu übergeben.
Grenzen und Fehler
- MCP-Timeout ist nicht fatal: Wenn MCP nach 10 Sekunden nicht verbindet, wird nur ein error protokolliert und weitergemacht (
mcp timeout catch:2179). Im folgenden Systemprompt können MCP-Werkzeuge dann fehlen, aber die Anfrage scheitert deshalb nicht. - Kontextüberlänge wird nur einmal automatisch wiederholt: Das
didAutomaticallyRetryFailedApiRequest-Flag stellt sicher, dasshandleContextWindowExceededErrornur einmal aufgerufen wird (auto retry flag:2407). Ist sie beim zweiten Mal immer noch zu lang, geht es zuask("api_req_failed")und die Entscheidung geht an den Nutzer. - Dialog ist bereits sehr kurz und trotzdem zu lang: Wenn die abgeschnittene Nachrichtenzahl ≤ 3 ist, wird dem Nutzer zwar «context window exceeded, retry to truncate» gemeldet, aber nicht mehr automatisch abgeschnitten (
conversation bricked:2423). In diesem Fall ist der Dialog im Wesentlichen kaputt und der Nutzer muss eingreifen. - api_req_started-Statusaktualisierung: Bei jedem Retry wird die letzte
api_req_started-Nachricht gesucht und ihrstreamingFailedMessageundretryStatusaktualisiert (update api_req_started:2433). Die UI nutzt dieses Feld, um «retry läuft» / «Retrys erschöpft» anzuzeigen. - Doppelte error_retry-Anzeige deduplizieren: Beim automatischen Retry wird zuerst
say("error_retry", ...)mit vollständiger Information gemacht, danach wirdstreamingFailedMessageausapi_req_startedentfernt, damit derselbe Fehler nicht gleichzeitig in ErrorRow und error_retry angezeigt wird (dedupe error display:2548). - Retry-Zähler manuell zurücksetzen: Wenn der Nutzer yes zum Retry klickt, wird
autoRetryAttemptsauf null gesetzt (reset counter:2586), um für künftige Fälle wieder 3 automatische Retrys offen zu halten. - Fehler nach dem ersten Chunk werden hier nicht behandelt: Scheitert der Stream nach dem ersten Chunk, übernimmt der äußere try/catch von
recursivelyMakeClineRequests(stream-mid failure:3935). Hier geht es nur um das «Scheitern vor Stream-Beginn».
Zusammenfassung
attemptApiRequest ist die Zusammenfassung von drei Dingen: vorbereiten, prüfen, wiederholen. Die Vorbereitungsphase lässt Regeln, Skills, MCP und Kontextabschnitt durchlaufen; die Prüfphase entscheidet am ersten Chunk über einen Retry; die Retry-Phase nutzt drei Schichten (Fehlerklassifikation + exponentielles Backoff + Nutzer-Fallback). Sobald der Stream einmal läuft, wird er per yield* herausgegeben und ist nicht mehr ihre Sache. Wie der herausgegebene Stream konsumiert und in Blöcke zerlegt wird, steht in /agent-loop/present-assistant-message; die äußere Rekursion steht in /agent-loop/task-class.
Siehe offizielle Dokumentation: Cline-Dokumentation · README