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