Un error también es un resultado, pero no es un dato
Consultar un peso puede devolver 76 kg, indicar que falta el registro o fallar porque no se puede leer el almacenamiento. Los tres casos producen una respuesta de la función, pero solo el primero contiene el dato solicitado. Si todo viaja como texto sin estado, el modelo tiene que deducir qué ocurrió a partir de la redacción.
Un error esperado forma parte del dominio: una fecha sin registro, un campo inexistente o argumentos que no cumplen el schema, la forma aceptada de la petición. Una excepción inesperada es otra cosa: una lectura o escritura que se interrumpe dentro de la aplicación. Separarlas permite corregir una petición sin tratar cada fallo como una caída del proveedor.
El harness debe distinguir ambos casos antes de volver a llamar al modelo. Una frase que empieza por «Error» no constituye un contrato. Tampoco se debe clasificar un texto guardado por el usuario como fallo porque contenga is_error: los datos siguen siendo datos.
Un contrato que indica cómo recuperarse
La ejecución devuelve dos piezas: el contenido que verá el modelo y una marca explícita de error que controla el programa. El ejemplo siguiente es un payload interno ilustrativo, no un campo obligatorio de todas las APIs. El mensaje está en español, como en el caso práctico. Además del código estable not_found, explica el alcance del fallo y propone una vía de recuperación.
{
"kind": "tool_error",
"version": 1,
"is_error": true,
"error": "not_found",
"message": "No hay registro para esa fecha. No implica que no haya registros en otras fechas.",
"recovery": "choose_alternative",
"recoverable": true
}correct_arguments permite corregir los argumentos; choose_alternative permite elegir otra consulta con información suficiente; stop_turn exige parar. Son etiquetas del contrato de esta aplicación. El modelo puede proponer una alternativa, pero el harness sigue validando cada llamada y aplicando el límite de rondas.
- Argumentos inválidos → corregir
- Ausencia conocida → elegir alternativa
- Escritura incierta → parar
«No hay registro en esta fecha» no significa «no hay registros». Esa precisión evita convertir una ausencia local en una afirmación global. La marca de error ayuda a distinguir el estado; el mensaje ayuda a razonar sobre el siguiente paso.
El mismo fallo en tres proveedores
Los adapters, las piezas que traducen el contrato local a cada API, conservan el identificador que enlaza la llamada con su resultado. Cambia el envoltorio, no la decisión sobre si la ejecución falló.
- Anthropic Messages: el bloque
tool_resultllevais_error: truey el contenido seguro del fallo. - Google Interactions: el
function_resulttambién admiteis_error. Esta comparación se refiere a Interactions. - OpenAI Responses:
function_call_output.outputcontiene el JSON serializado del error. No añadimos unis_errorajeno a ese formato en el nivel superior.
La marca se conserva cuando se reproduce un resultado ya registrado. No se vuelve a inferir leyendo su texto. Por eso un resultado recuperado del historial mantiene el mismo significado que cuando se ejecutó.
Dónde poner el try/catch
Validar antes de ejecutar evita efectos con argumentos inválidos. Después, el límite que captura excepciones debe rodear la ejecución real del handler, la función que realiza la operación. Capturar solo la llamada al proveedor deja fuera los fallos locales.
validate the tool name and arguments
if validation fails:
return a marked error with recovery = correct_arguments
try:
result = execute the handler
return result with its explicit success or error status
catch an unexpected exception:
if the tool only reads:
return a safe error that allows an alternative
else:
stop the turn locally
say that completion cannot be confirmed
do not repeat the action automaticallyEste pseudocódigo resume la decisión, no todo el bucle. Los fallos esperados salen por el contrato normal; las excepciones se convierten en mensajes seguros. La excepción original puede servir para diagnosticar localmente, pero no debe aparecer como una confirmación de éxito ni exponerse al modelo con detalles internos. Un fallo fatal local tampoco se etiqueta como «Error de proveedor».
Una conversación real con Coach
Prueba exploratoria del 4 de octubre de 2026 en la web de Gymnasia, versión 1.51.0, con OpenAI gpt-6-luna. Se usaron fechas y pesos ficticios. Leer medidas permite consultar el peso guardado para un día; no modifica registros.
Usuario: consulta el peso del 2020-01-02. Si no existe, consulta el 2020-01-01. No crees ni modifiques datos.
Coach: Falta el registro del 2020-01-02. El 2020-01-01 sí tiene un peso guardado: 76 kg.
La petición del usuario está abreviada aquí. Las llamadas reales mostraron la secuencia completa: read_measurement para el día 2, error marcado con choose_alternative, lectura del día 1 y respuesta final. Se comprobó el contenido enviado al proveedor, no solo el texto visible.
Lo que no se reprodujo: en otra prueba aislada, el mismo modelo recibió el formato antiguo de texto sin marca y entendió que un campo no existía. No observamos una confusión de ese error con un dato válido.
Con una fecha ausente, el formato antiguo produjo «No tienes medidas registradas para el 2020-01-02». El contrato nuevo produjo «No hay un registro de medidas para el 2020-01-02. Esto no permite saber si tienes registros en otras fechas». El mensaje nuevo también era más preciso: esta comparación no aísla el efecto de la marca ni demuestra que todos los modelos mejoren. El valor del contrato es hacer explícita la decisión del programa, incluso cuando un modelo ya interpreta bien el texto.
Una escritura incierta cambia la decisión
Una lectura puede repetirse sin crear un registro. Una escritura interrumpida puede haber terminado antes de fallar la confirmación. «Falló» no siempre equivale a «no hizo nada». Repetir por defecto puede duplicar una acción.
En la misma sesión se provocó un fallo controlado al persistir un peso ficticio. El modelo era real; el fallo de almacenamiento era una inyección de QA. Coach mostró:
Gymnasia no puede confirmar si la acción llegó a completarse. Para evitar duplicarla, no la ha repetido. Revisa tus datos antes de solicitarla de nuevo.
Hubo una sola petición al modelo, ninguna ronda adicional ni reintento automático. El registro del 2020-01-03 no existía después de recargar. Se retiró la inyección y una escritura posterior del 2020-01-04, con 77,4 kg, se guardó y se pudo leer tras recargar. Esto verifica la recuperación del almacenamiento en ese escenario; no demuestra que toda escritura fallida deje los datos intactos.
El mensaje conservador sigue siendo necesario cuando el efecto es incierto. Si una acción debe permitir reintentos seguros, necesita una estrategia adicional, como una identidad estable y un registro durable del resultado. Cambiar el texto del error no proporciona esa garantía.
Qué pruebas protegen este contrato
Los tests deterministas comprueban el contrato sin depender de cómo responda un modelo ese día. La QA real añade evidencia de comportamiento, pero no sustituye esos límites.
- Contrato: argumentos inválidos producen un error marcado sin ejecutar el handler; un texto de usuario parecido a un error sigue siendo un resultado válido.
- Integración con proveedores falsos: OpenAI, Anthropic y Google reciben su formato correcto; una llamada corregida persiste una sola medición.
- Regresión de escrituras: una excepción con las palabras
network timeoutno activa reintentos de transporte ni confirma éxito. - Persistencia: reproducir un resultado conserva su estado; recargar permite distinguir el registro confirmado del que no llegó a guardarse.
La entrega pasó 933 pruebas Vitest, 11 del almacén de desarrollo y 3 de fronteras móviles, además del type-check y los E2E con proveedores falsos. La prueba exploratoria descrita aquí se hizo en web con un solo modelo. No se presenta como una prueba nativa ni como una evaluación estadística de los tres proveedores.
Construir un agente, paso a paso
Este artículo pertenece a la serie sobre cómo construir un agente y su harness a través de una app de gimnasio. Las entregas anteriores explican cómo declarar tools, validar llamadas y cerrar el bucle; esta añade una decisión que debe vivir en el programa: cuándo un fallo permite continuar y cuándo exige parar.
Consulta el índice de la serie y la implementación de Gymnasia que sirve como caso práctico.