presentAssistantMessage: presentador por bloques del mensaje del asistente
Responsabilidades
presentAssistantMessage es el «avanzador por bloques» dentro de la clase Task. Una vez que el stream del LLM se corta en bloques text / tool_use / reasoning por parseAssistantMessageV2, este método se encarga de empujarlos uno a uno a la UI y al ejecutor de herramientas. No corre en un bucle propio, sino que es llamado cada vez por un callback del stream o por scheduleAssistantPresentation, avanza un bloque y luego se llama a sí mismo para avanzar el siguiente.
Su posición es el nivel más interno de todo el agent loop. recursivelyMakeClineRequests arranca attemptApiRequest para obtener el stream; los callbacks del stream van reparsing el texto acumulado en el array assistantMessageContent y luego llaman a presentAssistantMessage. Por eso, cuando leas este método, conviene pensar en él como una máquina de estados «despertada repetidas veces mientras el stream sigue soltando caracteres, para ver si hay un bloque nuevo que presentar».
El estado que gestiona vive en taskState: currentStreamingContentIndex indica hasta qué bloque se ha empujado; presentAssistantMessageLocked es un spinlock que evita la reentrada; presentAssistantMessageHasPendingUpdates marca «mientras yo ejecutaba entró contenido nuevo»; userMessageContentReady es la señal para el pWaitFor externo de «todos los bloques de esta ronda ya están procesados».
Motivación de diseño
- Lock + flag pending en vez de cola: en lugar de una cola de mensajes, usa un lock booleano con un control de reentrada de estilo tail-recursivo que dice «si hay pendiente, vuelvo a correr» (
lock check:2637). Simple, y fusiona naturalmente varios callbacks del stream en una sola ejecución. - Presentar a la vez que llega el stream: no espera a que termine toda la respuesta; el bloque text se envía con
say("text", content, ..., block.partial)de forma incremental; el bloque tool_use, una vez completo, va enseguida atoolExecutor.executeTool. - Bloques en serie: con parallel tool calling desactivado, el flag
didAlreadyUseToolhace que los bloques posteriores se salten la ejecución directamente (parallel gate:2668). La ejecución en serie asegura que, mientras el usuario aprueba una herramienta, no lo interrumpa una nueva. - cloneDeep contra la manipulación por referencia: al sacar un bloque se hace una copia profunda antes de procesarlo, porque el stream sigue actualizando propiedades de los objetos del array original y, con una referencia directa, se leerían productos a medias (
cloneDeep:2658). - Out-of-bounds es lo normal: un índice fuera de rango no es un error, es la señal de «el stream aún no ha sacado el bloque siguiente, has llegado demasiado pronto»; si además el stream ya terminó (
didCompleteReadingStream), se poneuserMessageContentReadyen true y se deja que el nivel externo continúe (oob handling:2650).
Archivos clave
presentAssistantMessage:2630— cuerpo del método; lock, reparto por tipo de bloque y la lógica de avance viven aquí.lock + pending:2637— protección contra reentrada: si ya está bloqueado, pone pending en true y sale.cloneDeep block:2658— copia profunda del bloque actual, para no leer lo que el stream está escribiendo a medias.switch block.type:2663— reparto portext/tool_use; reasoning va por otra ruta.thinking tag strip:2685— quita etiquetas como<thinking>,<function_calls>, etc., para no ensuciar el renderizado markdown.say text:2731— entrega el texto limpio a la UI;block.partialcontrola si es actualización incremental o versión final.checkpoint gate:2737— si hay un commit de checkpoint inicial corriendo, las herramientas que no son de solo lectura deben esperar a que termine.executeTool:2743— el bloque tool_use se entrega aToolExecutor.executeTool; él mismo no se preocupa por la herramienta concreta.userMessageContentReady:2769— se pone true al terminar el último bloque, para que elpWaitForexterno se desbloquee.tail recursion:2780— si hay más bloques detrás, se llama a sí mismo para avanzar el siguiente, sin esperar al callback del stream.parseAssistantMessageV2 call:3506— callback del stream que reparsa todo el texto del asistente y genera el array de bloques.flush callback:685— entrada de flush registrada porpresentationScheduler; al final cae aquí también.
Flujo de datos
Cada vez que presentAssistantMessage se despierta, primero toma el lock. Si lo obtiene, comprueba si el índice actual está fuera de rango; si no lo está, saca el bloque y lo reparte por tipo. Este es el núcleo de la lógica de avance al siguiente bloque:
// 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();
}Esto decide «tras procesar el bloque actual, paso enseguida al siguiente o no». Si el bloque es partial (el stream sigue), no se avanza por iniciativa propia, se espera al siguiente callback; si el bloque está completo, se incrementa el índice y, si el nuevo índice sigue dentro del array, se llama a sí mismo para avanzar el siguiente bloque sin esperar al callback. El presentAssistantMessageHasPendingUpdates del final es red de seguridad: si durante la ejecución el stream avanzó, se vuelve a correr. userMessageContentReady solo se pone en true «al completar el último bloque» (ready flag:2769).
Límites y fallos
- abort va primero: lo primero del método es mirar
taskState.abort; si se ha cancelado, lanza «Cline instance aborted» enseguida (abort guard:2631). Así, la señal de cancelación surte efecto antes de que se ejecute cualquier bloque. - Rechazada una herramienta, se saltan en serie: en cuanto
didRejectTooles true, los bloques text posteriores hacenbreakdirectamente, y los bloques tool_use, al pasar por ToolExecutor, también los para su propia comprobación de rechazo (reject gate:2667). El índice sigue avanzando hasta salir de rango, momento en que se poneuserMessageContentReadyy el nivel externo retoma el control. - Bloqueo del checkpoint inicial: si al empezar la tarea hay un
initialCheckpointCommitPromisecorriendo, las herramientas no de solo lectura (!READ_ONLY_TOOLS.includes(block.name)) deben esperar a que termine (checkpoint wait:2737). Las de solo lectura pueden correr en paralelo. - Protección contra fugas del lock: el lock se libera antes del switch de reparto (
early unlock:2754). Parece raro, pero es deliberado: después se va a llamar apresentAssistantMessagea sí mismo, y si se siguiera sosteniendo el lock, chocaría consigo mismo. - Limpieza de bloques partial: un bloque text en estado partial también se envía a la UI, pero al final puede quedar media etiqueta XML (por ejemplo
<thinksin cerrar). El código detecta si lo que sigue al último<es un nombre de etiqueta válido y, si es así, lo corta, para que la UI no parpadee (partial tag trim:2695). - El stream termina antes y los bloques llegan después: si
didCompleteReadingStreamya es true y el índice está fuera de rango, se poneuserMessageContentReadyen true y se deja que elpWaitForexterno continúe (stream done oob:2650). No se queda esperando un bloque que no llegará nunca.
Resumen
presentAssistantMessage es la máquina avanzadora del nivel más interno del agent loop. Lleva «lo que llega del stream se presenta» al extremo: si hay un bloque nuevo, corre; si no, espera; si el stream termina, libera. Toda la presentación de UI y toda la entrada a la ejecución de herramientas confluyen en este único método. Para seguir viendo cómo se ejecutan las herramientas, salta a /tools/coordinator y /tools/validator; para ver el driver recursivo del nivel superior, salta a /agent-loop/attempt-api-request y /agent-loop/task-class.
Véase la documentación oficial: Documentación de Cline · README