El modelo envió un array como string — ¿por qué solo se quejan algunas herramientas?

Le pides al agente que use clarify y haga cinco preguntas a la vez. Llama a la herramienta tan contento y vuelve con un error de tipo. Reintentas con otro modelo, lo formulas con otras palabras y funciona. Lo que lo empeora: cambias a otra herramienta, repites exactamente el mismo error — un array enviado como string JSON — y nunca se rompe nada.
Esa sensación de «a veces funciona» normalmente no es culpa del modelo. Son dos puntos de entrada distintos para las llamadas a herramientas: uno repara los argumentos en silencio y el otro no. Un patch mergeado el 2026-09-29 (PR #127781) los alineó. Esto es lo que iba mal, lo que cambió y lo que sigue comportándose igual que antes.
El punto de entrada compartido ya te reparaba los argumentos
Toda llamada a una herramienta del registry en Hermes pasa por un único sitio:
# model_tools.handle_function_call
args = coerce_tool_args(tool_name, args)
La lógica de tools/arg_coercion.py es el único sitio del código que repara tipos de argumento. Trabaja contra el propio JSON Schema de la herramienta y corrige:
"42"a42(string a número)"true"atrue(string a booleano)- un string codificado en JSON a un array u objeto, incluso cuando está anidado dentro de un contenedor
- un valor único a una lista de un elemento, cuando el schema pide un array
Es deliberadamente conservadora: solo cambia un valor cuando hay exactamente una respuesta clara, y mantiene el original cuando la corrección es ambigua. Por eso tampoco te das cuenta nunca: nadie menciona una reparación que funcionó, solo una llamada que falló.
El problema: las herramientas inline nunca toman ese camino
Hay un conjunto de herramientas que son «inline»: las implementa el propio agente y se registran en la tabla INLINE_TOOL_EXECUTORS de agent/inline_tool_executors.py. Ahí están clarify, todo_list, memory, session_search, read_terminal, drive_preview, delegate_task y más.
El detalle está en que ambas rutas de dispatch consultan esa tabla directamente, en lugar de pasar por handle_function_call:
- ruta secuencial:
_resolve_sequential_dispatchenagent/tool_executor.py - ruta concurrente:
invoke_toolenagent/agent_runtime_helpers.py
Así que esas herramientas reciben argumentos en bruto, nunca reparados:
clarifyrechazó un arrayquestionsque llegó como string JSON;todo_listse topó con el mismo error y sobrevivió porque llevaba su propio bloquejson.loads;memoryysession_searchno hacían absolutamente nada al respecto.
Una herramienta arreglada por la capa compartida, otra con un parche escrito a mano y el resto sin nada: esa es la respuesta a «por qué solo algunas herramientas reportan errores de argumentos».
model tool call
├─ registry tool → handle_function_call → coerce_tool_args → tool
└─ inline tool → INLINE_TOOL_EXECUTORS → tool gets raw args ← the gap
El fix: envolver la única tabla que leen ambas rutas
El patch es contenido: no toca ninguna de las dos rutas de dispatch. Envuelve la tabla que ambas consultan:
# 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()
}
Como ambas rutas de dispatch leen esta única tabla, un solo wrapper cubre por igual la ejecución secuencial y la concurrente. tools/todo_tool.py eliminó después su json.loads hecho a mano: su schema ya declara todos como array, así que la reparación compartida lo cubre, y mantener una segunda implementación solo significaba una segunda fuente de verdad.
Merece la pena decir lo que el fix no cubre: las herramientas de memory-provider en la ruta concurrente no tienen schema de registry, así que coerce_tool_args no tiene información de tipos sobre la que actuar. Eso sigue igual que antes, no es una regresión. El patch solo evitó que las herramientas con schema vivieran bajo dos estándares distintos.
Un detalle fácil de pasar por alto: antes hay que alinear los nombres de campo
Lo primero que hace realmente coerce_tool_args es mapear los nombres de argumento de vuelta a los nombres de cable del registry:
from tools.schema_sanitizer import unrename_tool_args
args = unrename_tool_args(schema.get("parameters"), args)
El schema que ve el modelo está saneado: las claves de propiedad que un provider rechaza se renombran. Así que los nombres de clave del modelo y los internos del registry no son del todo idénticos. La reparación tiene que restaurar los nombres usando el propio schema del registry antes de tipar nada por clave: si te equivocas en el orden, estás reparando un campo que no existe.
Cómo comprobar que funciona
El patch incluye un comando de verificación (requiere un checkout del repo):
scripts/run_tests.sh tests/tools/test_todo_tool_type_coercion.py tests/agent/ tests/tools/test_clarify_tool.py
En el día a día, la diferencia observable es que la misma formulación ahora se comporta igual en todas las herramientas. Antes solo las herramientas del registry perdonaban un string JSON donde se declaraba un array; ahora clarify, todo_list, memory y session_search también lo hacen. La forma más fácil de reproducirlo tú mismo es hacer que una sesión plantee varias preguntas a la vez a través de clarify, el escenario exacto que dejó al descubierto el hueco.
Tres conclusiones prácticas:
- Deja de interpretar los errores de tipo como un problema de capacidad del modelo. Cuando una herramienta reporte un tipo de argumento incorrecto, comprueba si recibió un array como string JSON; tras este patch, esa clase de ruido debería bajar de forma notable.
- Al escribir skills o prompts personalizados, describe los parámetros como lo hace el schema. La reparación es una red de seguridad, no el objetivo del diseño: decirle al modelo que
questionses un array de strings es más estable que esperar que acierte todas las veces. - Sigue declarando schemas en las herramientas que escribas tú. La reparación depende por completo del schema; los parámetros sin uno (como las herramientas de memory-provider de arriba) nunca se repararán de forma automática.
Esto continúa una línea abierta por dos artículos anteriores: el artículo sobre clarify por lotes cubre cuánto ida y vuelta ahorra plantear varias preguntas a la vez, y la dieta de schemas de herramientas cubre cómo encoger los schemas para ahorrar tokens. Este patch es el tercer ladrillo: hacer que la reparación de argumentos sea consistente antes de que una herramienta llegue a ver los argumentos. Para el lado de todo, consulta el artículo sobre subtareas anidadas.
La versión corta
- La reparación de argumentos solo ocurre en
handle_function_call→coerce_tool_args; a las herramientas inline las consultan directamente dos rutas de dispatch y nunca pasan por ahí. - Por eso el mismo error de «array enviado como string JSON» hacía fallar a
clarify, tapaba el problema entodo_listconjson.loadsy dejaba amemoryysession_searchsin hacer nada. - Ahora toda la tabla
INLINE_TOOL_EXECUTORSse envuelve una sola vez, cubriendo las rutas secuencial y concurrente; el parse hecho a mano entodo_listha desaparecido. - La reparación necesita un schema y debe mapear primero los nombres de campo saneados a los del registry; las herramientas de provider sin schema mantienen su comportamiento anterior.
- Trata los errores de tipo como un problema de mecanismo, describe los parámetros según el schema en tus prompts y sigue declarando schemas en tus propias herramientas.
Lectura relacionada: la documentación de tools y toolsets, y hermes config en este sitio.