Werkzeugsystem (tool system) ToolRegistry
Verantwortung
Tools sind atomare Funktionen, die der Agent aufrufen kann (Terminal, Browser, Datei, MCP, Skill-Aufruf, …). ToolRegistry entdeckt beim Modulimport per AST-Scan automatisch die Werkzeuge in tools/*.py und registriert sie; es bietet der Hauptschleife eine name → ToolEntry-Lookup-Tabelle für den Werkzeug-Dispatch (tool dispatch). Tools sind statisch – im Gegensatz zu den «evolvierbaren» Skills.
Designmotiv
Warum eine Registry (registry) statt fest codierter Verzweigungen? hermes-agent hat rund 80 Werkzeugdateien; würde man jedes neue Werkzeug in eine if name == "xxx"-Kette eintragen, verrottet die schnell. Der AST-Scan macht «eine Datei in tools/ ablegen, die registry.register aufruft» zur einzigen Anbindungsaktion. registry ist ein globales Singleton; dynamische MCP-Werkzeuge, Plugins (plugins) und eingebaute Werkzeuge teilen sich eine Tabelle.
Schlüsseldateien
discover_builtin_tools:67-86— AST-Scan übertools/*.py, findet Module, die Werkzeuge registrieren:
def discover_builtin_tools(tools_dir: Optional[Path] = None) -> List[str]:
tools_path = Path(tools_dir) if tools_dir is not None else Path(__file__).resolve().parent
module_names = [
f"tools.{path.stem}"
for path in sorted(tools_path.glob("*.py"))
if path.name not in {"__init__.py", "registry.py", "mcp_tool.py"}
and _module_registers_tools(path)
]
imported: List[str] = []
for mod_name in module_names:
try:
importlib.import_module(mod_name)
imported.append(mod_name)
except Exception as e:
logger.warning("Could not import tool module %s: %s", mod_name, e)
return imported__init__.py / registry.py / mcp_tool.py werden übersprungen; von den übrigen .py wird jedes importiert, dessen AST einen registry.register-Aufruf zeigt — der Import selbst stößt den modulweiten register(...)-Aufruf an. Schlägt ein einzelner Modul-Import fehl, gibt es nur ein Warning.
_is_registry_register_call / _module_registers_tools:30-43— prüft, ob ein Modulregistry.registeraufruftclass ToolEntry:87-153— Metadaten eines einzelnen Werkzeugs.__slots__hält 80+ Entries klein:
class ToolEntry:
__slots__ = (
"name", "toolset", "schema", "handler", "check_fn",
"requires_env", "is_async", "description", "emoji",
"max_result_size_chars", "dynamic_schema_overrides",
)dynamic_schema_overrides ist ein null-argumentiger Callable, der bei jedem get_definitions() aufgerufen wird und laufzeitabhängige Felder (etwa max_concurrent_children von delegate_task) ins Schema mergt, damit das Modell falsche Limits nicht sieht.
class ToolRegistry:217-365— Registry-Körperdef register:365-436— registriert ein Werkzeug; Kern ist die Überschreibungs- und Konfliktlogik:
def register(self, name, toolset, schema, handler, ..., override: bool = False):
with self._lock:
existing = self._tools.get(name)
if existing and existing.toolset != toolset:
both_mcp = (existing.toolset.startswith("mcp-")
and toolset.startswith("mcp-"))
if both_mcp:
... # MCP überschreibt MCP (Refresh)
elif override:
# Plugin überschreibt explizit eingebaut, benötigt operator allow_tool_override
...
else:
logger.error("Tool registration REJECTED: '%s' ...", name, toolset, existing.toolset)
return
self._tools[name] = ToolEntry(...)
self._generation += 1Gleichnamige Werkzeuge werden standardmäßig abgelehnt. MCP-gegen-MCP ist erlaubt (notifications/tools/list_changed-Refresh-Szenario); ein Plugin, das ein eingebautes Werkzeug überschreibt, braucht override=True + operatorseitiges allow_tool_override: true, sonst PermissionError. _generation inkrementiert und erlaubt äußeren Caches, ihren Memo zu invalidieren.
def get_entry(self, name: str) -> Optional[ToolEntry]:
"""Return a registered tool entry by name, or None."""
with self._lock:
return self._tools.get(name)Die Hauptschleife schlägt nach Erhalt von tool_call.name einmal get_entry nach und hat den Handler. register_toolset_alias lässt dynamische Toolsets wie tools="mcp-foo" in der Config kürzer schreiben.
register_plugin_override_policy:316-365— Namespace-Zulassungsstrategie für Plugin-WerkzeugeRegistry-Singleton:765—registry = ToolRegistry()(modulweites Singleton)tool_error / tool_result:784-810— Standard-Rückgabewert-Verpackung für Werkzeugetools/-Verzeichnis— ca. 80 Werkzeugdateien (terminal/browser/web/file/mcp/skills/delegate…)
Datenfluss
- Beim Start löst
init_agent:276die Toolset-Konfiguration aus. discover_builtin_tools:67durchläuft das Verzeichnistools/und entscheidet per AST, ob jedes Modulregistry.registeraufruft (tools/registry.py:30).- Treffer-Module werden importiert, ihr modulweiter
registry.register(...)-Aufruf wird ausgeführt und schreibt einToolEntryin dasRegistry-Singleton:765. - Die Hauptschleife sammelt jeden Turn (turn) die
ToolEntry.schemader aktivierten Toolsets instools-Feld für den Provider (provider). - Der Provider liefert
tool_call→ die Hauptschleife schlägt den Handler perget_entry(name)(tools/registry.py:274) nach, führt ihn aus → das Ergebnis wird mittool_result:798verpackt und zurückgefüllt.
Grenzen und Fehler
- Werkzeugnamen-Konflikt: Gleichnamige eingebaute Werkzeuge werden abgewiesen; Plugin-gegen-gebaut erfordert
override=True+ operatorseitigesallow_tool_override: true, sonstPermissionError. MCP-gegen-MCP (mcp--Präfix) ist eine Ausnahme — Server-Refresh als nuke-and-repave ist legitim. - check_fn-Flattern: Docker-/Playwright-Sonden können momentan
Falseliefern._check_fn_cachedhält einen TTL von 30 s + ein 60-s-Grace-Window: Innerhalb des Grace-Windows wird auch bei fehlschlagender neuer Sonde der letzte erfolgreicheTrue-Wert zurückgegeben, damit eindelegate_task-Subagent nicht plötzlich «Tool read_file does not exist» meldet. - MCP-Nebenläufigkeit: Wenn ein MCP-Server
tools/list_changedpusht, kann ein anderer Thread gerade lesen._lockist einRLock, Mutationen werden serialisiert, Reader arbeiten auf einem Snapshot._generationmacht Cache-Hit-Erkennung sichtbar. - Modul-Import schlägt fehl: Ein Importfehler in einer Werkzeugdatei reißt die Registry nicht mit, nur ein Warning und überspringen — aber das Werkzeug ist für die Hauptschleife nicht aufrufbar; in
agent.lognach «Could not import tool module» suchen.
Zusammenfassung
Das Clevere am Werkzeugsystem ist «Import = Registrierung»: Der AST-Scan macht eine manuell gepflegte Liste überflüssig; ein neues Werkzeug ist nur eine Datei in tools/, die register aufruft. Das registry-Singleton ist die einzige globale Wahrheit, die Hauptschleife liest sie nur. Der Unterschied zu Skills: Tools sind tote Funktionen, Skills sind von der Lernschleife umbuchbare Flüsse.