SubagentRunner : contexte indépendant pour sous-tâches
Responsabilités
SubagentRunner est l'exécuteur par lequel Cline encapsule « laisser le LLM faire tourner une sous-tâche dans sa propre fenêtre de contexte ». Quand l'agent principal rencontre un appel d'outil (tool_use) de type subagent (new SubagentRunner:215), il instancie un runner par prompt, et ce runner fait tourner en interne une mini boucle d'agent — tirer le stream, parser les tool_calls, exécuter, réinjecter le résultat dans la conversation — mais avec un tableau conversation totalement indépendant et son propre ContextManager, sans pollution de l'historique du Task principal.
Sa position se situe sous le Task principal, au-dessus des outils concrets. Le Task principal passe le prompt et le TaskConfig, le runner utilise SubagentBuilder pour calculer le sous-ensemble d'outils, le system prompt et l'API handler de ce sous-agent, puis boucle dans son propre run. Quand le sous-agent termine via attempt_completion, la chaîne de résultat est renvoyée telle quelle à l'agent principal, qui n'y voit qu'un simple résultat d'outil. Ce design permet au Task principal d'enfermer les actions coûteuses du type « explorer le code, lire plusieurs fichiers, synthétiser une réponse » dans une fenêtre isolée, sans que les tokens intermédiaires ne pèsent sur le contexte principal.
Motivation de conception
- Fenêtre de contexte indépendante : le sous-agent utilise son propre tableau
ClineStorageMessage[], et le Task principal ne voit pas les appels d'outils intermédiaires du sous-agent (conversation init:463). - Liste blanche d'outils : le sous-agent est en lecture seule par défaut,
SUBAGENT_DEFAULT_ALLOWED_TOOLSne contient que file_read / list_files / search / list_code_def / bash / use_skill / attempt_completion (default allowed tools:14). - attempt_completion forcé : quelle que soit la configuration d'outils choisie par l'utilisateur,
ATTEMPTest toujours ajouté (force ATTEMPT:96), sinon le sous-agent ne peut pas conclure. - Surcharge du modèle selon l'agent :
AgentConfigLoaderlit lemodelIdd'un agent donné, etSubagentBuilder.applyModelOverrideremplace l'apiHandler (applyModelOverride:57), pour que différents sous-agents puissent utiliser des modèles différents. - Compactage automatique du contexte : avant chaque tour, le runner vérifie le nombre de tokens de la requête précédente, et si le seuil est dépassé, il appelle
compactConversationForContextWindowpour d'abord tenter l'optimisation file read, puis la troncation (shouldCompactBeforeNextRequest:488). - Retry sur réponse vide : si le modèle ne sort aucun tool_use à un tour, on considère la réponse vide, on réinjecte un prompt
noToolsUsedet on redemande ; au-delà deMAX_EMPTY_ASSISTANT_RETRIES(3) tentatives, on échoue directement (empty response retry:662). - Plusieurs runners parallèles, annulation synchrone : un appel d'outil subagent peut porter plusieurs prompts, auquel cas plusieurs runners tournent en parallèle, et l'abort est scruté toutes les 100 ms (
abort poll:216), pour interrompre tous les runners d'un coup.
Fichiers clés
SubagentRunner class:254— détient agent, apiHandler, allowedTools, état d'abort.run method:330— boucle principale, chaque tour tire le stream, parse les tool_calls, exécute, réinjecte.while loop:485— boucle infinie, ne sort que via attempt_completion ou abort.createMessageWithInitialChunkRetry:519— tire le stream, et si le premier chunk renvoie « context window exceeded », compacte puis réessaie.tool whitelist check:726— si l'outil n'est pas dans allowedTools, renvoie toolError sans exécuter.attempt_completion handling:702— si on touche attempt, on renvoie la chaîne result à l'agent principal et on sort.abort method:272— appelle api.abort, annule la commande en cours d'exécution.compactConversationForContextWindow:895— tente d'abord l'optimisation file read, puis appellegetNextTruncationRangepour supprimer une tranche.shouldCompactBeforeNextRequest:977—useAutoCondense+ modèles next-gen à seuil 0.75, sinon surmaxAllowedSize.buildSystemPrompt:73— assemble<generated> + <agent identity> + SUBAGENT_SYSTEM_SUFFIX.spawn runners:215— le point où l'agent principal instancie réellement SubagentRunner.
Flux de données
Au démarrage, le runner prépare le system prompt et le user content initial. La conversation initiale ne contient que deux entrées : le prompt utilisateur + un bloc de métadonnées workspace, ce dernier servant à la boucle de task côté serveur pour valider :
// apps/vscode/src/core/task/tools/subagent/SubagentRunner.ts
const conversation: ClineStorageMessage[] = [
{
role: "user",
content: [
{ type: "text", text: prompt } as ClineTextContentBlock,
// Server-side task loop checks require workspace metadata to be present in the
// initial user message of subagent runs.
...(workspaceMetadataEnvironmentBlock
? [{ type: "text", text: workspaceMetadataEnvironmentBlock } as ClineTextContentBlock]
: []),
],
},
];
while (true) {
if (
usageState.lastRequest &&
this.shouldCompactBeforeNextRequest(usageState.lastRequest.totalTokens, api, providerInfo.model.id)
) {
const compactResult = this.compactConversationForContextWindow(
contextManager,
conversation,
contextState.conversationHistoryDeletedRange,
);
contextState.conversationHistoryDeletedRange = compactResult.conversationHistoryDeletedRange;
// ...
}
// ...
}Ce bloc se trouve à conversation + while:463. Une fois entré dans la boucle, chaque tour appelle createMessageWithInitialChunkRetry pour tirer le stream, et les chunks sont classés en usage / text / tool_calls / reasoning. À la fin du stream, les tool calls finalisés sont exécutés un par un ; un call qui atteint attempt_completion renvoie directement son result à l'agent principal (return on attempt:723) ; les autres outils passent par coordinator.getHandler(toolName).execute, et leur résultat est réinjecté comme user content pour continuer la boucle.
Limites et échecs
- Outil hors liste blanche = toolError direct : si le sous-agent tente
write_to_filepar exemple, c'est bloqué, et la chaîne d'erreur est réinjectée dans user content (whitelist check:726), la boucle continue sans interrompre toute la tâche. - attempt_completion sans result : si result est vide, on réinjecte
missingToolParameterErrorplutôt que de conclure (missing result:705). - Retry sur context window exceeded au premier chunk : si le premier chunk échoue avec une erreur liée à la fenêtre de contexte, on compacte la conversation puis on réessaie (
context window retry:1036), jusqu'àMAX_INITIAL_STREAM_ATTEMPTSfois. - Abort d'une commande en cours : l'abort ne se contente pas d'annuler le flux API, il délègue à
cancelRunningCommandToolpour stopper aussi le bash en cours (cancel running command:281). - Fallback native vs non-native tool calls : en mode non-native, à la réception d'un chunk tool_calls structuré, on l'exécute quand même mais on sérialise le résultat en texte brut pour éviter un appairage tool_result décalé (
non-native fallback:631). - Accumulation des stats : chaque chunk met à jour inputTokens / outputTokens / cacheWrite / cacheRead / totalCost, et pousse en temps réel au frontend via
onProgress(stats accumulation:534). - Assistant vide compte pour un tour : si l'assistant renvoie un content complètement vide, on injecte explicitement « Failure: I did not provide a response. » avant de réinjecter noToolsUsed (
empty assistant:671).
Résumé
SubagentRunner permet à l'agent principal d'externaliser les « explorations à large périmètre » vers un mini-agent isolé. Pour voir la stratégie de compression de contexte dont il dépend, lire context-manager ; pour voir comment le Task principal réinjecte les résultats d'outils et fait la récursion, lire agent-loop/task-class ; pour voir comment l'agent principal appelle les outils MCP, lire mcp-hub.
Voir la documentation officielle : Cline docs · README.