Skip to content

ToolExecutorCoordinator: tabla de ruteo de herramientas

源码版本v4.0.10

Responsabilidades

ToolExecutorCoordinator es la tabla de ruteo «nombre de herramienta → procesador (handler)» de Cline. El bloque tool_use que genera el LLM trae un campo name (por ejemplo read_file, write_to_file, use_mcp_tool); el Coordinator se encarga, a partir de ese nombre, de encontrar la instancia del handler correspondiente y cederle la ejecución. No contiene lógica de negocio: solo hace registro, búsqueda y reenvío.

Se sitúa entre ToolExecutor y los handlers concretos. ToolExecutor es la única entrada de herramientas que invoca la tarea (executeTool:212); tras aplicar comprobaciones de rechazo, restricciones del plan mode, bifurcación partial/complete y el hook PostToolUse, delega en el Coordinator la ejecución concreta de una herramienta (coordinator.execute:575). El Coordinator mantiene internamente un Map<string, IToolHandler> como registro; si no lo encuentra, lanza No handler registered for tool.

También cubre un caso especial: la normalización de nombres de herramienta MCP y la instanciación perezosa de herramientas dinámicas de subagent. Los nombres MCP tienen la forma mcp__server__tool; los que llevan el prefijo CLINE_MCP_TOOL_IDENTIFIER se colapsan a UseMcpToolHandler (mcp normalize:137). Los nombres de herramientas dinámicas de subagent registrados en AgentConfigLoader, si existen, se envuelven con SharedToolHandler sobre UseSubagentsToolHandler y se inyectan en la tabla dinámica (dynamic subagent:146).

Motivación de diseño

  • Tabla de handlers en lugar de un switch gigante: la versión antigua de ToolExecutor era un switch con cientos de líneas; añadir una herramienta obligaba a tocar el archivo principal. El Coordinator abstrae esa tabla en toolHandlersMap, de modo que añadir una herramienta solo requiere escribir un handler y registrar una línea en el map (toolHandlersMap:79).
  • Función de fábrica en vez de instancias directas: el map almacena (v: ToolValidator) => IToolHandler | undefined, no instancias. La instancia solo se crea al invocar registerByName, de modo que ToolValidator puede inyectarse en ese momento (registerByName:118).
  • Devolver undefined es válido: la fábrica para TODO (focus_chain) devuelve undefined (todo undefined:97). Esto indica «este nombre de herramienta no acepta handler por ahora»; la comprobación has de la capa superior lo tratará como no registrado.
  • SharedToolHandler reutiliza implementaciones: cuando varios nombres de herramienta comparten la misma implementación de handler (por ejemplo write_to_file, replace_in_file y new_rule usan los tres WriteToFileToolHandler), una clase envolvente envuelve el handler base y reescribe el campo name (SharedToolHandler:52), evitando escribir tres clases idénticas.
  • Normalización de nombres MCP al frente: los nombres de herramientas MCP son dinámicos, pero su comportamiento es único (llamar al servidor MCP). En la entrada de getHandler todos los nombres con CLINE_MCP_TOOL_IDENTIFIER se colapsan a MCP_USE, de modo que un único handler sirve todas las herramientas MCP (mcp normalize:137).

Archivos clave

  • IToolHandler:34 — interfaz del handler: name, execute, getDescription.
  • IPartialBlockHandler:40 — interfaz para partial block en streaming; el handler puede implementarla opcionalmente.
  • IFullyManagedTool:44 — tipo marcador para los que implementan a la vez la interfaz completa y la parcial.
  • SharedToolHandler:52 — clase envolvente que permite montar una misma implementación de handler bajo varios nombres.
  • ToolExecutorCoordinator:75 — definición de la clase; mantiene dos Map: handlers y dynamicSubagentHandlers.
  • toolHandlersMap:79 — registro estático: enum ClineDefaultTool → función de fábrica.
  • register:114 — almacena una instancia de handler por name en el Map handlers.
  • registerByName:118 — dado un valor del enum, toma la fábrica del map, construye la instancia y la registra.
  • has:128 — comprueba si un nombre de herramienta está registrado; internamente llama a getHandler y comprueba nulidad.
  • getHandler:135 — lógica de búsqueda: normalización MCP → tabla estática → subagent dinámico.
  • dynamic subagent branch:146 — creación perezosa de SharedToolHandler(UseSubagentsToolHandler) para herramientas dinámicas de subagent.
  • execute:162 — tras obtener el handler, invoca handler.execute(config, block); no envuelve en try/catch.
  • registerToolHandlers:201 — en el constructor de ToolExecutor recorre toolUseNames y registra todos.
  • toolUseNames:40 — array con todos los nombres de herramientas generado a partir del enum ClineDefaultTool; impulsa el bucle de registro.

