Módulo 7: Versionado y rollout seguro
Haciendo rollback
Descripción
La lección anterior dejó una decisión formal: rollout_decision(v1, v2) devuelve NO-GO. Esta lección resuelve la pregunta que sigue, inevitable: si v2 no puede salir a producción, ¿qué corre en su lugar? La respuesta es rollback — la función más corta de todo este módulo, y también la más importante de tener bien definida, porque es la que se ejecuta bajo presión, en el peor momento posible: cuando algo ya salió mal y hace falta volver atrás con confianza, no con improvisación.
Esta lección construye un escenario deliberadamente realista: v2 ya está activa —alguien la promovió sin correr el gate primero, exactamente la trampa que la lección 02 advirtió— y el gate, corrido después como una salvaguarda tardía, confirma que nunca debió estar ahí. rollback devuelve el sistema a v1, la última versión que sí tenía su pase vigente.
Conexión con el módulo
Esta lección cierra el ciclo GO/NO-GO/rollback que las lecciones 04-06 construyeron: comparar (L04), entender el chequeo (L05), decidir (L06), y ahora, actuar sobre esa decisión cuando es NO-GO. Con rollback en su lugar, el módulo tiene las tres piezas necesarias para el mini-proyecto de la lección 08: el registro, la decisión, y la acción que la decisión dispara.
Analogía: el piloto que no pasó el examen, pero ya estaba en la cabina
Retomando el pase médico de la introducción del módulo: lo normal es que el examen se haga antes de que el piloto suba al avión. Pero imagina que, por un error de coordinación, el piloto ya está en la cabina, con los motores encendidos, cuando llega el resultado del examen: no pasó. En ese momento, "rollback" no es una decisión abstracta de planificación — es una acción concreta e inmediata: ese piloto baja del avión, y el piloto que sí tiene su pase vigente —el que ya volaba ayer, sin ningún cambio— toma su lugar. No hay improvisación sobre quién vuela: hay un protocolo, y el protocolo dice, con precisión, a quién llamar.
rollback en este módulo es ese protocolo. No decide si hace falta un rollback —eso ya lo decidió rollout_decision en la lección anterior—; solo ejecuta, sin ambigüedad, el "vuelve a la versión que sabemos que funciona".
rollback: un cambio de puntero, nada más
def rollback(active_version_id, previous_version_id, registry):
"""Vuelve la version activa a la anterior. Es un cambio de puntero
determinista -- no hay mecanismo de despliegue real en esta guia."""
if previous_version_id not in registry:
raise KeyError(f"version desconocida en el registro: {previous_version_id!r}")
return previous_version_id
No hay nada que "deshacer" en el sentido de un git revert o de reconstruir un estado anterior — rollback recibe el version_id al que hay que volver, confirma que esa versión existe de verdad en PROMPT_REGISTRY (nunca confía en un string que alguien tipeó de memoria), y lo devuelve. La simplicidad es intencional: como AgentVersion es inmutable (frozen=True, lección 03) y el registro nunca borra una versión anterior, "volver a v1" nunca implica reconstruir nada — v1 nunca dejó de existir, completa, en PROMPT_REGISTRY["v1"].
El escenario completo: v2 ya activa, el gate como red de seguridad tardía
from ops.versions.prompt_registry import PROMPT_REGISTRY
from regression.harness import load_case_set, run_regression_gate
CASE_SET = load_case_set("regression/golden_cases.json")
# VERSION_OVERRIDES: retomado tal cual de la lección 04 -- v1 no sustituye
# nada; v2 sustituye únicamente quote_focus_pro_3h por el guion regresivo.
report_v1 = run_regression_gate(CASE_SET, overrides=VERSION_OVERRIDES["v1"])
report_v2 = run_regression_gate(CASE_SET, overrides=VERSION_OVERRIDES["v2"])
# Alguien promovio v2 directamente, sin correr el gate primero -- la
# trampa exacta que la leccion 02 advirtio ("lo probe a mano y se veia bien").
ACTIVE_VERSION = "v2"
print("version activa (antes de correr el gate como salvaguarda tardia):", ACTIVE_VERSION)
decision, broken = rollout_decision(report_v1, report_v2)
print(f"rollout_decision(v1, v2) -> {decision}, casos_rotos={broken}")
if decision == "NO-GO":
ACTIVE_VERSION = rollback(ACTIVE_VERSION, "v1", PROMPT_REGISTRY)
print("version activa (despues del rollback):", ACTIVE_VERSION)
Qué esperar:
version activa (antes de correr el gate como salvaguarda tardia): v2
rollout_decision(v1, v2) -> NO-GO, casos_rotos=['quote_focus_pro_3h']
version activa (despues del rollback): v1
Tres líneas de salida cuentan la historia completa: v2 estaba activa, sin haber pasado por el gate; el gate, corrido después, confirma con evidencia (quote_focus_pro_3h) que nunca debió promoverse; rollback la reemplaza por v1, sin ambigüedad, sin depender de que alguien recuerde a mano cuál era "la versión de antes" — esa información vive en el registro, no en la memoria de una persona.
Un rollback que falla con seguridad
rollback valida contra el registro antes de devolver nada. Si alguien pide volver a una versión que nunca se registró —un typo, una versión que se descartó sin llegar a documentarse—, la función se detiene con un error explícito, en vez de devolver silenciosamente un version_id que después no encuentra su AgentVersion en ningún lado:
try:
rollback("v2", "v99", PROMPT_REGISTRY)
except KeyError as exc:
print("KeyError capturado:", exc)
Qué esperar:
KeyError capturado: "version desconocida en el registro: 'v99'"
Esta validación no es un detalle menor: un rollback que "silenciosamente" no encuentra la versión de destino, y deja el sistema en un estado indefinido, es exactamente el tipo de fallo que hace que un rollback bajo presión sea peor que no tener ningún mecanismo — falla rápido, con un mensaje que dice con precisión qué salió mal, en vez de fallar en silencio.
Por qué este rollback es simple — y qué no lo sería
Vale la pena ser honesto sobre el alcance de lo que rollback hace aquí. En esta guía, el "sistema" es un version_id que apunta a una entrada de PROMPT_REGISTRY — cambiar ese puntero es, literalmente, todo lo que hace falta para que la próxima llamada al agente use el prompt correcto. En un sistema real de producción, un rollback de infraestructura completa involucra piezas que esta guía nunca toca: contenedores corriendo la versión nueva que hay que drenar de tráfico sin cortar conexiones activas, un balanceador de carga o un DNS con su propio tiempo de propagación (TTL), cachés en capas intermedias que podrían seguir sirviendo la versión vieja, y coordinación entre varias instancias del servicio para que el cambio de versión ocurra de forma consistente en todas al mismo tiempo. Nada de eso es parte del alcance $0 de esta guía —es, con precisión, infraestructura de despliegue, fuera de lo que un registro de prompts en Python puro puede o debe resolver—. Lo que esta lección enseña es la disciplina: nunca improvisar bajo presión cuál era la versión anterior, tenerla siempre identificable con un hash, y tener una función clara —no un procedimiento manual recordado de memoria— que ejecuta la vuelta.
Errores comunes
-
Confundir "rollback" con "reconstruir la versión anterior desde cero". No hace falta reconstruir nada:
v1nunca se borró dePROMPT_REGISTRY. Rollback es apuntar de nuevo a algo que sigue existiendo completo, no recrear algo que se perdió. -
No validar que la versión de destino existe antes de "hacer" el rollback. Sin la validación de
rollback, un typo ("v01"en vez de"v1") fallaría en silencio en algún punto posterior del sistema, en un momento mucho más difícil de diagnosticar que elKeyErrorinmediato y explícito que esta lección muestra. -
Pensar que un rollback siempre implica volver a la versión inmediatamente anterior en el tiempo.
rollbackrecibe cualquierversion_idque exista en el registro como destino — no está limitado a "la última". Siv3resultara problemática después de haber reemplazado av2(que a su vez había reemplazado av1), nada impide unrollbackdirecto av1, saltando por completo av2. -
Ejecutar un rollback sin dejar registro de por qué se hizo. Como advirtió la lección 06, una decisión sin registro escrito se pierde. El mini-proyecto de la lección 08 muestra cómo cada rollback se documenta en
AGENT_CHANGELOG.md, con el motivo exacto (el nombre del caso delCASE_SETque falló) junto a la acción tomada. -
Suponer que este rollback resuelve un rollback de infraestructura real. Como explicó la sección anterior, esta guía trata deliberadamente el rollback como un cambio de puntero — un despliegue de infraestructura completo (contenedores, balanceo, DNS) es una capa distinta, fuera del alcance $0 de esta guía.
Ejercicios
Ejercicio 1: Ejecuta un rollback simple y confirma su resultado (Fácil)
Con ACTIVE_VERSION = "v2", ejecuta rollback("v2", "v1", PROMPT_REGISTRY) y confirma que el resultado es exactamente el version_id "v1" — no el objeto AgentVersion completo, solo el identificador.
Ver solución
resultado = rollback("v2", "v1", PROMPT_REGISTRY)
print("resultado:", resultado)
print("tipo:", type(resultado).__name__)
Salida esperada:
resultado: v1
tipo: str
Explicación: rollback devuelve el version_id (un str), no el AgentVersion completo — quien llama a la función decide qué hacer con ese identificador (actualizar ACTIVE_VERSION, buscarlo en el registro para obtener el prompt completo, escribirlo en AGENT_CONFIG.md). Mantener la función acotada a "¿a qué versión volver?" en vez de mezclarla con "¿y ahora qué hago con ese resultado?" es la misma separación de responsabilidades que ya viste en agent-fundamentals entre el modelo (decide) y el loop (actúa sobre la decisión).
Ejercicio 2: Rollback que salta una versión intermedia (Medio)
Imagina esta secuencia cronológica: v1 (activa) → se promueve v3 (pasó el gate, GO) → v3 queda activa. Semanas después, alguien detecta —por un reporte manual de un usuario, no por el gate— un problema distinto en v3 que no está cubierto por ningún caso de CASE_SET, y el equipo decide volver directamente a v1, sin pasar por v2 (que, de hecho, nunca llegó a estar activa). Ejecuta ese rollback y confirma que funciona sin ningún problema, aunque v2 esté "en el medio" cronológicamente.
Ver solución
ACTIVE_VERSION_ejemplo = "v3" # activa despues de un GO anterior
version_destino = rollback(ACTIVE_VERSION_ejemplo, "v1", PROMPT_REGISTRY)
print("version activa antes:", ACTIVE_VERSION_ejemplo)
print("version activa despues del rollback:", version_destino)
Salida esperada:
version activa antes: v3
version activa despues del rollback: v1
Explicación: rollback no tiene ningún concepto de "orden cronológico" ni de "la versión inmediatamente anterior" — solo verifica que el destino exista en el registro. Esto es, deliberadamente, distinto de una pila de "deshacer" (como Ctrl+Z), que solo puede retroceder un paso a la vez: el registro le permite al equipo saltar directamente a cualquier versión conocida y confiable, sin tener que pasar, paso por paso, por cada versión intermedia que existió entre medio.
Ejercicio 3: Argumenta qué pieza de infraestructura real reemplazaría a ACTIVE_VERSION (Difícil)
Sin escribir ningún despliegue real: en un sistema de producción de verdad, ACTIVE_VERSION —la variable de Python de esta lección— tendría que vivir en algún lugar que todas las instancias del servicio consulten en tiempo real, no en una variable local de un solo proceso. Nombra dos tecnologías reales que cumplirían ese rol, y explica en una frase por qué una variable de Python en memoria no sirve para ese propósito en un sistema con más de una instancia corriendo a la vez.
Ver solución
Dos opciones razonables: un almacén de configuración centralizado (como una tabla en una base de datos, o un servicio de configuración dedicado tipo etcd/Consul) que todas las instancias del servicio consultan al arrancar y periódicamente mientras corren; o una variable de entorno inyectada al desplegar, leída una vez al iniciar cada instancia nueva, con el propio mecanismo de despliegue encargado de reiniciar las instancias cuando el valor cambia. Una variable de Python en memoria (ACTIVE_VERSION = "v1", como en esta lección) no sirve para ese propósito en un sistema real porque cada instancia del servicio —cada proceso, cada contenedor, cada réplica detrás de un balanceador de carga— tiene su propia copia de esa variable, en su propia memoria; cambiarla en un proceso nunca la cambia en los demás, así que un rollback real necesita, por definición, un lugar de verdad compartido entre todas las instancias, no una variable local a una de ellas. Esto es, con precisión, la clase de problema de infraestructura que queda fuera del alcance $0 de esta guía —y la razón exacta por la que esta lección fue explícita en llamar a rollback "un cambio de puntero" y no "un despliegue".
Resumen y siguiente paso
rollback(active_version_id, previous_version_id, registry)valida que la versión de destino exista en el registro y devuelve suversion_id— un cambio de puntero determinista, sin reconstruir nada, porque el registro nunca borra una versión anterior.- Ejecutado sobre el escenario realista de esta lección —
v2ya activa, sin haber pasado por el gate—: el gate corrido como salvaguarda tardía confirmaNO-GO, yrollbackdevuelve la versión activa av1. - Un intento de rollback a una versión que no existe en el registro (
"v99") falla rápido, con unKeyErrorexplícito, en vez de dejar el sistema en un estado indefinido. - Esta guía trata el rollback como un cambio de puntero deliberadamente simple — un rollback de infraestructura real (contenedores, balanceo, DNS) es una capa distinta, nombrada aquí con precisión como fuera de alcance.
Siguiente lección: 08 — Mini-proyecto: un rollout versionado para Reservo. Juntamos las siete piezas del módulo —registro, gate en dos versiones, decisión, rollback— en ops/versioned_rollout.py, y generamos, de verdad, los dos artefactos finales: AGENT_CONFIG.md y AGENT_CHANGELOG.md.
Recursos adicionales
- Anthropic — Building effective agents — Sobre la importancia de mecanismos simples y predecibles para operar un agente bajo presión.
- Python — excepciones incorporadas (
KeyError) — La excepción querollbacklevanta ante una versión desconocida, y por qué fallar explícito es preferible a fallar en silencio. - Python —
dicty la operaciónin— La base deprevious_version_id not in registry, la validación central derollback. - Python 3.14 — What's New — La versión con la que se ejecutó cada línea de código de esta lección.