Skip to content

Tool Handlers: The Handler Landscape ​

源码版本v4.0.10

Responsibilities ​

A handler is the concrete implementation of each tool. One handler class corresponds to one kind of tool behavior: reading a file, writing a file, running a command, calling MCP, launching a subagent, and so on. They all implement the IToolHandler interface (IToolHandler:34), providing the name, execute, and getDescription trio. Handlers that need to support streaming partial blocks additionally implement IPartialBlockHandler; handlers that implement both are marked as IFullyManagedTool.

Cline's tool inventory is defined in the ClineDefaultTool enum (ClineDefaultTool:8). It currently has 27 enum values, from ask_followup_question to use_subagents. These names map directly to the name field of the tool_use block the LLM produces. Immediately below the enum is toolUseNames = Object.values(ClineDefaultTool) (toolUseNames:40), which flattens the enum into an array that ToolExecutor iterates over at construction time to register every tool.

The handler files all live under apps/vscode/src/core/task/tools/handlers/, one file per tool, with class names shaped like XxxToolHandler. Every handler shares a single TaskConfig context object carrying cwd, taskState, messageState, api, various services, and callbacks; handlers reach all their external dependencies through it (asToolConfig:135).

Design motivation ​

  • One tool, one file: each handler lives in its own file, keeping responsibility boundaries clear. Adding a tool adds a file without touching existing code; removing a tool deletes one file.
  • Layered interfaces, not inheritance: IToolHandler is the minimal contract, IPartialBlockHandler is the streaming add-on capability, and IFullyManagedTool is the "full set" marker (IFullyManagedTool:44). Composition over inheritance — handlers pick the interfaces they need.
  • Handlers hold no state: handler instances are cached by the Coordinator, and all state is passed in through TaskConfig. The same handler instance may receive multiple execute calls, so it must not stash task-level state in its own fields.
  • TaskConfig built once, used everywhere: when ToolExecutor builds asToolConfig() it collects every dependency into one object (asToolConfig:135); the handler gets mcpHub, browserSession, diffViewProvider, clineIgnoreController, and other services from the config without doing its own injection.
  • Enum-driven registration: toolUseNames is generated from the enum automatically, so the registration loop needs no hand-maintained tool list (for of toolUseNames:204). A new enum value enters the loop automatically, provided the Coordinator's toolHandlersMap has a matching factory.
  • SharedToolHandler reuses implementations: when several names share one implementation (write_to_file / replace_in_file / new_rule all use WriteToFileToolHandler), the wrapper class renames it (SharedToolHandler:52), avoiding three identical classes.

Key files ​

Data flow ​

Adding a tool follows the flow "write a handler class → add an entry to the map → add an enum value". The map entry looks like this:

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,
}

The factory function takes a ToolValidator. Handlers that need path validation (read/write/search/list) accept it; handlers that do not (ask/attempt/browser/mcp, etc.) use _v as a placeholder. Only TODO returns undefined, signaling that the Coordinator does not own this tool name (todo undefined:97).

The config a handler receives at execution time looks like this:

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,
}

A note in the comments: handlers may read config fields but must not mutate the config itself (e.g. config.browserSession = ... does not change ToolExecutor's instance variable) (config warning:134). To switch the browser session, go through a dedicated entry like applyLatestBrowserSettings. config.coordinator passes the coordinator itself in, so a handler can invoke other tools mid-execution (the subagent tool relies on this).

Boundaries and failure modes ​

  • Tool names without a factory are silently skipped: toolHandlersMap is a Record<ClineDefaultTool, ...>, so TypeScript forces every enum value to have an entry. But the factory can return undefined; currently only TODO does so (todo undefined:97). When registerByName sees undefined it just skips, equivalent to "not registered".
  • Handlers are shared but must stay stateless: one WriteToFileToolHandler instance is shared by the three names write_to_file, replace_in_file, and new_rule (file_edit shared:83). If a handler stored task-level state in instance fields, the three names would pollute each other. Hence all state comes in via TaskConfig.
  • asToolConfig is rebuilt every time: each call to ToolExecutor.execute rebuilds the config via asToolConfig() (asToolConfig call:321). But the service instances inside the config are the same object, so mutations a handler makes to a service's state persist across calls.
  • Partial handlers don't push tool results: the handlePartialBlock comment states explicitly: "We don't push tool results in partial blocks" (no partial result:521). The partial stage only updates the UI; the real tool result is pushed when the complete block is processed.
  • Handler errors are caught by ToolExecutor: ToolExecutor.execute wraps coordinator.execute in try/catch (error catch:373). Any error a handler throws is captured by handleError, turned into a formatResponse.toolError, and pushed into the conversation — it never crashes the Task.
  • Three places must change to add a tool: adding a tool means editing three spots: a new value in the ClineDefaultTool enum, a new factory in toolHandlersMap, and a new handler file. toolUseNames is auto-generated from the enum and needs no edit, but the tool description in getSystemPrompt must also be added or the model will not know the tool exists.

Summary ​

Handlers are the leaves of Cline's tool system: one file per tool, implementing the IToolHandler interface. Registration is driven by the ClineDefaultTool enum and the toolHandlersMap factory table, so adding a tool takes three edits. To see how the router finds a handler, see /tools/coordinator; for the parameter and permission checks that run before execution, see /tools/validator; for who calls these tools, see /agent-loop/present-assistant-message.

See official docs: Cline 文档 · README