Tool Handlers:工具處理器全圖
職責
Handler 是每個工具的具體實作。一個 handler 類別對應一種工具行為:讀檔案、寫檔案、跑命令、調 MCP、起 subagent 等等。它們都實作 IToolHandler 介面 (IToolHandler:34),提供 name、execute、getDescription 三件套。需要支援流式 partial block 的額外實作 IPartialBlockHandler,兩個都實作的標記為 IFullyManagedTool。
Cline 的工具清單定義在 ClineDefaultTool 列舉裡 (ClineDefaultTool:8)。目前有 27 個列舉值,從 ask_followup_question 到 use_subagents。這些名字直接對應 LLM 看到的 tool_use block 的 name 欄位。列舉下面有一行 toolUseNames = Object.values(ClineDefaultTool) (toolUseNames:40) 把列舉拍平成陣列,ToolExecutor 建構時遍歷這個陣列完成全量註冊。
handler 檔案都在 apps/vscode/src/core/task/tools/handlers/ 目錄下,一個工具一個檔案,類別名形如 XxxToolHandler。所有 handler 共享一個 TaskConfig 上下文物件,裡面有 cwd、taskState、messageState、api、各種 service 和 callback,handler 透過它存取所有外部依賴 (asToolConfig:135)。
設計動機
- 一工具一檔案:每個 handler 一個檔案,職責邊界清晰。新增工具只加檔案不動現有程式碼,刪工具也只刪一個檔案。
- 介面分層而非繼承:
IToolHandler是最小契約,IPartialBlockHandler是流式附加能力,IFullyManagedTool是「全套」標記 (IFullyManagedTool:44)。組合優於繼承,handler 按需挑介面實作。 - Handler 不持有狀態:Handler 實例被 Coordinator 快取,所有狀態都從
TaskConfig傳進來。同一 handler 實例可能被多次execute呼叫,不能存任務級狀態在自己欄位裡。 - TaskConfig 一次建構多處用:ToolExecutor 建構
asToolConfig()時把所有依賴收集成一個物件 (asToolConfig:135),handler 拿到 config 就能存取 mcpHub、browserSession、diffViewProvider、clineIgnoreController 等服務,不用自己注入。 - ClineDefaultTool 列舉驅動註冊:
toolUseNames自動從列舉生成,註冊迴圈不用手維護工具清單 (for of toolUseNames:204)。新增列舉值就自動進入註冊迴圈,前提是 Coordinator 的toolHandlersMap也有對應工廠。 - SharedToolHandler 復用實作:多個名字共享一個實作時 (write_to_file / replace_in_file / new_rule 三個共用
WriteToFileToolHandler),用包裝類改名 (SharedToolHandler:52),避免寫三個一樣的類別。
關鍵檔案
ClineDefaultTool enum:8— 27 個工具名列舉,LLM 看到的 tool name 就來自這裡。toolUseNames:40— 列舉拍平成陣列,驅動 ToolExecutor 的全量註冊。toolHandlersMap:79— 27 個列舉值到 handler 工廠的映射表,新增工具改這裡。registerToolHandlers:201— 建構時遍歷toolUseNames全量註冊。asToolConfig:135— 每次執行工具前建構TaskConfig,把所有依賴打包給 handler。WriteToFileToolHandler:26— 實作IFullyManagedTool,支援 partial + complete,被write_to_file、replace_in_file、new_rule三個工具名共享。ReadFileToolHandler:142—read_file實作,建構接 ToolValidator 做路徑校驗。ExecuteCommandToolHandler:58—execute_command實作,bash 命令執行 + 命令權限檢查。AttemptCompletionHandler:39—attempt_completion實作,只實作IToolHandler + IPartialBlockHandler,標記任務完成。UseMcpToolHandler:14—use_mcp_tool實作,所有 MCP 工具呼叫都走這裡。UseSubagentsToolHandler:49—use_subagents實作,也用於動態 subagent 工具名 (透過 SharedToolHandler 包裝)。ListFilesToolHandler partial approval:48— partial block 階段就調shouldAutoApproveToolWithPath決定 UI 路徑。
資料流
新增工具的流程是「寫 handler 類別 → 在 map 裡登記一行 → 列舉加一項」。map 登記的樣子:
// apps/vscode/src/core/task/tools/ToolExecutorCoordinator.ts
private readonly toolHandlersMap: Record<ClineDefaultTool, (v: ToolValidator) => IToolHandler | undefined> = {
[ClineDefaultTool.ASK]: (_v: ToolValidator) => new AskFollowupQuestionToolHandler(),
[ClineDefaultTool.ATTEMPT]: (_v: ToolValidator) => new AttemptCompletionHandler(),
[ClineDefaultTool.BASH]: (v: ToolValidator) => new ExecuteCommandToolHandler(v),
[ClineDefaultTool.FILE_EDIT]: (v: ToolValidator) =>
new SharedToolHandler(ClineDefaultTool.FILE_EDIT, new WriteToFileToolHandler(v)),
[ClineDefaultTool.FILE_READ]: (v: ToolValidator) => new ReadFileToolHandler(v),
[ClineDefaultTool.FILE_NEW]: (v: ToolValidator) => new WriteToFileToolHandler(v),
// ...
[ClineDefaultTool.TODO]: (_v: ToolValidator) => undefined,
}工廠函式的參數是 ToolValidator,需要校驗路徑的 handler (read/write/search/list) 會接住它,不需要的 handler (ask/attempt/browser/mcp 等) 用 _v 占位。回傳 undefined 的只有 TODO 一項,表示這個工具名 Coordinator 不接管 (todo undefined:97)。
Handler 實際執行時拿到的 config 長這樣:
// apps/vscode/src/core/task/ToolExecutor.ts
const config: TaskConfig = {
taskId: this.taskId,
ulid: this.ulid,
mode: this.stateManager.getGlobalSettingsKey("mode"),
cwd: this.cwd,
workspaceManager: this.workspaceManager,
taskState: this.taskState,
messageState: this.messageStateHandler,
api: this.api,
autoApprover: this.autoApprover,
services: {
mcpHub: this.mcpHub,
browserSession: this.browserSession,
diffViewProvider: this.diffViewProvider,
fileContextTracker: this.fileContextTracker,
clineIgnoreController: this.clineIgnoreController,
commandPermissionController: this.commandPermissionController,
contextManager: this.contextManager,
stateManager: this.stateManager,
},
callbacks: {
say: this.say,
ask: this.ask,
shouldAutoApproveTool: this.shouldAutoApproveTool.bind(this),
shouldAutoApproveToolWithPath: this.shouldAutoApproveToolWithPath.bind(this),
// ...
},
coordinator: this.coordinator,
}註解裡有個警告:handler 可以讀 config 欄位,但不能改 config 自身欄位 (比如 config.browserSession = ... 不會改 ToolExecutor 的實例變數) (config warning:134)。需要換 browser session 時要走 applyLatestBrowserSettings 這種專門入口。config.coordinator 把協調器自己也傳進去,handler 在執行中可以再調其他工具 (subagent 工具就靠這個)。
邊界與失敗
- 缺工廠的工具名靜默跳過:
toolHandlersMap是Record<ClineDefaultTool, ...>,TS 強制每個列舉值都填一項。但工廠可以回傳undefined,目前只有TODO這麼做 (todo undefined:97)。registerByName看到 undefined 直接跳過,等效於「不註冊」。 - Handler 共享但要保持無狀態:
WriteToFileToolHandler被write_to_file、replace_in_file、new_rule三個名字共享一個實例 (file_edit shared:83)。如果 handler 在實例欄位裡存任務級狀態,三個名字會互相污染。所以所有狀態都從TaskConfig傳。 asToolConfig每次重新建構:每次ToolExecutor.execute進來都asToolConfig()拼一個新 config (asToolConfig call:321)。但 config 裡參考的 service 實例是同一個,handler 改 service 的狀態會跨呼叫生效。- Partial handler 不 push tool result:
handlePartialBlock註解明確說「We don't push tool results in partial blocks」(no partial result:521)。partial 階段只更新 UI,真正的 tool result 在 complete block 處理時才 push。 - handler 錯誤被 ToolExecutor 兜住:
ToolExecutor.execute用 try/catch 包住coordinator.execute(error catch:373)。handler 拋任何錯都被handleError捕獲,轉成formatResponse.toolErrorpush 進對話,不會讓 Task 崩潰。 - 新增工具三處必改:加工具要改三處:
ClineDefaultTool列舉加值、toolHandlersMap加工廠、新 handler 檔案。toolUseNames自動從列舉生成不用改,但getSystemPrompt裡工具描述也得加,否則模型不知道這個工具存在。
小結
Handler 是 Cline 工具體系的末端,每個工具一個檔案,實作 IToolHandler 介面。註冊靠 ClineDefaultTool 列舉 + toolHandlersMap 工廠表驅動,新增工具三處改動。要看路由本身怎麼找 handler,轉 /tools/coordinator;要看執行前的參數和權限檢查,轉 /tools/validator;要看工具被誰呼叫,轉 /agent-loop/present-assistant-message。