Skip to content

English · Español

Lab 03 — Tour por los modos de fallo: induce cuatro bugs clásicos de agente

Lee theory/01-react-and-planning.md, theory/02-memory.md. No consultes solutions/.

Objetivo

Induce deliberadamente cuatro modos de fallo clásicos de agente — iteración en bucle, herramientas alucinadas, fuga de scratchpad, agotamiento de presupuesto —, observa cada uno, documenta el síntoma y verifica que tu GrammarTutorAgent o bien previene cada uno o lo reporta con elegancia. Este es el lab de espacio negativo: un tour de lo que sale mal para que sepas contra qué defenderte.

Setup

Usa el agente del Lab 01. Construye variantes adversarias de Planner que disparen cada modo de fallo. No se necesita infraestructura nueva; solo subclases de MockPlanner que emitan los pasos equivocados.

Tareas

Modo de fallo 1 — iteración en bucle (misma acción repetida)

Construye un LoopingPlanner que emita el mismo ToolCall(tool="conjugate", args={"verb": "go", "person": "3sg", "tense": "past_simple"}) en cada paso.

Ejecuta el agente:

agent = GrammarTutorAgent(planner=LoopingPlanner(), ...)
result = agent.correct("He goed to school.")

Comportamiento esperado:

  • El detector de acción duplicada del agente debería dispararse en el paso 2.
  • El agente devuelve un CorrectionResult con corrected=None, rationale=["agent looped"], in_scope posiblemente True (simplemente no pudimos decidir).
  • Pasos totales gastados: ≤ 2 (no los max_steps completos).

Asserciones:

assert len(result.tool_trace) <= 2
assert "loop" in " ".join(result.rationale).lower()

Si tu agente recorre los 8 pasos antes de detenerse, la detección de duplicados no está cableada.

Modo de fallo 2 — agotamiento de presupuesto (sin iteración en bucle, simplemente lento)

Construye un IndecisivePlanner que emita un ToolCall diferente en cada paso — nunca un FinalAnswer. Efectivamente el planificador no puede decidir.

Ejecuta el agente. Esperado: el agente recorre todos los max_steps (default 8) y devuelve un resultado budget_exhausted con un rationale claro.

Asserciones:

assert len(result.tool_trace) == 8  # full budget spent
assert "budget" in " ".join(result.rationale).lower()
assert result.corrected is None

Modo de fallo 3 — nombre de herramienta alucinado

Si tu planificador está implementado según el Lab 00 (con JSONSchemaMask), el planificador no puede emitir una herramienta desconocida — la máscara enum lo previene. Para testear la red de seguridad, construye un HallucinatingPlanner que eluda la máscara (devuelva un ToolCall(tool="this_tool_does_not_exist", args={}) construido a mano).

Ejecuta el agente. Esperado: el dispatcher (cliente MCP de la Fase 31) lanza un ToolNotFoundError limpio; el agente lo captura y o bien:

  • (a) Reporta un fallo limpio (devuelve CorrectionResult con rationale "unknown tool: this_tool_does_not_exist"), O
  • (b) Re-prompta al planificador con el error como observación y vuelve a intentarlo.

Default: opción (a) para la Fase 32. La opción (b) es un patrón de retry que vale la pena conocer pero añade complejidad.

Asserciones:

assert result.corrected is None
assert any("unknown" in r.lower() or "does not exist" in r.lower() for r in result.rationale)

Modo de fallo 4 — fuga de scratchpad

Este es el que se esconde hasta producción: el scratchpad se comparte accidentalmente entre llamadas a correct(). Para testear:

agent = GrammarTutorAgent(planner=MockPlanner(scripts_for_two_sentences), ...)
result_a = agent.correct("He goed to school.")
result_b = agent.correct("I has a book.")

# The scratchpad for B should contain only B's steps.
# If the bug is present, B's trace will contain steps from A's correction.
for step in result_b.tool_trace:
    assert step.args.get("verb") in {"have", "has", None}, \
        f"scratchpad leaked: step references '{step.args.get('verb')}' from previous correction"

Esperado: el test pasa (el scratchpad es local a cada llamada correct()).

Si el test falla, el bug es el descrito en theory/02-memory.md §"Una trampa común: fuga de memoria entre correcciones" — arréglalo construyendo ScratchpadMemory() dentro de correct().

Tarea 5 — escribe una nota post-mortem corta

En learners/borja/phase-32/notes.md, escribe un resumen de 2–3 párrafos:

  • ¿Cuál de los 4 modos de fallo manejaba ya tu implementación correctamente?
  • ¿Cuál requirió un cambio de código?
  • ¿Qué otros modos de fallo deberías estar testeando, pero aún no? (Posibles respuestas: planificador emitiendo args mal formados, herramienta devolviendo datos mal tipados, timeout MCP a mitad de llamada, estado del agente corrupto entre ejecuciones.)

Esta nota se convierte en parte de PHASE_32_REPORT.md.

Medidas a capturar

  • Para cada uno de los 4 modos de fallo: assert pasa/falla, comportamiento del agente observado, tiempo gastado antes de detenerse.
  • Tiempo total de ejecución del tour de modos de fallo (debería ser ~segundos, no minutos — la iteración en bucle debe terminar rápido).

Guarda en experiments/<date>-phase-32-failure-tour/results.json.

Aceptación

  • Los 4 tests de modos de fallo escritos.
  • El detector de iteración en bucle se dispara en el paso 2.
  • El agotamiento de presupuesto se detiene limpiamente en el paso max_steps.
  • La herramienta desconocida devuelve un resultado de fallo limpio.
  • El scratchpad es local a cada llamada correct() (sin fuga).
  • Nota post-mortem en learners/borja/phase-32/notes.md.
  • Resultados del tour de modos de fallo guardados.

Trampas a esperar

  • Umbrales de detección de iteración en bucle. Detectar "misma acción dos veces seguidas" captura los bucles más simples. Un detector más sofisticado busca ciclos de longitud 2+ (A → B → A → B). La Fase 32 implementa solo longitud-1; los ciclos de mayor orden son una extensión. Documenta el límite.
  • Confusión del resultado de agotamiento de presupuesto. Si corrected=None y in_scope=True, ¿qué ve el usuario? La política de la Fase 32: el rationale debería decir explícitamente "no pude decidir dentro del presupuesto" — no confundas esto con "no se necesita corrección".
  • La detección de fuga de scratchpad es frágil. El test del Modo de fallo 4 depende de que las Frases A y B tengan vocabulario disjunto. Si las frases de prueba comparten un verbo por casualidad, el test pasa por coincidencia. Usa verbos claramente distintos en el test (go para A, have para B).
  • HallucinatingPlanner debería ser obviamente sintético. Márcalo con un docstring: "Solo para test; elude JSONSchemaMask para verificar la defensa del dispatcher". Si no, un mantenedor futuro podría confundirlo con una plantilla de planificador real.

Siguiente: Fase 33 — Inference Serving: From FastAPI to Continuous Batching (tras /quiz 32 y /phase-report 32).