Módulo 6: Estado compartido y el patrón blackboard

El trade-off medido: visibilidad contra aislamiento

Descripción

Las lecciones 02 a 05 construyeron el Blackboard completo, y lo usaron de las dos formas posibles: escribiendo (03), leyendo (04), auditando (05). En ningún momento, hasta ahora, se detuvieron a contar algo que la lección 01 adelantó como el eje del módulo: cuánto ve cada agente cuando lee el Blackboard, comparado con lo que su tarea necesita. Esta lección cuenta eso, con números reales, y lo compara con la alternativa: un paquete de handoff (Módulo 5) diseñado a la medida exacta del receptor.

El resultado no es una opinión — es una cuenta de campos. policy_agent, para responder "¿puedo cancelar mi reserva?", necesita exactamente un campo (booking_id). Leyendo el Blackboard completo, tiene acceso a seis. Esos cinco de más —member, room, tier, hours, price_cents— son ruido: información que el agente puede ver, sin que su tarea la requiera.

Conexión con el módulo

Esta lección reusa Blackboard sin ningún cambio, y agrega la medición: RELEVANT_FIELDS, un mapeo de qué necesita cada agente, y la comparación con un paquete de handoff construido a mano, siguiendo la forma genérica que describió el Módulo 1 (quién envía, a quién, la tarea, el payload mínimo). La lección 07 corre la corrida completa sobre la que se apoya esta medición; el Módulo 8 del agent-security-and-sandboxing-guide retoma la superficie que esta lección solo nombra.


Analogía: la pizarra que cualquiera puede leer entera, contra el sobre cerrado con un solo dato

Vuelve a la pizarra de la sala de guardia. Un enfermero que solo necesita saber en qué cama está un paciente, para llevarle una bandeja, lee la pizarra entera —incluidos el diagnóstico, la medicación, las notas del médico de guardia— para encontrar ese único dato que le importa. No hay forma de que la pizarra le muestre solo la cama y le oculte el resto; está toda a la vista, para quien sea que la mire. Un sobre cerrado, en cambio, dirigido a ese enfermero específico, contendría un solo papel: "cama 4". Nada más. Esta lección cuenta, literalmente, cuántos "papeles de más" trae la pizarra frente al sobre — para cada tarea concreta de Reservo.


Ejemplo trabajado: contando el ruido, campo por campo

import itertools
from dataclasses import dataclass, field, fields as dc_fields


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


bb = Blackboard()
bb.write("supervisor", member="Ana")
bb.write("booking_agent", room="Focus", tier="pro", hours=3, price_cents=6000)
bb.write("booking_agent", booking_id=1)

ALL_FIELDS = {f.name for f in dc_fields(Blackboard) if f.name != "log"}
print("--- campos del Blackboard, todos visibles para CUALQUIER agente con una referencia a bb ---")
print(sorted(ALL_FIELDS))

# Lo que cada tarea REALMENTE necesita -- no lo que la estructura expone.
RELEVANT_FIELDS = {
    "policy_agent": {"booking_id"},
    "pricing_agent": {"tier", "hours"},
}

print()
print("--- ruido: campos visibles menos campos relevantes, por agente ---")
for agent, relevant in RELEVANT_FIELDS.items():
    noise = sorted(ALL_FIELDS - relevant)
    print(f"{agent}:")
    print(f"  necesita:      {sorted(relevant)}")
    print(f"  ve además:     {noise}  ({len(noise)} campos de ruido)")

print()
print("--- comparación con un paquete de mensaje punto a punto (M5), del mismo tamaño mínimo ---")


def build_handoff_packet(sender, receiver, task, **payload):
    """La forma genérica del paquete de M5: quién envía, a quién, la
    tarea, y SOLO el payload que el receptor necesita -- nunca la
    estructura completa de datos del emisor."""
    return {"from": sender, "to": receiver, "task": task, "payload": payload}


