Skip to content

Tool Handlers: mapa completo de procesadores de herramientas

源码版本v4.0.10

Responsabilidades

Un handler es la implementación concreta de cada herramienta. Una clase handler corresponde a un tipo de comportamiento de herramienta: leer archivos, escribir archivos, ejecutar comandos, llamar a MCP, lanzar subagent, etc. Todas implementan la interfaz IToolHandler (IToolHandler:34), que ofrece el trío name, execute, getDescription. Las que necesitan soportar partial block en streaming implementan además IPartialBlockHandler; las que implementan ambas se marcan como IFullyManagedTool.

El catálogo de herramientas de Cline se define en el enum ClineDefaultTool (ClineDefaultTool:8). Actualmente tiene 27 valores, desde ask_followup_question hasta use_subagents. Esos nombres corresponden directamente con el campo name del bloque tool_use que ve el LLM. Debajo del enum hay una línea toolUseNames = Object.values(ClineDefaultTool) (toolUseNames:40) que aplana el enum a un array; el constructor de ToolExecutor recorre ese array para completar el registro total.

Los archivos de handler viven en apps/vscode/src/core/task/tools/handlers/, un archivo por herramienta, con clases llamadas XxxToolHandler. Todos los handlers comparten un objeto de contexto TaskConfig con cwd, taskState, messageState, api, varios services y callbacks; el handler accede a todas sus dependencias externas a través de él (asToolConfig:135).

Motivación de diseño

  • Un archivo por herramienta: cada handler en su propio archivo, con fronteras de responsabilidad claras. Añadir una herramienta solo suma un archivo sin tocar el código existente; eliminarla solo borra un archivo.
  • Interfaces por capas, no herencia: IToolHandler es el contrato mínimo, IPartialBlockHandler es la capacidad adicional de streaming, IFullyManagedTool es la marca «completo» (IFullyManagedTool:44). Composición sobre herencia: el handler elige qué interfaz implementar según necesidad.
  • El handler no mantiene estado: el Coordinator cachea la instancia del handler, y todo el estado entra a través de TaskConfig. Una misma instancia de handler puede recibir múltiples llamadas execute, así que no debe guardar estado de tarea en sus propios campos.
  • TaskConfig se construye una vez y se usa en muchos sitios: ToolExecutor recoge todas las dependencias en un único objeto al construir asToolConfig() (asToolConfig:135); el handler, con solo recibir config, accede a servicios como mcpHub, browserSession, diffViewProvider, clineIgnoreController sin tener que inyectarlos por su cuenta.
  • Registro dirigido por el enum ClineDefaultTool: toolUseNames se genera automáticamente a partir del enum, así que el bucle de registro no se mantiene a mano (for of toolUseNames:204). Añadir un valor al enum lo incorpora automáticamente al bucle de registro, siempre que toolHandlersMap del Coordinator tenga la fábrica correspondiente.
  • SharedToolHandler reutiliza implementaciones: cuando varios nombres comparten una misma implementación (write_to_file / replace_in_file / new_rule comparten los tres WriteToFileToolHandler), la clase envolvente renombra (SharedToolHandler:52), evitando escribir tres clases idénticas.

Archivos clave

  • ClineDefaultTool enum:8 — enum con 27 nombres de herramientas; el tool name que ve el LLM proviene de aquí.
  • toolUseNames:40 — enum aplanado a array; impulsa el registro total en ToolExecutor.
  • toolHandlersMap:79 — mapa de los 27 valores del enum a fábricas de handler; añadir una herramienta se modifica aquí.
  • registerToolHandlers:201 — recorre toolUseNames al construir y registra todo.
  • asToolConfig:135 — antes de ejecutar cada herramienta construye un TaskConfig que empaqueta todas las dependencias para el handler.
  • WriteToFileToolHandler:26 — implementa IFullyManagedTool, soporta partial + complete; lo comparten los tres nombres write_to_file, replace_in_file, new_rule.
  • ReadFileToolHandler:142 — implementación de read_file; el constructor recibe ToolValidator para validar rutas.
  • ExecuteCommandToolHandler:58 — implementación de execute_command; ejecuta comandos bash + comprueba permisos de comando.
  • AttemptCompletionHandler:39 — implementación de attempt_completion; solo implementa IToolHandler + IPartialBlockHandler; marca la tarea como completada.
  • UseMcpToolHandler:14 — implementación de use_mcp_tool; todas las llamadas a herramientas MCP pasan por aquí.
  • UseSubagentsToolHandler:49 — implementación de use_subagents; también sirve para nombres dinámicos de subagent (vía envoltorio SharedToolHandler).
  • ListFilesToolHandler partial approval:48 — ya en la fase partial block invoca shouldAutoApproveToolWithPath para decidir la ruta de UI.