Flujo de datos

El registro se hace de una sola vez al construir ToolExecutor; en la fase de ejecución, cada bloque tool_use pasa por una búsqueda + execute. El bucle de registro es muy corto:

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 se genera automáticamente a partir del enum ClineDefaultTool (toolUseNames:40). Añadir un valor al enum hace que se registre automáticamente aquí, siempre que toolHandlersMap tenga rellenada la línea con la fábrica correspondiente. El Validator lo envuelve ClineIgnoreController; todos los handlers de archivo/ruta lo necesitan para hacer la comprobación de acceso (ToolValidator:10).

La lógica de búsqueda en la fase de ejecución:

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
}

Búsqueda en tres niveles: tras normalizar el nombre MCP se busca en la tabla estática; si no hay coincidencia, se comprueba si es una herramienta dinámica de subagent; en ese caso, se envuelve con SharedToolHandler un UseSubagentsToolHandler, se registra en la tabla dinámica y se devuelve; si tampoco hay coincidencia se devuelve undefined, y la capa superior ToolExecutor.execute lo trata como «no registrado» y cae a la rama legacy (has check:316). La tabla de subagent dinámica usa un Map aparte porque sus nombres son cadenas en runtime, no entran en el enum ClineDefaultTool.

Límites y fallos

  • No registrado lanza error: execute llama a getHandler; si no lo obtiene lanza No handler registered for tool: ${block.name} (throw on missing:165). Pero en la práctica la capa superior ToolExecutor.execute comprueba primero con has, así que al llegar aquí hay coincidencia segura; este lanzamiento es un respaldo.
  • La herramienta TODO no monta handler: la fábrica de ClineDefaultTool.TODO (focus_chain) devuelve undefined (todo undefined:97). registerByName al ver undefined se salta el registro, equivalente a «este nombre no lo gestiona el Coordinator», y lo maneja por separado el módulo de focus chain.
  • La normalización de nombres MCP es un hack: el comentario lo dice literalmente, HACK (hack comment:136). Cualquier nombre de herramienta MCP que contenga la subcadena CLINE_MCP_TOOL_IDENTIFIER se normaliza; esto depende de que ni el nombre del servidor ni el de la herramienta contengan ese identificador.
  • Los handlers de subagent dinámicos nunca se limpian: el Map dynamicSubagentHandlers solo crece, nunca se eliminan entradas; su ciclo de vida coincide con el del Coordinator (dynamic cache set:152). El Coordinator a su vez coincide con el de ToolExecutor, y este con el de Task, así que durante toda la vida de una Task se cachean todos los nombres de herramientas subagent encontrados.
  • La fábrica crea una instancia nueva cada vez: registerByName invoca la fábrica y crea una instancia de handler nueva en cada llamada (factory call:119). Pero como toolUseNames no tiene duplicados, cada nombre se registra una sola vez y solo hay una instancia.
  • SharedToolHandler no comparte estado: SharedToolHandler guarda baseHandler como campo privado y todas las llamadas se reenvían a la misma instancia base (shared execute:62). Por tanto write_to_file, replace_in_file y new_rule son en realidad la misma instancia de handler, con estado interno compartido.

Resumen

El Coordinator desacopla el nombre de herramienta y su implementación, de modo que añadir una herramienta solo requiere modificar un único map y escribir una clase handler. Su búsqueda tiene tres niveles: normalización MCP, tabla estática y subagent dinámico. Para ver cómo es un handler y cómo se registra, consulta /tools/handlers-overview; para ver la validación de permisos y parámetros antes de la ejecución, consulta /tools/validator.

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