Módulo 8: Project The Reservo Agent In Production

El gate de regresión en el capstone

Descripción

Con costo y latencia ya medidos, esta lección pone en marcha la tercera disciplina: gatear. Esta lección corre el mismo mecanismo dos veces, con dos preguntas distintas — y vale la pena nombrar, con precisión, que es el mismo mecanismo, no dos fixtures separados: el CASE_SET de cinco casos que M5 (Lección 3) fijó, y la función run_regression_gate(case_set, overrides=None) que M5 (Lección 7) construyó. La primera pregunta, corriendo el CASE_SET sin ningún overrides, es "¿el agente de Reservo, tal como está hoy, sigue comportándose exactamente como se espera?". La segunda, retomando esa misma función con un overrides que sustituye el guion de un caso puntual, es la que M7 (Lecciones 3 a 7) construyó sobre esa base: "¿esta versión candidata del prompt es segura para reemplazar a la que corre hoy?". Esta lección corre las dos, con sus números reales, y confirma algo que vale la pena tener presente antes de empezar: ninguna de las dos preguntas necesitó un fixture nuevo — la segunda reusa, sin cambiar una línea, exactamente lo que la primera ya dejó listo.

Conexión con el módulo

Esta lección no reconstruye ningún chequeo — reusa, sin cambiar una línea, CASE_SET/run_case/run_regression_gate de M5 (Lección 7) para la primera pregunta, y esa misma run_regression_gate (a través de overrides) más PROMPT_REGISTRY/rollout_decision/rollback de M7 (Lecciones 3, 4, 6, 7) para la segunda. El tercer artefacto real de este capstone, regression_report.json, sale directamente de la primera parte.


Analogía: la inspección diaria, y la misma vara aplicada a un candidato

El restaurante de la introducción de este módulo tiene dos usos distintos para la misma inspección de cinco platos. Todas las mañanas, antes de abrir, alguien corre la inspección diaria: los mismos cinco platos de siempre, preparados por la cocina de hoy tal como está, para confirmar que nada cambió desde ayer. Por separado, cuando alguien propone un chef candidato con una receta nueva —"quiero que el plato principal se sirva más rápido, saltándome un paso"—, el restaurante no inventa una inspección distinta ni un menú de prueba más grande: le aplica la misma inspección de cinco platos, sustituyendo únicamente la receta del plato que el candidato quiere cambiar, y compara el resultado contra el mismo estándar de siempre. Las dos son la misma vara de medir —ninguna "juzga si la comida sabe bien" (eso lo haría un crítico gastronómico, no un inspector)— pero una confirma que la cocina de hoy sigue sana, y la otra decide si un candidato puede reemplazar al chef titular, con la ventaja de comparar contra exactamente los mismos cinco platos que ya se sabe que el chef titular prepara bien. Este capstone corre las dos, sobre el mismo restaurante, la misma mañana.


Parte 1: el gate, corriendo contra el agente tal como está hoy

run_regression_gate(CASE_SET): el CASE_SET de cinco casos de M5

Reusa, sin cambiar nada, regression/harness.py y regression/golden_cases.json tal como quedaron en M5 (Lección 8) — el CASE_SET que corre run_reservo_agent completo, con un guion de turnos por caso:

import harness as hn

report = hn.run_regression_gate(hn.CASE_SET)
hn.print_gate_summary(report)

Qué esperar:

=== GATE: PASS (5/5) ===
  quote_focus_pro_3h                     PASS
  quote_focus_basic_3h                   PASS
  book_focus_pro_3h_ana                  PASS
  book_boardroom_pro_1h_sofia            PASS
  book_and_cancel_studio_basic_1h_diego  PASS

Cinco de cinco. Esta es la respuesta a la primera pregunta: el agente de Reservo, exactamente como agent-fundamentals M8 lo entregó y como este capstone lo viene operando desde la Lección 3, sigue eligiendo la tool correcta, produciendo resultados con la forma correcta, y anclando el precio de get_quote(Focus, pro, 3h) en 6000 centavos — la misma ancla que acompaña toda esta guía desde el Módulo 1.

Persistiendo el veredicto: regression_report.json

import json
from dataclasses import asdict

with open("regression_report.json", "w", encoding="utf-8") as fh:
    json.dump(asdict(report), fh, ensure_ascii=False, indent=2)

