Tool Handlers:ツールハンドラ全図
役割
Handler は各ツールの具体的な実装である。一つの handler クラスが一種類のツール振る舞いに対応する: ファイル読み取り、ファイル書き込み、コマンド実行、MCP 呼び出し、subagent 起動など。いずれも IToolHandler インターフェース (IToolHandler:34) を実装し、name、execute、getDescription の三点セットを提供する。ストリーミング 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: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 は Coordinator 自身も handler に渡しており、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 は 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.executeはcoordinator.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 へ。