Tool Handlers: Gesamtbild der Werkzeug-Handler
Verantwortung
Ein Handler ist die konkrete Implementierung eines Werkzeugs. Eine Handler-Klasse entspricht einer Werkzeugverhalten: Datei lesen, Datei schreiben, Befehl ausführen, MCP aufrufen, Subagent starten usw. Sie alle implementieren das Interface IToolHandler (IToolHandler:34) und bieten das Dreigestirn name, execute, getDescription. Wer gestreamte Partial-Blöcke unterstützen will, implementiert zusätzlich IPartialBlockHandler; wer beides implementiert, wird als IFullyManagedTool markiert.
Die Werkzeugliste von Cline ist im ClineDefaultTool-Enum definiert (ClineDefaultTool:8). Es umfasst derzeit 27 Werte, von ask_followup_question bis use_subagents. Diese Namen entsprechen direkt dem Feld name des tool_use-Blocks, den das LLM sieht. Unter dem Enum steht eine Zeile toolUseNames = Object.values(ClineDefaultTool) (toolUseNames:40), die das Enum in ein Array flachklopft; der ToolExecutor durchläuft dieses Array beim Konstruieren und registriert alle Werkzeuge.
Die Handler-Dateien liegen alle unter apps/vscode/src/core/task/tools/handlers/, ein Werkzeug pro Datei, mit Klassennamen der Form XxxToolHandler. Alle Handler teilen sich ein TaskConfig-Kontextobjekt, das cwd, taskState, messageState, api, diverse Dienste und Callbacks enthält; darüber greift ein Handler auf sämtliche äusseren Abhängigkeiten zu (asToolConfig:135).
Entwurfsmotivation
- Ein Werkzeug, eine Datei: Pro Handler eine Datei; die Verantwortungsgrenzen sind klar. Ein neues Werkzeug bedeutet nur eine zusätzliche Datei, ohne bestehenden Code zu berühren; ein entferntes Werkzeug bedeutet nur eine gelöschte Datei.
- Interface-Schichtung statt Vererbung:
IToolHandlerist der minimale Vertrag,IPartialBlockHandlerdie optionale Streaming-Fähigkeit,IFullyManagedTooldie «vollständige» Markierung (IFullyManagedTool:44). Komposition vor Vererbung; Handler implementieren nur die Interfaces, die sie brauchen. - Handler halten keinen Zustand: Handler-Instanzen werden vom Coordinator gecacht; der gesamte Zustand kommt über
TaskConfighinein. Dieselbe Handler-Instanz kann mehrfach mitexecuteaufgerufen werden und darf keinen Task-bezogenen Zustand in eigenen Feldern ablegen. - TaskConfig einmal konstruiert, vielerorts genutzt: Der ToolExecutor sammelt beim Aufruf von
asToolConfig()alle Abhängigkeiten in einem Objekt (asToolConfig:135); der Handler greift über config auf mcpHub, browserSession, diffViewProvider, clineIgnoreController und weitere Dienste zu, ohne selbst zu injizieren. - ClineDefaultTool-Enum treibt Registrierung:
toolUseNameswird automatisch aus dem Enum erzeugt; die Registrierungsschleife muss die Werkzeugliste nicht per Hand pflegen (for of toolUseNames:204). Ein neuer Enum-Wert rückt automatisch in die Registrierungsschleife, vorausgesetzt dietoolHandlersMapdes Coordinator enthält eine entsprechende Fabrik. - SharedToolHandler zur Implementierungswiederverwendung: Wenn mehrere Namen eine Implementierung teilen (write_to_file / replace_in_file / new_rule teilen sich
WriteToFileToolHandler), hüllt eine Wrapper-Klasse ein und überschreibt den Namen (SharedToolHandler:52), um drei identische Klassen zu vermeiden.
Schlüsseldateien
ClineDefaultTool enum:8— Enum mit 27 Werkzeugnamen; hierher stammt der Werkzeugname, den das LLM sieht.toolUseNames:40— Aus dem Enum flachgeklopftes Array; treibt die vollständige Registrierung im ToolExecutor.toolHandlersMap:79— Mapping von 27 Enum-Werten auf Handler-Fabriken; hier wird angepasst, wenn ein neues Werkzeug dazukommt.registerToolHandlers:201— Durchläuft beim KonstruierentoolUseNamesund registriert vollständig.asToolConfig:135— Konstruiert vor jeder Werkzeugausführung eineTaskConfig, die alle Abhängigkeiten für den Handler bündelt.WriteToFileToolHandler:26— ImplementiertIFullyManagedTool, unterstützt Partial + Complete und wird von drei Werkzeugnamen geteilt:write_to_file,replace_in_file,new_rule.ReadFileToolHandler:142— Implementierung vonread_file; erhält im Konstruktor den ToolValidator für die Pfadprüfung.ExecuteCommandToolHandler:58— Implementierung vonexecute_command; Bash-Befehlsausführung plus Berechtigungsprüfung.AttemptCompletionHandler:39— Implementierung vonattempt_completion; implementiert nurIToolHandler + IPartialBlockHandlerund markiert den Task-Abschluss.UseMcpToolHandler:14— Implementierung vonuse_mcp_tool; sämtliche MCP-Werkzeugaufrufe laufen hier zusammen.UseSubagentsToolHandler:49— Implementierung vonuse_subagents; dient auch dynamischen Subagent-Werkzeugnamen (via SharedToolHandler-Wrapper).ListFilesToolHandler partial approval:48— Ruft bereits in der Partial-Block-PhaseshouldAutoApproveToolWithPathauf, um den UI-Pfad festzulegen.
Datenfluss
Der Ablauf beim Hinzufügen eines Werkzeugs lautet: «Handler-Klasse schreiben → eine Zeile in der Map registrieren → Enum um einen Wert erweitern». So sieht ein Map-Eintrag aus:
// 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,
}Parameter der Fabrikfunktion ist der ToolValidator. Handler, die Pfade prüfen müssen (read/write/search/list), nehmen ihn entgegen; die übrigen (ask/attempt/browser/mcp usw.) verwenden _v als Platzhalter. Nur TODO liefert undefined und zeigt damit, dass dieser Werkzeugname nicht vom Coordinator verwaltet wird (todo undefined:97).
Das config-Objekt, das ein Handler zur Ausführung erhält, sieht so aus:
// 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,
}Ein Kommentar warnt: Handler dürfen config-Felder lesen, aber nicht das config-Objekt selbst verändern (etwa config.browserSession = ... ändert nicht die Instanzvariable des ToolExecutor) (config warning:134). Soll die Browser-Session getauscht werden, ist ein eigener Einstieg wie applyLatestBrowserSettings zu nutzen. config.coordinator reicht den Coordinator selbst an den Handler weiter, sodass ein Handler während der Ausführung weitere Werkzeuge aufrufen kann (darauf setzt das Subagent-Werkzeug).
Grenzen und Fehler
- Werkzeugnamen ohne Fabrik werden still übersprungen:
toolHandlersMapist einRecord<ClineDefaultTool, ...>, TypeScript erzwingt für jeden Enum-Wert einen Eintrag. Die Fabrik darf jedoch undefined zurückgeben; derzeit tut das nurTODO(todo undefined:97).registerByNameüberspringt bei undefined die Registrierung; das entspricht «nicht registriert». - Handler werden geteilt, müssen aber zustandslos bleiben:
WriteToFileToolHandlerwird von drei Namen (write_to_file,replace_in_file,new_rule) als gemeinsame Instanz geteilt (file_edit shared:83). Würde ein Handler Task-bezogenen Zustand in Instanzfeldern ablegen, würden sich die drei Namen gegenseitig verschmutzen. Deshalb kommt der gesamte Zustand überTaskConfigherein. asToolConfigwird jedes Mal neu konstruiert: Bei jedem Eintreten vonToolExecutor.executewird perasToolConfig()ein neues config zusammengebaut (asToolConfig call:321). Die im config referenzierten Dienst-Instanzen sind jedoch dieselben; ändert ein Handler den Zustand eines Dienstes, wirkt das über Aufrufe hinweg.- Partial-Handler pushed kein Tool-Result: Der Kommentar zu
handlePartialBlockhält explizit fest: «We don't push tool results in partial blocks» (no partial result:521). In der Partial-Phase wird nur das UI aktualisiert; das echte tool-result wird erst bei der Behandlung des Complete-Blocks gepusht. - Fehler des Handlers werden vom ToolExecutor aufgefangen:
ToolExecutor.executeumschliesstcoordinator.executemit try/catch (error catch:373). Jede vom Handler geworfene Fehler wird vonhandleErrorgefangen, in einformatResponse.toolErrorumgewandelt und in die Konversation gepusht, ohne die Task zum Absturz zu bringen. - Neues Werkzeug erfordert drei Änderungen: Ein neues Werkzeug bedingt drei Anpassungen: einen Wert im Enum
ClineDefaultTool, eine Fabrik intoolHandlersMapund eine neue Handler-Datei.toolUseNameswird automatisch aus dem Enum erzeugt und muss nicht angefasst werden; allerdings muss auch ingetSystemPrompteine Werkzeugbeschreibung ergänzt werden, sonst weiss das Modell vom Werkzeug nichts.
Zusammenfassung
Handler sind das Ende von Clines Werkzeugsystem: ein Werkzeug pro Datei, das das Interface IToolHandler implementiert. Die Registrierung wird durch das ClineDefaultTool-Enum und die toolHandlersMap-Fabriktabelle getrieben; ein neues Werkzeug bedeutet drei Änderungen. Wie das Routing den Handler findet, steht in /tools/coordinator; für die Parameter- und Berechtigungsprüfung vor der Ausführung siehe /tools/validator; wer die Werkzeuge aufruft, siehe /agent-loop/present-assistant-message.
Siehe offizielle Dokumentation: Cline-Dokumentation · README