Skip to content

Tool Handlers:工具处理器全图

源码版本v4.0.10

职责

Handler 是每个工具的具体实现。一个 handler 类对应一种工具行为:读文件、写文件、跑命令、调 MCP、起 subagent 等等。它们都实现 IToolHandler 接口 (IToolHandler:34),提供 nameexecutegetDescription 三件套。需要支持流式 partial block 的额外实现 IPartialBlockHandler,两个都实现的标记为 IFullyManagedTool

Cline 的工具清单定义在 ClineDefaultTool 枚举里 (ClineDefaultTool:8)。当前有 27 个枚举值,从 ask_followup_questionuse_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),避免写三个一样的类。

关键文件

数据流

新增工具的流程是「写 handler 类 → 在 map 里登记一行 → 枚举加一项」。map 登记的样子:

typescript
// 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 长这样:

typescript
// 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 工具就靠这个)。

边界与失败

  • 缺工厂的工具名静默跳过:toolHandlersMapRecord<ClineDefaultTool, ...>,TS 强制每个枚举值都填一项。但工厂可以返回 undefined,目前只有 TODO 这么做 (todo undefined:97)。registerByName 看到 undefined 直接跳过,等效于「不注册」。
  • Handler 共享但要保持无状态:WriteToFileToolHandlerwrite_to_filereplace_in_filenew_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.toolError push 进对话,不会让 Task 崩溃。
  • 新增工具三处必改:加工具要改三处:ClineDefaultTool 枚举加值、toolHandlersMap 加工厂、新 handler 文件。toolUseNames 自动从枚举生成不用改,但 getSystemPrompt 里工具描述也得加,否则模型不知道这个工具存在。

小结

Handler 是 Cline 工具体系的末端,每个工具一个文件,实现 IToolHandler 接口。注册靠 ClineDefaultTool 枚举 + toolHandlersMap 工厂表驱动,新增工具三处改动。要看路由本身怎么找 handler,转 /tools/coordinator;要看执行前的参数和权限检查,转 /tools/validator;要看工具被谁调用,转 /agent-loop/present-assistant-message

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