print("regression_report.json escrito:", len(open("regression_report.json", encoding="utf-8").read()), "bytes")

Qué esperar:

regression_report.json escrito: 1700 bytes

Este es el tercer artefacto real de este capstone — el mismo tipo de archivo plano y auditable que RUN_LOG.jsonl (Lección 3): cualquiera puede abrirlo, sin volver a correr ni una línea de Python, y confirmar exactamente qué veredicto produjo el gate y por qué.


Parte 2: la misma disciplina, aplicada a comparar dos versiones completas

La primera parte confirmó que el agente de hoy sigue funcionando. La segunda retoma exactamente el mismo CASE_SET y la misma run_regression_gate para una pregunta distinta: comparar una versión candidata del system prompt contra la que corre hoy, antes de que esa candidata le hable a un solo usuario real. M7 (Lecciones 3 a 7) construyó esta pieza sin declarar un solo caso nuevo — el parámetro overrides de run_regression_gate(case_set, overrides=None) (M5, Lección 7) es, con precisión, "la pieza que el Módulo 7 va a reusar para comparar una versión vieja del agente contra una nueva", tal como la Lección 7 de M5 lo adelantó. Cada versión del registro se modela como un overrides: sustituye el guion de los casos puntuales que esa versión cambiaría, y deja el resto del CASE_SET sin tocar.

El registro: v1 y v2, cada uno con su hash

from prompt_registry import PROMPT_REGISTRY

v1 = PROMPT_REGISTRY["v1"]
v2 = PROMPT_REGISTRY["v2"]
print(f"v1: hash={v1.prompt_hash}  nota={v1.note!r}")
print(f"v2: hash={v2.prompt_hash}  nota={v2.note!r}")

Qué esperar:

v1: hash=c5757b6d6264  nota='System prompt original del capstone de agent-fundamentals M8.'
v2: hash=c364e85e5649  nota='Agrega una instruccion de proactividad para reducir turnos.'

v2 no es un cambio arbitrario — es exactamente el tipo de cambio que parece razonable en una revisión rápida: "si el agente ya tiene toda la información para completar una reserva, que la complete directamente, para ahorrarle un paso al usuario". Probado a mano, con una sola pregunta como "Reserva Focus pro 3h para Ana", el cambio se ve como una mejora — el agente reserva, que es exactamente lo que se esperaba. El problema aparece solo con preguntas que nunca debían terminar en una reserva.

El gate: v1 contra v2, sobre el mismo CASE_SET de cinco casos

from rollout import rollout_decision

V2_REGRESSED_SCRIPT = [
    {"stop_reason": "tool_use", "content": [
        {"type": "tool_use", "id": "toolu_01", "name": "book_room",
         "input": {"room": "Focus", "tier": "pro", "hours": 3, "member": "Ana"}}]},
    {"stop_reason": "end_turn", "content": [
        {"type": "text", "text": "Reservé Focus pro 3h para Ana."}]},
]
VERSION_OVERRIDES = {
    "v1": {},
    "v2": {"quote_focus_pro_3h": V2_REGRESSED_SCRIPT},
}

report_v1 = hn.run_regression_gate(hn.CASE_SET, overrides=VERSION_OVERRIDES["v1"])
report_v2 = hn.run_regression_gate(hn.CASE_SET, overrides=VERSION_OVERRIDES["v2"])

print("GATE v1:", "PASS" if report_v1.passed else "FAIL",
      f"({sum(c.passed for c in report_v1.cases)}/{len(report_v1.cases)})")
print("GATE v2:", "PASS" if report_v2.passed else "FAIL",
      f"({sum(c.passed for c in report_v2.cases)}/{len(report_v2.cases)})")
for c in report_v2.cases:
    if not c.passed:
        print(f"  {c.name:38} FAIL -- tool_choice_ok={c.tool_choice_ok} actual_tools={c.actual_tools}")

decision, broken = rollout_decision(report_v1, report_v2)
print(f"rollout_decision(v1, v2) -> {decision}, casos_rotos={broken}")

Qué esperar:

GATE v1: PASS (5/5)
GATE v2: FAIL (4/5)
  quote_focus_pro_3h                     FAIL -- tool_choice_ok=False actual_tools=['book_room']
rollout_decision(v1, v2) -> NO-GO, casos_rotos=['quote_focus_pro_3h']

