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 列舉。

邊界與失敗

  • 未註冊拋錯: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 永不清理: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