Skip to content

ToolExecutorCoordinator: Werkzeug-Routertabelle

源码版本v4.0.10

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 ToolExecutor bestanden aus einer switch-Anweisung über hunderte Zeilen; für jedes neue Werkzeug musste die Hauptdatei geändert werden. Der Coordinator hebt diese Tabelle als toolHandlersMap heraus; 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 bei registerByName wird 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) liefert undefined (todo undefined:97). Das bedeutet: «Dieser Werkzeugname erhält vorerst keinen Handler»; die obere has-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_rule alle mit WriteToFileToolHandler), hüllt eine Wrapper-Klasse den Basis-Handler ein und überschreibt das Feld name (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 mit CLINE_MCP_TOOL_IDENTIFIER auf MCP_USE zusammengezogen, 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ält handlers und dynamicSubagentHandlers als zwei Maps.
  • toolHandlersMap:79 — Statische Registry: ClineDefaultTool-Enum → Fabrikfunktion.
  • register:114 — Speichert eine Handler-Instanz unter name in der handlers-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 direkt getHandler mit anschliessender Null-Prüfung.
  • getHandler:135 — Such-Logik: MCP-Normalisierung → statische Tabelle → dynamischer Subagent.
  • dynamic subagent branch:146 — Verzögertes Erzeugen des SharedToolHandler(UseSubagentsToolHandler) für dynamische Subagent-Werkzeuge.
  • execute:162 — Nach Finden des Handlers Aufruf von handler.execute(config, block); eigenes try/catch gibt es hier nicht.
  • registerToolHandlers:201 — Durchläuft beim Konstruieren des ToolExecutor sämtliche toolUseNames und registriert sie.
  • toolUseNames:40 — Array mit allen Werkzeugnamen aus dem ClineDefaultTool-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:

typescript
// 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:

typescript
// 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: execute ruft getHandler auf und wirft bei Misserfolg direkt No handler registered for tool: ${block.name} (throw on missing:165). Tatsächlich prüft der vorgelagerte ToolExecutor.execute zuerst per has, 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) liefert undefined (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 Teilstring CLINE_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 dynamicSubagentHandlers wä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: registerByName legt bei jedem Fabrikaufruf eine neue Handler-Instanz an (factory call:119). Da toolUseNames keine Duplikate enthält, wird jeder Name aber nur einmal registriert und es entsteht genau eine Instanz.
  • SharedToolHandler teilt keinen State: SharedToolHandler speichert den baseHandler als privates Feld; alle Aufrufe werden an dieselbe Basis-Instanz weitergeleitet (shared execute:62). Die drei Namen write_to_file, replace_in_file und new_rule nutzen 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