v1 pasa los cinco casos — la línea base. v2 pasa cuatro, y falla exactamente en quote_focus_pro_3h, la misma pregunta —"¿Cuánto cuesta Focus pro 3h?"— anclada al mismo 6000 centavos que acompaña esta guía desde el Módulo 1: en vez de cotizar, v2 decide reservar directamente, sin que nadie lo haya pedido de forma explícita. rollout_decision —la regla dura de M7 (Lección 6): nunca puede romper un caso que la versión vieja ya pasaba— no necesita ningún juicio adicional para decidir NO-GO; la evidencia, caso por caso, ya lo dice todo.

El rollback: de vuelta a v1, un cambio de puntero

from rollout import rollback

ACTIVE_VERSION = "v2"  # alguien la promovió antes de correr el gate -- la trampa que M7 (Lección 2) advirtió
print("version activa (antes del gate):", ACTIVE_VERSION)

if decision == "NO-GO":
    ACTIVE_VERSION = rollback(ACTIVE_VERSION, "v1", PROMPT_REGISTRY)

print("version activa (después del rollback):", ACTIVE_VERSION)

Qué esperar:

version activa (antes del gate): v2
version activa (después del rollback): v1

rollback no reconstruye nada — AgentVersion es inmutable (frozen=True, M7 Lección 3) y v1 nunca dejó de existir, completa, dentro de PROMPT_REGISTRY. "Volver a v1" es, con precisión, un cambio de puntero: qué version_id está activa ahora mismo, nada más.


Las dos preguntas, sobre el mismo CASE_SET

Vale la pena decirlo una vez más, con los números de esta lección ya sobre la mesa, porque es el hallazgo central de la integración de este capstone:

PreguntaoverridesCorre run_reservo_agent completoVeredicto de esta lección
¿El agente de hoy sigue funcionando?{} (ninguno)PASS (5/5)
¿v2 es segura para reemplazar a v1?{"quote_focus_pro_3h": V2_REGRESSED_SCRIPT}Sí — el mismo loop, con el guion candidato sustituido en un casoNO-GO (rompe quote_focus_pro_3h)

Ninguna de las dos filas contradice a la otra — son, con precisión, dos preguntas distintas, sobre el mismo CASE_SET de cinco casos, con el mismo mecanismo (run_regression_gate) aplicado dos veces con distinto overrides. Y las dos comparten la misma disciplina de fondo, sin excepción: comparación literal contra un valor fijo, nunca un juez, nunca un score de calidad semántica. quote_focus_pro_3h no falla porque "reservar en vez de cotizar suene mal" — falla porque check_tool_choice compara, con ==, la lista ['book_room'] contra la lista ['get_quote'] que el caso exige. Si en algún momento la pregunta real fuera "¿la confirmación que v2 le mostró a Ana es clara y profesional?", este mecanismo no tiene ninguna forma de contestarla — esa pregunta pertenece, sin ambigüedad, a evaluation-frameworks-guide.


Errores comunes

  1. Pensar que M7 corre un chequeo distinto al de M5, con su propio conjunto de casos. No lo hace — reusa, sin cambiar una línea, el CASE_SET de cinco casos y run_regression_gate de M5. Lo único que cambia entre las dos preguntas de esta lección es el overrides que se le pasa a la misma función.

  2. Pensar que un NO-GO en quote_focus_pro_3h significa que v2 es "peor en todo". rollout_decision no evalúa "mejor en general" — evalúa, con precisión, si v2 rompió algo que v1 ya resolvía bien. v2 podría, en teoría, mejorar otras interacciones que ningún caso del CASE_SET cubre; la regla sigue siendo NO-GO porque rompió un caso que sí importaba, sin excepción de cortesía.

  3. Olvidar reset_reservo_state() antes de cada caso del gate. Sin ese aislamiento (M5, Lección 3), el orden de los casos que reservan podría producir un FAIL que no tiene nada que ver con ninguna regresión real — la Lección 3 de M5 lo demostró con un booking_id contaminado entre dos casos, y run_case (M5, Lección 7) lo llama al principio de cada corrida, sin excepción.

  4. Ejecutar rollback sin haber confirmado primero la decisión NO-GO. rollback en sí mismo no decide nada — solo mueve el puntero al version_id que se le pida, y valida que exista en el registro. La decisión de cuándo llamarlo vive, siempre, en rollout_decision, nunca en un juicio manual de última hora.

  5. Confundir el GateReport de la Parte 1 (sin overrides, veredicto sobre el sistema de hoy) con el de la Parte 2 (con overrides, veredicto sobre una versión candidata). Los dos tienen exactamente la misma forma —cases/passed—, así que lo único que distingue a uno del otro es qué overrides se usó para producirlo. AGENT_CHANGELOG.md, en la Lección 8, documenta ambos, con precisión sobre cuál responde cuál pregunta.


