# Convierte el fallo de un agente en una prueba que puedas repetir

*Práctica de Aprendiendo IA, AI Informator, edición del 5 de noviembre de 2026.*

> **Qué es esto:** una **demo de arnés determinista con traza sintética**. Todos los datos son ficticios y están escritos a mano. Aquí no se evalúa ningún LLM ni se ejecuta un agente real. Tampoco es una traza ni un benchmark de NVIDIA.

El ciclo que se practica es el siguiente. Partimos de una traza con un fallo, escribimos un criterio y una prueba, cambiamos el arnés y reevaluamos sobre el mismo conjunto de casos, que incluye un caso reservado. La idea del ciclo está inspirada en material de NVIDIA NeMo, pero aquí no se reproduce ni se atribuye ninguno de sus resultados.

## Requisitos

- Python 3.12. Solo se usa la biblioteca estándar.
- No hace falta red, GPU, servicios externos ni NeMo.

## Ficheros

| Fichero | Para qué sirve |
|---|---|
| `trace.json` | Traza sintética (`origin: synthetic_fixture`) con la petición, la llamada a la herramienta y la respuesta de error. Su `observed_baseline` es una **ilustración**, no una observación. |
| `cases.json` | Los 4 casos fijados **antes** de ejecutar nada, con su `expected` exacto. |
| `before.py` | Parser con el fallo. |
| `fixed.py` | El mismo parser con un único cambio general. |
| `test_harness.py` | Runner: ejecuta todos los casos, compara de forma exacta, imprime cada resultado y escribe un informe JSON. |

## Contexto

Un agente ficticio responde preguntas sobre una biblioteca usando `catalogo.buscar`. Es una herramienta de **solo lectura**: consulta disponibilidad, no envía mensajes y no tiene efectos externos. El arnés interpreta lo que devuelve la herramienta con `parse_tool_response(response: dict) -> dict`.

## Criterio (contrato del parser)

- Si `status == "ok"`, el parser devuelve `{"ok": true, "answer": <content>}`.
- Con cualquier otro `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`.
- «Sin resultados» con `status` ok es una respuesta válida, no un error.

Los `expected` de `cases.json` se escribieron a partir de este contrato, no copiando la salida de ninguna variante.

## El fallo

`before.py` solo comprueba que exista `content`. En la traza, la herramienta devuelve `status: "error"` con `TIMEOUT`, y el parser entrega «Tiempo de espera agotado al consultar el catálogo» como si fuera la respuesta del catálogo.

## El cambio (en el arnés, no en un modelo)

`fixed.py` añade una sola guarda general: si `status` no es `"ok"`, devuelve un fallo estructurado con su `error_code`. No mira el texto ni enumera códigos de error concretos.

## Los 4 casos fijados

| id | rol | qué comprueba |
|---|---|---|
| `reg-001-timeout-como-respuesta` | regression | El fallo original de la traza. |
| `ctl-001-sin-resultados-es-valido` | control | Una búsqueda vacía con status ok sigue siendo una respuesta válida. |
| `ctl-002-titulo-con-palabra-error` | control | Una respuesta válida que contiene la palabra «error». Bloquea el atajo `if "error" in content`. |
| `res-001-servicio-no-disponible` | reserved | Otro tipo y valor de fallo: `status: "unavailable"`, `SERVICE_UNAVAILABLE`, sin la palabra «error» en el texto. |

**Por qué hay un caso reservado:** sirve para comprobar que el cambio es general y no un parche hecho a medida de la regresión. Por ejemplo:

- Un atajo como `if "error" in content.lower()` rompería `ctl-002` y no detectaría `res-001`.
- Un parche como `if status == "error"` pasaría la regresión y los controles, pero no cubriría `res-001`.

El caso reservado **no** se usa para ajustar el cambio después de ver resultados. Si fallara con `fixed`, se informa tal cual en esta ronda.

## Cómo ejecutarlo

Desde la carpeta de la práctica:

```bash
python3.12 test_harness.py --variant before --output resultados/before.json
echo $?

python3.12 test_harness.py --variant fixed --output resultados/fixed.json
echo $?
```

Las dos variantes se ejecutan sobre el **mismo dataset completo** de 4 casos.

## Qué se espera (expectativa, no resultado)

Esta expectativa sale de leer el código. Lo que de verdad ocurre solo se sabe al ejecutar el runner.

- **before:** se espera que `reg-001` y `res-001` fallen y que los dos controles pasen. El proceso debería salir con código 1.
- **fixed:** se espera que pasen los 4 casos. El proceso debería salir con código 0.

El informe JSON contiene `variant`, `results` (`id`, `role`, `expected`, `observed`, `passed`, `error`) y `counts`. El runner sale con código 2 si `cases.json` no respeta el conjunto fijado (4 casos: 1 regression, 2 control, 1 reserved).

## Límites

- La traza es sintética y los fixtures son ficticios. Los títulos de libros también son inventados.
- No hay ningún LLM evaluado ni ninguna ejecución de un agente real.
- No es un benchmark de NVIDIA, y ninguna cifra de NVIDIA se atribuye a esta demo.
- Cuatro casos solo verifican este contrato concreto. Pasarlos no demuestra robustez general ni dice nada sobre otros agentes.
- El contrato `status == "ok"` es una decisión de esta demo. Antes de aplicar la idea a una herramienta real, lee su contrato.
- Es un artefacto educativo: no modifica ningún código productivo.

## Ejecución registrada

Una sola ronda registrada el 2026-11-05T07:28:14.802421+00:00, con Python 3.12.3. La versión before salió con código 1 y la versión fixed con código 0. Los resultados exactos están en `case-table.json`, `case-table.csv` y `logs/`. `inputs-frozen.json` fija hashes de casos, código y diseño anteriores a la ejecución.

El caso reservado se escribió junto al resto del diseño antes de ejecutar y no se utilizó para ajustar tras observar resultados. No es un holdout ciego ni desconocido para el autor. Se ejecutó también con before para registrar el control negativo. Todas las entradas comprobadas tienen content de tipo cadena; las ramas de content ausente/None y status ausente no tienen cobertura en este conjunto. El código conserva el rechazo EMPTY_RESPONSE cuando status es ok pero falta content; la descripción del contrato anterior se refiere a las cuatro entradas fijadas.

La ejecución del autor fue Claude Opus 5.5 por CLI OAuth. No se evaluó ese modelo ni se volvió a llamar para revisar resultados. `model-receipt.json` registra la ruta. La fuente del método es [NVIDIA — Use NVIDIA NeMo to Turn Agent Failures into Tests and Harness Fixes](https://developer.nvidia.com/blog/use-nvidia-nemo-to-turn-agent-failures-into-tests-and-harness-fixes/).

URL prevista para este paquete: https://twinforce.es/experimentos/2026-11-05/fallo-agente/ . Este artefacto aún no está publicado. La incorporación al sitio y su aprobación final se gestionan fuera de esta práctica. `private/` contiene recibos internos y no forma parte del paquete público.
