Skip to content

Tool Handlers:ツールハンドラ全図

源码版本v4.0.10

役割

Handler は各ツールの具体的な実装である。一つの handler クラスが一種類のツール振る舞いに対応する: ファイル読み取り、ファイル書き込み、コマンド実行、MCP 呼び出し、subagent 起動など。いずれも IToolHandler インターフェース (IToolHandler:34) を実装し、nameexecutegetDescription の三点セットを提供する。ストリーミング partial block をサポートする必要がある handler は追加で 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 などの service にアクセスでき、自前で注入する必要がない。
  • 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:26IFullyManagedTool を実装し、partial と complete をサポート。write_to_filereplace_in_filenew_rule の三つのツール名で共有される。
  • ReadFileToolHandler:142read_file 実装。構築時に ToolValidator を受け取りパス検証に使う。
  • ExecuteCommandToolHandler:58execute_command 実装。bash コマンド実行とコマンド権限チェック。
  • AttemptCompletionHandler:39attempt_completion 実装。IToolHandler + IPartialBlockHandler のみを実装し、タスク完了をマークする。
  • UseMcpToolHandler:14use_mcp_tool 実装。すべての MCP ツール呼び出しはここを通る。
  • UseSubagentsToolHandler:49use_subagents 実装。動的 subagent ツール名にも使われる (SharedToolHandler でラップ)。
  • ListFilesToolHandler partial approval:48 — partial block 段階で shouldAutoApproveToolWithPath を呼び出し UI パスを決定する。

データフロー

新規ツール追加の流れは「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 は Coordinator 自身も handler に渡しており、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 は tool result を push しない: handlePartialBlock のコメントは「We don't push tool results in partial blocks」と明記する (no partial result:521)。partial 段階では UI 更新だけで、本当の tool result は complete block 処理時に初めて push される。
  • handler のエラーは ToolExecutor が受け止める: ToolExecutor.executecoordinator.execute を try/catch で包む (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