Skip to content

ToolExecutorCoordinator : table de routage des outils

源码版本v4.0.10

Responsabilités

ToolExecutorCoordinator est la table de routage « nom d'outil → handler » de Cline. Le bloc tool_use émis par le LLM contient un champ name (par exemple read_file, write_to_file, use_mcp_tool) ; le Coordinator se charge, à partir de ce nom, de trouver l'instance de handler correspondante et de lui céder l'exécution. Il n'implémente lui-même aucune logique métier : il ne fait que trois choses — enregistrement, recherche, transfert.

Il se situe entre ToolExecutor et les handlers concrets. ToolExecutor est l'unique point d'entrée outil appelé par Task (executeTool:212) ; après avoir effectué les contrôles de rejet, les restrictions du mode plan, l'aiguillage partial/complete et le hook PostToolUse, il délègue au Coordinator l'exécution effective de l'outil (coordinator.execute:575). En interne, le Coordinator maintient un Map<string, IToolHandler> comme registre ; en cas d'absence, il lève No handler registered for tool.

Il gère aussi un cas particulier : la normalisation des noms d'outils MCP et l'instanciation différée des outils dynamiques de subagent. Les noms d'outils MCP ont la forme mcp__server__tool ; ceux portant le préfixe CLINE_MCP_TOOL_IDENTIFIER sont repliés sur UseMcpToolHandler (mcp normalize:137). Les noms d'outils dynamiques de subagent enregistrés auprès de AgentConfigLoader sont injectés dans la table dynamique via un SharedToolHandler enveloppant un UseSubagentsToolHandler (dynamic subagent:146).

Motivation de conception

  • Une table de handlers en remplacement d'un switch géant : les anciennes versions de ToolExecutor reposaient sur un switch de plusieurs centaines de lignes ; ajouter un outil imposait de modifier le fichier principal. Le Coordinator factorise cette table en toolHandlersMap : un nouvel outil se résume à une classe handler et une ligne dans la map (toolHandlersMap:79).
  • Des fonctions usines plutôt que des instances directes : la map stocke des usines (v: ToolValidator) => IToolHandler | undefined, pas des instances. L'instance n'est créée qu'à l'appel de registerByName, ce qui permet d'injecter le ToolValidator à la construction (registerByName:118).
  • undefined est une valeur licite : l'usine associée à TODO (focus_chain) renvoie undefined (todo undefined:97). Cela signifie « ce nom d'outil n'est pas rattaché à un handler » ; la vérification has côté appelant le considère comme non enregistré.
  • SharedToolHandler pour mutualiser l'implémentation : quand plusieurs noms d'outils partagent la même implémentation (par exemple write_to_file, replace_in_file, new_rule utilisent tous WriteToFileToolHandler), on encapsule le handler de base dans une classe enveloppante qui ne fait que changer le champ name (SharedToolHandler:52). Cela évite d'écrire trois classes identiques.
  • Normalisation des noms MCP en amont : les noms d'outils MCP sont dynamiques et variés, mais leur comportement est unique (appeler un MCP server). À l'entrée de getHandler, tous les noms contenant CLINE_MCP_TOOL_IDENTIFIER sont repliés sur MCP_USE, de sorte qu'un seul handler couvre l'ensemble des outils MCP (mcp normalize:137).

Fichiers clés

  • IToolHandler:34 — interface de handler : le trio name, execute, getDescription.
  • IPartialBlockHandler:40 — interface pour les partial blocks en streaming, implémentée facultativement par les handlers.
  • IFullyManagedTool:44 — marqueur pour les handlers qui implémentent à la fois l'interface complète et celle des partial blocks.
  • SharedToolHandler:52 — classe enveloppante qui raccorde une implémentation de handler à plusieurs noms d'outils.
  • ToolExecutorCoordinator:75 — définition de classe, détient les deux Map handlers et dynamicSubagentHandlers.
  • toolHandlersMap:79 — registre statique associant l'énumération ClineDefaultTool à une fonction usine.
  • register:114 — stocke une instance de handler sous son name dans la Map handlers.
  • registerByName:118 — à partir d'une valeur d'énumération, récupère l'usine dans la map, crée l'instance puis l'enregistre.
  • has:128 — détermine si un nom d'outil est enregistré ; s'appuie sur getHandler et teste la vacuité.
  • getHandler:135 — logique de recherche : normalisation MCP → table statique → subagent dynamique.
  • dynamic subagent branch:146 — création différée du SharedToolHandler(UseSubagentsToolHandler) pour les outils dynamiques de subagent.
  • execute:162 — une fois le handler récupéré, appelle handler.execute(config, block) ; n'encadre pas d'un try/catch.
  • registerToolHandlers:201 — à la construction de ToolExecutor, parcourt toolUseNames et enregistre tout.
  • toolUseNames:40 — tableau de tous les noms d'outils généré depuis l'énumération ClineDefaultTool, pilote la boucle d'enregistrement.

Flux de données

