Skip to content

presentAssistantMessage: Blockweiser Presenter für Assistant-Nachrichten

源码版本v4.0.10

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 an toolExecutor.executeTool weitergereicht.
  • 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), wird userMessageContentReady wahr 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 nach text / 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.partial steuert 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 an ToolExecutor.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ßere pWaitFor die 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, den presentationScheduler registriert 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:

typescript
// 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 didRejectTool wahr ist, wird bei nachfolgenden text-Blöcken direkt break ausgefü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 und userMessageContentReady gesetzt 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 wird presentAssistantMessage selbst 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. <think noch 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 didCompleteReadingStream bereits wahr, der Index aber out-of-bounds, wird direkt userMessageContentReady gesetzt, damit der äußere pWaitFor weiterlä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