packet = build_handoff_packet(
    sender="booking_agent", receiver="policy_agent",
    task="verificar política de cancelación", booking_id=bb.booking_id,
)
print("paquete armado a mano para policy_agent:", packet)
print(f"campos en el payload del paquete: {sorted(packet['payload'])}  ({len(packet['payload'])} campo)")
print(f"campos visibles leyendo el Blackboard completo: {sorted(ALL_FIELDS)}  ({len(ALL_FIELDS)} campos)")

print()
print("--- nada impide que un agente sin relación con la tarea lea un campo que no necesita ---")
print("pricing_agent nunca necesita saber quién es el socio, pero nada en Blackboard.write ni en")
print("los campos públicos se lo impide:")
print("bb.member leído desde 'pricing_agent':", bb.member)

Qué esperar:

--- campos del Blackboard, todos visibles para CUALQUIER agente con una referencia a bb ---
['booking_id', 'hours', 'member', 'price_cents', 'room', 'tier']

--- ruido: campos visibles menos campos relevantes, por agente ---
policy_agent:
  necesita:      ['booking_id']
  ve además:     ['hours', 'member', 'price_cents', 'room', 'tier']  (5 campos de ruido)
pricing_agent:
  necesita:      ['hours', 'tier']
  ve además:     ['booking_id', 'member', 'price_cents', 'room']  (4 campos de ruido)

--- comparación con un paquete de mensaje punto a punto (M5), del mismo tamaño mínimo ---
paquete armado a mano para policy_agent: {'from': 'booking_agent', 'to': 'policy_agent', 'task': 'verificar política de cancelación', 'payload': {'booking_id': 1}}
campos en el payload del paquete: ['booking_id']  (1 campo)
campos visibles leyendo el Blackboard completo: ['booking_id', 'hours', 'member', 'price_cents', 'room', 'tier']  (6 campos)

--- nada impide que un agente sin relación con la tarea lea un campo que no necesita ---
pricing_agent nunca necesita saber quién es el socio, pero nada en Blackboard.write ni en
los campos públicos se lo impide:
bb.member leído desde 'pricing_agent': Ana

El número central de esta lección: policy_agent necesita 1 campo y ve 6 — un factor de 6x de ruido para su tarea concreta. Un paquete de handoff diseñado a medida, en cambio, expone exactamente 1 campo, ni uno más. Esa diferencia —1 contra 6— es el costo real de la simplicidad de coordinación que ofrece un Blackboard: nadie tuvo que decidir, al escribir, qué le correspondía a quién; pero, a cambio, cualquiera que lea ve todo.


Por qué esto es exactamente "context bloat"

El nombre técnico de este ruido, en el ecosistema de esta guía, es context bloat: cuánto entra en la ventana de contexto de un agente —su system/messages en la próxima llamada— que no aporta nada a la tarea que tiene que resolver. Con seis campos en el Blackboard, el "bloat" de este ejemplo es pequeño (cinco campos de más, en el peor caso) — pero el mecanismo escala mal: un Blackboard real, con más agentes y más hechos compartidos a lo largo de una corrida más larga, puede terminar con decenas de campos, mientras que cada tarea individual sigue necesitando solo unos pocos. Presupuestar y comprimir ese contenido —decidir qué SÍ entra en la ventana de un agente, qué se resume, qué se descarta— es terreno de context-engineering-guide; esta lección solo mide la superficie del problema sobre el caso concreto de Reservo, sin construir ningún mecanismo de compresión.


La otra cara: nada impide una lectura que no debería pasar

La última sección del ejemplo trabajado —pricing_agent leyendo bb.member— no es un error de código: es una demostración de que el Blackboard, tal como está construido en este módulo, no tiene ningún control de acceso por campo. Cualquier función que reciba una referencia a bb puede leer cualquier atributo público, sin que exista una lista de "campos permitidos por agente" que se haga cumplir en tiempo de ejecución. RELEVANT_FIELDS de esta lección es solo documentación —un diccionario que describe la intención de diseño— no una restricción que el Blackboard imponga.

Esta ausencia de control de acceso es exactamente la puerta que deja abierta la superficie que la lección 01 nombró: un agente que, por un bug o por una instrucción maliciosa incrustada en algo que procesó, decide leer (o peor, escribir) un campo fuera de lo que su rol debería tocar. Construir esa defensa —permisos por campo, sandboxing, validación de qué puede hacer cada agente— es terreno de agent-security-and-sandboxing-guide; esta lección solo deja visible, con código ejecutado, que el problema es real y no hipotético.


