ToolExecutorCoordinator:工具路由表
职责
ToolExecutorCoordinator 是 Cline 的「工具名 → 处理器 (handler)」路由表。LLM 输出的 tool_use block 里带个 name 字段 (比如 read_file、write_to_file、use_mcp_tool),Coordinator 负责根据这个名字找到对应的 handler 实例,把执行权交出去。它本身不做任何业务逻辑,只做注册、查找、转发三件事。
它位于 ToolExecutor 和具体 handler 之间。ToolExecutor 是 Task 唯一调用的工具入口 (executeTool:212),它做了拒绝检查、plan mode 限制、partial/complete 分流、PostToolUse hook 这些通用动作后,具体怎么执行某个工具就委托给 Coordinator (coordinator.execute:575). Coordinator 内部用一个 Map<string, IToolHandler> 维护注册表,找不到就抛 No handler registered for tool。
它还负责一个特殊场景:MCP 工具名归一化和动态 subagent 工具的延迟实例化。MCP 工具名形如 mcp__server__tool,带 CLINE_MCP_TOOL_IDENTIFIER 前缀的会被折叠到 UseMcpToolHandler (mcp normalize:137)。动态 subagent 工具名在 AgentConfigLoader 注册过的话,会用 SharedToolHandler 包一层 UseSubagentsToolHandler 注入到动态表里 (dynamic subagent:146)。
设计动机
- handler 表替代巨型 switch:旧版本
ToolExecutor是一个几百行的 switch 语句,每加一个工具就得改主文件。Coordinator 把这张表抽成toolHandlersMap,新增工具只要写一个 handler 类并在 map 里登记一行 (toolHandlersMap:79)。 - 工厂函数而非直接实例:map 存的是
(v: ToolValidator) => IToolHandler | undefined工厂,不是实例。每次registerByName时才创建实例,这样 ToolValidator 能在创建时注入 (registerByName:118)。 - 返回 undefined 是合法值:
TODO(focus_chain) 对应的工厂返回undefined(todo undefined:97). 这表示「这个工具名暂时不接 handler」,上层has检查会判定为未注册。 - SharedToolHandler 复用实现:多个工具名共享同一个 handler 实现时 (比如
write_to_file、replace_in_file、new_rule都用WriteToFileToolHandler),用包装类把基础 handler 套一层,改name字段 (SharedToolHandler:52)。避免写三个一样的类。 - MCP 名归一化前置:MCP 工具名动态多变,但行为只有一种 (调 MCP server)。在
getHandler入口统一把所有带CLINE_MCP_TOOL_IDENTIFIER的名字折叠成MCP_USE,这样一个 handler 搞定所有 MCP 工具 (mcp normalize:137)。
关键文件
IToolHandler:34— handler 接口:name、execute、getDescription三件套。IPartialBlockHandler:40— 流式 partial block 接口,handler 可选实现。IFullyManagedTool:44— 同时实现完整 + partial 两接口的标记类型。SharedToolHandler:52— 包装类,让一个 handler 实现挂到多个工具名下。ToolExecutorCoordinator:75— 类定义,持有handlers和dynamicSubagentHandlers两个 Map。toolHandlersMap:79— 静态注册表,ClineDefaultTool枚举 → 工厂函数。register:114— 把一个 handler 实例按name存进handlersMap。registerByName:118— 给枚举值,从 map 取工厂造实例再 register。has:128— 判断工具名是否注册,内部直接getHandler判空。getHandler:135— 查找逻辑:MCP 归一化 → 静态表 → 动态 subagent。dynamic subagent branch:146— 动态 subagent 工具延迟创建SharedToolHandler(UseSubagentsToolHandler)。execute:162— 取到 handler 后调handler.execute(config, block),自身不 try/catch。registerToolHandlers:201— 在 ToolExecutor 构造时遍历toolUseNames全部注册。toolUseNames:40— 从ClineDefaultTool枚举生成的全工具名数组,驱动注册循环。
数据流
注册阶段在 ToolExecutor 构造时一次性完成,执行阶段每个 tool_use block 走一遍 lookup + execute。注册循环很短:
// 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 是从 ClineDefaultTool 枚举自动生成的 (toolUseNames:40)。新增一个枚举值,这里自动会去注册,前提是 toolHandlersMap 里那一行也填了工厂。Validator 是 ClineIgnoreController 包出来的,所有 file/path 类 handler 都需要它做访问检查 (ToolValidator:10)。
执行阶段的查找逻辑:
// 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
}三层查找:MCP 名归一化后查静态表;没命中再看是不是动态 subagent 工具,是的话用 SharedToolHandler 包一个 UseSubagentsToolHandler 注册到动态表并返回;都不命中返回 undefined,上层 ToolExecutor.execute 会判定为「未注册」走旧分支 (has check:316)。动态 subagent 表用单独 Map 是因为它的 name 是运行时字符串,不能进 ClineDefaultTool 枚举。
边界与失败
- 未注册抛错:
execute调getHandler拿不到直接抛No handler registered for tool: ${block.name}(throw on missing:165). 但实际上游ToolExecutor.execute先用has检查,真到这里时一定命中,这抛错是兜底。 - TODO 工具不挂 handler:
ClineDefaultTool.TODO(focus_chain) 的工厂返回undefined(todo undefined:97)。registerByName看到 undefined 跳过注册,等效于「这个工具名 Coordinator 不接管」,由 focus chain 模块单独处理。 - MCP 名归一是 hack:注释里直说是 HACK (
hack comment:136)。MCP 工具名里只要有CLINE_MCP_TOOL_IDENTIFIER子串就归一化,这依赖 server 名和 tool 名都不含这个标识符。 - 动态 subagent handler 永不清理:
dynamicSubagentHandlersMap 只增不删,生命周期跟 Coordinator 一致 (dynamic cache set:152)。Coordinator 又跟 ToolExecutor 一致,ToolExecutor 跟 Task 一致,所以一个 Task 跑完整个生命周期里碰到的所有 subagent 工具名都缓存着。 - 工厂每次调用都新建实例:
registerByName每次调工厂都新建一个 handler 实例 (factory call:119)。但因为toolUseNames不重复,实际每个 name 只注册一次,实例也就一个。 - SharedToolHandler 不共享 state:
SharedToolHandler把baseHandler存为私有字段,所有调用都转发给同一个 base 实例 (shared execute:62)。所以write_to_file、replace_in_file、new_rule三个名字其实是同一个 handler 实例,内部状态是共享的。
小结
Coordinator 把工具名和实现解耦,让新增工具只改一处 map、写一个 handler 类就行。它的查找是三层:MCP 归一化、静态表、动态 subagent。要看 handler 长什么样、怎么注册,转 /tools/handlers-overview;要看执行前的权限和参数校验,转 /tools/validator。