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。