Module 7: Versioning and Safe Rollout

Rolling Back

Description

The previous lesson left a formal decision: rollout_decision(v1, v2) returns NO-GO. This lesson solves the inevitable question that follows: if v2 can't go to production, what runs in its place? The answer is rollback — the shortest function in this entire module, and also the most important one to have well defined, because it's the one that runs under pressure, at the worst possible moment: when something has already gone wrong and you need to go back with confidence, not improvisation.

This lesson builds a deliberately realistic scenario: v2 is already active — someone promoted it without running the gate first, exactly the trap Lesson 02 warned about — and the gate, run afterward as a late safeguard, confirms it never should have been there. rollback returns the system to v1, the last version whose clearance was current.

Connection to the module

This lesson closes the GO/NO-GO/rollback cycle Lessons 04-06 built: compare (L04), understand the check (L05), decide (L06), and now, act on that decision when it's NO-GO. With rollback in place, the module has the three pieces Lesson 08's mini-project needs: the registry, the decision, and the action the decision triggers.


Analogy: the pilot who didn't pass the exam, but was already in the cockpit

Going back to the module introduction's medical clearance: normally the exam happens before the pilot boards the plane. But imagine that, because of a coordination mistake, the pilot is already in the cockpit, engines running, when the exam result comes in: they didn't pass. At that moment, "rollback" isn't an abstract planning decision — it's a concrete, immediate action: that pilot gets off the plane, and the pilot whose clearance is current — the one who already flew yesterday, with no change — takes their place. There's no improvising over who flies: there's a protocol, and the protocol says, precisely, who to call.

rollback in this module is that protocol. It doesn't decide whether a rollback is needed — rollout_decision already decided that in the previous lesson — it just executes, unambiguously, the "go back to the version we know works."


rollback: a pointer change, nothing more

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

There's nothing to "undo" in the sense of a git revert or rebuilding a previous state — rollback receives the version_id to go back to, confirms that version genuinely exists in PROMPT_REGISTRY (it never trusts a string someone typed from memory), and returns it. The simplicity is intentional: since AgentVersion is immutable (frozen=True, Lesson 03) and the registry never deletes a previous version, "going back to v1" never means rebuilding anything — v1 never stopped existing, complete, in PROMPT_REGISTRY["v1"].


The complete scenario: v2 already active, the gate as a late safety net

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)

What to expect:

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

Three lines of output tell the complete story: v2 was active, without having gone through the gate; the gate, run afterward, confirms with evidence (quote_focus_pro_3h) it never should have been promoted; rollback replaces it with v1, unambiguously, without depending on someone remembering by hand which was "the previous version" — that information lives in the registry, not in a person's memory.


A rollback that fails safely

rollback validates against the registry before returning anything. If someone asks to go back to a version that was never registered — a typo, a version that got discarded without ever being documented — the function stops with an explicit error, instead of silently returning a version_id that later can't find its AgentVersion anywhere:

try:
    rollback("v2", "v99", PROMPT_REGISTRY)
except KeyError as exc:
    print("KeyError capturado:", exc)

What to expect:

KeyError capturado: "version desconocida en el registro: 'v99'"

This validation isn't a minor detail: a rollback that "silently" can't find the target version, and leaves the system in an undefined state, is exactly the kind of failure that makes a rollback under pressure worse than having no mechanism at all — it fails fast, with a message that says precisely what went wrong, instead of failing silently.


Why this rollback is simple — and what wouldn't be

It's worth being honest about the scope of what rollback does here. In this guide, the "system" is a version_id pointing to a PROMPT_REGISTRY entry — changing that pointer is, literally, everything needed for the next call to the agent to use the correct prompt. In a real production system, a complete infrastructure rollback involves pieces this guide never touches: containers running the new version that need to be drained of traffic without cutting active connections, a load balancer or a DNS with its own propagation time (TTL), caches in intermediate layers that could keep serving the old version, and coordination across several service instances so the version change happens consistently on all of them at the same time. None of that is part of this guide's $0 scope — it is, precisely, deployment infrastructure, outside what a pure-Python prompt registry can or should solve. What this lesson teaches is the discipline: never improvise under pressure which was the previous version, always keep it identifiable with a hash, and have a clear function — not a manual procedure recalled from memory — that executes the return.


Common mistakes

  1. Confusing "rollback" with "rebuilding the previous version from scratch." Nothing needs rebuilding: v1 was never deleted from PROMPT_REGISTRY. Rollback is pointing back to something that still exists complete, not recreating something that got lost.

  2. Not validating the target version exists before "doing" the rollback. Without rollback's validation, a typo ("v01" instead of "v1") would fail silently at some later point in the system, at a much harder moment to diagnose than the immediate, explicit KeyError this lesson shows.

  3. Thinking a rollback always means going back to the immediately previous version in time. rollback accepts any version_id that exists in the registry as a target — it isn't limited to "the last one." If v3 turned out problematic after having replaced v2 (which had in turn replaced v1), nothing stops a direct rollback to v1, skipping v2 entirely.

  4. Running a rollback with no record of why it happened. As Lesson 06 warned, a decision with no written record gets lost. Lesson 08's mini-project shows how every rollback gets documented in AGENT_CHANGELOG.md, with the exact reason (the name of the CASE_SET case that failed) alongside the action taken.

  5. Assuming this rollback solves a real infrastructure rollback. As the previous section explained, this guide deliberately treats rollback as a pointer change — a complete infrastructure deployment (containers, load balancing, DNS) is a different layer, named here precisely as out of scope.


