Skip to content

La clase Task: núcleo del Agent de una sola ronda

源码版本v4.0.10

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 abortTask le basta con poner el flag taskState.abort y 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 recursivelyMakeClineRequests se 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, parseAssistantMessageV2 vuelve a parsear todo el texto del asistente (parseAssistantMessageV2 call:3509); presentAssistantMessage avanza 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:188export class Task, declara todos los campos centrales: taskId, taskState, api, controller, messageStateHandler, etc.
  • constructor:310 — recibe TaskParams e inicializa dependencias como clineIgnore, toolExecutor, streamHandler, presentationScheduler.
  • startTask:1253 — entrada; prepara el user content inicial, ejecuta el hook TaskStart y luego llama a initiateTaskLoop.
  • initiateTaskLoop:1717 — bucle while externo; si el modelo solo devuelve texto sin llamar a herramientas, lo vuelve a preguntar con el prompt noToolsUsed.
  • recursivelyMakeClineRequests:2790 — driver recursivo del nivel intermedio; comprueba el límite de errores, inicializa el checkpoint y llama a attemptApiRequest para 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 por say tras quitar etiquetas thinking; el tool_use se entrega a toolExecutor.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:

typescript
// 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 successful

Esto 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):

typescript
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 directamente return true y 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_message y se pide al usuario que reintente (empty response:3834).
  • Cancelación a mitad: abortTask primero captura si debe correr el hook TaskCancel y luego pone el flag abort, 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: attemptApiRequest espera con pWaitFor hasta 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 que presentAssistantMessage pueda avanzar y, al final, poner userMessageContentReady (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