La clase Task: núcleo del Agent de una sola ronda
Responsabilidades
La clase Task es la máquina de estados de «agent de una sola ronda» de Cline. Una instancia de Task corresponde a una tarea (task): nace cuando el usuario pulsa enter y muere cuando la tarea se cancela o se cierra con normalidad. No es un servicio residente, sino un objeto desechable que el Controller crea para correr una ronda y luego descarta. Este modelo de «una instancia por ronda» simplifica el control de concurrencia: una misma Task jamás atiende dos solicitudes LLM a la vez, porque solo ella misma está en movimiento.
Lo que hace cabe en tres tramos: primero ensambla la solicitud API completa con la entrada del usuario, el historial de mensajes, el system prompt, la lista de herramientas MCP, los archivos de reglas, etc.; luego impulsa la respuesta en streaming del LLM y, mientras llega, usa parseAssistantMessageV2 para cortar el texto en bloques text / tool_use / reasoning; finalmente entrega esos bloques a presentAssistantMessage, que los presenta uno a uno a la UI y ejecuta las herramientas que contienen. Los resultados de las herramientas refluyen como user content de la siguiente ronda y disparan la llamada recursiva (recursion), hasta que el modelo deja de emitir tool_use o el usuario interrumpe.
Basta con entenderlo como un bucle anidado de tres niveles: el más externo, initiateTaskLoop, cubre el caso en que «el modelo no llamó a ninguna herramienta y se le vuelve a preguntar»; el nivel intermedio, recursivelyMakeClineRequests, ejecuta una llamada API completa más la ejecución de herramientas por cada recursión; el nivel interno, presentAssistantMessage, avanza los bloques de forma incremental dentro del stream. Toda la interacción de UI —preguntar, pedir confirmación al usuario, reportar progreso— sale por las dos bocas ask y say, que meten el mensaje en messageStateHandler y lo postean a la webview.
Motivación de diseño
- Una instancia por tarea: el estado se aísla dentro de cada instancia de Task, de modo que cancelación, rollback y checkpoint pueden tratarse con la instancia como frontera. A
abortTaskle basta con poner el flagtaskState.aborty todas las rutas recursivas lo leerán y se retirarán por sí mismas (abortTask:1801). - Recursión en vez de bucle: tras cada respuesta del LLM, el resultado de las herramientas es por naturaleza el user content de la siguiente ronda, así que
recursivelyMakeClineRequestsse vuelve a llamar a sí misma al final (recurse:3830). Esta forma escribe una sola vez el código de «llamada API de una ronda + ejecución de herramientas»; la profundidad de la pila de llamadas refleja de forma natural el número de la ronda y, si algo falla, el stack se lee bien. - Parsing y presentación en streaming separados: la respuesta del LLM se parsea a medida que llega; cada vez que entra un fragmento de texto,
parseAssistantMessageV2vuelve a parsear todo el texto del asistente (parseAssistantMessageV2 call:3509);presentAssistantMessageavanza por las fronteras de los bloques, de modo que las herramientas no esperan a que termine toda la respuesta para arrancar. - Un solo lock contra la competencia de estado: toda modificación del estado de Task pasa por
withStateLock, que toma el mismo Mutex (withStateLock:205) para evitar que las tres vías —callbacks de streaming, ejecución de herramientas y la UI— corrompan el estado concurrentemente. - Modo YOLO y límite de errores: al alcanzar el número máximo de errores consecutivos, se termina directamente (
mistake limit:2826), para que el modelo no queme tokens en un bucle infinito.
Archivos clave
Task class definition:188—export class Task, declara todos los campos centrales: taskId, taskState, api, controller, messageStateHandler, etc.constructor:310— recibeTaskParamse inicializa dependencias como clineIgnore, toolExecutor, streamHandler, presentationScheduler.startTask:1253— entrada; prepara el user content inicial, ejecuta el hook TaskStart y luego llama ainitiateTaskLoop.initiateTaskLoop:1717— bucle while externo; si el modelo solo devuelve texto sin llamar a herramientas, lo vuelve a preguntar con el promptnoToolsUsed.recursivelyMakeClineRequests:2790— driver recursivo del nivel intermedio; comprueba el límite de errores, inicializa el checkpoint y llama aattemptApiRequestpara obtener el stream.attemptApiRequest:2175— espera a que MCP conecte, lee los archivos de reglas, ensambla el system prompt y, por último,yield*del stream del LLM.presentAssistantMessage:2630— avanzador de bloques del nivel interno; reparte por type: el text pasa porsaytras quitar etiquetas thinking; el tool_use se entrega atoolExecutor.executeTool.ask:789— boca para que el usuario responda; gestiona las actualizaciones partial del mensaje y el callback de respuesta de la webview.say:969— boca unidireccional para reportar progreso; el modo partial se usa para actualizar en streaming un mismo mensaje.abortTask:1801— cancelación por fases: primero decide si corre el hook TaskCancel, luego pone el flag abort, después cancela hooks y comandos en segundo plano y, al final, corre el hook.ToolExecutor.executeTool:212— Task delega la ejecución de herramientas; ella misma no maneja ninguna herramienta concreta.
Flujo de datos
La ruta central de una solicitud LLM es «recursión → traer stream → parsear → presentar → refluir → volver a recursar». Al entrar en recursivelyMakeClineRequests primero se resetea el estado de streaming de esta ronda y luego se arranca attemptApiRequest para obtener el stream:
// 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 successfulEsto está cerca de reset streaming state:3302. Tras el reset, StreamChunkCoordinator reparte el stream en chunks text / usage / reasoning con callbacks separados. Cada vez que llega un fragmento de texto, se vuelve a ejecutar parseAssistantMessageV2 para rebanar todo el texto del asistente en un array de bloques (parseAssistantMessageV2 call:3509):
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);Si el array de bloques cambió, scheduleAssistantPresentation dispara presentAssistantMessage. Este último se ramifica según block.type: el text pasa por say("text", ...); el tool_use pasa por toolExecutor.executeTool(block) (executeTool call:2743). El resultado de la herramienta se mete en taskState.userMessageContent; cuando termina todo el stream y se pone userMessageContentReady, recursivelyMakeClineRequests se vuelve a llamar a sí misma con ese user content (recurse:3830), y así hasta que el modelo no incluya tool_use; entonces el initiateTaskLoop externo lo vuelve a preguntar con noToolsUsed o el usuario lo da por terminado.
Límites y fallos
- Se dispara el límite de errores: al llegar a
maxConsecutiveMistakes, en modo YOLO se hace directamentereturn truey se termina la tarea; si no,ask("mistake_limit_reached")deja decidir al usuario (mistake limit:2826). - Respuesta vacía: la ronda entera del asistente no contiene ningún bloque text ni tool_use; se registra la telemetría
empty_assistant_messagey se pide al usuario que reintente (empty response:3834). - Cancelación a mitad:
abortTaskprimero captura si debe correr el hook TaskCancel y luego pone el flagabort, para que la decisión del hook no se pierda tras poner el flag (abortTask:1801). - Herramienta rechazada: puesto
didRejectTool, los bloques text posteriores se saltan y el stream se corta con[Response interrupted by user feedback](didRejectTool:3536). - MCP no conecta:
attemptApiRequestespera conpWaitForhasta 10 segundos; si expira, solo se registra un log sin bloquear, y el system prompt se genera igual (mcp wait:2177). - Checkpoint inicial sin terminar: mientras corre el primer commit de checkpoint, las herramientas que no son de solo lectura se bloquean con
await this.initialCheckpointCommitPromise; las de solo lectura pueden correr en paralelo (initialCheckpoint gate:2737). - Cierre de bloques partial: los bloques partial que queden al terminar el stream se fuerzan a
partial = false, para quepresentAssistantMessagepueda avanzar y, al final, poneruserMessageContentReady(finalize partial blocks:3783).
Resumen
La clase Task es toda la máquina de estados de Cline en la dimensión de «una ronda»: encapsula el ciclo de vida de una tarea en una cadena clara de «construcción → arranque → recursión de stream → ejecución de herramientas → cierre», toda la interacción de UI converge en las dos bocas ask / say y toda modificación de estado converge en un único Mutex. Esta decisión de «una instancia por ronda» permite que cancelación, checkpoint y límite de errores se resuelvan con la instancia como unidad.
Si quieres profundizar, sigue por aquí:
- La recursión misma:
/agent-loop/recursion - La llamada al LLM y el ensamblado del system prompt:
/agent-loop/attempt-api-request - Cómo se corta el texto del asistente en bloques:
/agent-loop/parse-assistant-message
Véase la documentación oficial: Documentación de Cline · README