La phase d'enregistrement s'effectue une seule fois à la construction de ToolExecutor ; la phase d'exécution enchaîne lookup + execute pour chaque bloc tool_use. La boucle d'enregistrement est très courte :

typescript
// apps/vscode/src/core/task/ToolExecutor.ts
private registerToolHandlers(): void {
    const validator = new ToolValidator(this.clineIgnoreController)
    // Register all tools via toolUseNames
    for (const tool of toolUseNames) {
        this.coordinator.registerByName(tool, validator)
    }
}

toolUseNames est généré automatiquement depuis l'énumération ClineDefaultTool (toolUseNames:40). Ajouter une valeur à l'énumération suffit pour que l'enregistrement la prenne en compte, sous réserve que la ligne d'usine correspondante soit aussi renseignée dans toolHandlersMap. Le Validator est construit à partir de ClineIgnoreController ; tous les handlers de type fichier/chemin en ont besoin pour leurs contrôles d'accès (ToolValidator:10).

Logique de recherche en phase d'exécution :

typescript
// apps/vscode/src/core/task/tools/ToolExecutorCoordinator.ts
getHandler(toolName: string): IToolHandler | undefined {
    // HACK: Normalize MCP tool names to the standard handler
    if (toolName.includes(CLINE_MCP_TOOL_IDENTIFIER)) {
        toolName = ClineDefaultTool.MCP_USE
    }

    const staticHandler = this.handlers.get(toolName)
    if (staticHandler) {
        return staticHandler
    }

    if (AgentConfigLoader.getInstance().isDynamicSubagentTool(toolName)) {
        const existingHandler = this.dynamicSubagentHandlers.get(toolName)
        if (existingHandler) {
            return existingHandler
        }
        const handler = new SharedToolHandler(toolName as ClineDefaultTool, new UseSubagentsToolHandler())
        this.dynamicSubagentHandlers.set(toolName, handler)
        return handler
    }

    return undefined
}

Recherche en trois couches : après normalisation du nom MCP, on interroge la table statique ; en cas d'échec, on teste s'il s'agit d'un outil dynamique de subagent, auquel cas on enregistre dans la table dynamique un SharedToolHandler enveloppant un UseSubagentsToolHandler et on le renvoie ; si rien ne correspond, on renvoie undefined, et ToolExecutor.execute côté appelant le traitera comme « non enregistré » et basculera sur la branche legacy (has check:316). La table dynamique des subagents utilise une Map distincte car son name est une chaîne déterminée à l'exécution, qui ne peut figurer dans l'énumération ClineDefaultTool.

Limites et échecs

  • Lève en cas de non-enregistrement : execute appelle getHandler et, s'il n'obtient rien, lève directement No handler registered for tool: ${block.name} (throw on missing:165). En pratique, l'appelant ToolExecutor.execute fait d'abord un has, donc on tombe toujours sur un cas résolu ; cette erreur est un filet de sécurité.
  • L'outil TODO n'a pas de handler : l'usine associée à ClineDefaultTool.TODO (focus_chain) renvoie undefined (todo undefined:97). registerByName voit undefined et saute l'enregistrement, ce qui équivaut à « le Coordinator ne prend pas en charge ce nom d'outil » ; le module focus chain s'en occupe séparément.
  • La normalisation MCP est un hack : le commentaire le qualifie d'ailleurs explicitement de HACK (hack comment:136). Tout nom d'outil MCP contenant la sous-chaîne CLINE_MCP_TOOL_IDENTIFIER est normalisé ; cela suppose que ni le nom du server ni celui de l'outil ne contiennent cet identifiant.
  • Les handlers dynamiques de subagent ne sont jamais nettoyés : la Map dynamicSubagentHandlers ne fait que croître, jamais diminuer ; sa durée de vie est alignée sur celle du Coordinator (dynamic cache set:152). Le Coordinator est lui-même aligné sur ToolExecutor, lui-même aligné sur Task : tous les noms d'outils de subagent rencontrés durant le cycle de vie d'une Task restent donc en cache.
  • L'usine crée une nouvelle instance à chaque appel : registerByName appelle l'usine en instanciant un nouveau handler (factory call:119). Comme toolUseNames ne contient pas de doublons, chaque name n'est enregistré qu'une fois, il n'existe donc qu'une instance par nom.
  • SharedToolHandler ne mutualise pas l'état : SharedToolHandler stocke baseHandler comme champ privé et transfère tous les appels à la même instance de base (shared execute:62). Les trois noms write_to_file, replace_in_file, new_rule pointent donc sur la même instance de handler et partagent le même état interne.

Résumé

Le Coordinator découple le nom d'outil de son implémentation : ajouter un outil se résume à modifier une ligne de la map et à écrire une classe handler. La recherche s'effectue en trois couches : normalisation MCP, table statique, subagent dynamique. Pour voir à quoi ressemble un handler et comment il s'enregistre, voir /tools/handlers-overview ; pour voir les contrôles de permissions et de paramètres précédant l'exécution, voir /tools/validator.

Voir la documentation officielle : documentation Cline · README.