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。