recursivelyMakeClineRequests : pilote récursif
Responsabilités
recursivelyMakeClineRequests est le pilote central de la classe Task, celui pour qui « chaque récursion = un appel LLM complet + une exécution d'outils », défini dans la classe Task de apps/vscode/src/core/task/index.ts. Il reçoit le userContent accumulé du tour précédent (résultats d'outils, feedback utilisateur, prompt noToolsUsed, etc.), appelle attemptApiRequest pour obtenir le flux, le confie à StreamChunkCoordinator pour le dispatch, attend que tous les blocs soient traités par presentAssistantMessage à la fin du flux, puis se rappelle lui-même avec le taskState.userMessageContent accumulé sur ce tour. La notion de « tour » de l'agent loop repose toute entière sur cette récursion.
Il se situe au milieu des trois boucles imbriquées. La plus externe est initiateTaskLoop, qui gère via while (!abort) le cas « le modèle ne renvoie que du texte sans appeler d'outil » en injectant un prompt noToolsUsed pour relancer la récursion (initiateTaskLoop:1717). La couche intermédiaire, c'est recursivelyMakeClineRequests lui-même : chaque appel représente une requête LLM API. La couche la plus interne est presentAssistantMessage, réveillé à chaque callback de flux ou par le scheduler pour faire avancer les blocs. recursivelyMakeClineRequests se rappelle à la fin via await this.recursivelyMakeClineRequests(this.taskState.userMessageContent) (recurse:3830), le résultat d'outil étant naturellement le user content de l'itération suivante, sans orchestration supplémentaire.
Motivation de conception
- Récursion plutôt que while : après chaque réponse LLM, le résultat d'outil est le user content de l'itération suivante, donc la fonction se rappelle naturellement à la fin via
await this.recursivelyMakeClineRequests(this.taskState.userMessageContent)(recurse:3830). Cette approche écrit une seule fois « appel API + exécution d'outils + réinitialisation d'état », la profondeur de pile reflète le nombre de tours, et les stacks d'erreur restent lisibles. - Contrôle prioritaire de la limite de mistakes : dès l'entrée, la fonction vérifie
consecutiveMistakeCount >= maxConsecutiveMistakes(mistake limit check:2826), le mode YOLO fait unreturn truepour terminer la tâche, sinonask("mistake_limit_reached")laisse l'utilisateur décider. Cela empêche le modèle de brûler des tokens dans une boucle infinie. - Réinitialisation complète de l'état de streaming : à chaque tour, on met à zéro
currentStreamingContentIndex,assistantMessageContent,userMessageContent,didRejectTool,presentAssistantMessageLockedet une dizaine d'autres champs (reset streaming state:3302), pour que le tour courant ne soit pas pollué par les restes du précédent. La réinitialisation couvre aussistreamHandler.reset()etpresentationScheduler.reset(). - Dispatch par StreamChunkCoordinator : plutôt que de
for awaitle flux directement, on l'enveloppe dans unStreamChunkCoordinator(stream coordinator:3368), qui répartit le flux en chunksreasoning/text/usageavec un callback distinct. reasoning va au reasoning handler, text va àparseAssistantMessageV2pour un re-parse complet, usage accumule les compteurs de tokens. - pWaitFor userMessageContentReady : à la fin du flux, on ne recurse pas immédiatement, on
await pWaitFor(() => this.taskState.userMessageContentReady)(pWaitFor ready:3808), pour attendre quepresentAssistantMessageait traité tous les blocs. Cela garantit que les résultats d'outils ont tous été empilés dansuserMessageContentavant le tour suivant. - noToolsUsed incrémente mistake : si l'assistant n'a émis aucun bloc tool_use sur le tour, on injecte le texte
formatResponse.noToolsUseddans userMessageContent et on faitconsecutiveMistakeCount++(noToolsUsed:3818). Au tour suivant, le modèle est notifié « appelle un outil ou tente un attempt_completion », et des tours sans outil successifs seront terminés par la limite de mistakes. - Réponse vide passe par le chemin d'erreur : si l'assistant n'a émis ni text ni tool_use sur tout le tour, on enregistre la télémétrie
empty_assistant_message, on say une erreur, puis onask("api_req_failed")pour laisser l'utilisateur décider de réessayer (empty response:3834), pour ne pas laisser la tâche continuer silencieusement.
Fichiers clés
recursivelyMakeClineRequests:2790— entrée de la fonction, signature(userContent, includeFileDetails?) => Promise<boolean>, renvoiedidEndLoop.abort check:2795— dès l'entrée, vérifietaskState.abort; si annulé, lèveTask instance aborted.apiRequestCount++:2804— incrémente le compteur de requêtes, utilisé pour la gestion de la focus chain list.mistake limit check:2826— branche de gestion des mistakes quandconsecutiveMistakeCount >= maxConsecutiveMistakes.yolo fail:2841— en mode YOLO, say error + return true pour terminer.ask mistake_limit_reached:2860— hors YOLO, ask à l'utilisateur, qui peut fournir un nouveau prompt pour continuer.reset streaming state:3302— réinitialisation complète de l'état de streaming, une dizaine de champs remis à zéro + reset handler/scheduler.attemptApiRequest call:3319— obtient le flux ; un échec sur le premier chunk est converti enapi_req_failedask par le try/catch interne de attemptApiRequest.StreamChunkCoordinator:3368— wrapper qui répartit le flux en chunks reasoning/text/usage.while true chunk loop:3387— boucle de consommation principale, récupère le prochain chunk auprès du coordinator et switch.accumulate assistantMessage:3503— les chunks text sont accumulés dans assistantMessage + assistantTextOnly, puis re-parsés.force partial false:3792— à la fin du flux, les blocs partial tool restants sont forcés àpartial = false, pour que presentAssistantMessage puisse finaliser.pWaitFor ready:3808— attend que tous les blocs soient traités, userMessageContentReady est armé.noToolsUsed bump:3818— en l'absence d'appel d'outil, injecte le prompt noToolsUsed + mistake++.recurse:3830— se rappelle avec le userMessageContent accumulé, renvoie didEndLoop.empty response:3834— chemin de réponse vide, say error + ask api_req_failed.outer catch:3935— catch de filet ; en théorie attemptApiRequest a déjà catché lui-même, c'est une double sécurité.initiateTaskLoop:1717— while externe, gère le prompt noToolsUsed + la sortie sur didEndLoop.
Flux de données
À chaque entrée dans recursivelyMakeClineRequests, on fait d'abord le contrôle de la limite de mistakes et de la détection du workspace distant, puis on réinitialise l'état de streaming et on lance le flux. Voici le cœur de la réinitialisation + lancement du flux :
// 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);Cette partie se trouve vers reset streaming state:3302. Une fois réinitialisé, StreamChunkCoordinator prend le relais sur le flux (stream coordinator:3368), et une boucle while (true) récupère les chunks auprès du coordinator. Les chunks text vont dans assistantMessage += chunk.text; assistantTextOnly += chunk.text; puis on relance immédiatement parseAssistantMessageV2(assistantMessage) pour tout re-parser (parseAssistantMessageV2 call:3509), et si la longueur augmente, on appelle scheduleAssistantPresentation pour que presentAssistantMessage fasse avancer les nouveaux blocs. Les chunks reasoning passent par le reasoning handler pour mettre à jour incrémentalement le message thinking. Les chunks usage accumulent les compteurs de tokens et le coût. À la fin du flux, processNativeToolCalls traite les appels d'outils natifs, flushAssistantPresentationOrThrow force la finalisation des blocs partial restants, puis on await pWaitFor(() => userMessageContentReady) pour attendre que tous les blocs soient traités par presentAssistantMessage (pWaitFor ready:3808). Ensuite, on vérifie s'il y a eu du tool_use : si oui, on appelle le checkpoint + on recurse avec userMessageContent ; si non, on injecte le prompt noToolsUsed et on recurse ; en cas de réponse totalement vide, on passe par le chemin d'erreur. Le didEndLoop renvoyé par la récursion remonte jusqu'à initiateTaskLoop : vrai on sort du while, faux on laisse la couche externe injecter un prompt noToolsUsed pour un nouveau tour.
Limites et échecs
- abort avant tout : la première ligne de la fonction vérifie
taskState.abort(abort check:2795) ; si annulé, lève directementTask instance aborted, sans entrer dans la réinitialisation du flux ni dans l'appel API. Cela garantit qu'après une annulation, même une récursion encore active se termine immédiatement. - Limite de mistakes : en mode YOLO,
return truetermine la tâche (yolo return:2847) ; hors YOLO, ask à l'utilisateur, et si l'utilisateur donne un nouveau prompt, il sert de userContent pour la récursion suivante (ask mistake_limit_reached:2860). - Réponse vide : si l'assistant n'a émis ni text ni tool_use sur le tour, on enregistre la télémétrie
empty_assistant_message(empty telemetry:3840), on say une erreur avec le request ID, puis onask("api_req_failed")pour laisser l'utilisateur décider de réessayer. - Échec en milieu de flux : un échec du flux après le premier chunk est capté par le try/catch externe de
recursivelyMakeClineRequests(outer catch:3935). En théorieattemptApiRequesta déjà catché en interne l'erreur du premier chunk ; ce catch est une double sécurité pour éviter les unhandled rejection. - Blocs partial restants : à la fin du flux, s'il reste des blocs partial (sans tag fermant vu), on force
partial = false(force partial false:3792), pour quepresentAssistantMessagepuisse avancer et finalement armeruserMessageContentReady = true, sinon lepWaitForattendrait indéfiniment. - Détection de workspace distant non terminée :
await this.remoteWorkspaceDetectionPromiseest attendu en début de récursion (remote workspace wait:2801), pour que le presentation scheduler utilise le bon cadence dès le premier flush. - Incrément de apiRequestCount : à chaque tour,
apiRequestCount++(apiRequestCount++:2804) etapiRequestsSinceLastTodoUpdate++, utilisés pour la gestion de la focus chain list et pour le rythme de mise à jour des todos. - Checkpoint avant la récursion : une fois les outils exécutés et
userMessageContentReadyarmé, on appellecheckpointManager.saveCheckpoint(saveCheckpoint:3811), pour que le checkpoint reflète toutes les modifications de fichiers du tour. Ce n'est qu'après qu'on entre dans la récursion suivante.
Résumé
recursivelyMakeClineRequests est le « pilote à un tour » de l'agent loop de Cline. Il enchaîne « réinitialiser l'état → tirer le flux → parser → présenter/exécuter → attendre la fin → recurse avec les résultats d'outils », et concentre les gestions de la limite de mistakes, de la réponse vide et de l'abort. Pour voir la partie qui tire le flux et gère les retries, aller à /agent-loop/attempt-api-request ; pour voir la couche la plus interne qui pousse les blocs vers l'UI et les outils, aller à /agent-loop/present-assistant-message ; pour voir les frontières de la machine à états globale, aller à /agent-loop/task-class.
Voir la documentation officielle : documentation Cline · README.