La forma anunciada y los datos recibidos
Una tool es una función que el modelo puede pedir. Su input schema describe la forma de los argumentos: qué campos necesita y de qué tipo son. Si una función guarda una medición corporal, anunciar que el peso es un número no hace que la función compruebe automáticamente cada dato que recibe. Necesitas ejecutar esa comprobación en tu propio programa.
Las restricciones que ofrezca el proveedor pueden ayudar a generar datos correctos. Aun así, el harness debe hacer cumplir su contrato antes de actuar, especialmente si admite distintos proveedores. Una declaración describe lo aceptado; el validador compara esa declaración con una llamada concreta.
Tres preguntas antes de ejecutar
Hay tres comprobaciones distintas. Primero, ¿terminó la respuesta del proveedor? Después, ¿se pueden interpretar los argumentos como un objeto JSON? Por último, ¿ese objeto cumple el schema? Una respuesta completa puede contener argumentos incorrectos; un JSON legible también.
- Los eventos del proveedor llegan al parser, que reconstruye la llamada pendiente.
- Tras el resultado del parser, el harness comprueba que la respuesta está completa e interpreta los argumentos.
- El validador compara campos y tipos con el schema de esa tool.
- Si cumple el schema, entra en el handler; si falla, devuelve el error sin ejecutarlo.
- El resultado vuelve al modelo, que puede corregir la llamada o responder.
Un solo punto de comprobación
El despachador, la función que elige qué código ejecutar según el nombre de la tool, es un buen lugar para esta comprobación. Las lecturas y escrituras atraviesan el mismo punto. Si cada handler inventa sus propias reglas sobre tipos y campos requeridos, añadir una tool nueva puede abrir un hueco sin que nadie lo note.
Define la tool una sola vez y usa esa definición para anunciarla al proveedor y para comprobar los argumentos recibidos. El patrón no necesita un servidor: en Gymnasia la comprobación ocurre localmente. Fíjate en que el handler solo se ejecuta después de que el validador haya aceptado la llamada:
when a complete tool call arrives:
find the tool definition
parse the arguments as a JSON object
errors = validate(arguments, definition.input_schema)
if errors exist:
return a tool result with the failing fields and reasons
return run_handler(arguments)
append the tool result to the conversation
ask the model for the next turn, within the round limitOpenAI, Anthropic y Google Interactions usan formatos distintos para pedir tools y recibir resultados. Una vez reconstruida la llamada, las tools de Coach llegan al mismo ejecutor. El proveedor compatible con OpenAI también usa ese ejecutor. No hacen falta cuatro versiones de las reglas de entrada.
Un error que permite continuar
Para guardar un peso, Gymnasia usa una tool llamada write_measurement. Este ejemplo no pasa: el campo data.weight_kg contiene texto, aunque tenga aspecto de número. La fecha y el resto del objeto pueden estar bien:
{"date":"2024-04-11","data":{"weight_kg":"75,5"}}El resultado dice que la tool no se ejecutó y señala data.weight_kg como campo que debe ser un número. El harness devuelve ese resultado al modelo con la identidad de la llamada correspondiente. En la siguiente ronda, el modelo puede enviar:
{"date":"2024-04-11","data":{"weight_kg":75.5}}El harness no convierte el texto ni vuelve a ejecutar por su cuenta. El modelo propone la corrección y la nueva llamada vuelve a comprobarse. Si es válida, se ejecuta; si sigue fallando, recibe otro error. Todo ocurre dentro del límite de rondas existente. Un error de argumentos conocido permite continuar; una escritura cuyo resultado sea incierto exige otro tratamiento y no debe repetirse como si nada hubiera pasado.
Casos reproducibles en Gymnasia
Estos casos proceden de pruebas del ejecutor y de fixtures reproducibles, respuestas de proveedor preparadas para tests. No son transcripciones de conversaciones privadas ni una medida de la frecuencia con que un modelo se equivoca.
| Entrada problemática | Comportamiento anterior | Con la validación central |
|---|---|---|
| Medición enviada como JSON dentro de un string, con un número escrito como texto. | El contrato de mediciones aceptaba ese formato heredado y normalizaba el valor. | Se pide un objeto con valores numéricos, como anuncia el schema. La medición existente se conserva mientras se corrige. |
| Una comida escrita como “ desayuno ”. | El handler normalizaba espacios y mayúsculas. | Coach recibe los valores permitidos del enum y puede reenviar “Desayuno”. |
| JSON ilegible en OpenAI para una tool sin campos requeridos. | El parser lo sustituía por un objeto vacío, que podía llegar al handler. | Se devuelve un fallo de argumentos sin ejecutar la tool. Un objeto vacío legítimo sigue siendo válido si el schema lo permite. |
| Un campo numérico opcional enviado como null. | El validador omitía ese valor. | Se rechaza si el tipo no admite null, incluso en objetos o arrays anidados. |
La forma no basta para decidir
El schema comprueba la forma, no toda la intención ni las reglas del producto. Una fecha puede ser un string y representar un día imposible. Un identificador puede tener el tipo correcto y referirse a un ejercicio inexistente. La validación de fechas, referencias y series sigue perteneciendo al dominio. Pasar el schema tampoco concede permiso para una acción que necesita confirmación.
En Gymnasia el validador propio cubre los tipos y restricciones que declara su catálogo, no toda la especificación JSON Schema. No añade una dependencia nueva al bundle móvil ni transforma los argumentos. Los campos extra se permiten cuando el schema no los prohíbe y se rechazan cuando additionalProperties es false. Un campo opcional puede omitirse; eso no convierte null en un número. Estas reglas están documentadas en las referencias de JSON Schema sobre objetos y null.
Si tus schemas empiezan a necesitar uniones, referencias o nuevas restricciones, amplía el validador y su contrato o adopta una librería que las soporte. No anuncies una regla que el programa nunca comprueba. Las tools que declaran un campo como string con JSON dentro necesitan además interpretar y validar ese contenido.
El contrato también puede cambiar durante el transporte. En una prueba real con OpenAI Responses, pedir solo el peso hizo que el proveedor exigiera todas las medidas y el modelo rellenara las demás con 0,01. En un ensayo temporal con strict:false, el schema conservó los campos opcionales y el modelo envió solo el peso. La validación local siguió activa. La documentación de OpenAI explica el valor por defecto de strict.
Pruebas del contrato y de sus efectos
Las pruebas unitarias deben cubrir argumentos válidos sin modificación, campos requeridos ausentes, tipos incorrectos, enums, límites y objetos anidados. Gymnasia compara además su validador con Ajv en tests para el subconjunto de schemas usado. La generación de argumentos arbitrarios, o fuzzing, busca entradas que causen excepciones o discrepancias.
La propiedad que importa al ejecutar es observable: una llamada inválida no carga datos, resuelve catálogos, crea IDs ni produce una escritura del handler. Los tests de integración reproducen una medición inválida, el resultado de error, una llamada corregida y una sola escritura. El E2E hace ese recorrido desde el chat con OpenAI, Anthropic y Google simulados, comprueba el almacenamiento y vuelve a cargar la app. Esto demuestra el comportamiento local con esas respuestas, no que un modelo real siempre se corrija.
La QA con OpenAI real detectó además que el transporte cambiaba el contrato de los campos opcionales. La corrección adicional ya está publicada en web. La QA final con el código publicado, sin intervención temporal de red, registró solo 75,9 kg y conservó las demás medidas al recargar.
Una serie sobre cómo construir agentes
Esta entrega forma parte de la serie para construir un agente y su harness usando una aplicación de gimnasio como caso práctico. Cada artículo desarrolla una decisión que puedes trasladar a tu propio agente.