presentAssistantMessage : présentateur de blocs du message assistant
Responsabilités
presentAssistantMessage est le « pousse-blocs » de la classe Task. Une fois que la réponse en streaming du LLM a été découpée par parseAssistantMessageV2 en blocs text / tool_use / reasoning, cette méthode est chargée de pousser les blocs un à un vers l'UI et l'exécuteur d'outils. Elle ne tourne pas en boucle autonome : à chaque appel, par un callback de flux ou par scheduleAssistantPresentation, elle fait avancer un bloc, puis se rappelle pour faire avancer le suivant.
Elle se situe au niveau le plus interne de l'agent loop. recursivelyMakeClineRequests lance attemptApiRequest pour obtenir le flux, les callbacks de flux re-parse le texte accumulé en tableau assistantMessageContent, puis appellent presentAssistantMessage. Donc quand on lit cette méthode, il faut la voir comme une machine à états « pendant que le flux crache encore des caractères, réveillée à plusieurs reprises pour voir s'il y a un nouveau bloc à présenter ».
Son état vit sur taskState : currentStreamingContentIndex indique le bloc courant, presentAssistantMessageLocked est un verrou tournant anti-réentrance, presentAssistantMessageHasPendingUpdates signale « pendant que je m'exécutais, du nouveau contenu est arrivé », et userMessageContentReady est le signal « tous les blocs de ce tour sont traités » vu par le pWaitFor de la couche externe.
Motivation de conception
- Verrou + flag pending en guise de queue : pas de file de messages, juste un verrou booléen combiné à un contrôle de réentrance façon récursion terminale « si pending, on refait un tour » (
lock check:2637). Simple, et fusionne naturellement plusieurs callbacks de flux en une seule exécution. - Présenter au fil du flux : on n'attend pas la fin de la réponse, on pousse au fur et à mesure. Un bloc text fait un
say("text", content, ..., block.partial)incrémental, un bloc tool_use dès qu'il est complet lancetoolExecutor.executeTool. - Sérialisation entre blocs : quand parallel tool calling est désactivé, le flag
didAlreadyUseToolfait que les blocs suivants skip l'exécution (parallel gate:2668). L'exécution sérielle garantit que l'utilisateur ne soit pas interrompu par un nouvel outil pendant qu'il en approuve un autre. - cloneDeep pour éviter les altérations par référence : quand on récupère un bloc, on en fait une copie profonde avant de le traiter, car le flux continue de mettre à jour les propriétés des objets dans le tableau d'origine, et garder une référence ferait lire des demi-produits (
cloneDeep:2658). - Out-of-bounds est la norme : un index hors borne n'est pas une erreur, c'est le signal « le flux n'a pas encore sorti le bloc suivant, tu arrives trop tôt » ; si le flux est déjà terminé (
didCompleteReadingStream), alors seulement on armeuserMessageContentReadypour débloquer la couche externe (oob handling:2650).
Fichiers clés
presentAssistantMessage:2630— corps de la méthode : verrou, dispatch par type de bloc, logique d'avancement.lock + pending:2637— protection anti-réentrance : si déjà verrouillé, on arme pending et on return.cloneDeep block:2658— copie profonde du bloc courant, pour ne pas lire un demi-produit en cours d'écriture par le flux.switch block.type:2663— dispatch selontext/tool_use; reasoning passe par un autre chemin.thinking tag strip:2685— retire les tags<thinking>,<function_calls>, etc., pour qu'ils ne polluent pas le rendu markdown.say text:2731— envoie le contenu text nettoyé à l'UI,block.partialcontrôle mise à jour incrémentale vs version finale.checkpoint gate:2737— si un commit de checkpoint initial est en cours, les outils non read-only doivent attendre qu'il se termine.executeTool:2743— les blocs tool_use sont délégués àToolExecutor.executeTool, la méthode elle-même ne se soucie pas de l'outil spécifique.userMessageContentReady:2769— armé quand le dernier bloc est terminé, débloque lepWaitForexterne.tail recursion:2780— s'il y a un bloc suivant, on se rappelle soi-même pour le pousser, sans attendre un callback de flux.parseAssistantMessageV2 call:3506— dans le callback de flux, on re-parse tout le texte assistant, ce qui génère le tableau de blocs.flush callback:685— entrée de flush enregistrée parpresentationScheduler, qui finit par retomber ici.
Flux de données
À chaque réveil de presentAssistantMessage, on prend d'abord le verrou. Une fois acquis, on regarde si l'index courant est hors borne ; s'il ne l'est pas, on récupère le bloc et on dispatch selon le type. Voici le cœur de la logique qui, après dispatch, fait avancer au bloc suivant :
// 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();
}Ce passage décide « après avoir traité le bloc courant, faut-il enchaîner immédiatement sur le suivant ». Si le bloc est partial (le flux continue), on n'avance pas proactivement, on attend le prochain callback de flux ; si le bloc est complete, on incrémente l'index, et si le nouvel index est encore dans le tableau, on se rappelle directement pour avancer au bloc suivant, sans attendre de callback. Le presentAssistantMessageHasPendingUpdates final est un filet : si le flux a avancé pendant l'exécution, on refait un tour. userMessageContentReady n'est armé que « quand le dernier bloc est complete » (ready flag:2769).
Limites et échecs
- abort passe en priorité : la première chose à l'entrée de la méthode est de regarder
taskState.abort; si annulé, on lève directement "Cline instance aborted" (abort guard:2631). Cela garantit que le signal d'annulation prend effet avant toute exécution de bloc. - Skip sérialisé après rejet d'outil : une fois
didRejectToolvrai, les blocs text suivants font directementbreak, et les blocs tool_use passant par ToolExecutor sont aussi interceptés par la vérification de rejet interne à celui-ci (reject gate:2667). L'index continue d'avancer jusqu'à sortir de borne, puis on armeuserMessageContentReadypour que la couche externe reprenne la main. - Blocage par checkpoint initial : au démarrage de la tâche, si un
initialCheckpointCommitPromiseest en cours, les outils non read-only (!READ_ONLY_TOOLS.includes(block.name)) doivent attendre qu'il se termine (checkpoint wait:2737). Les outils read-only peuvent tourner en parallèle. - Protection contre les fuites de verrou : le verrou est relâché avant le switch de dispatch (
early unlock:2754). C'est contre-intuitif, mais volontaire — la suite va rappelerpresentAssistantMessagelui-même, et garder le verrou ferait se heurter à soi-même. - Nettoyage des blocs partial : un bloc text à l'état partial doit quand même être envoyé à l'UI, mais la fin peut contenir un demi-tag XML (par ex.
<thinknon refermé). Le code détecte si ce qui suit le dernier<est un nom de tag valide, et le coupe si oui, pour éviter que l'UI saute (partial tag trim:2695). - Flux terminé avant les blocs : quand
didCompleteReadingStreamest vrai mais l'index est hors borne, on arme directementuserMessageContentReadypour que le pWaitFor externe continue (stream done oob:2650). On n'attend pas bêtement un bloc qui ne viendra jamais.
Résumé
presentAssistantMessage est la machine à avancer de la couche la plus interne de l'agent loop. Elle pousse « flux d'un côté, présentation de l'autre » à l'extrême : nouveau bloc on tourne, pas de nouveau bloc on attend, flux terminé on laisse passer. Toutes les présentations UI et entrées d'exécution d'outils convergent vers cette seule méthode. Pour voir comment les outils s'exécutent à la suite, aller à /tools/coordinator et /tools/validator ; pour voir la couche récursive au-dessus, aller à /agent-loop/attempt-api-request et /agent-loop/task-class.
Voir la documentation officielle : documentation Cline · README.