ToolExecutorCoordinator:ツールルーティングテーブル
役割
ToolExecutorCoordinator は Cline の「ツール名 → ハンドラ (handler)」ルーティングテーブルである。LLM が出力する tool_use block には name フィールド (例: read_file、write_to_file、use_mcp_tool) が付いており、Coordinator はこの名前をもとに対応する handler インスタンスを探し出し、実行権を渡す。Coordinator 自身は一切の業務ロジックを持たず、登録・検索・転送の三つだけを行う。
これは ToolExecutor と各 handler の間に位置する。ToolExecutor は Task が唯一呼ぶツール入口 (executeTool:212) であり、拒否チェック、plan mode 制限、partial/complete の振り分け、PostToolUse hook といった共通動作を処理した後、個々のツールをどう実行するかは Coordinator に委譲する (coordinator.execute:575)。Coordinator 内部では Map<string, IToolHandler> でレジストリを管理しており、見つからなければ No handler registered for tool を投げる。
もう一つの特殊な役割は、MCP ツール名の正規化と動的 subagent ツールの遅延インスタンス化である。MCP ツール名は mcp__server__tool の形式をとり、CLINE_MCP_TOOL_IDENTIFIER プレフィックスを持つものは UseMcpToolHandler に畳み込まれる (mcp normalize:137)。動的 subagent ツール名が AgentConfigLoader に登録済みであれば、SharedToolHandler で UseSubagentsToolHandler をくるんで動的テーブルに注入する (dynamic subagent:146)。
設計動機
- handler テーブルで巨大 switch を置換: 旧バージョンの
ToolExecutorは数百行の switch 文で、ツールを追加するたびに主ファイルを書き換える必要があった。Coordinator はこの表をtoolHandlersMapとして切り出し、新規ツールの追加は handler クラスを書いて map に一行追加するだけで済む (toolHandlersMap:79)。 - 直接インスタンスではなくファクトリ関数: map が保持するのはインスタンスではなく
(v: ToolValidator) => IToolHandler | undefinedファクトリである。registerByNameのタイミングで初めてインスタンスを生成することで、ToolValidator を生成時に注入できる (registerByName:118)。 - undefined 返却は合法値:
TODO(focus_chain) に対応するファクトリはundefinedを返す (todo undefined:97)。これは「このツール名は当面 handler を受け付けない」ことを意味し、上位のhasチェックは未登録と判定する。 - SharedToolHandler で実装を再利用: 複数のツール名が同一の handler 実装を共有する場合 (例:
write_to_file、replace_in_file、new_ruleはすべてWriteToFileToolHandlerを使用)、ラッパークラスでベース handler をくるみ、nameフィールドだけを差し替える (SharedToolHandler:52)。これにより同じクラスを三つ書く手間を省く。 - MCP 名前正規化を前置: MCP ツール名は動的に変化するが、実際の振る舞いは一種類 (MCP server の呼び出し) しかない。
getHandlerの入口でCLINE_MCP_TOOL_IDENTIFIERを含むすべての名前をMCP_USEに畳み込むことで、一つの handler で全 MCP ツールを賄える (mcp normalize:137)。
主要ファイル
IToolHandler:34— handler インターフェース:name、execute、getDescriptionの三点セット。IPartialBlockHandler:40— ストリーミング partial block 向けインターフェース。handler は任意実装。IFullyManagedTool:44— complete と partial の両インターフェースを実装するマーカー型。SharedToolHandler:52— ラッパークラス。一つの handler 実装を複数のツール名に紐付けられる。ToolExecutorCoordinator:75— クラス定義。handlersとdynamicSubagentHandlersの二つの Map を保持する。toolHandlersMap:79— 静的レジストリ。ClineDefaultTool列挙値 → ファクトリ関数。register:114— handler インスタンスをnameをキーにhandlersMap に格納。registerByName:118— 列挙値を受け取り、map からファクトリを取り出してインスタンスを生成し register する。has:128— ツール名が登録済みかを判定。内部ではgetHandlerを呼び出して非 null かを見る。getHandler:135— 検索ロジック: MCP 正規化 → 静的テーブル → 動的 subagent。dynamic subagent branch:146— 動的 subagent ツール向けにSharedToolHandler(UseSubagentsToolHandler)を遅延生成。execute:162— handler 取得後にhandler.execute(config, block)を呼ぶ。自身は try/catch しない。registerToolHandlers:201— ToolExecutor のコンストラクタでtoolUseNamesを走査して全登録。toolUseNames:40—ClineDefaultTool列挙から生成した全ツール名配列。登録ループを駆動する。
データフロー
登録フェーズは ToolExecutor のコンストラクト時に一回だけ行われ、実行フェーズでは各 tool_use block が lookup + execute を一度ずつ通る。登録ループは短い:
// apps/vscode/src/core/task/ToolExecutor.ts
private registerToolHandlers(): void {
const validator = new ToolValidator(this.clineIgnoreController)
// Register all tools via toolUseNames
for (const tool of toolUseNames) {
this.coordinator.registerByName(tool, validator)
}
}toolUseNames は ClineDefaultTool 列挙から自動生成される (toolUseNames:40)。列挙値を一つ追加すれば自動的に登録ループに入るが、前提として toolHandlersMap の対応行にもファクトリを書く必要がある。Validator は ClineIgnoreController から生成され、file/path 系 handler はすべてアクセスチェックにこれを使う (ToolValidator:10)。
実行フェーズの検索ロジック:
// apps/vscode/src/core/task/tools/ToolExecutorCoordinator.ts
getHandler(toolName: string): IToolHandler | undefined {
// HACK: Normalize MCP tool names to the standard handler
if (toolName.includes(CLINE_MCP_TOOL_IDENTIFIER)) {
toolName = ClineDefaultTool.MCP_USE
}
const staticHandler = this.handlers.get(toolName)
if (staticHandler) {
return staticHandler
}
if (AgentConfigLoader.getInstance().isDynamicSubagentTool(toolName)) {
const existingHandler = this.dynamicSubagentHandlers.get(toolName)
if (existingHandler) {
return existingHandler
}
const handler = new SharedToolHandler(toolName as ClineDefaultTool, new UseSubagentsToolHandler())
this.dynamicSubagentHandlers.set(toolName, handler)
return handler
}
return undefined
}三段階の検索: MCP 名を正規化した上で静的テーブルを引く。ヒットしなければ動的 subagent ツールか確認し、そうであれば SharedToolHandler で UseSubagentsToolHandler を包んで動的テーブルに登録して返す。どちらもヒットしなければ undefined を返し、上位の ToolExecutor.execute は未登録と判定して旧分岐へ進む (has check:316)。動的 subagent テーブルを別 Map にしているのは、その name が実行時文字列で ClineDefaultTool 列挙に入れられないためである。
境界と失敗
- 未登録時の例外送出:
executeはgetHandlerで取れない場合、No handler registered for tool: ${block.name}をそのまま投げる (throw on missing:165)。ただし実際には上位のToolExecutor.executeがhasで事前チェックしており、ここに到達した時点で必ずヒットするため、この例外は安全網である。 - TODO ツールは handler を紐付けない:
ClineDefaultTool.TODO(focus_chain) のファクトリはundefinedを返す (todo undefined:97)。registerByNameは undefined を見ると登録をスキップし、結果として「このツール名は Coordinator が管理しない」ことになり、focus chain モジュールが別途処理する。 - MCP 名正規化は HACK: コメントにも HACK と明記されている (
hack comment:136)。MCP ツール名にCLINE_MCP_TOOL_IDENTIFIER部分文字列が含まれていれば正規化する仕様であり、server 名と tool 名のどちらにもこの識別子が含まれないことに依存している。 - 動的 subagent handler はクリーンアップされない:
dynamicSubagentHandlersMap は増えるだけで削除されず、ライフサイクルは Coordinator と一致する (dynamic cache set:152)。Coordinator は ToolExecutor と、ToolExecutor は Task とライフサイクルを共有するため、一つの Task が全ライフサイクルで出会ったすべての subagent ツール名がキャッシュされる。 - ファクトリは呼び出しごとに新インスタンスを生成:
registerByNameはファクトリを呼ぶたびに新しい handler インスタンスを生成する (factory call:119)。ただしtoolUseNamesには重複がないため、実際には各 name は一度しか登録されず、インスタンスも一つしかできない。 - SharedToolHandler は state を共有しない:
SharedToolHandlerはbaseHandlerをプライベートフィールドに保持し、すべての呼び出しを同一のベースインスタンスへ転送する (shared execute:62)。したがってwrite_to_file、replace_in_file、new_ruleの三つの名前は実質的に一つの handler インスタンスであり、内部状態は共有される。
まとめ
Coordinator はツール名と実装を疎結合にし、新規ツールの追加が map の一行変更と handler クラス一つで済むようにする。検索は三層構造: MCP 正規化、静的テーブル、動的 subagent。handler の中身や登録の様子を見るには /tools/handlers-overview へ、実行前の権限・パラメータ検証を見るには /tools/validator へ。