ToolExecutorCoordinator: Werkzeug-Routertabelle
Verantwortung
Der ToolExecutorCoordinator ist Clines Routing-Tabelle von «Werkzeugname → Handler». Der tool_use-Block, den ein LLM ausgibt, enthält ein Feld name (etwa read_file, write_to_file, use_mcp_tool); der Coordinator ist dafür zuständig, anhand dieses Namens die entsprechende Handler-Instanz zu finden und die Ausführung an sie zu übergeben. Er selbst enthält keinerlei Geschäftslogik, sondern übernimmt nur drei Aufgaben: Registrieren, Finden, Weiterleiten.
Er sitzt zwischen dem ToolExecutor und den konkreten Handlern. Der ToolExecutor ist der einzige Werkzeugeinstieg, den die Task aufruft (executeTool:212); er übernimmt die generischen Aufgaben wie Ablehnungsprüfung, Plan-Mode-Beschränkung, Partial-/Complete-Aufteilung und PostToolUse-Hook und delegiert dann das «wie genau dieses Werkzeug auszuführen ist» an den Coordinator (coordinator.execute:575). Der Coordinator hält intern eine Map<string, IToolHandler> als Registry; findet er keinen Handler, wirft er No handler registered for tool.
Zusätzlich übernimmt er einen Sonderfall: die Normalisierung von MCP-Werkzeugnamen und die verzögerte Instanziierung dynamischer Subagent-Tools. MCP-Werkzeugnamen haben die Form mcp__server__tool; Namen mit dem Präfix CLINE_MCP_TOOL_IDENTIFIER werden auf UseMcpToolHandler zusammengezogen (mcp normalize:137). Ist ein dynamischer Subagent-Werkzeugname im AgentConfigLoader registriert, wird ein SharedToolHandler um einen UseSubagentsToolHandler herumgelegt und in die dynamische Tabelle injiziert (dynamic subagent:146).
Entwurfsmotivation
- Handler-Tabelle statt riesigem switch: Ältere Versionen von
ToolExecutorbestanden aus einer switch-Anweisung über hunderte Zeilen; für jedes neue Werkzeug musste die Hauptdatei geändert werden. Der Coordinator hebt diese Tabelle alstoolHandlersMapheraus; neue Werkzeuge benötigen nur eine neue Handler-Klasse und eine Zeile in der Map (toolHandlersMap:79). - Fabrikfunktion statt direkter Instanz: In der Map liegt nicht die Instanz, sondern die Fabrik
(v: ToolValidator) => IToolHandler | undefined. Erst beiregisterByNamewird die Instanz erzeugt, sodass der ToolValidator bei der Erzeugung injiziert werden kann (registerByName:118). - undefined ist ein legitimer Rückgabewert: Die Fabrik für
TODO(focus_chain) liefertundefined(todo undefined:97). Das bedeutet: «Dieser Werkzeugname erhält vorerst keinen Handler»; die oberehas-Prüfung wertet das als nicht registriert. - SharedToolHandler zur Implementierungswiederverwendung: Wenn mehrere Werkzeugnamen dieselbe Handler-Implementierung teilen (beispielsweise
write_to_file,replace_in_file,new_rulealle mitWriteToFileToolHandler), hüllt eine Wrapper-Klasse den Basis-Handler ein und überschreibt das Feldname(SharedToolHandler:52). So vermeidet man drei identische Klassen. - MCP-Namensnormalisierung vorab: MCP-Werkzeugnamen sind dynamisch, aber das Verhalten ist stets dasselbe (Aufruf eines MCP-Servers). Im
getHandler-Einstieg werden alle Namen mitCLINE_MCP_TOOL_IDENTIFIERaufMCP_USEzusammengezogen, sodass ein einziger Handler sämtliche MCP-Werkzeuge abdeckt (mcp normalize:137).
Schlüsseldateien
IToolHandler:34— Handler-Interface:name,execute,getDescription.IPartialBlockHandler:40— Interface für gestreamte Partial-Blöcke; Handler können es optional implementieren.IFullyManagedTool:44— Markierungstyp für Handler, die sowohl das vollständige als auch das Partial-Interface implementieren.SharedToolHandler:52— Wrapper-Klasse, die eine Handler-Implementierung an mehrere Werkzeugnamen bindet.ToolExecutorCoordinator:75— Klassendefinition; hälthandlersunddynamicSubagentHandlersals zwei Maps.toolHandlersMap:79— Statische Registry:ClineDefaultTool-Enum → Fabrikfunktion.register:114— Speichert eine Handler-Instanz unternamein derhandlers-Map.registerByName:118— Nimmt einen Enum-Wert, ruft die Fabrik aus der Map auf und registriert die Instanz.has:128— Prüft, ob ein Werkzeugname registriert ist; intern direktgetHandlermit anschliessender Null-Prüfung.getHandler:135— Such-Logik: MCP-Normalisierung → statische Tabelle → dynamischer Subagent.dynamic subagent branch:146— Verzögertes Erzeugen desSharedToolHandler(UseSubagentsToolHandler)für dynamische Subagent-Werkzeuge.execute:162— Nach Finden des Handlers Aufruf vonhandler.execute(config, block); eigenes try/catch gibt es hier nicht.registerToolHandlers:201— Durchläuft beim Konstruieren des ToolExecutor sämtlichetoolUseNamesund registriert sie.toolUseNames:40— Array mit allen Werkzeugnamen aus demClineDefaultTool-Enum; treibt die Registrierungsschleife an.
Datenfluss
Die Registrierung erfolgt beim Konstruieren des ToolExecutor in einem Durchgang; die Ausführung läuft für jeden tool_use-Block über Lookup und Execute. Die Registrierungsschleife ist kurz:
// 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 wird automatisch aus dem ClineDefaultTool-Enum erzeugt (toolUseNames:40). Ein neuer Enum-Wert wird hier automatisch registriert, vorausgesetzt die entsprechende Zeile in toolHandlersMap ist ebenfalls gepflegt. Der Validator wird aus dem ClineIgnoreController erzeugt; alle file/path-Handler benötigen ihn für die Zugriffsprüfung (ToolValidator:10).
Die Suchlogik in der Ausführungsphase:
// 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
}Dreistufige Suche: Nach MCP-Namensnormalisierung wird die statische Tabelle befragt; schlägt das fehl, wird geprüft, ob es sich um ein dynamisches Subagent-Tool handelt – falls ja, wird per SharedToolHandler ein UseSubagentsToolHandler umhüllt, in der dynamischen Tabelle abgelegt und zurückgegeben; schlägt beides fehl, wird undefined zurückgegeben und der obere ToolExecutor.execute wertet das als «nicht registriert» und geht den Legacy-Pfad (has check:316). Die dynamische Subagent-Tabelle nutzt eine separate Map, weil ihre Namen Laufzeit-Strings sind, die nicht ins ClineDefaultTool-Enum aufgenommen werden können.
Grenzen und Fehler
- Nicht registriert wirft:
executeruftgetHandlerauf und wirft bei Misserfolg direktNo handler registered for tool: ${block.name}(throw on missing:165). Tatsächlich prüft der vorgelagerteToolExecutor.executezuerst perhas, sodass ein Treffer sicher ist; dieser Wurf dient nur als Ausfallsicherung. - Das TODO-Werkzeug erhält keinen Handler: Die Fabrik für
ClineDefaultTool.TODO(focus_chain) liefertundefined(todo undefined:97).registerByNameüberspringt bei undefined die Registrierung; das ist gleichbedeutend mit «dieser Werkzeugname wird vom Coordinator nicht verwaltet» und wird vom focus-chain-Modul separat behandelt. - MCP-Namensnormalisierung ist ein Hack: Der Kommentar sagt das explizit (
hack comment:136). Enthält ein MCP-Werkzeugname den TeilstringCLINE_MCP_TOOL_IDENTIFIER, wird er normalisiert; das setzt voraus, dass weder Servername noch Werkzeugname diesen Bezeichner enthalten. - Dynamische Subagent-Handler werden nie aufgeräumt: Die Map
dynamicSubagentHandlerswächst nur und wird nie gelöscht; ihre Lebensdauer entspricht der des Coordinator (dynamic cache set:152). Der Coordinator wiederum lebt so lange wie der ToolExecutor, dieser so lange wie die Task; somit werden alle Subagent-Werkzeugnamen, die während einer Task auftauchen, für die gesamte Lebensdauer gecacht. - Die Fabrik erzeugt bei jedem Aufruf eine neue Instanz:
registerByNamelegt bei jedem Fabrikaufruf eine neue Handler-Instanz an (factory call:119). DatoolUseNameskeine Duplikate enthält, wird jeder Name aber nur einmal registriert und es entsteht genau eine Instanz. - SharedToolHandler teilt keinen State:
SharedToolHandlerspeichert denbaseHandlerals privates Feld; alle Aufrufe werden an dieselbe Basis-Instanz weitergeleitet (shared execute:62). Die drei Namenwrite_to_file,replace_in_fileundnew_rulenutzen also tatsächlich dieselbe Handler-Instanz und teilen sich den internen Zustand.
Zusammenfassung
Der Coordinator entkoppelt Werkzeugnamen und Implementierung: Ein neues Werkzeug erfordert nur eine Änderung in der Map und eine neue Handler-Klasse. Die Suche läuft in drei Stufen: MCP-Normalisierung, statische Tabelle, dynamischer Subagent. Wie ein Handler aussieht und wie er registriert wird, steht in /tools/handlers-overview; für die Berechtigungs- und Parameterprüfung vor der Ausführung siehe /tools/validator.
Siehe offizielle Dokumentation: Cline-Dokumentation · README