Exercises

Exercise 1: Run a simple rollback and confirm its result (Easy)

With ACTIVE_VERSION = "v2", run rollback("v2", "v1", PROMPT_REGISTRY) and confirm the result is exactly the version_id "v1" — not the complete AgentVersion object, just the identifier.

See solution
resultado = rollback("v2", "v1", PROMPT_REGISTRY)
print("resultado:", resultado)
print("tipo:", type(resultado).__name__)

Expected output:

resultado: v1
tipo: str

Explanation: rollback returns the version_id (a str), not the complete AgentVersion — whoever calls the function decides what to do with that identifier (update ACTIVE_VERSION, look it up in the registry to get the complete prompt, write it to AGENT_CONFIG.md). Keeping the function bounded to "which version to go back to?" instead of mixing it with "and now what do I do with that result?" is the same separation of responsibilities you already saw in agent-fundamentals between the model (decides) and the loop (acts on the decision).

Exercise 2: Rollback that skips an intermediate version (Medium)

Imagine this timeline: v1 (active) → v3 gets promoted (passed the gate, GO) → v3 stays active. Weeks later, someone catches — through a manual user report, not the gate — a different problem in v3 not covered by any CASE_SET case, and the team decides to go straight back to v1, skipping v2 (which, in fact, never got to be active). Run that rollback and confirm it works with no problem, even though v2 is chronologically "in the middle."

See solution
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)

Expected output:

version activa antes: v3
version activa despues del rollback: v1

Explanation: rollback has no concept of "chronological order" or "the immediately previous version" at all — it only verifies the target exists in the registry. This is, deliberately, different from an "undo" stack (like Ctrl+Z), which can only step back one step at a time: the registry lets the team jump directly to any known, trusted version, without having to go, step by step, through every intermediate version that existed in between.

Exercise 3: Argue which real infrastructure piece would replace ACTIVE_VERSION (Hard)

Without writing any real deployment: in a genuine production system, ACTIVE_VERSION — this lesson's Python variable — would have to live somewhere every service instance consults in real time, not in a single process's local variable. Name two real technologies that would fill that role, and explain in one sentence why an in-memory Python variable doesn't serve that purpose in a system with more than one instance running at once.

See solution

Two reasonable options: a centralized configuration store (like a database table, or a dedicated configuration service such as etcd/Consul) that every service instance consults on startup and periodically while running; or an environment variable injected at deploy time, read once when each new instance starts, with the deployment mechanism itself responsible for restarting instances when the value changes. An in-memory Python variable (ACTIVE_VERSION = "v1", like in this lesson) doesn't serve that purpose in a real system because every service instance — every process, every container, every replica behind a load balancer — has its own copy of that variable, in its own memory; changing it in one process never changes it in the others, so a real rollback needs, by definition, a source of truth shared across all instances, not a variable local to one of them. This is, precisely, the kind of infrastructure problem that falls outside this guide's $0 scope — and the exact reason this lesson was explicit about calling rollback "a pointer change" and not "a deployment."


Summary and next step

  • rollback(active_version_id, previous_version_id, registry) validates the target version exists in the registry and returns its version_id — a deterministic pointer change, rebuilding nothing, because the registry never deletes a previous version.
  • Run over this lesson's realistic scenario — v2 already active, without having gone through the gate — the gate, run as a late safeguard, confirms NO-GO, and rollback returns the active version to v1.
  • An attempted rollback to a version that doesn't exist in the registry ("v99") fails fast, with an explicit KeyError, instead of leaving the system in an undefined state.
  • This guide treats rollback as a deliberately simple pointer change — a real infrastructure rollback (containers, load balancing, DNS) is a different layer, named here precisely as out of scope.

Next lesson: 08 — Mini-Project: A Versioned Rollout for Reservo. We bring the module's seven pieces together — registry, gate on two versions, decision, rollback — into ops/versioned_rollout.py, and genuinely generate the two final artifacts: AGENT_CONFIG.md and AGENT_CHANGELOG.md.


Additional resources

  1. Anthropic — Building effective agents — On the importance of simple, predictable mechanisms for operating an agent under pressure.
  2. Python — built-in exceptions (KeyError) — The exception rollback raises for an unknown version, and why failing explicitly is preferable to failing silently.
  3. Python — dict and the in operator — The foundation of previous_version_id not in registry, rollback's central validation.
  4. Python 3.14 — What's New — The version every line of code in this lesson ran on.