Módulo 8: Project The Reservo Agent In Production
Proyecto: entrega el agente listo para producción
Descripción
Esta es la última lección de la guía completa: ocho módulos, sesenta y cuatro lecciones, y un solo agente que fue ganando, capa por capa, la operación que necesita para sobrevivir a tráfico real. No hay ingeniería nueva que aprender — hay cuatro artefactos que confirmar, generados de punta a punta en una sola corrida, y un reto final que combina las cuatro disciplinas de una forma que ninguna lección anterior mostró exactamente así.
El entregable de esta lección es un checklist ejecutado —cada disciplina confirmada con código real, no de memoria— que demuestra que la capa de operación completa de esta guía cumple la promesa del DISEÑO.md: observar (M2), medir (M3/M4), gatear (M5), y endurecer + versionar (M6/M7), todas envolviendo a run_reservo_agent sin tocar una sola línea de su lógica. El reto final —con su solución completa en un desplegable— te pide operar un incidente nuevo: una versión candidata lista para promover, exactamente el mismo día en que book_room vuelve a fallar, con un apagón más corto que el de la Lección 6.
Conexión con el módulo
Esta lección no construye nada nuevo — verifica lo que las Lecciones 1-7 de este módulo ya construyeron, y las pone a prueba una última vez contra un escenario que combina piezas de M2, M3/M4, M6 y M7 en un orden que ninguna lección anterior mostró junto.
Analogía: el cierre de caja, después de un año completo de servicio
El restaurante de la introducción de este módulo sobrevivió un año entero de tráfico real, con las cinco disciplinas de operación funcionando a la vez. Antes de cerrar los libros de ese año, alguien hace el cierre de caja completo: confirma que cada bitácora cuadra con cada factura, que el archivo de versiones del menú está al día, que el generador de respaldo sigue calibrado. No es que se dude de que el año salió bien —ya se vio, capa por capa, que salió—. Es la disciplina final de una operación seria: cerrar con evidencia, no con una impresión. Esta lección es ese cierre de caja: cada disciplina que este capstone prometió, confirmada una última vez con código que corre y produce cuatro artefactos verificables.
Checklist de "hecho", ejecutado
Un capstone de operación está "hecho" cuando cada punto de esta lista se puede confirmar con código, no con una lectura del código. Ejecuta este guion completo, de punta a punta, en tu directorio de trabajo:
import json
from dataclasses import asdict
import run_logger as rl
import cost_calculator as cc
import latency_model as lm
import harness as hn
from tool_circuit_breaker import CircuitBreaker, CircuitOpenError, call_with_breaker
from prompt_registry import PROMPT_REGISTRY
from rollout import rollout_decision, rollback
checklist = {}
# 1. OBSERVAR (M2): RUN_LOG.jsonl existe, con trace_id deterministas.
events = [json.loads(line) for line in open("RUN_LOG.jsonl", encoding="utf-8")]
checklist["observar"] = len(events) > 0 and all("trace_id" in e for e in events)
# 2. MEDIR (M3/M4): el pricing fijo y TOOL_LATENCY_MS siguen calibrados.
checklist["medir"] = (
cc.estimate_cost_cents(64, 56) == 0
and lm.TOOL_LATENCY_MS == {"list_rooms": 40, "get_quote": 25, "book_room": 120, "cancel_booking": 90}
)
# 3. GATEAR (M5): el gate corre contra el CASE_SET y produce un veredicto.
gate_report = hn.run_regression_gate(hn.CASE_SET)
checklist["gatear"] = gate_report.passed
# 4. ENDURECER (M6): el circuit breaker abre en el umbral configurado.
probe_breaker = CircuitBreaker("book_room", failure_threshold=3, cooldown_calls=2)
for _ in range(3):
probe_breaker.on_failure()
checklist["endurecer"] = probe_breaker.state == "OPEN"
# 5. VERSIONAR (M7): el registro tiene v1/v2, y el rollback vuelve a v1.
checklist["versionar"] = (
set(PROMPT_REGISTRY.keys()) >= {"v1", "v2"}
and rollback("v2", "v1", PROMPT_REGISTRY) == "v1"
)
for disciplina, ok in checklist.items():
print(f"{disciplina:12} {'✅ OK' if ok else '❌ FALTA'}")
print()
print("capa de operación completa:", "✅ TODAS LAS DISCIPLINAS CONFIRMADAS" if all(checklist.values()) else "❌ REVISAR")
Qué esperar:
observar ✅ OK
medir ✅ OK
gatear ✅ OK
endurecer ✅ OK
versionar ✅ OK
capa de operación completa: ✅ TODAS LAS DISCIPLINAS CONFIRMADAS
Cada línea de este checklist llama a una función que ya corriste, con salida real, en algún punto de este módulo o de M2-M7. Eso es, precisamente, lo que hace confiable a este capstone: no es la primera vez que corre nada de lo que confirma.
Los cuatro artefactos, generados de punta a punta
1. RUN_LOG.jsonl (M2, Lección 3) — ya escrito a disco
print("RUN_LOG.jsonl:", len(events), "eventos,", len({e['trace_id'] for e in events}), "trace_ids distintos")
RUN_LOG.jsonl: 28 eventos, 4 trace_ids distintos
2. El resumen de métricas (M3/M4, Lección 4)
from metrics_summary import build_run_metrics, aggregate_metrics
# Reconstruido a partir de los mismos tres runs completados de la Lección 3.
metrics = [
build_run_metrics("run-8487582448eb", "Reserva Focus pro 3h para Ana", history_ana),
build_run_metrics("run-c720132bf969", "Reserva Boardroom pro 1h para Sofia", history_sofia),
build_run_metrics("run-8d26276b0d45", "Cancela la reserva 999", history_cancel),
]
batch = aggregate_metrics(metrics)
print(f"costo total: {batch.total_cost_cents}c | p50={batch.p50_latency_ms}ms | p95={batch.p95_latency_ms}ms")
costo total: 0c | p50=185ms | p95=185ms
3. regression_report.json (M5, Lección 5) — ya escrito a disco
raw = open("regression_report.json", encoding="utf-8").read()
print("regression_report.json:", len(raw), "bytes, passed =", json.loads(raw)["passed"])
regression_report.json: 1700 bytes, passed = True
4. AGENT_CHANGELOG.md — el cuarto artefacto, escrito en esta lección
Con las tres disciplinas anteriores ya confirmadas, este último artefacto documenta, en texto legible por un humano, la historia completa de versionado que la Lección 5 ejecutó: v2 propuesta, NO-GO, rollback a v1.
changelog = f"""# AGENT_CHANGELOG.md -- agente de Reservo
## v1 -- ACTIVA (hash {PROMPT_REGISTRY['v1'].prompt_hash})
Versión original, segura. Pasa el gate de regresión completo: 5/5 (M5, el
agente completo) y 5/5 (M7, mismo CASE_SET, sin overrides).
## v2 -- RECHAZADA, NO-GO (hash {PROMPT_REGISTRY['v2'].prompt_hash})
Propuesta: más proactiva, completa una reserva directamente cuando ya tiene
toda la información, sin pasar por get_quote primero.
Resultado del gate (Módulo 7, Lección 5 de este capstone, mismo CASE_SET de
M5 con overrides): 4/5 -- FAIL en quote_focus_pro_3h ("Cuanto cuesta Focus
pro 3h?"). v2 reservó en vez de cotizar, creando una reserva real para Ana
sin que lo pidiera de forma explícita.
Decisión: NO-GO (rollout_decision, regla dura -- nunca romper un caso que
v1 ya pasaba). Rollback ejecutado: version activa vuelve a v1.
## Incidente de resiliencia -- book_room (Módulo 6, Lección 6 de este capstone)
book_room cayó de forma sostenida durante un lote de 7 usuarios (9 llamadas
reales fallidas antes de recuperarse). El CircuitBreaker abrió tras 3 fallos
consecutivos, rechazó 2 llamadas sin tocar la tool, y cerró de nuevo tras
una sonda exitosa en HALF_OPEN. 11 llamadas reales contra 21 sin breaker.
"""
with open("AGENT_CHANGELOG.md", "w", encoding="utf-8") as fh:
fh.write(changelog)
print("AGENT_CHANGELOG.md escrito:", len(changelog), "caracteres")
AGENT_CHANGELOG.md escrito: 1017 caracteres
Los cuatro artefactos —RUN_LOG.jsonl, el resumen de métricas, regression_report.json, AGENT_CHANGELOG.md— son la entrega completa de este capstone: texto plano, parseable o legible, que sobrevive al proceso que lo generó. Cualquiera puede abrirlos, sin volver a correr ni una línea de Python, y reconstruir exactamente qué pasó, cuánto costó, qué se gateó, y qué se decidió.
El reto final: un incidente nuevo, con un giro que ninguna lección anterior mostró
El checklist confirma que la capa de operación funciona sobre los escenarios que ya viste. El reto de esta lección te pide operar un incidente nuevo, con tus propias manos, antes de mirar la solución: el equipo tiene lista una versión v3 del prompt —la que corrigió la regresión de v2 en el Ejercicio 3 de la Lección 5— y quiere promoverla exactamente el mismo día en que book_room vuelve a fallar. Esta vez el apagón es más corto: solo 4 llamadas reales caídas, no 9. Antes de promover v3, confirma con el gate que la promoción es segura, y confirma con el circuit breaker si este apagón, más corto, alcanza siquiera a abrir el breaker.
Este reto combina, en un orden que ninguna lección anterior de este módulo mostró junto: la decisión GO/NO-GO de M7 (Lección 5), la máquina de estados del CircuitBreaker de M6 (Lección 6) con un parámetro nuevo (OUTAGE_CALLS=4 en vez de 9), y la actualización de AGENT_CHANGELOG.md con ambos hallazgos.
Antes de mirar la solución: calcula a mano cuántas llamadas reales le tomaría al breaker de failure_threshold=3 llegar a abrirse, sabiendo que cada usuario dispara hasta 3 intentos internos vía retry_with_backoff antes de rendirse.
Ver solución
Paso 1: confirmar que promover v3 es seguro
from prompt_registry import hash_prompt, AgentVersion
# La misma v3 que la Lección 5 (Ejercicio 3) ya registró -- el hash real de
# M7 (Lección 8, mini-proyecto), reusado sin recalcular el texto a mano.
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_v1 = hn.run_regression_gate(hn.CASE_SET, overrides=VERSION_OVERRIDES["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}")
Qué esperar:
v3: hash=c5c4c4631f7e
GATE v3: PASS (5/5)
rollout_decision(v1, v3) -> GO, casos_rotos=[]
Paso 2: el apagón corto — ¿el breaker llega a abrirse?
_state_short = {"count": 0}
OUTAGE_CALLS_SHORT = 4
def flaky_book_room_short(room, tier, hours, member):
_state_short["count"] += 1
if _state_short["count"] <= OUTAGE_CALLS_SHORT:
raise ConnectionError(f"timeout de red simulado (llamada real #{_state_short['count']})")
return {"booking_id": 1, "confirmed": True, "price_cents": 6000}
breaker_short = CircuitBreaker("book_room", failure_threshold=3, cooldown_calls=2)
for run_n in range(1, 5):
before = breaker_short.state
try:
result = call_with_breaker(breaker_short, flaky_book_room_short, room="Focus", tier="pro",
hours=3, member=f"user{run_n}", max_retries=3, base_delay_ms=100)
print(f"run {run_n}: antes={before} OK -> {result} failure_count={breaker_short.failure_count}")
except CircuitOpenError as exc:
print(f"run {run_n}: antes={before} RECHAZADO -> {exc}")
except ConnectionError as exc:
print(f"run {run_n}: antes={before} FALLO -> {exc} failure_count={breaker_short.failure_count}")
print()
print("llamadas reales totales:", _state_short["count"], "| estado final del breaker:", breaker_short.state)
Qué esperar:
run 1: antes=CLOSED FALLO -> timeout de red simulado (llamada real #3) failure_count=1
run 2: antes=CLOSED OK -> {'booking_id': 1, 'confirmed': True, 'price_cents': 6000} failure_count=0
run 3: antes=CLOSED OK -> {'booking_id': 1, 'confirmed': True, 'price_cents': 6000} failure_count=0
run 4: antes=CLOSED OK -> {'booking_id': 1, 'confirmed': True, 'price_cents': 6000} failure_count=0
llamadas reales totales: 7 | estado final del breaker: CLOSED
El giro: el breaker nunca se abre. run 1 agota sus tres intentos internos de retry_with_backoff (max_retries=3) sobre las tres primeras llamadas reales del apagón (#1, #2, #3 — todas ≤ 4, todas fallan) y se rinde con failure_count=1, todavía muy por debajo del failure_threshold=3. run 2 reintenta desde failure_count=1: su primer intento interno es la llamada real #4 (la última caída), y su segundo intento interno es la llamada real #5 — que ya está fuera de la ventana de apagón (> OUTAGE_CALLS_SHORT), así que run 2 se recupera solo, dentro de su propio presupuesto de reintentos, y on_success() resetea failure_count a 0 antes de que el breaker tuviera siquiera la oportunidad de acercarse al umbral.
Esta es la lección que ningún ejemplo anterior de esta guía mostró con tanta claridad: el CircuitBreaker protege contra apagones que duran más que el presupuesto de reintentos de un solo usuario — no contra cualquier fallo transitorio. Un apagón corto, que cabe dentro de los max_retries de retry_with_backoff de uno o dos usuarios, se resuelve solo, sin que la memoria entre runs del breaker llegue a activarse. Esto no es una falla del diseño de M6 — es, con precisión, la razón por la que M6 construyó dos capas, no una: el backoff acotado (Lección 3) ya resuelve los apagones cortos; el breaker (Lecciones 4-5) existe específicamente para los que duran más que eso.
Paso 3: AGENT_CHANGELOG.md, actualizado con los dos hallazgos
changelog_v2 = changelog + f"""
## v3 -- PROMOVIDA (hash {v3_hash})
Corrige la regresión de v2: la instrucción de proactividad queda acotada
exactamente al caso que la justificaba (completar una reserva ya pedida
explícitamente), sin generalizarla a "cualquier pregunta con suficiente
información". Gate: 5/5 PASS. rollout_decision(v1, v3) -> GO.
## Incidente de resiliencia #2 -- book_room, apagón corto
OUTAGE_CALLS=4 (mas corto que el incidente anterior). El breaker NUNCA
abrió: el apagón se resolvió dentro del presupuesto de reintentos de
retry_with_backoff de los primeros dos usuarios (7 llamadas reales, 0
rechazos). Confirma que el backoff acotado (Modulo 6, Leccion 3) ya cubre
apagones cortos -- el breaker existe para los que duran mas que eso.
"""
with open("AGENT_CHANGELOG.md", "w", encoding="utf-8") as fh:
fh.write(changelog_v2)
print("AGENT_CHANGELOG.md actualizado:", len(changelog_v2), "caracteres")
AGENT_CHANGELOG.md actualizado: 1667 caracteres
Este reto es, en miniatura, todo lo que este capstone existe para demostrar: una decisión de versionado con evidencia (M7), un incidente de resiliencia con un resultado que contradice la intuición inicial pero que el código, ejecutado, deja indiscutible (M6), y un artefacto final que documenta ambos para quien lo lea después, sin tener que volver a correr nada.
El recorrido completo, en una tabla
Antes de cerrar, vale la pena ver los ocho módulos de esta guía uno junto al otro, con la pieza exacta que cada uno aportó a la capa de operación que acabas de verificar:
| Módulo | Disciplina | Pieza central | Artefacto |
|---|---|---|---|
| M1 | El puente | run_and_observe, y su límite exacto | — |
| M2 | Observar | traced_run, trace_id determinista | RUN_LOG.jsonl |
| M3 | Medir (costo) | estimate_cost_cents, cost_for_run | CostReport |
| M4 | Medir (latencia) | TOOL_LATENCY_MS, percentile | LatencyReport/BatchLatencyReport |
| M5 | Gatear | run_regression_gate, comparación literal | regression_report.json |
| M6 | Endurecer | CircuitBreaker, retry_with_backoff | — |
| M7 | Versionar | PROMPT_REGISTRY, rollout_decision, rollback | AGENT_CHANGELOG.md |
| M8 | Capstone | Las cuatro disciplinas, envolviendo al mismo agente, a la vez | Los cuatro artefactos, juntos |
Ocho módulos, sesenta y cuatro lecciones, y ningún mecanismo repetido dos veces: cada pieza se construyó una sola vez, se ejecutó con salida real, y este capstone la reusó exactamente como quedó.
Errores comunes
-
Pensar que este checklist "certifica" al agente para producción real. Certifica, con precisión, las cuatro disciplinas que esta guía prometió: observabilidad, medición, un gate de forma, y resiliencia + versionado sobre la capa de tool-calls. No certifica calidad semántica, infraestructura, seguridad contra manipulación, ni costo optimizado — la Lección 7 de este módulo trazó esas fronteras con precisión.
-
Confundir el
GOdel reto final con una garantía de quev3nunca va a fallar en producción real.GOsignifica, con precisión, "no rompió ningún caso delCASE_SETquev1ya pasaba" — una condición necesaria, nunca suficiente, para un despliegue real sin ningún riesgo. -
Generalizar el hallazgo del Paso 2 del reto ("el breaker nunca se abrió") a "el breaker nunca sirve". Al contrario — el ejemplo de la Lección 6 (
OUTAGE_CALLS=9) mostró exactamente el caso donde sí abre y sí ahorra llamadas reales. El reto de esta lección muestra el otro extremo, igual de real: un apagón corto que el backoff acotado ya resuelve solo. Las dos lecciones, juntas, son la imagen completa. -
Escribir
AGENT_CHANGELOG.mduna sola vez y no volver a actualizarlo. El archivo de esta lección se sobrescribe dos veces —una tras el rollback de la Lección 5, otra tras el reto final— porque un changelog que no refleja el estado más reciente es tan poco confiable como no tener ninguno. -
Pensar que "terminé la guía" significa que no hace falta volver a M2-M7 nunca más. Este capstone reusó cada pieza sin reconstruirla — pero un sistema real, con tráfico que cambia, va a necesitar ajustar umbrales (
failure_threshold,cost_threshold_cents), agregar casos alCASE_SET, o revisar el pricing cuando cambie. Las ocho lecciones de M2-M7 siguen siendo la referencia a la que volver, no un capítulo cerrado.
Resumen y siguiente paso
- Confirmamos, con un checklist ejecutado, las cinco piezas de la capa de operación: observar (M2), medir (M3/M4), gatear (M5), endurecer (M6) y versionar (M7) — cada una con código que corre, no con una lectura de memoria.
- Generamos los cuatro artefactos finales del capstone:
RUN_LOG.jsonl(28 eventos), el resumen de métricas (costo total0c, p50/p95 de latencia),regression_report.json(PASS), yAGENT_CHANGELOG.md, documentando la historia completa de versionado y resiliencia de este agente. - Resolvimos un reto que combina M6 y M7 de una forma nueva:
v3promovida conGO, y un apagón corto debook_roomque —a diferencia del Módulo 6— nunca llega a abrir elCircuitBreaker, porque se resuelve dentro del presupuesto de reintentos deretry_with_backoff— la prueba de que las dos capas de resiliencia de esta guía cubren, cada una, un rango distinto de duración de fallo. - Cerramos el mapa de las ocho lecciones de este módulo, y con él, la guía completa:
agents-in-production-guide, ocho módulos, sesenta y cuatro lecciones, un solo agente operado de punta a punta.
Siguiente paso, fuera de esta guía: cuando el agente de Reservo operado necesite infraestructura y respuesta a incidentes a escala → sre-and-incident-response-guide. Cuando necesite juicio semántico sobre la calidad de sus respuestas → evaluation-frameworks-guide. Cuando el costo medido en M3 necesite bajar de verdad → cost-optimization-caching-guide. Cuando necesite resiliencia genérica a fondo, más allá de la capa de tool-calls → resilience-and-reliability-patterns-guide. Cuando vaya a atender usuarios reales → agent-security-and-sandboxing-guide, sin excepción.
Recursos adicionales
- Anthropic — Tool use (function calling) overview — El protocolo completo que esta guía instrumentó, midió, gateó y endureció, de punta a punta.
- Anthropic — Building effective agents — La disciplina de operar, con criterio, un sistema que ya funciona — la premisa completa de esta guía.
- Python 3.14 — What's New — La versión exacta con la que se ejecutó toda la ingeniería de operación de esta guía, sin ninguna dependencia externa.
- Python —
dataclasses,logging,hashlib,statistics,enum— La biblioteca estándar completa sobre la que se construyó cada artefacto de esta guía:RunEvent/CostReport/LatencyReport(dataclasses),traced_run(logging),trace_id/prompt_hash(hashlib),percentile(statistics),CircuitBreaker(estados como strings, sin necesitarenum.Enumde forma obligatoria).