Skip to content

recursivelyMakeClineRequests: el driver recursivo

源码版本v4.0.10

Responsabilidades

recursivelyMakeClineRequests es el driver central dentro de la clase Task donde «cada recursión = una llamada completa al LLM más la ejecución de herramientas», definido en la clase Task de apps/vscode/src/core/task/index.ts. Recibe el userContent acumulado de la ronda anterior (con resultados de herramientas, feedback del usuario, el prompt noToolsUsed, etc.), llama a attemptApiRequest para obtener el stream, lo entrega a StreamChunkCoordinator para repartirlo, y cuando el stream termina espera a que presentAssistantMessage procese todos los bloques; luego se llama a sí mismo con el taskState.userMessageContent acumulado en esta ronda. El concepto de «ronda» de todo el agent loop vive dentro de esta recursión.

Se encuentra en el nivel intermedio de un bucle anidado de tres niveles. El más externo es initiateTaskLoop, que con while (!abort) cubre el caso en que «el modelo solo devuelve texto y no llama a herramientas» metiendo un prompt noToolsUsed y reentrando a la recursión (initiateTaskLoop:1717). El intermedio es el propio recursivelyMakeClineRequests: cada llamada representa una solicitud a la API del LLM. El más interno es presentAssistantMessage, despertado repetidas veces por callbacks del stream o por el scheduler para avanzar los bloques. recursivelyMakeClineRequests al final hace await this.recursivelyMakeClineRequests(this.taskState.userMessageContent) y se vuelve a llamar (recurse:3830); los resultados de las herramientas son por naturaleza el user content de la siguiente ronda, sin necesidad de orquestación extra.

Motivación de diseño

  • Recursión en vez de un while: tras cada respuesta del LLM, el resultado de las herramientas es el user content de la siguiente ronda, así que el final de la función hace naturalmente await this.recursivelyMakeClineRequests(this.taskState.userMessageContent) (recurse:3830). Esta forma escribe una sola vez «llamada API de una ronda + ejecución de herramientas + reset de estado»; la profundidad de la pila refleja de forma natural el número de la ronda y, si algo falla, el stack se lee bien.
  • Comprobación previa del límite de errores: al inicio, la función comprueba enseguida consecutiveMistakeCount >= maxConsecutiveMistakes (mistake limit check:2826); en modo YOLO hace return true y termina la tarea, si no, ask("mistake_limit_reached") deja decidir al usuario. Esto evita que el modelo queme tokens en un bucle infinito.
  • Reset completo del estado de streaming: cada ronda pone a cero una docena de campos como currentStreamingContentIndex, assistantMessageContent, userMessageContent, didRejectTool, presentAssistantMessageLocked (reset streaming state:3302), de modo que la ronda actual no se vea afectada por restos de la anterior. El reset también cubre streamHandler.reset() y presentationScheduler.reset().
  • Reparto por StreamChunkCoordinator: en lugar de hacer for await directo sobre el stream, lo envuelve en StreamChunkCoordinator (stream coordinator:3368), que reparte el stream en tres tipos de chunk —reasoning / text / usage— con callbacks distintos. El reasoning va al reasoning handler; el text pasa por parseAssistantMessageV2 para reparsear; el usage acumula el conteo de tokens.
  • pWaitFor userMessageContentReady: al terminar el stream no recurre enseguida, sino que hace await pWaitFor(() => this.taskState.userMessageContentReady) (pWaitFor ready:3808), esperando a que presentAssistantMessage procese todos los bloques. Así se garantiza que los resultados de las herramientas se acumulen en userMessageContent antes de la siguiente ronda.
  • noToolsUsed incrementa el contador de errores: si la ronda del asistente no tiene ningún bloque tool_use, se mete el texto formatResponse.noToolsUsed en userMessageContent y se hace consecutiveMistakeCount++ (noToolsUsed:3818). En la siguiente ronda se avisa al modelo de que «o llama a una herramienta o hace attempt_completion»; si sigue sin llamarlas, el límite de errores lo termina.
  • Respuesta vacía va por la vía de error: si la ronda del asistente no tiene text ni tool_use, se registra la telemetría empty_assistant_message, se hace say con un error y luego ask("api_req_failed") para que el usuario decida si reintentar (empty response:3834), sin dejar que la tarea continúe en silencio.

Archivos clave

  • recursivelyMakeClineRequests:2790 — entrada de la función; firma (userContent, includeFileDetails?) => Promise<boolean>, devuelve didEndLoop.
  • abort check:2795 — al entrar comprueba enseguida taskState.abort; si se canceló, lanza Task instance aborted.
  • apiRequestCount++:2804 — incrementa el contador de solicitudes, usado para la gestión del focus chain list.
  • mistake limit check:2826consecutiveMistakeCount >= maxConsecutiveMistakes entra en la rama de manejo de errores.
  • yolo fail:2841 — en modo YOLO hace say de error + return true para terminar.
  • ask mistake_limit_reached:2860 — en modo no-YOLO, ask al usuario; este puede dar un nuevo prompt para continuar.
  • reset streaming state:3302 — reset completo del estado de streaming: una docena de campos a cero + reset de handler/scheduler.
  • attemptApiRequest call:3319 — trae el stream; si falla el primer chunk, el try/catch interno de attemptApiRequest lo convierte en un ask con api_req_failed.
  • StreamChunkCoordinator:3368 — envoltorio que reparte el stream en tres tipos de chunk: reasoning/text/usage.
  • while true chunk loop:3387 — bucle principal de consumo; toma el siguiente chunk del coordinator y se ramifica por switch.
  • accumulate assistantMessage:3503 — el chunk de text se acumula en assistantMessage + assistantTextOnly y luego se reparsa.
  • force partial false:3792 — tras el stream, los tool blocks partial que queden se fuerzan a partial = false para que presentAssistantMessage pueda finalizarlos.
  • pWaitFor ready:3808 — espera a que se procesen todos los bloques; userMessageContentReady pasa a true.
  • noToolsUsed bump:3818 — si no hubo llamada a herramienta, mete el prompt noToolsUsed y mistake++.
  • recurse:3830 — se vuelve a llamar con el userMessageContent acumulado; devuelve didEndLoop.
  • empty response:3834 — vía de respuesta vacía: say de error + ask api_req_failed.
  • outer catch:3935 — catch de respaldo; en teoría attemptApiRequest ya hizo catch interno; aquí es doble seguro.
  • initiateTaskLoop:1717 — while externo; maneja el prompt noToolsUsed y la salida por didEndLoop.

