Skip to content

SubagentRunner : contexte indépendant pour sous-tâches

源码版本v4.0.10

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_TOOLS ne 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, ATTEMPT est toujours ajouté (force ATTEMPT:96), sinon le sous-agent ne peut pas conclure.
  • Surcharge du modèle selon l'agent : AgentConfigLoader lit le modelId d'un agent donné, et SubagentBuilder.applyModelOverride remplace 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 compactConversationForContextWindow pour 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 noToolsUsed et on redemande ; au-delà de MAX_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

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 :

typescript
// 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_file par 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 missingToolParameterError plutô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_ATTEMPTS fois.
  • Abort d'une commande en cours : l'abort ne se contente pas d'annuler le flux API, il délègue à cancelRunningCommandTool pour 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.