recursivelyMakeClineRequests: el driver recursivo
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 hacereturn truey 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 cubrestreamHandler.reset()ypresentationScheduler.reset(). - Reparto por StreamChunkCoordinator: en lugar de hacer
for awaitdirecto sobre el stream, lo envuelve enStreamChunkCoordinator(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 porparseAssistantMessageV2para 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 quepresentAssistantMessageprocese todos los bloques. Así se garantiza que los resultados de las herramientas se acumulen enuserMessageContentantes 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.noToolsUseden userMessageContent y se haceconsecutiveMistakeCount++(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 hacesaycon un error y luegoask("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>, devuelvedidEndLoop.abort check:2795— al entrar comprueba enseguidataskState.abort; si se canceló, lanzaTask instance aborted.apiRequestCount++:2804— incrementa el contador de solicitudes, usado para la gestión del focus chain list.mistake limit check:2826—consecutiveMistakeCount >= maxConsecutiveMistakesentra en la rama de manejo de errores.yolo fail:2841— en modo YOLO hacesayde error +return truepara terminar.ask mistake_limit_reached:2860— en modo no-YOLO,askal 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 unaskconapi_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 apartial = falsepara 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:sayde error +askapi_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:
// 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ó, lanzaTask instance aborteddirectamente, 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 truetermina la tarea (yolo return:2847); en modo no-YOLO se haceaskal 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 hacesaycon un error que incluye el request ID y luegoask("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íaattemptApiRequestya 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 quepresentAssistantMessagepueda avanzar y, al final, poneruserMessageContentReady = true; si no,pWaitForesperaría indefinidamente. - Detección de workspace remoto sin acabar:
await this.remoteWorkspaceDetectionPromisese 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) yapiRequestsSinceLastTodoUpdate++, 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 hacecheckpointManager.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