Die Task-Klasse: Ein-Turn-Agent-Kern
Verantwortung
Die Task-Klasse ist Clines «Ein-Turn-Agent»-Zustandsautomat. Eine Task-Instanz entspricht einer Aufgabe (task), von dem Moment an, in dem der Nutzer Enter drückt, bis zu dem Moment, in dem die Aufgabe abgebrochen oder ordnungsgemäß beendet wird. Sie ist kein dauerhafter Dienst, sondern ein vom Controller erzeugtes Wegwerf-Objekt, das eine Runde abarbeitet und dann verworfen wird. Dieses Modell «pro Runde eine Instanz» macht die Nebenläufigkeitskontrolle einfach: Dieselbe Task-Instanz behandelt nie gleichzeitig zwei LLM-Anfragen, weil nur sie selbst aktiv ist.
Was sie tut, lässt sich in drei Abschnitte gliedern: Erstens werden aktuelle Nutzereingabe, Historie, Systemprompt, MCP-Werkzeugliste, Regeldateien usw. zu einer vollständigen API-Anfrage zusammengesetzt; zweitens wird die Streaming-Antwort des LLM angetrieben und nebenbei per parseAssistantMessageV2 in Blöcke vom Typ text / tool_use / reasoning zerlegt; drittens werden diese Blöcke von presentAssistantMessage nacheinander an die UI präsentiert und die darin enthaltenen Werkzeuge ausgeführt. Die Werkzeugresultate fließen als user content der nächsten Runde zurück und lösen eine Rekursion (recursion) aus, bis das Modell keinen tool_use mehr sendet oder der Nutzer abbricht.
Man kann sie als dreischichtig verschachtelte Schleife begreifen: Die äußerste Schicht initiateTaskLoop fängt den Fall ab, dass das Modell keine Werkzeuge ruft und noch einmal nachfragt; die mittlere Schicht recursivelyMakeClineRequests ist pro Rekursion ein vollständiger API-Aufruf + Werkzeugausführung; die innerste Schicht presentAssistantMessage schiebt Blöcke im Stream inkrementell vor. Sämtliche UI-Interaktionen wie «Frage stellen», «Nutzer-Bestätigung einholen», «Fortschritt melden» laufen über die beiden Ausgänge ask und say, die Nachrichten in den messageStateHandler packen und ans Webview posten.
Entwurfsmotivation
- Eine Instanz pro Aufgabe: Zustand wird pro Task-Instanz isoliert, sodass Abbruch, Rollback und Checkpoint an der Instanz als Grenze behandelt werden können.
abortTaskmuss nur das FlagtaskState.abortsetzen; alle Rekursionspfade lesen es und beenden sich selbst (abortTask:1801). - Rekursion statt Schleife: Nach jeder LLM-Antwort ist das Werkzeugresultat von Natur aus der user content der nächsten Runde, deshalb ruft
recursivelyMakeClineRequestsam Ende sich selbst erneut auf (recurse:3830). Diese Schreibweise schreibt den Code für «einzelner API-Aufruf + Werkzeugausführung» nur einmal; die Aufrufstapeltiefe spiegelt natürlich die Turn-Zahl wider, und bei Fehlern bleibt der Stack gut lesbar. - Streaming-Parsing und Präsentation getrennt: Die LLM-Antwort wird schon beim Eintreffen geparst;
parseAssistantMessageV2parst bei jedem Textstück den gesamten Assistant-Text neu (parseAssistantMessageV2 call:3509), undpresentAssistantMessageschiebt an den Block-Grenzen vor, sodass Werkzeuge nicht erst auf die gesamte Antwort warten müssen, bevor sie starten. - Ein einzelnes Lock gegen Zustandskonkurrenz: Alle Task-Zustandsänderungen gehen über
withStateLockund nehmen denselben Mutex (withStateLock:205), damit Streaming-Callback, Werkzeugausführung und UI-Interaktion den Zustand bei dreifacher Nebenläufigkeit nicht korrumpieren. - YOLO-Modus und Mistake-Limit: Bei Erreichen der maximalen aufeinanderfolgenden Fehlerzahl wird direkt abgebrochen (
mistake limit:2826), um zu verhindern, dass das Modell in einer Endlosschleife Token verbrennt.
Schlüsseldateien
Task class definition:188—export class Task, deklariert alle Kernfelder: taskId, taskState, api, controller, messageStateHandler usw.constructor:310— NimmtTaskParamsentgegen, initialisiert clineIgnore, toolExecutor, streamHandler, presentationScheduler und weitere Abhängigkeiten.startTask:1253— Einstieg, bereitet initialen user content vor, führt TaskStart-Hook aus und ruft danninitiateTaskLoopauf.initiateTaskLoop:1717— Äußere while-Schleife; wenn das Modell nur Text zurückgibt und kein Werkzeug aufruft, wird einnoToolsUsed-Hinweis angehängt und erneut nachgefragt.recursivelyMakeClineRequests:2790— Mittlerer rekursiver Treiber; zuständig für Mistake-Limit-Prüfung, Checkpoint-Initialisierung und Aufruf vonattemptApiRequestzum Stream-Ziehen.attemptApiRequest:2175— Wartet auf MCP-Verbindung, liest Regeldateien, baut den Systemprompt und yieldet am Ende den LLM-Strom.presentAssistantMessage:2630— Innerer Block-Vorschreiber, verteilt nach Typ: text wird nach Entfernen der Thinking-Tags gesagt, tool_use geht antoolExecutor.executeTool.ask:789— Ausgang für Antworten vom Nutzer, verwaltet partial-Nachrichtenaktualisierungen und Webview-Antwort-Callbacks.say:969— Einseitiger Ausgang zur Fortschrittsmeldung; der partial-Modus wird für inkrementelle Updates derselben Nachricht verwendet.abortTask:1801— Stufenweiser Abbruch: zuerst entscheiden, ob der TaskCancel-Hook läuft, dann das abort-Flag setzen, dann Hook / Hintergrundkommando abbrechen, zuletzt den Hook ausführen.ToolExecutor.executeTool:212— Task delegiert die Werkzeugausführung nach außen und behandelt selbst kein konkretes Werkzeug direkt.
Datenfluss
Der Kernpfad einer LLM-Anfrage ist «rekursieren → Stream ziehen → parsen → präsentieren → zurückfließen → nochmals rekursieren». recursivelyMakeClineRequests setzt beim Eintritt zuerst den Streaming-Zustand dieser Runde zurück und startet dann attemptApiRequest für den Stream:
// 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;
// ...
const stream = this.attemptApiRequest(previousApiReqIndex); // yields only if the first chunk is successfulDieses Snippet liegt in der Nähe von reset streaming state:3302. Nach dem Reset teilt StreamChunkCoordinator den Stream in text / usage / reasoning Chunk-Arten auf, die jeweils per Callback verarbeitet werden. Bei jedem Text-Stück wird parseAssistantMessageV2 erneut ausgeführt und der gesamte Assistant-Text in ein Block-Array zerlegt (parseAssistantMessageV2 call:3509):
assistantMessage += chunk.text;
assistantTextOnly += chunk.text; // Accumulate text separately
// parse raw assistant message into content blocks
const prevLength = this.taskState.assistantMessageContent.length;
this.taskState.assistantMessageContent =
parseAssistantMessageV2(assistantMessage);Hat sich das Block-Array verändert, wird per scheduleAssistantPresentation presentAssistantMessage getriggert. Letzteres verzweigt nach block.type: text geht über say("text", ...), tool_use über toolExecutor.executeTool(block) (executeTool call:2743). Das Werkzeugresultat wird in taskState.userMessageContent gepackt; wenn der gesamte Stream durchlaufen ist und userMessageContentReady gesetzt wurde, ruft recursivelyMakeClineRequests mit diesem user content sich selbst erneut auf (recurse:3830), bis das Modell keinen tool_use mehr sendet; dann hängt die äußere initiateTaskLoop den noToolsUsed-Hinweis an und fragt noch einmal nach, oder der Nutzer beendet.
Grenzen und Fehler
- Mistake-Limit ausgelöst: Bei Erreichen von
maxConsecutiveMistakesaufeinanderfolgenden Fehlern bricht der YOLO-Modus direkt mitreturn trueab; sonst übernimmtask("mistake_limit_reached")die Entscheidung des Nutzers (mistake limit:2826). - Leere Antwort: Enthält die gesamte Assistant-Antwort weder Text noch tool_use-Block, wird
empty_assistant_message-Telemetrie notiert und der Nutzer zum Retry aufgefordert (empty response:3834). - Nutzer bricht mittendrin ab:
abortTaskerfasst zuerst, ob der TaskCancel-Hook laufen soll, und setzt erst danach dasabort-Flag, damit die Hook-Entscheidung nach dem Setzen des Flags nicht übersehen wird (abortTask:1801). - Werkzeug wird abgelehnt: Sobald
didRejectToolgesetzt ist, werden nachfolgende text-Blöcke direkt übersprungen und der Stream durch[Response interrupted by user feedback]abgeschnitten (didRejectTool:3536). - MCP nicht verbunden:
attemptApiRequestwartet perpWaitFormaximal 10 Sekunden; bei Timeout wird nur protokolliert und nicht blockiert, der Systemprompt wird trotzdem generiert (mcp wait:2177). - Initialer Checkpoint nicht abgeschlossen: Während der erste Checkpoint-Commit läuft, werden nicht-readonly Werkzeuge durch
await this.initialCheckpointCommitPromiseblockiert; readonly Werkzeuge dürfen parallel laufen (initialCheckpoint gate:2737). - Finalize von partial-Blöcken: Residuale partial-Blöcke beim Stream-Ende werden per
partial = falseerzwungen, damitpresentAssistantMessagenormal vorschieben und letztlichuserMessageContentReadysetzen kann (finalize partial blocks:3783).
Zusammenfassung
Die Task-Klasse ist Clines vollständiger Zustandsautomat auf der «Ein-Turn»-Ebene: Sie kapselt den Lebenszyklus einer Aufgabe in eine klare Kette «konstruieren → starten → rekursiv Stream ziehen → Werkzeuge ausführen → abschließen», führt alle UI-Interaktionen über die beiden Ausgänge ask / say zusammen und alle Zustandsänderungen über einen einzigen Mutex. Dieser Kompromiss «pro Runde eine Instanz» macht es einfach, Abbruch, Checkpoint und Mistake-Limit an der Instanz als Einheit zu behandeln.
Wer weiter eintauchen möchte, kann sich anschauen:
- Die Rekursion selbst:
/agent-loop/recursion - LLM-Aufruf und Systemprompt-Zusammensetzung:
/agent-loop/attempt-api-request - Wie der Assistant-Text in Blöcke zerlegt wird:
/agent-loop/parse-assistant-message
Siehe offizielle Dokumentation: Cline-Dokumentation · README