Flujo de datos

El flujo para añadir una herramienta es «escribir la clase handler → añadir una línea en el map → añadir un valor al enum». La entrada del map tiene esta pinta:

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,
}

El parámetro de la fábrica es ToolValidator; los handlers que necesitan validar rutas (read/write/search/list) lo reciben, y los que no (ask/attempt/browser/mcp, etc.) usan _v como marcador. Solo TODO devuelve undefined, lo que indica que el Coordinator no gestiona ese nombre (todo undefined:97).

El config que recibe el handler al ejecutar tiene esta forma:

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,
}

Hay una advertencia en los comentarios: el handler puede leer los campos de config, pero no modificar los propios campos de config (por ejemplo, config.browserSession = ... no cambia la variable de instancia de ToolExecutor) (config warning:134). Para cambiar de browser session hay que pasar por una entrada específica tipo applyLatestBrowserSettings. config.coordinator pasa también al propio coordinador, de modo que el handler puede invocar otras herramientas durante la ejecución (la herramienta subagent se apoya en esto).

Límites y fallos

  • Nombres de herramienta sin fábrica se saltan silenciosamente: toolHandlersMap es Record<ClineDefaultTool, ...>, así que TS obliga a rellenar una entrada por cada valor del enum. Pero la fábrica puede devolver undefined; hoy solo TODO lo hace (todo undefined:97). registerByName al ver undefined se salta el registro, equivalente a «no registrar».
  • El handler se comparte pero debe mantenerse sin estado: WriteToFileToolHandler lo comparten los tres nombres write_to_file, replace_in_file, new_rule en una misma instancia (file_edit shared:83). Si el handler guardara estado de tarea en campos de instancia, los tres nombres se contaminarían entre sí. Por eso todo el estado entra vía TaskConfig.
  • asToolConfig se construye cada vez: cada vez que entra un ToolExecutor.execute se monta un config nuevo con asToolConfig() (asToolConfig call:321). Pero las instancias de service a las que refiere config son las mismas, así que los cambios de estado en un service que haga el handler sí se hacen efectivos entre llamadas.
  • El handler de partial no push tool result: el comentario de handlePartialBlock lo dice claramente: «We don't push tool results in partial blocks» (no partial result:521). La fase partial solo actualiza la UI; el verdadero tool result se pushea al procesar el bloque complete.
  • Los errores del handler los atrapa ToolExecutor: ToolExecutor.execute envuelve coordinator.execute en try/catch (error catch:373). Cualquier error lanzado por el handler es capturado por handleError y convertido en un formatResponse.toolError que se pushea a la conversación; la Task no cae.
  • Añadir una herramienta obliga a tocar tres sitios: para añadir una herramienta hay que modificar tres lugares: añadir un valor al enum ClineDefaultTool, añadir la fábrica en toolHandlersMap y crear el archivo del handler. toolUseNames se genera automáticamente desde el enum y no se toca, pero la descripción de la herramienta en getSystemPrompt también hay que añadirla, o el modelo no sabrá que existe.

Resumen

El handler es el extremo final del sistema de herramientas de Cline: una herramienta, un archivo, implementando la interfaz IToolHandler. El registro está dirigido por el enum ClineDefaultTool + la tabla de fábricas toolHandlersMap; añadir una herramienta requiere tres cambios. Para ver cómo el ruteo encuentra el handler, consulta /tools/coordinator; para ver la comprobación de parámetros y permisos antes de la ejecución, consulta /tools/validator; para ver quién llama a las herramientas, consulta /agent-loop/present-assistant-message.

Véase la documentación oficial: Cline 文档 · README