ToolExecutorCoordinator : table de routage des outils
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
ToolExecutorreposaient sur un switch de plusieurs centaines de lignes ; ajouter un outil imposait de modifier le fichier principal. Le Coordinator factorise cette table entoolHandlersMap: 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 deregisterByName, ce qui permet d'injecter le ToolValidator à la construction (registerByName:118). undefinedest une valeur licite : l'usine associée àTODO(focus_chain) renvoieundefined(todo undefined:97). Cela signifie « ce nom d'outil n'est pas rattaché à un handler » ; la vérificationhascô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_ruleutilisent tousWriteToFileToolHandler), on encapsule le handler de base dans une classe enveloppante qui ne fait que changer le champname(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 contenantCLINE_MCP_TOOL_IDENTIFIERsont repliés surMCP_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 trioname,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 MaphandlersetdynamicSubagentHandlers.toolHandlersMap:79— registre statique associant l'énumérationClineDefaultToolà une fonction usine.register:114— stocke une instance de handler sous sonnamedans la Maphandlers.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 surgetHandleret teste la vacuité.getHandler:135— logique de recherche : normalisation MCP → table statique → subagent dynamique.dynamic subagent branch:146— création différée duSharedToolHandler(UseSubagentsToolHandler)pour les outils dynamiques de subagent.execute:162— une fois le handler récupéré, appellehandler.execute(config, block); n'encadre pas d'un try/catch.registerToolHandlers:201— à la construction de ToolExecutor, parcourttoolUseNameset enregistre tout.toolUseNames:40— tableau de tous les noms d'outils généré depuis l'énumérationClineDefaultTool, 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 :
// 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 :
// 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 :
executeappellegetHandleret, s'il n'obtient rien, lève directementNo handler registered for tool: ${block.name}(throw on missing:165). En pratique, l'appelantToolExecutor.executefait d'abord unhas, 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) renvoieundefined(todo undefined:97).registerByNamevoit 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îneCLINE_MCP_TOOL_IDENTIFIERest 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
dynamicSubagentHandlersne 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 :
registerByNameappelle l'usine en instanciant un nouveau handler (factory call:119). CommetoolUseNamesne 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 :
SharedToolHandlerstockebaseHandlercomme champ privé et transfère tous les appels à la même instance de base (shared execute:62). Les trois nomswrite_to_file,replace_in_file,new_rulepointent 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.