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

  1. "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.

  2. 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 leer tier/ hours como en la lección 07.

  3. Usar require() en TODOS los campos, incluso los que legítimamente pueden ser None. Si una corrida solo cotiza sin reservar, booking_id=None es un estado válido — llamar require(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á.

  4. 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_id de los Escenarios A y B pueden no salir 1 — cada escenario de esta guía asume su propio Reservo desechable, recién iniciado.

  5. 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 Blackboard entre corridas filtra datos de un socio hacia otro — la corrección es un Blackboard() nuevo por corrida, nunca "limpiar" el viejo a mano. El Escenario C mostró que una guarda simple (require) convierte un None que 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

  1. 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.
  2. 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.
  3. Python — Excepciones y raise — El mecanismo detrás de require(), la misma disciplina de "falla ruidoso, no silencioso" que esta guía usa desde el Módulo 2.
  4. Python — dataclasses — La base de Blackboard y WriteLogEntry, usada sin cambios en los tres escenarios de este mini-proyecto.