{
  "title": "Convierte el fallo de un agente en una prueba que puedas repetir",
  "slug": "aprendiendo-ia-fallo-agente-prueba-regresion",
  "edition": "AI Informator, 5 de noviembre de 2026",
  "nature": "Demo de arnés determinista con traza sintética. Datos ficticios escritos a mano. No hay un LLM evaluado ni una ejecución real de un agente. No reproduce ninguna traza, resultado ni benchmark de NVIDIA NeMo.",
  "scope": "Un único parser del arnés, parse_tool_response(response: dict) -> dict, interpreta la respuesta de una herramienta inocua de solo lectura: catalogo.buscar, que consulta la disponibilidad de un libro en un catálogo de biblioteca ficticio. La herramienta no envía mensajes ni tiene efectos externos. Se trabaja con Python 3.12 y solo la biblioteca estándar, sin red, sin GPU, sin servicios y sin NeMo. El cambio se hace en el arnés (el parser), no en un modelo. Ningún código productivo se modifica: es solo un artefacto educativo.",
  "criterion": "Contrato del parser: devuelve {\"ok\": true, \"answer\": <content>} solo si la herramienta declara status == \"ok\". Con cualquier otro valor de status, o si falta, devuelve {\"ok\": false, \"error_code\": <error_code o \"UNKNOWN\">, \"message\": <content o \"\">}. El veredicto depende del campo estructurado status y nunca del texto de content. Un resultado vacío con status ok («sin resultados») es una respuesta válida, no un fallo. Los expected de cases.json se escriben a partir de este contrato, no a partir de la salida de ninguna variante.",
  "failure": "before.py solo comprueba que exista content y lo da por respuesta válida. Por eso un error de la herramienta (status \"error\", error_code \"TIMEOUT\") llega al agente como si fuera la respuesta del catálogo.",
  "steps": [
    "1. Traza: trace.json recoge, como fixture sintético, la petición, la llamada a catalogo.buscar y una respuesta de herramienta con status \"error\". El campo observed_baseline es una ilustración derivada de leer before.py, no una observación.",
    "2. Criterio: se fija por escrito el contrato del parser (ver criterion) antes de ejecutar nada.",
    "3. Casos: se fijan exactamente 4 casos con su expected exacto antes de la ejecución: la regresión del fallo original, dos controles independientes (uno de ellos bloquea el atajo de buscar la palabra «error») y un caso reservado con otro tipo y valor de fallo.",
    "4. Baseline: se ejecuta test_harness.py --variant before sobre el dataset completo de 4 casos.",
    "5. Cambio del arnés: fixed.py añade una única comprobación general de status.",
    "6. Reevaluación: se ejecuta test_harness.py --variant fixed sobre el mismo dataset completo. El caso reservado no se mira ni se usa para ajustar el cambio. Si falla, se informa tal cual en esta ronda."
  ],
  "fixed_cases": [
    {
      "id": "reg-001-timeout-como-respuesta",
      "role": "regression",
      "purpose": "Fallo original de la traza: status \"error\" con TIMEOUT."
    },
    {
      "id": "ctl-001-sin-resultados-es-valido",
      "role": "control",
      "purpose": "Una búsqueda vacía con status ok es una respuesta válida. Evita un fix que trate «no encontrado» como error."
    },
    {
      "id": "ctl-002-titulo-con-palabra-error",
      "role": "control",
      "purpose": "Una respuesta válida cuyo texto contiene la palabra «error». Bloquea el atajo if 'error' in content."
    },
    {
      "id": "res-001-servicio-no-disponible",
      "role": "reserved",
      "purpose": "Otro tipo y valor de fallo (status \"unavailable\", SERVICE_UNAVAILABLE). Su texto no contiene «error». Un cambio general debe cubrirlo; un parche específico para status == \"error\" no lo cubriría."
    }
  ],
  "reserved_case_id": "res-001-servicio-no-disponible",
  "intended_minimal_change": "Añadir al inicio de parse_tool_response una única guarda general: si response.get(\"status\") != \"ok\", devolver un fallo estructurado con error_code (o \"UNKNOWN\") y message. El resto del parser no cambia. No se inspecciona el texto de content ni se enumeran códigos de error concretos.",
  "expectation_not_result": "Expectativa derivada de leer el código, no un resultado observado. Con before se espera que fallen reg-001 y res-001, que pasen los dos controles y que el proceso salga con código 1. Con fixed se espera que pasen los 4 casos y que el proceso salga con código 0. Los resultados reales solo existen al ejecutar test_harness.py.",
  "cautions": [
    "Traza sintética y fixtures ficticios: no proceden de un LLM, de un agente real ni de NVIDIA NeMo.",
    "La fuente de NVIDIA NeMo solo inspira el ciclo traza → criterio/test → cambio del arnés → reevaluación con tareas reservadas. No se reproducen ni se atribuyen sus resultados ni sus cifras.",
    "No hay métricas porcentuales, costes de ejecución ni afirmaciones de generalización a agentes.",
    "Cuatro casos solo verifican este contrato concreto. Pasar los 4 no demuestra que el parser sea robusto en general.",
    "El contrato (status == \"ok\" como única señal de éxito) es una decisión de esta demo. Las herramientas reales pueden tener contratos distintos que habría que leer primero.",
    "Una ronda fijada: no se generan variantes para elegir el resultado más bonito, y el caso reservado no se usa para ajustar tras observarlo.",
    "Sin datos personales ni secretos. Los títulos de libros son ficticios."
  ]
}
