Classe Task : cœur de l'agent à un tour
Responsabilités
La classe Task est la machine à états de l'« agent à un tour » de Cline. Une instance Task correspond à une tâche (task), qui naît quand l'utilisateur appuie sur Entrée et meurt quand la tâche est annulée ou se termine normalement. Ce n'est pas un service persistant, c'est un objet jetable que le Controller instancie pour mener à bien un tour puis jette. Ce modèle « une instance par tour » rend le contrôle de concurrence direct : une même Task ne traite jamais simultanément deux requêtes LLM, car elle est seule à avancer.
Ce qu'elle fait tient en trois temps : d'abord assembler une requête API complète à partir de l'entrée utilisateur courante, de l'historique des messages, du system prompt, de la liste des outils MCP, des règles, etc. ; ensuite piloter la réponse en streaming du LLM, et en même temps utiliser parseAssistantMessageV2 pour découper le texte en blocs text / tool_use / reasoning ; enfin confier ces blocs à presentAssistantMessage qui les présente un à un à l'UI et exécute les outils qu'ils contiennent. Les résultats d'outils reviennent comme user content de la prochaine itération, déclenchant un appel récursif (recursion), jusqu'à ce que le modèle n'émette plus de tool_use ou que l'utilisateur interrompe.
Pour la comprendre, il suffit de la voir comme trois boucles imbriquées : la plus externe, initiateTaskLoop, gère le cas « le modèle n'a appelé aucun outil, on redemande » ; la couche intermédiaire recursivelyMakeClineRequests effectue à chaque récursion un appel API complet + une exécution d'outils ; la couche interne presentAssistantMessage fait avancer les blocs en flux au fur et à mesure. Toutes les interactions UI — poser une question, demander confirmation, rapporter de la progression — passent par deux sorties, ask et say, qui injectent les messages dans messageStateHandler puis les postent au webview.
Motivation de conception
- Une tâche, une instance : isoler l'état dans chaque instance Task permet de gérer l'annulation, le rollback et les checkpoints (points de contrôle) à la granularité de l'instance.
abortTaskse contente d'armer le flagtaskState.abort, et tous les chemins récursifs le lisent et se terminent eux-mêmes (abortTask:1801). - Récursion plutôt que boucle : après chaque réponse LLM, le résultat d'outil est naturellement le user content de l'itération suivante, donc
recursivelyMakeClineRequestsse rappelle elle-même à la fin (recurse:3830). Cette approche écrit une seule fois le code « appel API + exécution d'outils », la profondeur de la pile d'appels reflète le nombre de tours, et les stacks d'erreur restent lisibles. - Séparation entre parsing et présentation en streaming : la sortie du LLM est parsée au fil de l'eau,
parseAssistantMessageV2re-parse tout le texte assistant à chaque segment reçu (parseAssistantMessageV2 call:3509), etpresentAssistantMessageavance à la frontière des blocs, de sorte que les outils n'attendent pas la fin de la réponse pour se lancer. - Un verrou unique contre les courses d'état : toute modification de l'état de Task passe par
withStateLockqui prend le même Mutex (withStateLock:205), afin d'éviter que les callbacks de streaming, l'exécution des outils et les interactions UI ne corrompent l'état en concurrence. - Mode YOLO et limite de mistakes : quand les erreurs consécutives atteignent le plafond, on termine immédiatement (
mistake limit:2826), pour empêcher le modèle de brûler des tokens dans une boucle infinie.
Fichiers clés
Task class definition:188—export class Task, déclare tous les champs essentiels : taskId, taskState, api, controller, messageStateHandler, etc.constructor:310— reçoitTaskParams, initialise clineIgnore, toolExecutor, streamHandler, presentationScheduler et autres dépendances.startTask:1253— point d'entrée, prépare le user content initial, lance le hook TaskStart, puis appelleinitiateTaskLoop.initiateTaskLoop:1717— boucle while externe ; quand le modèle ne renvoie que du texte sans appeler d'outil, utilise le promptnoToolsUsedpour relancer.recursivelyMakeClineRequests:2790— pilote récursif intermédiaire, responsable du contrôle de la limite de mistakes, de l'initialisation des checkpoints, et de l'appel àattemptApiRequestpour tirer le flux.attemptApiRequest:2175— attend les connexions MCP, lit les règles, assemble le system prompt, puisyield*le flux LLM.presentAssistantMessage:2630— pousse-blocs interne, dispatch selon le type : text (on retire les tags thinking puis say), tool_use (délégué àtoolExecutor.executeTool).ask:789— sortie demandant une réponse utilisateur, gère les mises à jour partial et le callback de réponse webview.say:969— sortie unidirectionnelle pour rapporter de la progression, le mode partial sert à mettre à jour en streaming une même message.abortTask:1801— annulation en plusieurs phases : décide d'abord s'il faut lancer le hook TaskCancel, puis arme le flag abort, puis annule hooks/commandes en arrière-plan, enfin lance le hook.ToolExecutor.executeTool:212— Task délègue l'exécution des outils, elle ne traite elle-même aucun outil spécifique.
Flux de données
Le chemin central d'une requête LLM est « récursion → tirer le flux → parser → présenter → reflux → récursion ». En entrant, recursivelyMakeClineRequests réinitialise l'état de streaming de ce tour, puis lance attemptApiRequest pour obtenir le 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;
// ...
const stream = this.attemptApiRequest(previousApiReqIndex); // yields only if the first chunk is successfulCette partie se trouve vers reset streaming state:3302. Après la réinitialisation, StreamChunkCoordinator répartit le flux en chunks text / usage / reasoning avec un callback distinct par type. À chaque segment de text reçu, on relance parseAssistantMessageV2 pour découper tout le texte assistant en tableau de blocs (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);Quand le tableau de blocs change, scheduleAssistantPresentation déclenche presentAssistantMessage. Celui-ci se répartit selon block.type : text va vers say("text", ...), tool_use va vers toolExecutor.executeTool(block) (executeTool call:2743). Les résultats d'outils sont poussés dans taskState.userMessageContent, et quand le flux est entièrement consommé et que userMessageContentReady est armé, recursivelyMakeClineRequests utilise ce user content pour se rappeler (recurse:3830), jusqu'à ce que le modèle n'émette plus de tool_use, auquel cas la boucle externe initiateTaskLoop utilise le prompt noToolsUsed pour relancer, ou bien l'utilisateur met fin.
Limites et échecs
- Limite de mistakes atteinte : quand les erreurs consécutives atteignent
maxConsecutiveMistakes, le mode YOLO fait unreturn truepour terminer la tâche ; sinonask("mistake_limit_reached")laisse l'utilisateur décider (mistake limit:2826). - Réponse vide : si l'assistant n'a émis aucun bloc text ni tool_use sur tout le tour, on enregistre une télémétrie
empty_assistant_messageet on invite l'utilisateur à réessayer (empty response:3834). - Annulation utilisateur en cours :
abortTaskcapture d'abord s'il faut lancer le hook TaskCancel, puis arme le flagabort, pour éviter que le hook ne soit mal évalué une fois le flag posé (abortTask:1801). - Outil rejeté : une fois
didRejectToolarmé, les blocs text suivants sont skip et le flux est tronqué par[Response interrupted by user feedback](didRejectTool:3536). - MCP non connecté :
attemptApiRequestattend viapWaitForau plus 10 secondes ; en cas de timeout, on se contente de logger sans bloquer, et le system prompt est généré normalement (mcp wait:2177). - Checkpoint initial non terminé : pendant que le premier commit de checkpoint est en cours, les outils non read-only sont bloqués par
await this.initialCheckpointCommitPromise, les outils read-only peuvent tourner en parallèle (initialCheckpoint gate:2737). - Finalisation des blocs partial : à la fin du flux, les blocs partial restants sont forcés à
partial = falsepour quepresentAssistantMessagepuisse avancer et finalement armeruserMessageContentReady(finalize partial blocks:3783).
Résumé
La classe Task est la machine à états complète de Cline à l'échelle d'un tour : elle encapsule le cycle de vie d'une tâche comme une chaîne claire « construction → démarrage → récursion de streaming → exécution d'outils → finalisation », toutes les interactions UI convergent vers les sorties ask / say, et toutes les modifications d'état convergent vers un seul Mutex. Ce compromis « une instance par tour » permet à l'annulation, aux checkpoints et à la limite de mistakes d'être traitées simplement à l'échelle de l'instance.
Pour creuser, voir :
- la récursion elle-même :
/agent-loop/recursion - l'appel LLM et l'assemblage du system prompt :
/agent-loop/attempt-api-request - comment le texte assistant est découpé en blocs :
/agent-loop/parse-assistant-message
Voir la documentation officielle : documentation Cline · README