presentAssistantMessage: Blockweiser Presenter für Assistant-Nachrichten
Verantwortung
presentAssistantMessage ist die «blockweise Vorschub-Maschine» in der Task-Klasse. Nachdem die Streaming-Antwort des LLM durch parseAssistantMessageV2 in Blöcke vom Typ text / tool_use / reasoning zerlegt wurde, schiebt diese Methode sie einen nach dem anderen an UI und Werkzeugausführung weiter. Sie läuft nicht als eigene Schleife, sondern wird pro Aufruf durch den Stream-Callback oder scheduleAssistantPresentation jeweils einmal geweckt, schiebt einen Block vor und ruft sich dann selbst auf, um den nächsten zu behandeln.
Sie sitzt in der innersten Schicht des gesamten Agent-Loops. recursivelyMakeClineRequests startet attemptApiRequest, um an den Stream zu kommen; im Stream-Callback wird der akkumulierte Text neu geparst in ein assistantMessageContent-Array und dann presentAssistantMessage aufgerufen. Beim Lesen dieser Methode ist sie also als «Zustandsmaschine zu verstehen, die während der Stream noch Zeichen ausspuckt, immer wieder geweckt wird und prüft, ob ein neuer Block präsentiert werden kann».
Ihr Zustand liegt vollständig auf taskState: currentStreamingContentIndex zeigt, bei welchem Block sie gerade ist, presentAssistantMessageLocked ist ein Spinlock gegen Reentranz, presentAssistantMessageHasPendingUpdates markiert «während ich lief ist weiterer Inhalt reingekommen», und userMessageContentReady ist das Signal «alle Blöcke dieser Runde sind verarbeitet», auf das der äußere pWaitFor wartet.
Entwurfsmotivation
- Lock + Pending-Flag statt Queue: Statt einer Nachrichten-Queue gibt es einen booleschen Lock kombiniert mit «bei Pending einfach nochmal durchlaufen» als endrekursive Reentranz-Steuerung (
lock check:2637). Einfach und fasst mehrere Stream-Callbacks von Natur aus zu einem Lauf zusammen. - Während des Streams präsentieren: Nicht erst auf das Ende der gesamten Antwort warten, sondern Strom für Strom weiterreichen. Text-Blöcke werden inkrementell
say("text", content, ..., block.partial)übergeben, und ein tool_use-Block wird sobald er vollständig ist sofort antoolExecutor.executeToolweitergereicht. - Blöcke seriell zwischen: Wenn paralleles Tool Calling deaktiviert ist, sorgt das
didAlreadyUseTool-Flag dafür, dass nachfolgende Blöcke direkt übersprungen werden (parallel gate:2668). Serielle Ausführung stellt sicher, dass der Nutzer bei der Freigabe eines Werkzeugs nicht von einem neuen Werkzeug unterbrochen wird. - cloneDeep gegen Referenz-Manipulation: Beim Holen eines Blocks wird eine Tiefenkopie angefertigt und dann verarbeitet, weil der Stream noch die Eigenschaften der Originalobjekte im Array aktualisiert – mit einer direkten Referenz würde man Halbprodukte lesen (
cloneDeep:2658). - Out-of-bounds ist normal: Ein Index außerhalb der Array-Grenzen ist kein Fehler, sondern das Signal «der Stream hat den nächsten Block noch nicht geliefert, du bist zu früh»; nur wenn der Stream bereits beendet ist (
didCompleteReadingStream), wirduserMessageContentReadywahr gesetzt, damit die Außenschicht weitermacht (oob handling:2650).
Schlüsseldateien
presentAssistantMessage:2630— Methodenrumpf, Lock, Block-Typ-Verteilung und Vorschublogik liegen hier.lock + pending:2637— Reentranz-Schutz: Wenn gesperrt, Pending setzen und return.cloneDeep block:2658— Tiefenkopie des aktuellen Blocks, damit keine Halbprodukte aus dem noch schreibenden Stream gelesen werden.switch block.type:2663— Verteilung nachtext/tool_use; reasoning geht einen anderen Pfad.thinking tag strip:2685— Entfernt<thinking>,<function_calls>und ähnliche Tags, damit sie das Markdown-Rendering nicht verschmutzen.say text:2731— Sendet den bereinigten Text an die UI;block.partialsteuert inkrementelles Update vs. Endversion.checkpoint gate:2737— Läuft gerade ein initialer Checkpoint-Commit, dürfen nicht-readonly Werkzeuge erst nach dessen Abschluss ausgeführt werden.executeTool:2743— tool_use-Block geht anToolExecutor.executeTool; die Methode selbst interessiert sich nicht für das konkrete Werkzeug.userMessageContentReady:2769— Beim Abschluss des letzten Blocks wird das Flag wahr gesetzt, damit der äußerepWaitFordie Blockade aufhebt.tail recursion:2780— Sind weitere Blöcke vorhanden, ruft sie sich selbst auf, um den nächsten zu schieben, ohne auf den Stream-Callback zu warten.parseAssistantMessageV2 call:3506— Im Stream-Callback wird der gesamte Assistant-Text neu geparst und erzeugt das Block-Array.flush callback:685— Flush-Einstieg, denpresentationSchedulerregistriert hat; landet schließlich auch hier.
Datenfluss
Jedes Mal, wenn presentAssistantMessage geweckt wird, wird zuerst das Lock akquiriert. Ist es erhalten, prüft sie, ob der aktuelle Index noch im gültigen Bereich liegt. Ist er das nicht, ist der Block draußen und wird nach Typ verteilt. Das folgende Snippet ist die Kernlogik, die nach der Verteilung entscheidet, ob zum nächsten Block übergegangen wird:
// apps/vscode/src/core/task/index.ts
if (
!block.partial ||
this.taskState.didRejectTool ||
(!this.isParallelToolCallingEnabled() && this.taskState.didAlreadyUseTool)
) {
// block is finished streaming and executing
if (
this.taskState.currentStreamingContentIndex ===
this.taskState.assistantMessageContent.length - 1
) {
// last block is complete and it is finished executing
this.taskState.userMessageContentReady = true; // will allow pwaitfor to continue
}
// call next block if it exists (if not then read stream will call it when its ready)
this.taskState.currentStreamingContentIndex++; // need to increment regardless, so when read stream calls this function again it will be streaming the next block
if (
this.taskState.currentStreamingContentIndex <
this.taskState.assistantMessageContent.length
) {
// there are already more content blocks to stream, so we'll call this function ourselves
await this.presentAssistantMessage();
return;
}
}
// block is partial, but the read stream may have finished
if (this.taskState.presentAssistantMessageHasPendingUpdates) {
await this.presentAssistantMessage();
}Dieses Snippet entscheidet «nach Abschluss des aktuellen Blocks sofort zum nächsten übergehen oder nicht». Ist der Block noch partial (streamt noch), wird nicht selbst vorgeschoben, sondern auf den nächsten Stream-Callback gewartet; ist er abgeschlossen, wird der Index inkrementiert und liegt der neue Index noch innerhalb des Arrays, wird direkt selbst der nächste Block vorgeschoben, ohne auf den Stream-Callback zu warten. Das abschließende presentAssistantMessageHasPendingUpdates ist der Fallback: Ist der Stream während der Ausführung weitergelaufen, wird die Methode nochmal aufgerufen. userMessageContentReady wird nur gesetzt, wenn der letzte Block abgeschlossen ist (ready flag:2769).
Grenzen und Fehler
- abort hat Vorrang: Die allererste Aktion am Methodeneintritt ist das Prüfen von
taskState.abort; bei Abbruch wird direkt «Cline instance aborted» geworfen (abort guard:2631). Damit greift das Abbruch-Signal vor Ausführung jedes Blocks. - Nach Werkzeugablehnung seriell überspringen: Sobald
didRejectToolwahr ist, wird bei nachfolgenden text-Blöcken direktbreakausgeführt; tool_use-Blöcke werden beim Durchlauf durch ToolExecutor von dessen interner Ablehnungsprüfung ebenfalls gestoppt (reject gate:2667). Der Index wird weiter inkrementiert, bis er out-of-bounds geht unduserMessageContentReadygesetzt wird, womit die Außenschicht die Kontrolle zurückerhält. - Initialer Checkpoint blockiert: Läuft zu Task-Beginn ein
initialCheckpointCommitPromise, müssen nicht-readonly Werkzeuge (!READ_ONLY_TOOLS.includes(block.name)) auf dessen Abschluss warten (checkpoint wait:2737). Readonly-Werkzeuge dürfen parallel laufen. - Schutz vor Lock-Leck: Die Lock-Freigabe erfolgt vor dem Verteilungs-Switch (
early unlock:2754). Das wirkt merkwürdig, ist aber Absicht – danach wirdpresentAssistantMessageselbst aufgerufen, und mit gehaltenem Lock würde man sich selbst blockieren. - Bereinigung von partial-Blöcken: Ein text-Block wird auch im partial-Zustand an die UI geschickt, aber am Ende kann ein halber XML-Tag stehen (z. B.
<thinknoch nicht geschlossen). Der Code prüft, ob nach dem letzten<ein gültiger Tag-Name folgt, und schneidet im Fall ja ab, um UI-Sprünge zu vermeiden (partial tag trim:2695). - Stream endet zuerst, Block kommt später: Ist
didCompleteReadingStreambereits wahr, der Index aber out-of-bounds, wird direktuserMessageContentReadygesetzt, damit der äußerepWaitForweiterläuft (stream done oob:2650). Es wird nicht stur auf einen Block gewartet, der nie kommt.
Zusammenfassung
presentAssistantMessage ist die Vorschub-Maschine in der innersten Schicht des Agent-Loops. Sie treibt «so weit der Stream reicht, so weit wird vorgeschoben» auf die Spitze: neuer Block da – los; kein neuer – warten; Stream beendet – freigeben. Sämtliche UI-Präsentation und Werkzeugausführung laufen über diesen einen Eingang zusammen. Wie die Werkzeuge ausgeführt werden, steht in /tools/coordinator und /tools/validator; die darüberliegende rekursive Ansteuerung steht in /agent-loop/attempt-api-request und /agent-loop/task-class.
Siehe offizielle Dokumentation: Cline-Dokumentation · README