Módulo 6: Estado compartido y el patrón blackboard
Mini-proyecto: el Blackboard de Reservo
Descripción
Siete lecciones dejaron el patrón blackboard completo: la estructura (02), la escritura anclada en datos reales (03), la lectura sin coordinación directa (04), el log de auditoría (05), el ruido medido contra un paquete a medida (06), y la corrida completa de cuatro roles (07). Este mini-proyecto no agrega ningún concepto nuevo — te da tres escenarios de Reservo que nunca viste y te pide aplicar el patrón completo: escribir lo que corresponde, leer lo que hace falta, y citar el log de cada corrida.
El escenario más importante del lote es el B — a propósito, es el error más común de este patrón:
reusar un mismo Blackboard para dos socios distintos. Reconocer ese error, y corregirlo creando
un Blackboard nuevo por corrida, es tan parte del criterio de este módulo como saber construirlo
bien.
Conexión con el módulo
Este mini-proyecto es la síntesis de las siete lecciones anteriores, no una lección nueva. De la 02
usas Blackboard y WriteLogEntry. De la 03, el anclaje en tool_result real con
all_tool_results. De la 04, la lectura sin coordinación directa. De la 05, el log como herramienta
de auditoría. De la 06, el criterio de qué campos son relevantes para cada tarea. De la 07, la
corrida de varios roles compartiendo un objeto. Cuando termines, el Módulo 7 toma este mismo
Reservo y combina blackboard con los cuatro patrones anteriores —supervisor, pipeline, fan-out,
handoff— sobre una petición todavía más compleja.
El encargo
Reservo te pasa tres situaciones que llegaron la misma semana:
Escenario A: Marta reserva Boardroom pro 4h, pregunta si hay cargo por no presentarse,
y compara Focus y Studio, ambas pro 2h.
Escenario B: el sistema reusa por error el MISMO Blackboard entre la corrida de Ana
y la corrida de Marta -- identifica el error y corrígelo.
Escenario C: Marta pregunta por su reserva ANTES de haber reservado nada en esta
corrida -- decide qué debería pasar cuando un campo necesario todavía
es None.
Tu encargo tiene tres partes:
a) Para cada escenario, corre el Blackboard correspondiente, citando qué escribió cada agente
y qué leyó cada uno.
b) En el Escenario B, identifica el error concreto —qué dato se filtra, de quién a quién— y muestra la corrección.
c) En el Escenario C, construye una guarda que convierta un campo None en un error ruidoso, en
vez de dejar que se cuele como texto sin sentido en la tarea del siguiente agente.
La solución completa (el entregable)
Ver la solución completa
import ast
import concurrent.futures
import itertools
from dataclasses import dataclass, field
import reservo_tools as rt
def dispatch_parallel(tool_use_blocks, tools):
with concurrent.futures.ThreadPoolExecutor(max_workers=len(tool_use_blocks)) as pool:
futures = [pool.submit(tools[b["name"]], **b["input"]) for b in tool_use_blocks]
results = [f.result() for f in futures]
return [
{"type": "tool_result", "tool_use_id": b["id"], "content": str(r)}
for b, r in zip(tool_use_blocks, results)
]
def run_agent_parallel(question, model_script, tools, max_iterations=10):
messages = [{"role": "user", "content": question}]
for step in range(max_iterations):
turn = model_script[step]
messages.append({"role": "assistant", "content": turn["content"]})
if turn["stop_reason"] != "tool_use":
return turn, messages
tool_result_blocks = dispatch_parallel(turn["content"], tools)
messages.append({"role": "user", "content": tool_result_blocks})
raise RuntimeError(f"max_iterations alcanzado ({max_iterations})")
POLICY_DOCS = {
"no-show-policy": (
"Si un miembro no se presenta a una reserva confirmada y no cancela "
"con al menos 2 horas de anticipación, Reservo cobra el 50% del "
"precio cotizado como cargo por no-presentación."
),
"cancellation-policy": (
"Las reservas se pueden cancelar sin cargo hasta 2 horas antes del "
"horario reservado. Cancelaciones dentro de esas 2 horas aplican "
"el cargo de no-presentación."
),
}
def search_docs(query):
q = query.lower()
if "cancela" in q:
return f"[cancellation-policy] {POLICY_DOCS['cancellation-policy']}"
if "no" in q and ("present" in q or "show" in q):
return f"[no-show-policy] {POLICY_DOCS['no-show-policy']}"
return "No se encontró una política relevante para esa pregunta."
SPECIALISTS = {
"booking_agent": {
"tools": {
"list_rooms": rt.list_rooms, "get_quote": rt.get_quote,
"book_room": rt.book_room, "cancel_booking": rt.cancel_booking,
},
},
"policy_agent": {"tools": {"search_docs": search_docs}},
"pricing_agent": {"tools": {"get_quote": rt.get_quote}},
}
def run_specialist(name, task, model_script):
tools = SPECIALISTS[name]["tools"]
return run_agent_parallel(task, model_script, tools)
def all_tool_results(history):
results = []
for m in history:
if isinstance(m["content"], list):
for b in m["content"]:
if b["type"] == "tool_result":
try:
results.append(ast.literal_eval(b["content"]))
except (ValueError, SyntaxError):
results.append(b["content"])
return results
WRITE_SEQ = itertools.count(1)
@dataclass
class WriteLogEntry:
seq: int
writer: str
field: str
value: object
@dataclass
class Blackboard:
member: str | None = None
room: str | None = None
tier: str | None = None
hours: int | None = None
price_cents: int | None = None
booking_id: int | None = None
log: list = field(default_factory=list)
def write(self, writer, **fields):
for key, value in fields.items():
setattr(self, key, value)
self.log.append(
WriteLogEntry(seq=next(WRITE_SEQ), writer=writer, field=key, value=value)
)
def require(bb, field_name, reader):
"""Lee un campo del Blackboard, pero falla RUIDOSO si nadie lo escribió
todavía -- en vez de dejar que un None se cuele en la tarea del
siguiente agente como si fuera un dato real."""
value = getattr(bb, field_name)
if value is None:
raise ValueError(
f"{reader} necesita bb.{field_name}, pero nadie lo escribió "
f"todavía en este Blackboard."
)
return value
# =====================================================================
# Escenario A: Marta reserva Boardroom pro 4h, pregunta por no-show,
# y compara Focus/Studio pro 2h.
# =====================================================================
print("=== Escenario A ===")
bb_a = Blackboard()
bb_a.write("supervisor", member="Marta")
model_script_booking_a = [
{"stop_reason": "tool_use", "content": [
{"type": "tool_use", "id": "toolu_01", "name": "get_quote",
"input": {"room": "Boardroom", "tier": "pro", "hours": 4}}]},
{"stop_reason": "tool_use", "content": [
{"type": "tool_use", "id": "toolu_02", "name": "book_room",
"input": {"room": "Boardroom", "tier": "pro", "hours": 4, "member": "Marta"}}]},
{"stop_reason": "end_turn", "content": [
{"type": "text", "text": "Boardroom pro 4h cuesta 25600 centavos. Reservé la sala para Marta (confirmación #1)."}]},
]
final_b, history_b = run_specialist("booking_agent", "Cotiza y reserva Boardroom pro 4h para Marta.", model_script_booking_a)
quote_r, booking_r = all_tool_results(history_b)
bb_a.write("booking_agent", room="Boardroom", tier="pro", hours=4, price_cents=quote_r["price_cents"])
bb_a.write("booking_agent", booking_id=booking_r["booking_id"])
print("respuesta booking_agent:", final_b["content"][0]["text"])
task_policy_a = f"¿Hay cargo por no presentarme a la reserva {require(bb_a, 'booking_id', 'policy_agent')}?"
print("tarea policy_agent (leyendo booking_id):", repr(task_policy_a))
model_script_policy_a = [
{"stop_reason": "tool_use", "content": [
{"type": "tool_use", "id": "toolu_01", "name": "search_docs",
"input": {"query": "no me presento a mi reserva"}}]},
{"stop_reason": "end_turn", "content": [
{"type": "text", "text": (
f"Sí -- si no te presentas a la reserva {bb_a.booking_id} y no cancelas con al "
"menos 2 horas de anticipación, se cobra el 50% del precio cotizado."
)}]},
]
final_p, history_p = run_specialist("policy_agent", task_policy_a, model_script_policy_a)
print("respuesta policy_agent:", final_p["content"][0]["text"])
task_pricing_a = "Compara Focus y Studio, ambas pro, 2h."
print("tarea pricing_agent (parámetros del socio, NO leídos del Blackboard):", repr(task_pricing_a))
model_script_pricing_a = [
{"stop_reason": "tool_use", "content": [
{"type": "tool_use", "id": "toolu_01", "name": "get_quote",
"input": {"room": "Focus", "tier": "pro", "hours": 2}},
{"type": "tool_use", "id": "toolu_02", "name": "get_quote",
"input": {"room": "Studio", "tier": "pro", "hours": 2}},
]},
{"stop_reason": "end_turn", "content": [
{"type": "text", "text": "Focus pro 2h: 4000 centavos. Studio pro 2h: 6400 centavos. Focus es más barata."}]},
]
final_pr, history_pr = run_specialist("pricing_agent", task_pricing_a, model_script_pricing_a)
print("respuesta pricing_agent:", final_pr["content"][0]["text"])
print()
print("--- log del Escenario A ---")
for entry in bb_a.log:
print(f" #{entry.seq} {entry.writer:<14} escribió {entry.field}={entry.value!r}")
print()
# =====================================================================
# Escenario B: el error de reusar UN Blackboard para DOS socios distintos.
# =====================================================================
print("=== Escenario B ===")
bb_bug = Blackboard()
print("--- corrida 1: Ana cotiza y reserva Focus pro 3h ---")
bb_bug.write("supervisor", member="Ana")
model_script_booking_ana = [
{"stop_reason": "tool_use", "content": [
{"type": "tool_use", "id": "toolu_01", "name": "get_quote",
"input": {"room": "Focus", "tier": "pro", "hours": 3}}]},
{"stop_reason": "tool_use", "content": [
{"type": "tool_use", "id": "toolu_02", "name": "book_room",
"input": {"room": "Focus", "tier": "pro", "hours": 3, "member": "Ana"}}]},
{"stop_reason": "end_turn", "content": [
{"type": "text", "text": "Focus pro 3h cuesta 6000 centavos. Reservé la sala para Ana (confirmación #2)."}]},
]
_, history_ana = run_specialist("booking_agent", "Cotiza y reserva Focus pro 3h para Ana.", model_script_booking_ana)
quote_ana, booking_ana = all_tool_results(history_ana)
bb_bug.write("booking_agent", room="Focus", tier="pro", hours=3, price_cents=quote_ana["price_cents"])
bb_bug.write("booking_agent", booking_id=booking_ana["booking_id"])
print(f"reserva de Ana: booking_id={bb_bug.booking_id}")
print()
print("--- corrida 2 (BUG): llega Marta, el sistema reusa el MISMO bb en vez de crear uno nuevo ---")
bb_bug.write("supervisor", member="Marta")
print(f"bb_bug.member ahora es {bb_bug.member!r}, pero bb_bug.booking_id sigue siendo {bb_bug.booking_id!r}")
print("(la reserva de ANA -- Marta todavía no reservó nada en esta corrida)")
task_policy_bug = f"¿Puedo cancelar la reserva {bb_bug.booking_id} sin cargo?"
print(f"tarea armada para Marta, leyendo bb_bug.booking_id: {task_policy_bug!r}")
print(">>> ESTO ESTÁ MAL: el número de reserva que se le va a contestar a Marta es el de Ana <<<")
print()
print("--- la corrección: un Blackboard NUEVO por corrida ---")
bb_marta = Blackboard()
bb_marta.write("supervisor", member="Marta")
print(f"bb_marta.member={bb_marta.member!r} bb_marta.booking_id={bb_marta.booking_id!r}")
print("con el Blackboard correcto, booking_id es None -- Marta no tiene ninguna reserva todavía")
print("en esta corrida, y el sistema no puede -- ni debe -- inventarle una.")
print()
# =====================================================================
# Escenario C: leer un campo que nadie escribió todavía, con una guarda.
# =====================================================================
print("=== Escenario C ===")
print("bb_marta ya existe del Escenario B, con member='Marta' y booking_id=None")
print()
print("--- sin guarda: el None se cuela en la tarea, como texto literal ---")
task_sin_guarda = f"¿Puedo cancelar la reserva {bb_marta.booking_id} sin cargo?"
print(f"tarea armada sin guarda: {task_sin_guarda!r}")
print(">>> 'la reserva None' no es una tarea válida para ningún especialista <<<")
print()
print("--- con la guarda require(): falla ruidoso, en el momento exacto ---")
try:
task_con_guarda = f"¿Puedo cancelar la reserva {require(bb_marta, 'booking_id', 'policy_agent')} sin cargo?"
except ValueError as e:
print(f"ValueError capturado: {e}")
print()
print("--- una vez que Marta SÍ reserva, require() no falla ---")
model_script_booking_marta = [
{"stop_reason": "tool_use", "content": [
{"type": "tool_use", "id": "toolu_01", "name": "get_quote",
"input": {"room": "Studio", "tier": "basic", "hours": 1}}]},
{"stop_reason": "tool_use", "content": [
{"type": "tool_use", "id": "toolu_02", "name": "book_room",
"input": {"room": "Studio", "tier": "basic", "hours": 1, "member": "Marta"}}]},
{"stop_reason": "end_turn", "content": [
{"type": "text", "text": "Studio básico 1h cuesta 4000 centavos. Reservé la sala para Marta (confirmación #3)."}]},
]
_, history_marta = run_specialist("booking_agent", "Cotiza y reserva Studio básico 1h para Marta.", model_script_booking_marta)
quote_marta, booking_marta = all_tool_results(history_marta)
bb_marta.write("booking_agent", room="Studio", tier="basic", hours=1, price_cents=quote_marta["price_cents"])
bb_marta.write("booking_agent", booking_id=booking_marta["booking_id"])
task_ok = f"¿Puedo cancelar la reserva {require(bb_marta, 'booking_id', 'policy_agent')} sin cargo?"
print(f"tarea armada: {task_ok!r}")
print("cálculo a mano Studio básico 1h:", 4000 * 1)
Qué esperar:
=== Escenario A ===
respuesta booking_agent: Boardroom pro 4h cuesta 25600 centavos. Reservé la sala para Marta (confirmación #1).
tarea policy_agent (leyendo booking_id): '¿Hay cargo por no presentarme a la reserva 1?'
respuesta policy_agent: Sí -- si no te presentas a la reserva 1 y no cancelas con al menos 2 horas de anticipación, se cobra el 50% del precio cotizado.
tarea pricing_agent (parámetros del socio, NO leídos del Blackboard): 'Compara Focus y Studio, ambas pro, 2h.'
respuesta pricing_agent: Focus pro 2h: 4000 centavos. Studio pro 2h: 6400 centavos. Focus es más barata.
--- log del Escenario A ---
#1 supervisor escribió member='Marta'
#2 booking_agent escribió room='Boardroom'
#3 booking_agent escribió tier='pro'
#4 booking_agent escribió hours=4
#5 booking_agent escribió price_cents=25600
#6 booking_agent escribió booking_id=1
=== Escenario B ===
--- corrida 1: Ana cotiza y reserva Focus pro 3h ---
reserva de Ana: booking_id=2
--- corrida 2 (BUG): llega Marta, el sistema reusa el MISMO bb en vez de crear uno nuevo ---
bb_bug.member ahora es 'Marta', pero bb_bug.booking_id sigue siendo 2
(la reserva de ANA -- Marta todavía no reservó nada en esta corrida)
tarea armada para Marta, leyendo bb_bug.booking_id: '¿Puedo cancelar la reserva 2 sin cargo?'
>>> ESTO ESTÁ MAL: el número de reserva que se le va a contestar a Marta es el de Ana <<<
--- la corrección: un Blackboard NUEVO por corrida ---
bb_marta.member='Marta' bb_marta.booking_id=None
con el Blackboard correcto, booking_id es None -- Marta no tiene ninguna reserva todavía
en esta corrida, y el sistema no puede -- ni debe -- inventarle una.
=== Escenario C ===
bb_marta ya existe del Escenario B, con member='Marta' y booking_id=None
--- sin guarda: el None se cuela en la tarea, como texto literal ---
tarea armada sin guarda: '¿Puedo cancelar la reserva None sin cargo?'
>>> 'la reserva None' no es una tarea válida para ningún especialista <<<
--- con la guarda require(): falla ruidoso, en el momento exacto ---
ValueError capturado: policy_agent necesita bb.booking_id, pero nadie lo escribió todavía en este Blackboard.
--- una vez que Marta SÍ reserva, require() no falla ---
tarea armada: '¿Puedo cancelar la reserva 3 sin cargo?'
cálculo a mano Studio básico 1h: 4000
Fíjate en la progresión de los booking_id a lo largo del encargo: 1 (Marta, Escenario A), 2
(Ana, Escenario B), 3 (Marta otra vez, Escenario C). No es un error — es exactamente el
comportamiento real de reservo_tools: _booking_ids es un contador del proceso completo, no
uno por corrida ni por socio. En un Reservo real, con muchos socios reservando durante el mismo día,
el número de confirmación de cada uno depende del orden en que las reservas se crearon, sin
importar de quién sea cada una. Esta es la misma razón, en los hechos, por la que el Escenario B es
un error: el Blackboard viejo seguía teniendo un booking_id válido y real (el 2 de Ana) —
no un dato corrupto o inventado —, lo que hace que el bug sea más difícil de detectar a simple vista
que un error que produjera basura obvia.
El razonamiento por escenario:
Escenario A — la corrida "de manual", sin sorpresas. Cuatro escrituras seguidas de
booking_agent, después dos lecturas —una de policy_agent (booking_id), y pricing_agent
resolviendo con parámetros que el socio dio explícitamente, no leídos del Blackboard (Marta pidió
comparar Focus y Studio, salas distintas a la que reservó). 25600 = 8000 * 4 * 80 // 100,
4000 = 2500 * 2 * 80 // 100, 6400 = 4000 * 2 * 80 // 100.
Escenario B — el error central del mini-proyecto. Reusar bb_bug entre la corrida de Ana y la
de Marta filtra el booking_id de Ana hacia una tarea que se le arma a Marta — un dato de un socio
apareciendo en la respuesta a otro, sin que nadie lo haya decidido así. La corrección no es
"limpiar" el Blackboard viejo entre corridas —eso seguiría siendo frágil, dependiente de acordarse
de limpiarlo—: es crear un Blackboard() nuevo, con member fresco, para cada corrida.
Escenario C — la guarda que convierte un bug silencioso en un error ruidoso. Sin require(), un
booking_id=None se cuela como el texto literal "None" en la tarea del siguiente agente — sin que
Python se queje, porque interpolar None en un f-string es válido. Con require(), el mismo caso
falla de inmediato, con un mensaje que dice exactamente qué faltaba y quién lo necesitaba — el mismo
principio de "que falle ruidoso, no silencioso" que atraviesa toda esta guía desde el Módulo 2.
Errores comunes
-
"Arreglar" el Escenario B limpiando los campos del Blackboard viejo, en vez de crear uno nuevo. Funciona hasta que alguien olvida limpiar un campo — el bug reaparece de otra forma. Un
Blackboard()nuevo por corrida no depende de que nadie se acuerde de nada. -
Pensar que el Escenario A "no necesitaba" pricing_agent leyendo del Blackboard porque pricing_agent no leyó nada esta vez. No es un error del
Blackboard— Marta pidió comparar salas distintas a la que reservó, así que no había ningún dato relevante que leer. Otra corrida, donde pidiera comparar la MISMA sala que ya reservó, sí tendría sentido leertier/hourscomo en la lección 07. -
Usar
require()en TODOS los campos, incluso los que legítimamente pueden serNone. Si una corrida solo cotiza sin reservar,booking_id=Nonees un estado válido — llamarrequire(bb, "booking_id", ...)en ese punto de la corrida fallaría por algo que no es un error, solo un paso que todavía no ocurrió.require()tiene sentido cuando el campo debería existir ya, dado el punto de la corrida en el que se está. -
Ejecutar los tres escenarios en el mismo proceso sin reiniciar el estado entre corridas. Si corres este mini-proyecto después de otro ejemplo de la guía en el mismo intérprete, los
booking_idde los Escenarios A y B pueden no salir1— cada escenario de esta guía asume su propio Reservo desechable, recién iniciado. -
Confundir el Escenario B con un problema de seguridad (agente malicioso). No lo es — es un bug de coordinación (reusar un objeto que no debería reusarse), no un agente que escribe algo a propósito para engañar a otro. La superficie de seguridad nombrada en la lección 06 es un problema distinto, aunque relacionado: ahí el riesgo es que un agente escriba un valor falso deliberadamente; acá, el sistema completo comete el error de reusar el estado equivocado.
Ejercicios
Ejercicio 1: Confirma tu propia ejecución (Fácil)
Ejecuta los tres escenarios del encargo tú mismo y confirma, línea por línea, que tu salida coincide con la solución completa. Para el Escenario B, escribe en una frase adicional qué dato exacto se filtró, y de quién a quién.
Ver solución
No hay una única "solución de código" para la primera parte — es una verificación: si tu salida coincide con la de la solución completa, tu Reservo desechable arrancó limpio.
Sobre el Escenario B: el dato que se filtra es booking_id=2 — la reserva de Ana— apareciendo
en la tarea que se le arma a Marta, porque el sistema reusó bb_bug sin crear un Blackboard
nuevo para la segunda corrida.
Ejercicio 2: Un cuarto escenario, D — dos escrituras del mismo campo, dos agentes distintos (Medio)
En un proceso nuevo (para que WRITE_SEQ arranque en 1), diseña un escenario donde
booking_agent escribe price_cents=6000 (Focus pro 3h), y luego, por error de diseño,
pricing_agent también escribe a price_cents (algo que, según el criterio de la lección 02, nada
en Blackboard.write le impide hacer). Ejecútalo, y usa history_of —el método de la lección 05—
para mostrar que el log revela ese segundo escritor, aunque no debería haber escrito ahí.
Ver solución
bb_d = Blackboard()
bb_d.write("booking_agent", price_cents=6000)
bb_d.write("pricing_agent", price_cents=9600) # fuera de su rol -- pricing_agent solo compara, no reserva
def history_of(bb, field_name):
return [e for e in bb.log if e.field == field_name]
for entry in history_of(bb_d, "price_cents"):
print(f" #{entry.seq} {entry.writer} puso price_cents={entry.value}")
print()
print("bb_d.price_cents final:", bb_d.price_cents)
print("¿quién debería haber escrito este campo? Solo booking_agent, según SPECIALISTS.")
print("¿el Blackboard lo impidió? No -- write() acepta cualquier writer, sin validar su rol.")
Salida esperada:
#1 booking_agent puso price_cents=6000
#2 pricing_agent puso price_cents=9600
bb_d.price_cents final: 9600
¿quién debería haber escrito este campo? Solo booking_agent, según SPECIALISTS.
¿el Blackboard lo impidió? No -- write() acepta cualquier writer, sin validar su rol.
Explicación: el log revela el problema con total claridad —dos escritores distintos tocaron
price_cents, y el segundo, pricing_agent, no tenía ningún rol de reserva en SPECIALISTS—. Pero
el Blackboard, tal como está construido en este módulo, no previno la escritura — solo la
registró. Prevenir esto (validar que un writer solo pueda escribir los campos que le
corresponden a su rol) es exactamente el tipo de control de acceso que
agent-security-and-sandboxing-guide construiría; este módulo, según su frontera declarada desde la
lección 01, solo deja visible que la ausencia de esa validación es real.
Ejercicio 3: Verifica el precio de Marta en el Escenario C con el cálculo a mano (Difícil)
Usando require() de la solución completa, confirma que la tarea final del Escenario C usa el
booking_id correcto de Marta (no el de Ana, ni None), y verifica el precio de su reserva
(Studio básico 1h) con el cálculo a mano de reservo_tools.get_quote.
Ver solución
print("booking_id final de Marta:", bb_marta.booking_id)
print("tarea final:", task_ok)
# get_quote real, para confirmar el precio sin adivinar la fórmula a mano
quote_check = rt.get_quote("Studio", "basic", 1)
print("get_quote real:", quote_check)
print("¿coincide con bb_marta.price_cents?", quote_check["price_cents"] == bb_marta.price_cents)
Salida esperada (continuando el mismo proceso de la solución completa, donde ya se crearon dos reservas antes de esta):
booking_id final de Marta: 3
tarea final: '¿Puedo cancelar la reserva 3 sin cargo?'
get_quote real: {'price_cents': 4000}
¿coincide con bb_marta.price_cents? True
Explicación: booking_id=3 es el de la reserva real de Marta en el Escenario C — distinto
del 2 que había leído por error en el Escenario B (que era, de hecho, la reserva de Ana). El
_booking_ids de reservo_tools es un contador del proceso completo, no uno por Blackboard
ni por socio: la primera reserva del encargo (Marta, Escenario A) recibió 1; la segunda (Ana,
Escenario B) recibió 2; esta, la tercera (Marta otra vez, Escenario C), recibe 3 — sin importar
que sea la primera reserva real de Marta en su propio Blackboard (bb_marta). Que
bb_marta.price_cents coincida con get_quote("Studio", "basic", 1) confirma que el precio
guardado está anclado en la tool real, exactamente el principio de la lección 03 — nunca en un
número inventado a mano.
Resumen y siguiente paso
- El mini-proyecto no agregó ningún concepto nuevo: aplicó las siete lecciones anteriores —estructura, escritura anclada, lectura sin coordinación directa, log de auditoría, ruido medido, la corrida de varios roles— sobre tres escenarios de Reservo nuevos.
- El Escenario A confirmó que el patrón escala sin cambios a datos nuevos (otro socio, otras salas,
otras horas). El Escenario B expuso el error central del módulo: reusar un
Blackboardentre corridas filtra datos de un socio hacia otro — la corrección es unBlackboard()nuevo por corrida, nunca "limpiar" el viejo a mano. El Escenario C mostró que una guarda simple (require) convierte unNoneque se cuela silencioso en un error ruidoso, capturable. - Con este módulo completo, tienes los cinco patrones de la guía construidos: supervisor (M2, decide quién), pipeline (M3, encadena sin decidir), fan-out (M4, reparte y agrega), handoff (M5, un agente en curso cede el control), y blackboard (este módulo, un estado compartido sin dirección).
Con esto termina el Módulo 6. Construiste el patrón blackboard completo: la estructura con su log de
auditoría, la escritura anclada en datos reales, la lectura sin coordinación directa, el trade-off
medido contra un mensaje dirigido, la corrida de cuatro roles, y el error más común del patrón,
identificado y corregido. En el Módulo 7 combinamos los cinco patrones sobre el sistema completo
de Reservo: una petición compleja que dispara supervisor, pipeline donde corresponde, fan-out donde
corresponde, un handoff a mitad de tarea, todo sobre un Blackboard compartido — ningún patrón
nuevo, solo la pregunta de cuándo combinar cuáles.
Recursos adicionales
- Anthropic — Multi-agent research system — Un caso real donde el estado compartido entre sub-agentes tuvo que diseñarse con cuidado explícito para no mezclar datos de tareas distintas, el mismo riesgo que expone el Escenario B de este encargo.
- Anthropic — Building effective agents — El principio de mantener el estado del sistema simple y verificable — la guarda
require()de este mini-proyecto es una aplicación directa de ese principio. - Python — Excepciones y
raise— El mecanismo detrás derequire(), la misma disciplina de "falla ruidoso, no silencioso" que esta guía usa desde el Módulo 2. - Python —
dataclasses— La base deBlackboardyWriteLogEntry, usada sin cambios en los tres escenarios de este mini-proyecto.