ToolExecutorCoordinator: tabla de ruteo de herramientas
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
ToolExecutorera un switch con cientos de líneas; añadir una herramienta obligaba a tocar el archivo principal. El Coordinator abstrae esa tabla entoolHandlersMap, 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 invocarregisterByName, de modo que ToolValidator puede inyectarse en ese momento (registerByName:118). - Devolver undefined es válido: la fábrica para
TODO(focus_chain) devuelveundefined(todo undefined:97). Esto indica «este nombre de herramienta no acepta handler por ahora»; la comprobaciónhasde 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_fileynew_ruleusan los tresWriteToFileToolHandler), una clase envolvente envuelve el handler base y reescribe el camponame(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
getHandlertodos los nombres conCLINE_MCP_TOOL_IDENTIFIERse colapsan aMCP_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:handlersydynamicSubagentHandlers.toolHandlersMap:79— registro estático: enumClineDefaultTool→ función de fábrica.register:114— almacena una instancia de handler pornameen el Maphandlers.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 agetHandlery 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 deSharedToolHandler(UseSubagentsToolHandler)para herramientas dinámicas de subagent.execute:162— tras obtener el handler, invocahandler.execute(config, block); no envuelve en try/catch.registerToolHandlers:201— en el constructor de ToolExecutor recorretoolUseNamesy registra todos.toolUseNames:40— array con todos los nombres de herramientas generado a partir del enumClineDefaultTool; 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:
// 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:
// 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:
executellama agetHandler; si no lo obtiene lanzaNo handler registered for tool: ${block.name}(throw on missing:165). Pero en la práctica la capa superiorToolExecutor.executecomprueba primero conhas, 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) devuelveundefined(todo undefined:97).registerByNameal 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 subcadenaCLINE_MCP_TOOL_IDENTIFIERse 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
dynamicSubagentHandlerssolo 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:
registerByNameinvoca la fábrica y crea una instancia de handler nueva en cada llamada (factory call:119). Pero comotoolUseNamesno tiene duplicados, cada nombre se registra una sola vez y solo hay una instancia. - SharedToolHandler no comparte estado:
SharedToolHandlerguardabaseHandlercomo campo privado y todas las llamadas se reenvían a la misma instancia base (shared execute:62). Por tantowrite_to_file,replace_in_fileynew_ruleson 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.