recursivelyMakeClineRequests: Rekursiver Treiber
Verantwortung
recursivelyMakeClineRequests ist der zentrale Treiber in der Task-Klasse, bei dem «jede Rekursion = ein vollständiger LLM-Aufruf + Werkzeugausführung» bedeutet; definiert in der Task-Klasse in apps/vscode/src/core/task/index.ts. Er nimmt den akkumulierten userContent der vorigen Runde (enthält Werkzeugresultate, Nutzer-Feedback, noToolsUsed-Hinweis usw.), ruft attemptApiRequest auf, um an den Stream zu kommen, übergibt den Stream an StreamChunkCoordinator zur Verteilung, wartet danach, bis alle Blöcke durch presentAssistantMessage abgearbeitet sind, und ruft sich dann mit dem in dieser Runde angesammelten taskState.userMessageContent selbst erneut auf. Das Konzept des «Turns» im gesamten Agent-Loop steckt in dieser Rekursion.
Sie sitzt in der Mitte der dreischichtigen verschachtelten Schleife. Die äußerste Schicht ist initiateTaskLoop, die mit while (!abort) den Fall abfängt, dass das Modell nur Text zurückgibt und keine Werkzeuge aufruft; sie hängt einen noToolsUsed-Hinweis an und geht erneut in die Rekursion (initiateTaskLoop:1717). Die mittlere Schicht ist recursivelyMakeClineRequests selbst, wobei jeder Aufruf eine LLM-API-Anfrage repräsentiert. Die innerste Schicht ist presentAssistantMessage, das durch Stream-Callbacks oder den Scheduler immer wieder geweckt wird, um Blöcke vorzuschieben. recursivelyMakeClineRequests ruft sich am Ende mit await this.recursivelyMakeClineRequests(this.taskState.userMessageContent) selbst auf (recurse:3830); die Werkzeugresultate sind von Natur aus der user content der nächsten Runde, ohne dass zusätzliche Orchestrierung nötig wäre.
Entwurfsmotivation
- Rekursion statt while-Schleife: Nach jeder LLM-Antwort ist das Werkzeugresultat der user content der nächsten Runde, deshalb heißt es am Funktionsende schlicht
await this.recursivelyMakeClineRequests(this.taskState.userMessageContent)(recurse:3830). Diese Schreibweise schreibt «einzelner API-Aufruf + Werkzeugausführung + Zustandsreset» nur einmal; die Aufrufstapeltiefe spiegelt natürlich die Turn-Zahl wider, und bei Fehlern bleibt der Stack lesbar. - Mistake-Limit vorab prüfen: Am Funktionsbeginn wird sofort
consecutiveMistakeCount >= maxConsecutiveMistakesgeprüft (mistake limit check:2826); im YOLO-Modus wird die Aufgabe mitreturn truebeendet, sonst übernimmtask("mistake_limit_reached")die Entscheidung des Nutzers. So wird verhindert, dass das Modell in einer Endlosschleife Token verbrennt. - Vollständiger Reset des Streaming-Zustands: Zu Beginn jeder Runde werden
currentStreamingContentIndex,assistantMessageContent,userMessageContent,didRejectTool,presentAssistantMessageLockedund rund zehn weitere Felder auf null gesetzt (reset streaming state:3302), damit die aktuelle Runde nicht durch Reste der vorigen beeinflusst wird. Der Reset deckt auchstreamHandler.reset()undpresentationScheduler.reset()ab. - StreamChunkCoordinator teilt den Strom: Statt den Stream direkt per
for awaitzu konsumieren, wird er von einerStreamChunkCoordinator-Hülle umschlossen (stream coordinator:3368), die den Strom inreasoning/text/usagedrei Chunk-Arten aufteilt und jeweils per Callback weiterreicht. Reasoning geht an den Reasoning-Handler, Text anparseAssistantMessageV2zum Neu-Parse, Usage akkumuliert Token-Zählung. - pWaitFor userMessageContentReady: Nach Stream-Ende wird nicht sofort rekursiert, sondern
await pWaitFor(() => this.taskState.userMessageContentReady)abgewartet (pWaitFor ready:3808), bispresentAssistantMessagealle Blöcke verarbeitet hat. Das stellt sicher, dass erst dann, wenn alle Werkzeugresultate inuserMessageContentakkumuliert sind, die nächste Runde betreten wird. - noToolsUsed inkrementiert mistake: Enthält die gesamte Assistant-Antwort keinen einzigen tool_use-Block, wird der
formatResponse.noToolsUsed-Text in userMessageContent gepackt undconsecutiveMistakeCount++ausgeführt (noToolsUsed:3818). In der nächsten Runde wird das Modell darauf hingewiesen, «entweder ein Werkzeug rufen oder attempt_completion»; bei andauernder Werkzeuglosigkeit wird die Runde über das Mistake-Limit beendet. - Leere Antwort geht in den Fehlerpfad: Enthält die gesamte Assistant-Antwort weder Text noch tool_use, wird
empty_assistant_message-Telemetrie notiert, ein error gesagt und anschließendask("api_req_failed")ausgeführt, damit der Nutzer über einen Retry entscheidet (empty response:3834) – die Aufgabe wird nicht stillschweigend fortgesetzt.
Schlüsseldateien
recursivelyMakeClineRequests:2790— Funktionseinstieg, Signatur(userContent, includeFileDetails?) => Promise<boolean>, RückgabewertdidEndLoop.abort check:2795— Sofortiges Prüfen vontaskState.abortbeim Eintritt; bei Abbruch wirdTask instance abortedgeworfen.apiRequestCount++:2804— Inkrementiert den Anfragezähler, verwendet für focus chain list-Management.mistake limit check:2826— BeiconsecutiveMistakeCount >= maxConsecutiveMistakeswird der Mistake-Behandlungszweig betreten.yolo fail:2841— Im YOLO-Modus direkt say error + return true zum Abbruch.ask mistake_limit_reached:2860— Im Nicht-YOLO-Modus ask an den Nutzer; der Nutzer kann mit neuem Prompt weitermachen.reset streaming state:3302— Vollständiger Reset des Streaming-Zustands, rund zehn Felder auf null + handler/scheduler reset.attemptApiRequest call:3319— Holt den Stream; schlägt yield des ersten Chunks fehl, wird er durch den internen try/catch von attemptApiRequest in einenapi_req_failed-ask umgewandelt.StreamChunkCoordinator:3368— Hüllt den Stream und teilt ihn in reasoning/text/usage drei Chunk-Arten auf.while true chunk loop:3387— Haupt-Konsumschleife, holt vom Coordinator den nächsten Chunk und verteilt per switch.accumulate assistantMessage:3503— Text-Chunk wird in assistantMessage + assistantTextOnly akkumuliert und danach neu geparst.force partial false:3792— Nach Stream-Ende werden residuelle partial tool-Blöcke perpartial = falseerzwungen, damit presentAssistantMessage finalisieren kann.pWaitFor ready:3808— Wartet, bis alle Blöcke verarbeitet sind und userMessageContentReady wahr ist.noToolsUsed bump:3818— Bei keinem Werkzeugaufruf wird der noToolsUsed-Hinweis angehängt + mistake++.recurse:3830— Ruft sich mit dem akkumulierten userMessageContent selbst auf; gibt didEndLoop zurück.empty response:3834— Pfad für leere Antwort, say error + ask api_req_failed.outer catch:3935— Auffang-Catch; theoretisch hat attemptApiRequest selbst schon abgefangen, hier ist es Doppelabsicherung.initiateTaskLoop:1717— Äußere while, behandelt noToolsUsed-Hinweis + didEndLoop-Ausstieg.
Datenfluss
Bei jedem Eintritt in recursivelyMakeClineRequests erfolgt zuerst die Mistake-Limit-Prüfung und die Remote-Workspace-Erkennung, danach wird der Streaming-Zustand zurückgesetzt und der Stream gestartet. Das folgende Snippet ist der Kern aus Reset des Streaming-Zustands und Stream-Start:
// 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);Dieses Snippet liegt in der Nähe von reset streaming state:3302. Nach dem Reset übernimmt StreamChunkCoordinator den Stream (stream coordinator:3368), und die while (true)-Schleife holt Chunks vom Coordinator. Text-Chunks gehen in assistantMessage += chunk.text; assistantTextOnly += chunk.text; und werden sofort per parseAssistantMessageV2(assistantMessage) neu geparst (parseAssistantMessageV2 call:3509); ist die Länge gewachsen, wird scheduleAssistantPresentation aufgerufen, damit presentAssistantMessage den neuen Block vorschiebt. Reasoning-Chunks gehen an den Reasoning-Handler und inkrementieren die Thinking-Nachricht. Usage-Chunks akkumulieren Token-Zählung und Kosten. Nach Stream-Ende behandelt processNativeToolCalls native Tool-Calls, flushAssistantPresentationOrThrow erzwingt den Finalize für residuelle partial-Blöcke, und danach await pWaitFor(() => userMessageContentReady) wartet darauf, dass alle Blöcke durch presentAssistantMessage verarbeitet sind (pWaitFor ready:3808). Danach wird geprüft, ob tool_use vorhanden ist: Wenn ja, wird ein Checkpoint gesetzt + mit userMessageContent rekursiert; wenn nein, wird der noToolsUsed-Hinweis angehängt und rekursiert; eine komplett leere Antwort geht in den Fehlerpfad. Der didEndLoop-Wert aus der Rekursion wird bis zu initiateTaskLoop durchgereicht; ist er wahr, wird die while beendet, ist er falsch, hängt die Außenschicht den noToolsUsed-Hinweis an und startet eine weitere Runde.
Grenzen und Fehler
- abort hat Vorrang vor allem: Die erste Zeile der Funktion prüft
taskState.abort(abort check:2795); bei Abbruch wird direktTask instance abortedgeworfen, ohne Streaming-Reset oder API-Aufruf. Das garantiert, dass nach einem Abbruch auch hängende Rekursionen sofort aussteigen. - Mistake-Limit: Im YOLO-Modus
return truezum Abbruch der Aufgabe (yolo return:2847); im Nicht-YOLO-Modus ask an den Nutzer; gibt der Nutzer einen neuen Prompt, wird dieser als userContent der nächsten Runde rekursiert (ask mistake_limit_reached:2860). - Leere Antwort: Enthält die gesamte Assistant-Antwort weder Text noch tool_use, wird
empty_assistant_message-Telemetrie notiert (empty telemetry:3840); der say-error-Text enthält die Request-ID, und anschließendask("api_req_failed")für die Retry-Entscheidung des Nutzers. - Stream-Abbruch in der Mitte: Ein Stream-Fehler nach dem ersten Chunk wird vom äußeren try/catch von
recursivelyMakeClineRequestsübernommen (outer catch:3935); theoretisch hatattemptApiRequestintern schon den ersten Chunk-Fehler abgefangen, dieser catch ist Doppelabsicherung gegen unhandled rejection. - Residuale partial-Blöcke: Ist beim Stream-Ende noch ein partial-Block vorhanden (kein schließender Tag erkannt), wird
partial = falseerzwungen (force partial false:3792), damitpresentAssistantMessagevorschieben und letztlichuserMessageContentReady = truesetzen kann – sonst würdepWaitForendlos warten. - Remote-Workspace-Erkennung nicht abgeschlossen:
await this.remoteWorkspaceDetectionPromisewird am Beginn der Rekursion erwartet (remote workspace wait:2801), damit der Presentation Scheduler ab dem ersten Flush die korrekte Cadence nutzt. - apiRequestCount-Inkrement: In jeder Runde
apiRequestCount++(apiRequestCount++:2804) undapiRequestsSinceLastTodoUpdate++, verwendet für focus chain list-Management und Todo-Aktualisierungsrhythmus. - Checkpoint vor der Rekursion: Nachdem alle Werkzeuge ausgeführt sind und
userMessageContentReadygesetzt ist, wirdcheckpointManager.saveCheckpointaufgerufen (saveCheckpoint:3811), damit der Checkpoint alle Dateiänderungen dieser Runde erfasst. Erst danach erfolgt die nächste Rekursionsrunde.
Zusammenfassung
recursivelyMakeClineRequests ist der «Ein-Turn-Treiber» des Cline-Agent-Loops. Es verknüpft die Kette «Zustand resetten → Stream ziehen → parsen → präsentieren/ausführen → auf Abschluss warten → mit Werkzeugresultat rekursieren» und bündelt die Behandlung der Grenzen wie Mistake-Limit, leere Antwort und abort an dieser Stelle. Wie es den Stream zieht und retryed, steht in /agent-loop/attempt-api-request; die innerste Schicht, die Blöcke an UI und Werkzeuge vorschiebt, in /agent-loop/present-assistant-message; die Grenzen des Gesamt-Zustandsautomaten in /agent-loop/task-class.
Siehe offizielle Dokumentation: Cline-Dokumentation · README