Errores comunes

  1. Pensar que "ruido" significa que el Blackboard está mal diseñado. No — el ruido es el costo estructural del patrón, no un error de esta implementación en particular. Cualquier Blackboard con más de un campo tiene, para cualquier tarea que solo necesite un subconjunto, algo de ruido. La pregunta de diseño no es "¿cómo elimino el ruido?" sino "¿el ahorro de coordinación que da el Blackboard justifica el ruido, para este caso?".

  2. Confundir RELEVANT_FIELDS con una restricción real. Es solo un diccionario de documentación en esta lección — no impide que el código de pricing_agent lea bb.member si alguien lo escribiera así, como demuestra el ejemplo trabajado.

  3. Medir el ruido en bytes o caracteres, en vez de en campos. Esta lección cuenta campos, no tamaño de texto — es una medida más simple y suficiente para comparar diseños, aunque context-engineering-guide sí trabaja con medidas más finas (tokens) cuando hace falta precisión real de presupuesto.

  4. Pensar que un paquete de handoff (M5) nunca tiene ruido. Sí puede tenerlo, si se diseña mal — por ejemplo, un paquete que incluye el Blackboard completo como payload, "por si acaso". La ventaja del handoff no es automática: depende de que quien arma el paquete elija bien qué incluir. Un Blackboard nunca da esa opción; un paquete mal diseñado, sí puede desperdiciarla.

  5. Sacar la conclusión de que "blackboard siempre pierde" contra un mensaje dirigido. Esta lección mide un solo eje (ruido de lectura) — no mide el costo de diseñar un paquete distinto para cada combinación de emisor y receptor, que un Blackboard evita por completo. La lección 01 ya lo dejó claro: son dos herramientas para preguntas de diseño distintas, no una mejor que la otra en abstracto.


Ejercicios

Ejercicio 1: Confirma tu propia ejecución (Fácil)

Ejecuta el ejemplo trabajado tú mismo y confirma que el ruido de policy_agent (5 campos) es mayor que el de pricing_agent (4 campos), y explica en una frase por qué son distintos aunque los dos lean el mismo Blackboard.

Ver solución

No hay una única "solución de código" para este ejercicio — es una verificación, más la explicación: el ruido de cada agente depende de cuántos campos necesita (RELEVANT_FIELDS), no solo de cuántos campos tiene el Blackboard. policy_agent necesita 1 de 6 (ruido de 5); pricing_agent necesita 2 de 6 (ruido de 4) — mismo Blackboard, distinta cantidad de campos relevantes, distinto ruido.

Ejercicio 2: El ruido de booking_agent mismo (Medio)

booking_agent necesita room, tier, hours y member para hacer su trabajo (cotizar, reservar), pero no necesita booking_id de una reserva anterior en la misma corrida (si el socio ya reservó algo antes y ahora pide algo nuevo). Agrega "booking_agent": {"room", "tier", "hours", "member"} a RELEVANT_FIELDS y calcula su ruido.

Ver solución
RELEVANT_FIELDS["booking_agent"] = {"room", "tier", "hours", "member"}
noise_booking = sorted(ALL_FIELDS - RELEVANT_FIELDS["booking_agent"])
print(f"booking_agent necesita: {sorted(RELEVANT_FIELDS['booking_agent'])}")
print(f"booking_agent ve además: {noise_booking}  ({len(noise_booking)} campo de ruido)")

Salida esperada:

booking_agent necesita: ['hours', 'member', 'room', 'tier']
booking_agent ve además: ['booking_id', 'price_cents']  (2 campos de ruido)

Explicación: booking_agent es el agente con menos ruido de los tres — necesita 4 de los 6 campos, así que solo le sobran booking_id y price_cents (datos que él mismo produce en la mayoría de las corridas, pero que no necesita leer para decidir qué cotizar o reservar). Esto confirma un patrón: el ruido de un agente depende de cuán cerca está su expertise del centro de gravedad de los datos del Blackboardbooking_agent, que escribe la mayoría de los campos, naturalmente necesita leer la mayoría también.

