Das Modell schickte ein Array als String — warum beschweren sich nur manche Tools?


Du bittest den Agenten, clarify zu verwenden und fünf Fragen auf einmal zu stellen. Er ruft das Tool fröhlich auf und kommt mit einem Typfehler zurück. Du versuchst es mit einem anderen Modell, formulierst es leicht anders — und es funktioniert. Was es schlimmer macht: Wechsle zu einem anderen Tool, wiederhole exakt denselben Fehler — ein Array, das als JSON-String gesendet wird — und nichts bricht je.

Dieses Gefühl von „manchmal funktioniert es“ kommt meist nicht vom Modell. Es sind zwei verschiedene Einstiegspunkte für Tool-Aufrufe, von denen einer die Argumente stillschweigend repariert und einer nicht. Ein am 2026-09-29 zusammengeführter Patch (PR #127781) hat sie auf eine Linie gebracht. Hier ist, was schiefging, was sich geändert hat und was sich weiterhin wie vorher verhält.

Der gemeinsame Einstiegspunkt hat deine Argumente schon repariert

Jeder Registry-Tool-Aufruf in Hermes läuft über eine Stelle:

# model_tools.handle_function_call
args = coerce_tool_args(tool_name, args)

Die Logik in tools/arg_coercion.py ist die einzige Stelle der Codebasis, an der Argumenttypen repariert werden. Sie arbeitet gegen das JSON-Schema des Tools selbst und korrigiert:

  • "42" zu 42 (String zu Zahl)
  • "true" zu true (String zu Boolean)
  • einen JSON-kodierten String zu einem Array oder Objekt, auch wenn er in einem Container verschachtelt ist
  • einen einzelnen Wert zu einer einelementigen Liste, wenn das Schema ein Array verlangt

Sie ist bewusst konservativ: Sie ändert einen Wert nur, wenn es genau eine klare Antwort gibt, und behält das Original, wenn die Korrektur mehrdeutig ist. Deshalb fällt sie dir auch nie auf — niemand erwähnt eine Reparatur, die funktioniert hat, nur einen Aufruf, der gescheitert ist.

Das Problem: Inline-Tools nehmen diesen Weg nie

Eine Gruppe von Tools ist „inline“: Sie wird vom Agenten selbst implementiert und in der Tabelle INLINE_TOOL_EXECUTORS in agent/inline_tool_executors.py registriert. Dazu gehören clarify, todo_list, memory, session_search, read_terminal, drive_preview, delegate_task und weitere.

Der Haken: Beide Dispatch-Pfade schlagen diese Tabelle direkt nach, statt über handle_function_call zu gehen:

  • sequenzieller Pfad: _resolve_sequential_dispatch in agent/tool_executor.py
  • konkurrierender Pfad: invoke_tool in agent/agent_runtime_helpers.py

Also erhalten diese Tools rohe Argumente, nie repariert:

  • clarify lehnte ein questions-Array ab, das als JSON-String ankam;
  • todo_list traf denselben Fehler und überlebte, weil es einen eigenen json.loads-Block mitbrachte;
  • memory und session_search unternahmen überhaupt nichts dagegen.

Ein Tool durch die gemeinsame Schicht repariert, eines mit handgeschriebenem Patch, der Rest ohne alles — das ist die Antwort auf die Frage „warum melden nur manche Tools Argumentfehler“.

model tool call
  ├─ registry tool → handle_function_call → coerce_tool_args → tool
  └─ inline tool   → INLINE_TOOL_EXECUTORS → tool gets raw args   ← the gap

Der Fix: die eine Tabelle umhüllen, die beide Pfade lesen

Der Patch ist zurückhaltend — er berührt keinen der beiden Dispatch-Pfade. Er hüllt die Tabelle ein, die beide konsultieren:

# agent/inline_tool_executors.py
def _coerced(name: str, executor: InlineToolExecutor) -> InlineToolExecutor:
    def _exec(agent, args: dict, ctx: InlineToolContext) -> Any:
        return executor(agent, coerce_tool_args(name, args), ctx)
    return _exec

INLINE_TOOL_EXECUTORS: Dict[str, InlineToolExecutor] = {
    name: _coerced(name, executor) for name, executor in _RAW_INLINE_TOOL_EXECUTORS.items()
}

Da beide Dispatch-Pfade diese eine Tabelle lesen, deckt ein einziger Wrapper sowohl die sequenzielle als auch die konkurrierende Ausführung ab. tools/todo_tool.py hat daraufhin sein handgebautes json.loads fallen gelassen: Sein Schema deklariert todos bereits als Array, die gemeinsame Reparatur deckt es also ab — und eine zweite Implementierung zu behalten bedeutete nur eine zweite Quelle der Wahrheit.

Es lohnt sich zu sagen, was der Fix nicht abdeckt: Memory-Provider-Tools auf dem konkurrierenden Pfad haben kein Registry-Schema, coerce_tool_args hat also keine Typinformation, auf die es reagieren könnte. Das ist unverändert gegenüber vorher — keine Regression. Der Patch hat nur damit aufgehört, dass Tools mit Schema nach zwei verschiedenen Maßstäben leben.

Ein leicht zu übersehendes Detail: Feldnamen müssen zuerst zusammenpassen

Das Erste, was coerce_tool_args tatsächlich tut, ist, Argumentnamen auf die Wire-Namen der Registry zurückzubilden:

from tools.schema_sanitizer import unrename_tool_args
args = unrename_tool_args(schema.get("parameters"), args)

Das Schema, das das Modell sieht, ist bereinigt: Property-Keys, die ein Provider ablehnt, werden umbenannt. Die Key-Namen des Modells und die internen der Registry sind also nicht ganz identisch. Die Reparatur muss die Namen anhand des Registry-eigenen Schemas wiederherstellen, bevor sie irgendetwas per Key typisiert — stimmt die Reihenfolge nicht, reparierst du ein Feld, das nicht existiert.

Wie du prüfst, dass es funktioniert

Der Patch liefert einen Verifikationsbefehl mit (erfordert einen Repo-Checkout):

scripts/run_tests.sh tests/tools/test_todo_tool_type_coercion.py tests/agent/ tests/tools/test_clarify_tool.py

Im Alltag zeigt sich der Unterschied darin, dass dieselbe Formulierung sich jetzt über Tools hinweg gleich verhält. Vorher verziehen nur Registry-Tools einen JSON-String, wo ein Array deklariert war; jetzt tun clarify, todo_list, memory und session_search dasselbe. Am einfachsten reproduzierst du es selbst, indem du eine Session über clarify mehrere Fragen auf einmal stellen lässt — genau das Szenario, das die Lücke aufgedeckt hat.

Drei praktische Erkenntnisse:

  1. Lies Typfehler nicht mehr als Problem der Modellfähigkeit. Wenn ein Tool einen falschen Argumenttyp meldet, prüfe, ob es ein Array als JSON-String erhalten hat; nach diesem Patch sollte diese Art von Rauschen deutlich zurückgehen.
  2. Beschreibe Parameter so, wie es das Schema tut, wenn du Skills oder eigene Prompts schreibst. Die Reparatur ist ein Sicherheitsnetz, nicht das Designziel — dem Modell zu sagen, dass questions ein Array von Strings ist, ist stabiler, als zu hoffen, dass es jedes Mal richtig liegt.
  3. Deklariere weiterhin Schemas für Tools, die du selbst schreibst. Die Reparatur hängt vollständig vom Schema ab; Parameter ohne eines (wie die oben genannten Memory-Provider-Tools) werden nie automatisch repariert.

Das führt eine Linie aus zwei früheren Beiträgen fort: der Beitrag zum Batch-Clarify behandelt, wie viel Hin und Her das Stellen mehrerer Fragen auf einmal spart, und die Tool-Schema-Diät behandelt das Verkleinern von Schemas, um Tokens zu sparen. Dieser Patch ist der dritte Baustein: die Argument-Reparatur konsistent machen, bevor ein Tool die Argumente überhaupt sieht. Für die Todo-Seite davon siehe den Beitrag zu verschachtelten Subtasks.

Die Kurzfassung

  • Argument-Reparatur findet nur in handle_function_call → coerce_tool_args statt; Inline-Tools werden von zwei Dispatch-Pfaden direkt nachgeschlagen und nie dadurch geleitet.
  • Also ließ derselbe Fehler „Array als JSON-String gesendet“ clarify scheitern, todo_list ihn mit json.loads überkleben und memory plus session_search gar nichts tun.
  • Jetzt ist die ganze Tabelle INLINE_TOOL_EXECUTORS einmal umhüllt und deckt sowohl den sequenziellen als auch den konkurrierenden Pfad ab; das handgebaute Parsing in todo_list ist weg.
  • Die Reparatur braucht ein Schema und muss zuerst bereinigte Feldnamen auf Registry-Namen zurückbilden; Provider-Tools ohne Schema behalten ihr vorheriges Verhalten.
  • Behandle Typfehler als Mechanikproblem, beschreibe Parameter in deinen Prompts gemäß Schema und deklariere weiterhin Schemas für deine eigenen Tools.

Weiterführende Lektüre: die Dokumentation zu Tools und Toolsets und hermes config auf dieser Seite.