Skip to content

recursivelyMakeClineRequests: Rekursiver Treiber

源码版本v4.0.10

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 >= maxConsecutiveMistakes geprüft (mistake limit check:2826); im YOLO-Modus wird die Aufgabe mit return true beendet, sonst übernimmt ask("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, presentAssistantMessageLocked und 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 auch streamHandler.reset() und presentationScheduler.reset() ab.
  • StreamChunkCoordinator teilt den Strom: Statt den Stream direkt per for await zu konsumieren, wird er von einer StreamChunkCoordinator-Hülle umschlossen (stream coordinator:3368), die den Strom in reasoning / text / usage drei Chunk-Arten aufteilt und jeweils per Callback weiterreicht. Reasoning geht an den Reasoning-Handler, Text an parseAssistantMessageV2 zum 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), bis presentAssistantMessage alle Blöcke verarbeitet hat. Das stellt sicher, dass erst dann, wenn alle Werkzeugresultate in userMessageContent akkumuliert 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 und consecutiveMistakeCount++ 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ßend ask("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ückgabewert didEndLoop.
  • abort check:2795 — Sofortiges Prüfen von taskState.abort beim Eintritt; bei Abbruch wird Task instance aborted geworfen.
  • apiRequestCount++:2804 — Inkrementiert den Anfragezähler, verwendet für focus chain list-Management.
  • mistake limit check:2826 — Bei consecutiveMistakeCount >= maxConsecutiveMistakes wird 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 einen api_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 per partial = false erzwungen, 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:

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);

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 direkt Task instance aborted geworfen, ohne Streaming-Reset oder API-Aufruf. Das garantiert, dass nach einem Abbruch auch hängende Rekursionen sofort aussteigen.
  • Mistake-Limit: Im YOLO-Modus return true zum 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ßend ask("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 hat attemptApiRequest intern 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 = false erzwungen (force partial false:3792), damit presentAssistantMessage vorschieben und letztlich userMessageContentReady = true setzen kann – sonst würde pWaitFor endlos warten.
  • Remote-Workspace-Erkennung nicht abgeschlossen: await this.remoteWorkspaceDetectionPromise wird 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) und apiRequestsSinceLastTodoUpdate++, verwendet für focus chain list-Management und Todo-Aktualisierungsrhythmus.
  • Checkpoint vor der Rekursion: Nachdem alle Werkzeuge ausgeführt sind und userMessageContentReady gesetzt ist, wird checkpointManager.saveCheckpoint aufgerufen (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