Skip to content

ToolExecutorCoordinator:工具路由表

源码版本v4.0.10

职责

ToolExecutorCoordinator 是 Cline 的「工具名 → 处理器 (handler)」路由表。LLM 输出的 tool_use block 里带个 name 字段 (比如 read_filewrite_to_fileuse_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_filereplace_in_filenew_rule 都用 WriteToFileToolHandler),用包装类把基础 handler 套一层,改 name 字段 (SharedToolHandler:52)。避免写三个一样的类。
  • MCP 名归一化前置:MCP 工具名动态多变,但行为只有一种 (调 MCP server)。在 getHandler 入口统一把所有带 CLINE_MCP_TOOL_IDENTIFIER 的名字折叠成 MCP_USE,这样一个 handler 搞定所有 MCP 工具 (mcp normalize:137)。

关键文件

数据流

注册阶段在 ToolExecutor 构造时一次性完成,执行阶段每个 tool_use block 走一遍 lookup + execute。注册循环很短:

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 是从 ClineDefaultTool 枚举自动生成的 (toolUseNames:40)。新增一个枚举值,这里自动会去注册,前提是 toolHandlersMap 里那一行也填了工厂。Validator 是 ClineIgnoreController 包出来的,所有 file/path 类 handler 都需要它做访问检查 (ToolValidator:10)。

执行阶段的查找逻辑:

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
}

三层查找:MCP 名归一化后查静态表;没命中再看是不是动态 subagent 工具,是的话用 SharedToolHandler 包一个 UseSubagentsToolHandler 注册到动态表并返回;都不命中返回 undefined,上层 ToolExecutor.execute 会判定为「未注册」走旧分支 (has check:316)。动态 subagent 表用单独 Map 是因为它的 name 是运行时字符串,不能进 ClineDefaultTool 枚举。

边界与失败

  • 未注册抛错:executegetHandler 拿不到直接抛 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 永不清理:dynamicSubagentHandlers Map 只增不删,生命周期跟 Coordinator 一致 (dynamic cache set:152)。Coordinator 又跟 ToolExecutor 一致,ToolExecutor 跟 Task 一致,所以一个 Task 跑完整个生命周期里碰到的所有 subagent 工具名都缓存着。
  • 工厂每次调用都新建实例:registerByName 每次调工厂都新建一个 handler 实例 (factory call:119)。但因为 toolUseNames 不重复,实际每个 name 只注册一次,实例也就一个。
  • SharedToolHandler 不共享 state:SharedToolHandlerbaseHandler 存为私有字段,所有调用都转发给同一个 base 实例 (shared execute:62)。所以 write_to_filereplace_in_filenew_rule 三个名字其实是同一个 handler 实例,内部状态是共享的。

小结

Coordinator 把工具名和实现解耦,让新增工具只改一处 map、写一个 handler 类就行。它的查找是三层:MCP 归一化、静态表、动态 subagent。要看 handler 长什么样、怎么注册,转 /tools/handlers-overview;要看执行前的权限和参数校验,转 /tools/validator

对照官方资料:Cline 文档 · README