Documentación
Pizarra reactiva para agentes
El caso de uso estelar de Yoltra en Python es una pizarra reactiva (reactive blackboard) para sistemas agénticos y de plugins: una única memoria de trabajo compartida que las herramientas, monitores y sub-agentes observan con la granularidad exacta que les importa, coordinada por reacciones asíncronas en lugar de sondeo (polling) o callbacks enhebrados a mano.
#El problema
Los frameworks de agentes (LangGraph, CrewAI, AutoGen, LlamaIndex, …) mantienen
estado grueso. Ninguno te permite decir "reacciona cuando cambie
plan.steps[2].status". La coordinación termina como bucles de sondeo o callbacks
enredados.
#El patrón
Modela la memoria de trabajo como un árbol de slices — plan, scratchpad,
tool_results, sub_agents — y deja que cada participante:
connecta las rutas precisas que le corresponden (un monitor sobreplan.steps.*.status),emiteventos que otros pliegan (un planificador que emite("plan", "step_done", i)),- reaccione vía efectos que disparan la siguiente acción.
La comparación estructural despierta solo a los suscriptores relevantes, y el log de eventos te da una traza reproducible de la corrida.
#Un ejemplo trabajado
Un planificador avanza un plan; un monitor reacciona al estado de cada paso sin sondear; un efecto escala ante un fallo.
from yoltra import create_store
# effects_mode "inline" mantiene determinista la salida de este script; usa el
# modo "auto" por defecto (más abajo) para reacciones asíncronas reales.
store = create_store("Agent", effects_mode="inline")
@store.reducer(
name="plan",
state={"steps": [
{"name": "research", "status": "pending"},
{"name": "draft", "status": "pending"},
{"name": "review", "status": "pending"},
]},
when={"keys": [("plan", "start"), ("plan", "complete"), ("plan", "fail")]},
)
def plan(prev, event):
steps = [dict(s) for s in prev["steps"]]
idx = event.payload
if event.type == "start":
steps[idx]["status"] = "running"
elif event.type == "complete":
steps[idx]["status"] = "done"
elif event.type == "fail":
steps[idx]["status"] = "failed"
return {"steps": steps}
# Un monitor reacciona a cualquier cambio de status de un paso — sin sondeo.
def on_status(change):
print(f"{change.path}: {change.old_value} → {change.new_value}")
store.connect(reducer="plan", property="steps.*.status", handler=on_status)
# Un efecto escala cuando un paso falla.
@store.effect(when={"keys": [("plan", "fail")]})
def escalate(event, get_state, emit):
step = get_state()["plan"]["steps"][event.payload]
print(f"[escalate] step {step['name']!r} failed — notifying supervisor")
# Ejecuta el plan.
store.emit("plan", "start", 0)
store.emit("plan", "complete", 0)
store.emit("plan", "start", 1)
store.emit("plan", "fail", 1)Salida:
steps.0.status: pending → running
steps.0.status: running → done
steps.1.status: pending → running
steps.1.status: running → failed
[escalate] step 'draft' failed — notifying supervisorEl monitor nunca sondea; despierta solo cuando una hoja status realmente cambia,
y el efecto de escalada corre después de que el fallo se confirma.
#Múltiples participantes, un solo store
Como las suscripciones tienen alcance por ruta, muchos agentes comparten un store sin acoplarse:
# Una herramienta escribe resultados en su propia región.
@store.reducer(name="tool_results", state={},
when={"keys": [("tool", "result")]})
def tool_results(prev, event):
name, value = event.payload
return {**prev, name: value}
# Un sub-agente solo observa los resultados de los que depende.
store.connect(reducer="tool_results", property="search",
handler=lambda c: print("search result ready:", c.new_value))
store.emit("tool", "result", ("search", ["doc-1", "doc-2"]))
# search result ready: ['doc-1', 'doc-2']Cada participante se suscribe a las regiones que le importan y emite eventos que otros pliegan — el patrón de pizarra/fuente-de-conocimiento, vuelto reactivo.
#Reacciones asíncronas
Las reacciones que hacen E/S (llamar a un modelo, pegarle a un servidor de
herramientas) van en efectos, que corren después de la confirmación y pueden
ser async. Los efectos async requieren el modo "auto" por defecto (o
"asyncio"), no "inline":
agent = create_store("Agent") # modo "auto" por defecto
@agent.effect(when={"keys": [("plan", "complete")]})
async def kick_off_next(event, get_state, emit):
idx = event.payload
steps = get_state()["plan"]["steps"]
if idx + 1 < len(steps):
emit("plan", "start", idx + 1) # emit reentrante; nunca causa interbloqueoLos emits reentrantes desde dentro de un efecto son seguros — los efectos de cada evento son su propia tarea.
#Trazas reproducibles
Como cada escritura es un evento, una corrida es una secuencia que puedes capturar
y reproducir a través de los reducers para una depuración determinista (protegido
detrás de allow_replay):
store = create_store("Agent", allow_replay=True)
# ... captura los eventos (channel, type, payload) que emites ...
store._replay_events(snapshot, captured_events) # re-ejecuta solo a través de los reducers#Por qué encaja
El trío de suscripciones atómicas + efectos asíncronos + replay mapea directamente sobre el patrón de pizarra/fuente-de-conocimiento que necesita la coordinación de agentes — y Python no tenía antes una primitiva agnóstica al framework para ello.
#Ver también
- Guía para desarrolladores — el modelo completo.
- Inicio rápido — la versión de cinco minutos.