Ejercicio 3: Un Blackboard con el doble de campos, el mismo ruido relativo (Difícil)

Imagina que el Blackboard de Reservo creciera a 12 campos (agregando, por ejemplo, check_in_time, check_out_time, payment_method, discount_code, member_tier, notes — sin construirlos, solo para el cálculo). Si policy_agent siguiera necesitando solo booking_id, ¿cuál sería su ruido absoluto, y cómo cambiaría el porcentaje de campos que necesita, comparado con el Blackboard de 6 campos de esta lección?

Ver solución
fields_6 = 6
fields_12 = 12
needed = 1  # policy_agent, en ambos casos

noise_6 = fields_6 - needed
noise_12 = fields_12 - needed
pct_6 = needed / fields_6 * 100
pct_12 = needed / fields_12 * 100

print(f"Blackboard de {fields_6} campos: ruido={noise_6}, necesita el {pct_6:.1f}% de los campos")
print(f"Blackboard de {fields_12} campos: ruido={noise_12}, necesita el {pct_12:.1f}% de los campos")

Salida esperada:

Blackboard de 6 campos: ruido=5, necesita el 16.7% de los campos
Blackboard de 12 campos: ruido=11, necesita el 8.3% de los campos

Explicación: el ruido absoluto casi se duplica (de 5 a 11 campos), y el porcentaje de campos que policy_agent realmente necesita cae a la mitad (de 16.7% a 8.3%) — porque el numerador (lo que necesita) se mantiene fijo en 1, mientras el denominador (lo que el Blackboard expone) crece. Esto confirma que el ruido de un Blackboard no es un costo fijo: crece con la cantidad de hechos compartidos, mientras que cada tarea individual sigue necesitando, casi siempre, un subconjunto pequeño y estable. Es exactamente la razón por la que context-engineering-guide existe como disciplina propia: a medida que un sistema crece, decidir qué SÍ entra en la ventana de cada agente deja de ser opcional.


Resumen y siguiente paso

  • policy_agent necesita 1 de los 6 campos del Blackboard; pricing_agent necesita 2 — el ruido medido (5 y 4 campos respectivamente) es el costo real de la simplicidad de coordinación que ofrece el patrón.
  • Un paquete de handoff (Módulo 5), diseñado a medida, expone exactamente lo que el receptor necesita — 1 campo para policy_agent, contra los 6 visibles en el Blackboard completo.
  • Este ruido es, con otro nombre, context bloat — cuánto entra en la ventana de un agente sin aportar a su tarea. Presupuestarlo y comprimirlo es terreno de context-engineering-guide, nombrado aquí, no construido.
  • El Blackboard no tiene ningún control de acceso por campo — cualquier agente con una referencia al objeto puede leer (o escribir) cualquier cosa. Esa ausencia es la superficie que agent-security-and-sandboxing-guide endurece; esta lección solo la deja visible.
  • El ruido crece con la cantidad de campos del Blackboard, mientras que lo que cada tarea necesita se mantiene estable — el trade-off empeora, no mejora, a medida que el sistema crece.

Siguiente lección: 07 — La corrida completa: supervisor y tres especialistas comparten un Blackboard. La pieza central del módulo: los cuatro roles, un solo objeto, de punta a punta.


Recursos adicionales

  1. Anthropic — Effective context engineering for AI agents — El principio de tratar el contexto de un agente como un recurso finito y valioso — la base conceptual del "ruido" que mide esta lección.
  2. Anthropic — Multi-agent research system — Un caso real donde limitar qué ve cada sub-agente, en vez de compartirlo todo, fue una decisión de diseño explícita para mantener la calidad de las respuestas.
  3. Python — dataclasses.fields — La función que enumera los campos declarados de Blackboard para construir ALL_FIELDS, sin hardcodear la lista a mano.
  4. Python — Operaciones de conjuntos — La resta de conjuntos (ALL_FIELDS - relevant) detrás del cálculo de ruido de esta lección.