Ejercicios

Ejercicio 1: Confirma que book_focus_pro_3h_ana no se contamina bajo v2 (Fácil)

book_focus_pro_3h_ana también llama a book_room —como parte de su secuencia list_roomsget_quotebook_room—, igual que la versión regresiva de quote_focus_pro_3h. Sin mirar la salida de arriba otra vez: confirma, con report_v2.cases, que este caso sigue en PASS bajo v2.

Ver solución
ana_case = next(c for c in report_v2.cases if c.name == "book_focus_pro_3h_ana")
print("passed:", ana_case.passed, " actual_tools:", ana_case.actual_tools)

Salida esperada:

passed: True  actual_tools: ['list_rooms', 'get_quote', 'book_room']

Explicación: run_case llama a reset_reservo_state() al principio de cada caso (M5, Lección 3), así que la reserva que la regresión de quote_focus_pro_3h crea por error nunca se filtra al booking_id que book_focus_pro_3h_ana espera encontrar. El overrides de esta lección solo sustituye el guion nombrado explícitamente en el diccionario — los otros cuatro casos del CASE_SET corren con su guion original, sin ningún efecto colateral.

Ejercicio 2: Simula una v4 que rompe dos casos a la vez (Medio)

Construye VERSION_OVERRIDES["v4"] combinando el guion regresivo de quote_focus_pro_3h con un segundo guion que reserva book_and_cancel_studio_basic_1h_diego pero nunca cancela. Corre el gate y confirma el conteo y los dos casos rotos.

Ver solución
no_cancel_script = [
    {"stop_reason": "tool_use", "content": [
        {"type": "tool_use", "id": "toolu_01", "name": "book_room",
         "input": {"room": "Studio", "tier": "basic", "hours": 1, "member": "Diego"}}]},
    {"stop_reason": "end_turn", "content": [
        {"type": "text", "text": "Reservé Studio basic 1h para Diego."}]},
]
VERSION_OVERRIDES["v4"] = {
    "quote_focus_pro_3h": V2_REGRESSED_SCRIPT,
    "book_and_cancel_studio_basic_1h_diego": no_cancel_script,
}
report_v4 = hn.run_regression_gate(hn.CASE_SET, overrides=VERSION_OVERRIDES["v4"])
print("GATE v4:", "PASS" if report_v4.passed else "FAIL",
      f"({sum(c.passed for c in report_v4.cases)}/{len(report_v4.cases)})")
for c in report_v4.cases:
    if not c.passed:
        print("  roto:", c.name, c.actual_tools)

Salida esperada:

GATE v4: FAIL (3/5)
  roto: quote_focus_pro_3h ['book_room']
  roto: book_and_cancel_studio_basic_1h_diego ['book_room']

Explicación: overrides no tiene ningún límite de cuántos casos puede sustituir a la vez —cada entrada del diccionario se aplica de forma independiente—, y run_regression_gate sigue agregando el veredicto global con all(...) (M5, Lección 7), sin importar si uno o varios casos fallaron.

Ejercicio 3: Diseña una v3 que corrige la regresión, y confirma GO (Difícil)

Retoma SYSTEM_PROMPT_V3 —la versión corregida que M7 (Lección 8, mini-proyecto) ya registró y calculó, con su propio hash real—: la misma instrucción de v1, pero explícita en que la proactividad de v2 nunca debe aplicarse a una pregunta de solo cotización. Regístrala en PROMPT_REGISTRY, corre el gate contra v1 como línea base —sin ningún overrides, porque el comportamiento de v3 para los cinco casos del CASE_SET vuelve a coincidir con el de v1—, y confirma que la decisión es GO.

Ver solución
from prompt_registry import hash_prompt, AgentVersion