Flujo de datos

Cada vez que se entra en recursivelyMakeClineRequests, primero se hace la comprobación del límite de errores y la detección de workspace remoto, luego se resetea el estado de streaming y se arranca el stream. Este es el núcleo del reset y del arranque del 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;
this.taskState.presentAssistantMessageHasPendingUpdates = false;
this.taskState.didAutomaticallyRetryFailedApiRequest = false;
await this.diffViewProvider.reset();
this.streamHandler.reset();
this.presentationScheduler.reset();
this.taskState.toolUseIdMap.clear();

const { toolUseHandler, reasonsHandler } =
    this.streamHandler.getHandlers();
const stream = this.attemptApiRequest(previousApiReqIndex);

Esto está cerca de reset streaming state:3302. Tras el reset, StreamChunkCoordinator toma el stream (stream coordinator:3368); un bucle while (true) va sacando chunks del coordinator. Los text chunks entran en assistantMessage += chunk.text; assistantTextOnly += chunk.text; y luego parseAssistantMessageV2(assistantMessage) reparsa todo el fragmento enseguida (parseAssistantMessageV2 call:3509); si la longitud aumenta, scheduleAssistantPresentation deja que presentAssistantMessage avance los nuevos bloques. Los reasoning chunks van al reasoning handler, que actualiza incrementalmente el mensaje thinking. Los usage chunks acumulan conteo de tokens y cost. Al acabar el stream, processNativeToolCalls procesa las tool calls nativas, flushAssistantPresentationOrThrow fuerza el cierre de los partial blocks residuales y luego await pWaitFor(() => userMessageContentReady) espera a que presentAssistantMessage termine con todos los bloques (pWaitFor ready:3808). Después se comprueba si hubo tool_use: si sí, se llama a checkpoint y se recurre con userMessageContent; si no, se mete el prompt noToolsUsed y se recurre; la respuesta totalmente vacía va por la vía de error. El didEndLoop que devuelve la recursión se propaga hasta initiateTaskLoop; si es true, sale del while; si es false, el nivel externo añade el prompt noToolsUsed y vuelve a hacer otra ronda.

Límites y fallos

  • abort por encima de todo: la primera línea de la función comprueba taskState.abort (abort check:2795); si se canceló, lanza Task instance aborted directamente, sin entrar en el reset de streaming ni en la llamada API. Así, aunque la recursión siga colgada tras una cancelación, se sale enseguida.
  • Límite de errores: en modo YOLO, return true termina la tarea (yolo return:2847); en modo no-YOLO se hace ask al usuario, y si este da un nuevo prompt se usa como userContent de la siguiente ronda recursiva (ask mistake_limit_reached:2860).
  • Respuesta vacía: la ronda del asistente no tiene text ni tool_use; se registra la telemetría empty_assistant_message (empty telemetry:3840), se hace say con un error que incluye el request ID y luego ask("api_req_failed") para que el usuario decida reintentar.
  • Fallo a mitad de stream: los fallos del stream posteriores al primer chunk los asume el try/catch externo de recursivelyMakeClineRequests (outer catch:3935); en teoría attemptApiRequest ya hizo catch del error del primer chunk, y este catch es un doble seguro contra unhandled rejection.
  • Restos de partial blocks: si al terminar el stream quedan partial blocks (sin etiqueta de cierre), se fuerzan a partial = false (force partial false:3792) para que presentAssistantMessage pueda avanzar y, al final, poner userMessageContentReady = true; si no, pWaitFor esperaría indefinidamente.
  • Detección de workspace remoto sin acabar: await this.remoteWorkspaceDetectionPromise se espera al inicio de la recursión (remote workspace wait:2801) para que el presentation scheduler, desde el primer flush, use el cadence correcto.
  • apiRequestCount se incrementa: cada ronda hace apiRequestCount++ (apiRequestCount++:2804) y apiRequestsSinceLastTodoUpdate++, usado para la gestión del focus chain list y el ritmo de actualización de todos.
  • Checkpoint antes de recursar: tras ejecutar todas las herramientas y poner userMessageContentReady, se hace checkpointManager.saveCheckpoint (saveCheckpoint:3811), para que el checkpoint refleje todos los cambios de archivos de esta ronda. Solo entonces se entra en la siguiente recursión.

Resumen

recursivelyMakeClineRequests es el «driver de una ronda» del agent loop de Cline. Encadena «reset de estado → traer stream → parsear → presentar/ejecutar → esperar a que termine → volver a recursar con los resultados de las herramientas», y concentra aquí el manejo del límite de errores, la respuesta vacía y el abort. Para ver el tramo donde trae el stream y reintenta, salta a /agent-loop/attempt-api-request; para ver el nivel más interno, donde empuja los bloques a la UI y a las herramientas, salta a /agent-loop/present-assistant-message; para ver los límites de la máquina de estados completa, salta a /agent-loop/task-class.

Véase la documentación oficial: Documentación de Cline · README