Skip to content

Tool Handlers : cartographie complète des handlers d'outils

源码版本v4.0.10

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 : IToolHandler est le contrat minimal, IPartialBlockHandler ajoute la capacité de streaming, IFullyManagedTool est 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 via execute ; 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 : toolUseNames est 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 que toolHandlersMap du 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 — parcourt toolUseNames à la construction pour tout enregistrer.
  • asToolConfig:135 — construit un TaskConfig avant chaque exécution d'outil pour passer l'ensemble des dépendances au handler.
  • WriteToFileToolHandler:26 — implémente IFullyManagedTool, prend en charge partial + complete, mutualisé par les trois noms write_to_file, replace_in_file, new_rule.
  • ReadFileToolHandler:142 — implémentation de read_file, construit avec ToolValidator pour la validation des chemins.
  • ExecuteCommandToolHandler:58 — implémentation de execute_command, exécution de commandes bash et contrôle des permissions.
  • AttemptCompletionHandler:39 — implémentation de attempt_completion, n'implémente que IToolHandler + IPartialBlockHandler, marque la fin de tâche.
  • UseMcpToolHandler:14 — implémentation de use_mcp_tool, point de passage unique pour tous les appels MCP.
  • UseSubagentsToolHandler:49 — implémentation de use_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, appelle shouldAutoApproveToolWithPath pour 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 :

typescript
// 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 :

typescript
// 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é : toolHandlersMap est de type Record<ClineDefaultTool, ...>, TypeScript impose donc une entrée pour chaque valeur d'énumération. Mais l'usine peut renvoyer undefined ; seul TODO le fait aujourd'hui (todo undefined:97). registerByName voit undefined et saute l'enregistrement, ce qui équivaut à « non enregistré ».
  • Le handler est mutualisé mais doit rester sans état : WriteToFileToolHandler est partagé par les trois noms write_to_file, replace_in_file, new_rule sur 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 via TaskConfig.
  • asToolConfig est reconstruit à chaque exécution : à chaque entrée dans ToolExecutor.execute, un nouveau config est assemblé via asToolConfig() (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 handlePartialBlock le 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.execute encadre coordinator.execute d'un try/catch (error catch:373). Toute erreur levée par un handler est interceptée par handleError et convertie en formatResponse.toolError injecté 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. toolUseNames est 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 dans getSystemPrompt, 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.