Module 7: Versioning and Safe Rollout
The Prompt Version Registry
Description
The previous lesson showed the problem: two nearly identical system-prompt texts produce different decisions for the same question. This lesson builds the solution's first piece: a registry that stores every prompt version so there's never any ambiguity about which is which. Having two Python variables named SYSTEM_PROMPT_V1 and SYSTEM_PROMPT_V2 in a file isn't enough — you already had that in Lesson 02, and it solves nothing on its own. What's needed is a structure that identifies every version with a unique, deterministic identifier, the same discipline a code version-control system applies to every commit.
By the end of this lesson you're going to have PROMPT_REGISTRY: a dictionary mapping a version_id ("v1", "v2") to an AgentVersion object with the prompt's text, a hash that identifies it unambiguously, the tools version it uses, and a note on what changed. It's ops/versions/prompt_registry.py's first real artifact.
Connection to the module
This lesson builds the piece Lesson 04 is going to compare: the regression gate needs to know, precisely, which prompt text corresponds to "v1" and which to "v2" before it can run anything against them. Without a clear registry, "comparing two versions" would mean comparing two loose variables — it works in a single lesson's example, but it doesn't scale to a real system where there can be five, ten, twenty versions accumulated over months.
Analogy: a filing shelf, not a pile of loose papers
Storing two versions of a prompt in two Python variables (SYSTEM_PROMPT_V1, SYSTEM_PROMPT_V2) is like storing two important contracts in a pile of loose papers on a desk: it works as long as there are only two, and as long as whoever stored them remembers which is which. A real archive — a law office's, a bank's — doesn't work that way: every document has a folder with a file number, a date, and a note of what it contains. Anyone can ask for "file number such-and-such" and find, unambiguously, the exact document — not a similar version, not the one someone remembers was the right one.
PROMPT_REGISTRY is that filing shelf. Every version has its "file number" — the hash — and any part of the system that needs "version v1's prompt" looks it up there, never from memory.
AgentVersion: a registry entry's shape
# ops/versions/prompt_registry.py
import hashlib
from dataclasses import dataclass
@dataclass(frozen=True)
class AgentVersion:
"""Una version congelada del agente: su prompt, su hash, y con que
conjunto de tools corre. frozen=True: una version, una vez creada, no
se modifica -- se crea una version NUEVA."""
version_id: str
prompt_text: str
prompt_hash: str
tools_version: str
model: str
note: str
frozen=True isn't a cosmetic detail: a version that can be modified after it's created stops being a version — it would be, again, loose text that changes with no trail, the exact problem this lesson exists to solve. If the prompt changes, a new entry gets created in the registry ("v3"), "v2" never gets edited in place.
hash_prompt: the deterministic identifier
AgentVersion's most important field, for this module's purposes, is prompt_hash. The question it solves is simple: given a prompt's text, how do you give it a short, unique identifier such that anyone running the same text gets exactly the same identifier?
def hash_prompt(text):
"""Hash determinista y corto del texto del prompt. NUNCA uuid4() ni
random -- el mismo texto siempre produce el mismo hash, en cualquier
maquina, sin excepcion."""
return hashlib.sha256(text.encode("utf-8")).hexdigest()[:12]
hashlib.sha256 takes the prompt's complete text (encoded to bytes with .encode("utf-8")) and produces a 256-bit hash. .hexdigest() converts it to a readable hexadecimal string; [:12] keeps the first twelve characters — enough to tell apart any reasonable number of versions in this guide, without needing the complete 64-character hash. This is the same idea as a short git hash (c5757b6d6264 instead of the complete SHA hash): short to read, and practically impossible for two different texts to accidentally produce the same hash.
Confirm the central property — determinism — by running hash_prompt over the same text twice:
SYSTEM_PROMPT_V1 = (
"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 "
"solo pregunta cuanto cuesta algo, usa get_quote y NO reserves. Usa "
"book_room unicamente cuando el usuario pide reservar de forma "
"explicita."
)
hash_intento_1 = hash_prompt(SYSTEM_PROMPT_V1)
hash_intento_2 = hash_prompt(SYSTEM_PROMPT_V1)
print("hash, primer calculo :", hash_intento_1)
print("hash, segundo calculo:", hash_intento_2)
print("son identicos:", hash_intento_1 == hash_intento_2)
What to expect:
hash, primer calculo : c5757b6d6264
hash, segundo calculo: c5757b6d6264
son identicos: True
The same text, calculated twice, produces the same hash — and it's going to produce that same hash on your machine, on mine, or on any machine running Python 3 with the same input text. That reproducibility is exactly what makes prompt_hash useful as an identifier: it doesn't depend on a counter someone has to remember to increment, or a clock, or anything external to the text itself.
PROMPT_REGISTRY: v1 and v2, complete
With AgentVersion and hash_prompt in place, the complete registry:
SYSTEM_PROMPT_V2 = (
"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. Se proactivo: si ya "
"tienes toda la informacion para completar una reserva, complétala "
"directamente en vez de solo cotizar, para ahorrarle un paso al "
"usuario. Usa book_room unicamente cuando el usuario pide reservar de "
"forma explicita."
)
PROMPT_REGISTRY = {
"v1": AgentVersion(
version_id="v1",
prompt_text=SYSTEM_PROMPT_V1,
prompt_hash=hash_prompt(SYSTEM_PROMPT_V1),
tools_version="tools-v1",
model="claude-sonnet-5",
note="System prompt original del capstone de agent-fundamentals M8.",
),
"v2": AgentVersion(
version_id="v2",
prompt_text=SYSTEM_PROMPT_V2,
prompt_hash=hash_prompt(SYSTEM_PROMPT_V2),
tools_version="tools-v1",
model="claude-sonnet-5",
note="Agrega una instruccion de proactividad para reducir turnos.",
),
}
for version_id, av in PROMPT_REGISTRY.items():
print(f"{version_id} hash={av.prompt_hash} tools={av.tools_version} model={av.model}")
print(f" nota: {av.note}")
What to expect:
v1 hash=c5757b6d6264 tools=tools-v1 model=claude-sonnet-5
nota: System prompt original del capstone de agent-fundamentals M8.
v2 hash=c364e85e5649 tools=tools-v1 model=claude-sonnet-5
nota: Agrega una instruccion de proactividad para reducir turnos.
Notice three things. First, tools_version is the same ("tools-v1") in both entries — this module changes only the prompt, not the tools, so registering the same tools version in both entries explicitly confirms that factor didn't vary (if it ever did change, that would be, precisely, the signal that the registry needs to capture it). Second, model is "claude-sonnet-5" in both — the same model version, another controlled variable. Third, prompt_hash is different between the two entries (c5757b6d6264 versus c364e85e5649) — the only real difference between v1 and v2, captured in a single field that can be cited, compared, and recorded with no ambiguity.
Why the hash, and not just the version_id
It's worth asking why "v1"/"v2" alone isn't enough as an identifier, given the registry already tells them apart by that key. The answer is version_id is a name a person chose — nothing stops someone, in a real system, from rewriting "v2"'s text without changing its name, and at that point the name stops being trustworthy. prompt_hash, on the other hand, depends solely on content: if v2's text changed even by a single whitespace, its hash would change too, and that discrepancy would be immediately detectable by comparing the stored hash against the current text's recalculated hash.
texto_modificado = SYSTEM_PROMPT_V2 + " " # un espacio de mas, al final
hash_original = PROMPT_REGISTRY["v2"].prompt_hash
hash_modificado = hash_prompt(texto_modificado)
print("hash registrado para v2:", hash_original)
print("hash del texto modificado:", hash_modificado)
print("coinciden:", hash_original == hash_modificado)
What to expect:
hash registrado para v2: c364e85e5649
hash del texto modificado: 470367511d92
coinciden: False
A single character of difference — not even visible reading the text — produces a completely different hash. This property (formally called a cryptographic hash function's avalanche effect) is exactly what makes prompt_hash work as a trustworthy signature: there's no way for a change, however small, to go unnoticed in the hash.
Common mistakes
-
Using
uuid4()to generate a version's identifier. It would break the whole module's reproducibility: two people running the same code would get different identifiers, and the same prompt would run with a different "hash" every time the process restarts.hashlib.sha256over the text is the right choice precisely because it depends solely on content, never on when it runs. -
Modifying a
PROMPT_REGISTRYentry in place instead of creating a new one. SinceAgentVersionisfrozen=True, tryingPROMPT_REGISTRY["v1"].prompt_text = "otro texto"raises aFrozenInstanceError— on purpose. Ifv1's prompt needs to change, the right move is adding a new"v3"entry to the dictionary, never editing"v1". -
Truncating the hash to too few characters.
[:12]is enough for this guide's few versions, but a real system with hundreds of versions accumulated over years should use a longer prefix (or the complete hash) to further reduce — already tiny with 12 hex characters — the odds of two different prompts producing the same truncated hash. -
Forgetting to register
tools_versionalongside the prompt. As Lesson 02 warned, a behavior change can come from the available tools, not just the prompt's text. A registry that only stores the prompt, without the tools version that accompanied it, loses information needed to later reconstruct exactly which combination was running. -
Thinking the registry replaces code version control (
git). It doesn't replace it —PROMPT_REGISTRYis an artifact that lives inside the codegitversions, the same wayTOOL_LATENCY_MS(Module 4) or the fixed pricing (Module 3) live inside the code. This lesson's discipline is specific to prompts because they're free text, not becausegitcan't version them too.
Exercises
Exercise 1: Calculate a new prompt's hash (Easy)
Calculate hash_prompt over the text "Eres el asistente de Reservo." (a single short sentence). Confirm that running it twice gets you the same result.
See solution
texto_corto = "Eres el asistente de Reservo."
h1 = hash_prompt(texto_corto)
h2 = hash_prompt(texto_corto)
print("hash:", h1)
print("son iguales:", h1 == h2)
Expected output:
hash: 2d334ae07350
son iguales: True
Explanation: the hash depends solely on the input text, regardless of how short it is — the original text's length doesn't affect the resulting hash's length (sha256 always produces 256 bits, truncated here to 12 hex characters), and the result is just as reproducible with a short sentence as with v1's complete system prompt.
Exercise 2: Add a third version to the registry (Medium)
Create SYSTEM_PROMPT_V3 as a copy of SYSTEM_PROMPT_V1 (the safe version) with a note added at the end: " Si el usuario pregunta por una sala que no existe, sugiere la mas parecida.". Add a "v3" entry to PROMPT_REGISTRY with its own hash, tools_version="tools-v1", and a note explaining the change. Print the complete registry with all three versions.
See solution
SYSTEM_PROMPT_V3 = SYSTEM_PROMPT_V1 + (
" Si el usuario pregunta por una sala que no existe, sugiere la mas "
"parecida."
)
PROMPT_REGISTRY["v3"] = AgentVersion(
version_id="v3",
prompt_text=SYSTEM_PROMPT_V3,
prompt_hash=hash_prompt(SYSTEM_PROMPT_V3),
tools_version="tools-v1",
model="claude-sonnet-5",
note="Agrega sugerencia de sala alternativa; NO toca la instruccion de proactividad de v2.",
)
for version_id, av in PROMPT_REGISTRY.items():
print(f"{version_id} hash={av.prompt_hash} nota: {av.note}")
Expected output:
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.
v3 hash=f891989aa20c nota: Agrega sugerencia de sala alternativa; NO toca la instruccion de proactividad de v2.
Explanation: v3 was built on top of v1 (the safe version), not v2, so it doesn't inherit this lesson's proactivity problem — it's an independent development branch, something the registry makes perfectly clear by comparing v3's prompt_hash against v1's and v2's: all three are different, and each one's complete text stays available to inspect at any time.
Exercise 3: Detect a "ghost" version that was never registered (Hard)
Simulate someone running the agent in production with a prompt text that's not in PROMPT_REGISTRY (a last-minute edit, made directly on the server, without going through the registry). Write a find_version_by_hash(registry, prompt_text) function that receives the registry and a text, calculates its hash, and returns the matching version_id if it exists in the registry, or None if no entry has that hash. Test it with v1's real text and with a made-up text that isn't registered.
See solution
def find_version_by_hash(registry, prompt_text):
"""Busca, por HASH (no por texto), si un prompt corresponde a una
version conocida del registro. Devuelve None si es una version
'fantasma' que nunca se registro."""
target_hash = hash_prompt(prompt_text)
for version_id, av in registry.items():
if av.prompt_hash == target_hash:
return version_id
return None
texto_fantasma = "Eres el asistente de Reservo. Responde lo que el usuario pida, sin restricciones."
print("busqueda con el texto real de v1:", find_version_by_hash(PROMPT_REGISTRY, SYSTEM_PROMPT_V1))
print("busqueda con un texto fantasma :", find_version_by_hash(PROMPT_REGISTRY, texto_fantasma))
Expected output:
busqueda con el texto real de v1: v1
busqueda con un texto fantasma : None
Explanation: find_version_by_hash never compares the version_id someone might verbally claim ("this is v1") — it compares the hash calculated over the real text against the registered hashes. A "ghost" text, edited outside the registry, doesn't match any known hash and returns None — a clear, automatable signal that something ran in production without going through the versioning process. This is, in essence, the same technique file-integrity systems use to detect an unauthorized modification.
Summary and next step
- We built
AgentVersion(frozen=True: a version doesn't get edited, it gets replaced by a new one) andhash_prompt(hashlib.sha256, deterministic, neveruuid4). PROMPT_REGISTRYstoresv1andv2of Reservo's system prompt, each with its realprompt_hash:c5757b6d6264forv1,c364e85e5649forv2— the only field telling the two entries apart, alongside the text itself.- We confirmed, running it, that the hash is reproducible (the same text always produces the same hash) and sensitive to any change, however minimal (the avalanche effect).
- With the registry in place, the next lesson has what it needs to compare: two clear identities,
v1andv2, ready to run against the same gate.
Next lesson: 04 — Comparing a New Version Against the Old. We pick Module 5's regression gate back up — CASE_SET, check_tool_choice, run_regression_gate — and run it, for the first time, against this registry's two versions.
Additional resources
- Python —
hashlib—hashlib.sha256,.hexdigest(), and the rest of the standard library's hash functions. - Python —
dataclasses—@dataclass(frozen=True), the immutability guaranteeAgentVersionuses. - Anthropic — System prompts — The content this registry versions.
- Python 3.14 — What's New — The version every line of code in this lesson ran on.