Skip to content

Die Task-Klasse: Ein-Turn-Agent-Kern

源码版本v4.0.10

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. abortTask muss nur das Flag taskState.abort setzen; 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 recursivelyMakeClineRequests am 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; parseAssistantMessageV2 parst bei jedem Textstück den gesamten Assistant-Text neu (parseAssistantMessageV2 call:3509), und presentAssistantMessage schiebt 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 withStateLock und 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:188export class Task, deklariert alle Kernfelder: taskId, taskState, api, controller, messageStateHandler usw.
  • constructor:310 — Nimmt TaskParams entgegen, initialisiert clineIgnore, toolExecutor, streamHandler, presentationScheduler und weitere Abhängigkeiten.
  • startTask:1253 — Einstieg, bereitet initialen user content vor, führt TaskStart-Hook aus und ruft dann initiateTaskLoop auf.
  • initiateTaskLoop:1717 — Äußere while-Schleife; wenn das Modell nur Text zurückgibt und kein Werkzeug aufruft, wird ein noToolsUsed-Hinweis angehängt und erneut nachgefragt.
  • recursivelyMakeClineRequests:2790 — Mittlerer rekursiver Treiber; zuständig für Mistake-Limit-Prüfung, Checkpoint-Initialisierung und Aufruf von attemptApiRequest zum 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 an toolExecutor.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:

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;
// ...
const stream = this.attemptApiRequest(previousApiReqIndex); // yields only if the first chunk is successful

Dieses 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):

typescript
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 maxConsecutiveMistakes aufeinanderfolgenden Fehlern bricht der YOLO-Modus direkt mit return true ab; sonst übernimmt ask("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: abortTask erfasst zuerst, ob der TaskCancel-Hook laufen soll, und setzt erst danach das abort-Flag, damit die Hook-Entscheidung nach dem Setzen des Flags nicht übersehen wird (abortTask:1801).
  • Werkzeug wird abgelehnt: Sobald didRejectTool gesetzt ist, werden nachfolgende text-Blöcke direkt übersprungen und der Stream durch [Response interrupted by user feedback] abgeschnitten (didRejectTool:3536).
  • MCP nicht verbunden: attemptApiRequest wartet per pWaitFor maximal 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.initialCheckpointCommitPromise blockiert; readonly Werkzeuge dürfen parallel laufen (initialCheckpoint gate:2737).
  • Finalize von partial-Blöcken: Residuale partial-Blöcke beim Stream-Ende werden per partial = false erzwungen, damit presentAssistantMessage normal vorschieben und letztlich userMessageContentReady setzen 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