Tool Handlers : cartographie complète des handlers d'outils
Responsabilités
Un handler est l'implémentation concrète de chaque outil. Une classe handler correspond à un type de comportement : lire un fichier, écrire un fichier, exécuter une commande, appeler un MCP, lancer un subagent, etc. Tous implémentent l'interface IToolHandler (IToolHandler:34) et exposent le trio name, execute, getDescription. Ceux qui doivent prendre en charge les partial blocks en streaming implémentent en plus IPartialBlockHandler ; ceux qui implémentent les deux sont marqués par IFullyManagedTool.
Le catalogue d'outils de Cline est défini dans l'énumération ClineDefaultTool (ClineDefaultTool:8). Elle compte actuellement 27 valeurs, de ask_followup_question à use_subagents. Ces noms correspondent directement au champ name du bloc tool_use vu par le LLM. Sous l'énumération, une ligne toolUseNames = Object.values(ClineDefaultTool) (toolUseNames:40) aplatit l'énumération en tableau, que ToolExecutor parcourt à la construction pour procéder à l'enregistrement exhaustif.
Les fichiers de handler se trouvent tous dans apps/vscode/src/core/task/tools/handlers/, un fichier par outil, et les classes suivent la convention XxxToolHandler. Tous partagent un même objet de contexte TaskConfig, qui contient cwd, taskState, messageState, api, divers services et callbacks ; le handler y accède pour joindre toutes ses dépendances externes (asToolConfig:135).
Motivation de conception
- Un fichier par outil : un handler par fichier, aux responsabilités clairement délimitées. Ajouter un outil se résume à créer un fichier sans toucher au code existant ; retirer un outil se résume à supprimer un fichier.
- Composition d'interfaces plutôt que d'héritage :
IToolHandlerest le contrat minimal,IPartialBlockHandlerajoute la capacité de streaming,IFullyManagedToolest un marqueur « tout-en-un » (IFullyManagedTool:44). La composition l'emporte sur l'héritage : le handler choisit les interfaces à implémenter selon ses besoins. - Le handler ne porte pas d'état : l'instance de handler est mise en cache par le Coordinator, et tout état vient via
TaskConfig. Une même instance peut être appelée plusieurs fois viaexecute; elle ne doit pas stocker d'état au niveau tâche dans ses propres champs. - TaskConfig construit une fois, utilisé partout : ToolExecutor collecte toutes ses dépendances dans un objet unique au moment de construire
asToolConfig()(asToolConfig:135) ; le handler reçoit config et accède ainsi à mcpHub, browserSession, diffViewProvider, clineIgnoreController, etc., sans avoir à les injecter lui-même. - Enregistrement piloté par l'énumération ClineDefaultTool :
toolUseNamesest généré automatiquement depuis l'énumération ; la boucle d'enregistrement n'a pas à maintenir la liste des outils à la main (for of toolUseNames:204). Ajouter une valeur à l'énumération l'intègre automatiquement à la boucle d'enregistrement, sous réserve quetoolHandlersMapdu Coordinator fournisse l'usine correspondante. - SharedToolHandler pour mutualiser l'implémentation : quand plusieurs noms partagent une même implémentation (write_to_file / replace_in_file / new_rule utilisent tous les trois
WriteToFileToolHandler), une classe enveloppante se contente de renommer (SharedToolHandler:52), évitant d'écrire trois classes identiques.
Fichiers clés
ClineDefaultTool enum:8— énumération de 27 noms d'outils, source du tool name vu par le LLM.toolUseNames:40— énumération aplatie en tableau, pilote l'enregistrement exhaustif de ToolExecutor.toolHandlersMap:79— table de mappage des 27 valeurs d'énumération vers des usines de handler ; à modifier pour ajouter un outil.registerToolHandlers:201— parcourttoolUseNamesà la construction pour tout enregistrer.asToolConfig:135— construit unTaskConfigavant chaque exécution d'outil pour passer l'ensemble des dépendances au handler.WriteToFileToolHandler:26— implémenteIFullyManagedTool, prend en charge partial + complete, mutualisé par les trois nomswrite_to_file,replace_in_file,new_rule.ReadFileToolHandler:142— implémentation deread_file, construit avec ToolValidator pour la validation des chemins.ExecuteCommandToolHandler:58— implémentation deexecute_command, exécution de commandes bash et contrôle des permissions.AttemptCompletionHandler:39— implémentation deattempt_completion, n'implémente queIToolHandler + IPartialBlockHandler, marque la fin de tâche.UseMcpToolHandler:14— implémentation deuse_mcp_tool, point de passage unique pour tous les appels MCP.UseSubagentsToolHandler:49— implémentation deuse_subagents, utilisée aussi pour les noms dynamiques de subagent (via l'enveloppe SharedToolHandler).ListFilesToolHandler partial approval:48— dès la phase de partial block, appelleshouldAutoApproveToolWithPathpour décider du chemin UI.
Flux de données
Le flux d'ajout d'un outil est : « écrire une classe handler → enregistrer une ligne dans la map → ajouter une valeur à l'énumération ». Voici la tête de l'enregistrement dans la map :
// apps/vscode/src/core/task/tools/ToolExecutorCoordinator.ts
private readonly toolHandlersMap: Record<ClineDefaultTool, (v: ToolValidator) => IToolHandler | undefined> = {
[ClineDefaultTool.ASK]: (_v: ToolValidator) => new AskFollowupQuestionToolHandler(),
[ClineDefaultTool.ATTEMPT]: (_v: ToolValidator) => new AttemptCompletionHandler(),
[ClineDefaultTool.BASH]: (v: ToolValidator) => new ExecuteCommandToolHandler(v),
[ClineDefaultTool.FILE_EDIT]: (v: ToolValidator) =>
new SharedToolHandler(ClineDefaultTool.FILE_EDIT, new WriteToFileToolHandler(v)),
[ClineDefaultTool.FILE_READ]: (v: ToolValidator) => new ReadFileToolHandler(v),
[ClineDefaultTool.FILE_NEW]: (v: ToolValidator) => new WriteToFileToolHandler(v),
// ...
[ClineDefaultTool.TODO]: (_v: ToolValidator) => undefined,
}La fonction usine reçoit ToolValidator en paramètre ; les handlers qui valident des chemins (read/write/search/list) le récupèrent, tandis que ceux qui n'en ont pas besoin (ask/attempt/browser/mcp, etc.) l'ignorent via _v. Seul TODO renvoie undefined, ce qui signifie que le Coordinator ne prend pas en charge ce nom d'outil (todo undefined:97).
Le config que le handler reçoit à l'exécution ressemble à ceci :
// apps/vscode/src/core/task/ToolExecutor.ts
const config: TaskConfig = {
taskId: this.taskId,
ulid: this.ulid,
mode: this.stateManager.getGlobalSettingsKey("mode"),
cwd: this.cwd,
workspaceManager: this.workspaceManager,
taskState: this.taskState,
messageState: this.messageStateHandler,
api: this.api,
autoApprover: this.autoApprover,
services: {
mcpHub: this.mcpHub,
browserSession: this.browserSession,
diffViewProvider: this.diffViewProvider,
fileContextTracker: this.fileContextTracker,
clineIgnoreController: this.clineIgnoreController,
commandPermissionController: this.commandPermissionController,
contextManager: this.contextManager,
stateManager: this.stateManager,
},
callbacks: {
say: this.say,
ask: this.ask,
shouldAutoApproveTool: this.shouldAutoApproveTool.bind(this),
shouldAutoApproveToolWithPath: this.shouldAutoApproveToolWithPath.bind(this),
// ...
},
coordinator: this.coordinator,
}Un avertissement figure en commentaire : le handler peut lire les champs de config, mais ne doit pas modifier les champs de config lui-même (par exemple config.browserSession = ... ne changera pas la variable d'instance de ToolExecutor) (config warning:134). Pour changer de browser session, il faut passer par une entrée dédiée comme applyLatestBrowserSettings. config.coordinator transmet aussi le coordinateur lui-même, afin qu'un handler puisse rappeler d'autres outils en cours d'exécution (c'est ce qu'utilise l'outil de subagent).
Limites et échecs
- Nom d'outil sans usine silencieusement ignoré :
toolHandlersMapest de typeRecord<ClineDefaultTool, ...>, TypeScript impose donc une entrée pour chaque valeur d'énumération. Mais l'usine peut renvoyerundefined; seulTODOle fait aujourd'hui (todo undefined:97).registerByNamevoit undefined et saute l'enregistrement, ce qui équivaut à « non enregistré ». - Le handler est mutualisé mais doit rester sans état :
WriteToFileToolHandlerest partagé par les trois nomswrite_to_file,replace_in_file,new_rulesur une même instance (file_edit shared:83). Si le handler stockait un état de tâche dans ses champs, les trois noms se pollueraient mutuellement. Tout état vient donc viaTaskConfig. asToolConfigest reconstruit à chaque exécution : à chaque entrée dansToolExecutor.execute, un nouveau config est assemblé viaasToolConfig()(asToolConfig call:321). En revanche, les instances de service référencées dans config sont les mêmes : si le handler modifie l'état d'un service, cela s'applique entre appels.- Le handler de partial block ne pousse pas de tool result : le commentaire de
handlePartialBlockle précise explicitement : « We don't push tool results in partial blocks » (no partial result:521). En phase partial, on ne met à jour que l'UI ; le véritable tool result n'est poussé qu'au traitement du complete block. - Les erreurs du handler sont rattrapées par ToolExecutor :
ToolExecutor.executeencadrecoordinator.executed'un try/catch (error catch:373). Toute erreur levée par un handler est interceptée parhandleErroret convertie enformatResponse.toolErrorinjecté dans la conversation, sans faire planter la Task. - Trois endroits obligatoires pour ajouter un outil : ajouter un outil impose trois modifications : ajouter une valeur à l'énumération
ClineDefaultTool, ajouter une usine àtoolHandlersMap, créer le fichier de handler.toolUseNamesest généré automatiquement depuis l'énumération, donc pas besoin d'y toucher ; en revanche, il faut aussi ajouter la description de l'outil dansgetSystemPrompt, sinon le modèle ignore son existence.
Résumé
Les handlers constituent l'extrémité terminale du système d'outils de Cline : un fichier par outil, implémentant l'interface IToolHandler. L'enregistrement est piloté par l'énumération ClineDefaultTool associée à la table d'usines toolHandlersMap ; ajouter un outil se résume à trois modifications. Pour le mécanisme de recherche de handler, voir /tools/coordinator ; pour les contrôles de paramètres et de permissions précédant l'exécution, voir /tools/validator ; pour savoir qui appelle les outils, voir /agent-loop/present-assistant-message.
Voir la documentation officielle : documentation Cline · README.