SYSTEM_PROMPT_V3 = (
    "Eres el asistente de reservas de Reservo, un sistema de coworking. "
    "Ayudas a los usuarios a consultar salas, cotizar precios, reservar y "
    "cancelar reservas. Usa siempre las tools disponibles para cotizar y "
    "reservar -- nunca inventes un precio de memoria. Cuando el usuario "
    "pregunta un precio, usa get_quote y NO reserves, incluso si podrias "
    "inferir todos los datos necesarios para reservar. Usa book_room "
    "unicamente cuando el usuario pide reservar de forma explicita."
)
v3_hash = hash_prompt(SYSTEM_PROMPT_V3)
PROMPT_REGISTRY["v3"] = AgentVersion(
    version_id="v3", prompt_text=SYSTEM_PROMPT_V3, prompt_hash=v3_hash,
    tools_version="tools-v1", model="claude-sonnet-5",
    note="Corrige la regresion de v2 en quote_focus_pro_3h.",
)

VERSION_OVERRIDES["v3"] = {}  # v3 vuelve a decidir get_quote en quote_focus_pro_3h, igual que v1
report_v3 = hn.run_regression_gate(hn.CASE_SET, overrides=VERSION_OVERRIDES["v3"])
decision_v3, broken_v3 = rollout_decision(report_v1, report_v3)
print(f"v3: hash={v3_hash}")
print("GATE v3:", "PASS" if report_v3.passed else "FAIL",
      f"({sum(c.passed for c in report_v3.cases)}/{len(report_v3.cases)})")
print(f"rollout_decision(v1, v3) -> {decision_v3}, casos_rotos={broken_v3}")

Salida esperada:

v3: hash=c5c4c4631f7e
GATE v3: PASS (5/5)
rollout_decision(v1, v3) -> GO, casos_rotos=[]

Explicación: ninguna línea de rollout_decision cambió entre esta corrida y la de v2 — el resultado cambió porque la evidencia cambió. v3 acota la instrucción de proactividad exactamente al caso que la justificaba (completar una reserva que el usuario ya pidió de forma explícita) sin generalizarla a "cualquier pregunta con suficiente información" — la misma disciplina de "regla explícita, nunca vibra" que sostiene todo el Módulo 7.


Resumen y siguiente paso

  • Corrimos el gate contra el agente tal como está hoy —CASE_SET de cinco casos, sin ningún overrides—: PASS (5/5), persistido en regression_report.json, el tercer artefacto real de este capstone.
  • Corrimos, sin cambiar una línea, el mismo mecanismo con overrides comparando v1 contra v2: PASS (5/5) para v1, FAIL (4/5) para v2 —exactamente en quote_focus_pro_3h— y rollout_decision(v1, v2) -> NO-GO, casos_rotos=['quote_focus_pro_3h'].
  • Ejecutamos rollback("v2", "v1", PROMPT_REGISTRY): la versión activa vuelve a v1, un cambio de puntero determinista, sin ningún mecanismo de despliegue real.
  • Confirmamos que las dos preguntas de esta lección —¿el sistema de hoy sigue funcionando?, ¿la versión candidata es segura?— comparten el mismo CASE_SET y la misma función, run_regression_gate, aplicados con distinto overrides — nunca dos fixtures separados, y en ambos casos bajo la misma disciplina de forma, nunca de juicio semántico.

Siguiente lección: 06 — La capa de resiliencia. Con el gate y el rollout ya ejecutados, ponemos a book_room a fallar de verdad y observamos al CircuitBreaker de M6 abrirse, integrado con el resto de la capa de operación de este capstone.


Recursos adicionales

  1. Anthropic — Tool use (function calling) overview — El protocolo que este mecanismo valida de punta a punta, aplicado dos veces con distinto overrides.
  2. evaluation-frameworks-guide — cuando la pregunta deje de ser "¿la forma es correcta?" y pase a ser "¿la respuesta es semánticamente buena?" — su módulo evaluating-agents cubre exactamente ese territorio, con sus propias herramientas.
  3. Python — hashlib — La base de hash_prompt, reusado sin cambios de M7 en esta lección.
  4. Python — dataclasses.frozen — Por qué rollback nunca "reconstruye" v1: AgentVersion(frozen=True) garantiza que nunca dejó de existir, completa, en el registro.