Módulo 8: Project The Reservo Multi Agent System
Proyecto: el sistema multi-agente de Reservo
Descripción
Siete lecciones de este módulo construyeron, una por una, las piezas del sistema: el registro de especialistas (02), el pipeline conectado con la Demo A de Luis (03), el fan-out conectado con la Demo B de Marta (04), el handoff de Valentina en aislado (05), el blackboard confirmado sobre las tres mesas (06), y el conteo de costo de coordinación probado contra las Demos A y B (07). Esta lección —el capstone de toda la guía— las junta todas en un solo sistema, agrega la tercera demo que faltaba (Valentina, con sus tres tracks combinados: pipeline, fan-out y handoff), y presenta el conteo de costo comparado de las tres.
No hay ninguna pieza de bajo nivel nueva en esta lección. Es la primera vez que ves el sistema
completo —supervisor, booking_agent, policy_agent, pricing_agent, blackboard, y el conteo de
costo generalizado— funcionando de punta a punta, sobre tres peticiones de forma genuinamente
distinta, en una sola corrida.
Conexión con el módulo
Esta lección reusa, sin cambios, absolutamente todo lo construido en las lecciones 02 a 07:
SPECIALISTS, run_specialist, run_pipeline, run_tracks_parallel, run_with_handoff,
Blackboard, Track, coordination_cost. Lo único nuevo es la Demo C completa (Valentina, los
tres patrones a la vez) y el resumen comparado de las tres demos — el cierre de este módulo, y de
toda la guía.
Analogía: el negocio, entregado, con las cuentas ya hechas
Ya viste, pieza por pieza, cada rol del restaurante: el maître que decide (M2), la línea de montaje (M3), los mozos que resuelven preguntas a la vez (M4), el mozo que llama al sommelier (M5), la pizarra de la cocina (M6), y el libro de cuentas que mide cuánto cuesta cada tipo de mesa (M7 de este módulo). Esta última lección es la noche de apertura: tres mesas reales, de forma completamente distinta, atendidas por el mismo negocio, con el libro de cuentas abierto al lado de cada una. Al final de la noche, no solo el restaurante funcionó — hay un número exacto de cuánto costó atender a cada mesa, y por qué.
El sistema completo
Todo lo que sigue es exactamente el código de las lecciones 02 a 07, ensamblado en un solo script. Ninguna función cambia de firma; el único código genuinamente nuevo de esta lección son las tres demos y el resumen final.
import ast
import concurrent.futures
import itertools
from dataclasses import dataclass, field
import reservo_tools as rt
# ---- runner base (agent-fundamentals, sin cambios) ----
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})")
# ---- M2: policy_agent (stub) + SPECIALISTS + run_specialist (lección 02) ----
def search_docs(query):
q = query.lower()
if "cancela" in q:
return "[cancellation-policy] Puedes cancelar sin cargo hasta 2 horas antes del horario reservado."
if "no" in q and ("present" in q or "show" in q or "llega" in q):
return ("[no-show-policy] Si no te presentas a una reserva confirmada y no cancelas "
"con al menos 2 horas de anticipación, Reservo cobra el 50% del precio "
"cotizado como cargo por no-presentación.")
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)
# ---- M3: pipeline (lección 03) ----
@dataclass
class PipelineStage:
kind: str
name: str
label: str
def last_tool_result(history):
for m in reversed(history):
if isinstance(m["content"], list):
for b in m["content"]:
if b["type"] == "tool_result":
try:
return ast.literal_eval(b["content"])
except (ValueError, SyntaxError):
return b["content"]
return None
def build_stage_task(stage, payload):
if stage.kind == "quote":
return f"Cotiza {payload['room']} {payload['tier']} {payload['hours']}h."
if stage.kind == "validate_policy":
return (f"¿Cuál es la política de cancelación para una reserva de "
f"{payload['room']} de {payload['hours']}h, antes de confirmarla?")
if stage.kind == "confirm":
if payload.get("cleared_to_book"):
return (f"Reserva {payload['room']} {payload['tier']} {payload['hours']}h "
f"para {payload['member']} -- la política de cancelación ya se validó.")
return (f"Reserva {payload['room']} {payload['tier']} {payload['hours']}h "
f"para {payload['member']}.")
raise ValueError(f"no sé armar la tarea de la etapa {stage.kind!r}")
def extract_payload(stage, history, payload):
new_payload = dict(payload)
result = last_tool_result(history)
if stage.kind == "quote":
new_payload["price_cents"] = result["price_cents"]
elif stage.kind == "validate_policy":
new_payload["cancellation_policy"] = result
new_payload["cleared_to_book"] = True
elif stage.kind == "confirm":
new_payload["booking_id"] = result["booking_id"]
new_payload["confirmed"] = result["confirmed"]
return new_payload
def run_pipeline(stages, model_scripts, initial_payload):
payload = dict(initial_payload)
trace = []
for i, stage in enumerate(stages):
task = build_stage_task(stage, payload)
final, history = run_specialist(stage.name, task, model_scripts[i])
payload = extract_payload(stage, history, payload)
trace.append({
"stage": i + 1, "kind": stage.kind, "agent": stage.name,
"label": stage.label, "task": task, "history": history,
"output": final["content"][0]["text"],
})
return payload, trace
# ---- M5: handoff (lección 05) ----
HANDOFF_TOOL_NAME = "handoff_to_specialist"
@dataclass
class HandoffPackage:
sender: str
receiver: str
reason: str
task: str
context: dict = field(default_factory=dict)
def run_agent_with_handoff(question, model_script, tools, self_name, 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, None
block = turn["content"][0]
if block["name"] == HANDOFF_TOOL_NAME:
inp = block["input"]
package = HandoffPackage(
sender=self_name, receiver=inp["receiver"], reason=inp["reason"],
task=inp["task"], context=inp.get("context", {}),
)
return None, messages, package
tool_result_blocks = dispatch_parallel(turn["content"], tools)
messages.append({"role": "user", "content": tool_result_blocks})
raise RuntimeError(f"max_iterations alcanzado ({max_iterations})")
def run_specialist_with_handoff(name, task, model_script):
tools = SPECIALISTS[name]["tools"]
return run_agent_with_handoff(task, model_script, tools, self_name=name)
def run_with_handoff(name, task, model_scripts):
final, history, package = run_specialist_with_handoff(name, task, model_scripts[name])
trace = [{"agent": name, "history": history, "package": package}]
if package is None:
return final, trace
receiver_final, receiver_history, receiver_package = run_specialist_with_handoff(
package.receiver, package.task, model_scripts[package.receiver],
)
trace.append({"agent": package.receiver, "history": receiver_history, "package": receiver_package})
return receiver_final, trace
# ---- M6: blackboard (lección 06) ----
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)
)
# ---- M7: la capa de composición ----
@dataclass
class Track:
key: str
pattern: str # "pipeline" | "fanout" | "handoff"
description: str
def run_tracks_parallel(jobs):
results = {}
with concurrent.futures.ThreadPoolExecutor(max_workers=len(jobs)) as pool:
future_to_key = {pool.submit(fn): key for key, fn in jobs.items()}
for future in concurrent.futures.as_completed(future_to_key):
key = future_to_key[future]
results[key] = future.result()
return results
# ---- lección 07: el conteo generalizado de costo de coordinación ----
def count_model_calls(history):
return sum(1 for m in history if m["role"] == "assistant")
def count_tool_calls(history):
total = 0
for m in history:
if isinstance(m["content"], list):
total += sum(1 for b in m["content"] if b["type"] == "tool_use")
return total
PLAN_CALLS = 1
COMPOSE_CALLS = 1
DISPATCH_HOPS = 2
def track_calls_and_hops(track, result):
if track.pattern == "pipeline":
_, trace = result
calls = sum(count_model_calls(s["history"]) for s in trace)
tool_calls = sum(count_tool_calls(s["history"]) for s in trace)
hops = DISPATCH_HOPS + (len(trace) - 1)
elif track.pattern == "fanout":
_, history = result
calls = count_model_calls(history)
tool_calls = count_tool_calls(history)
hops = DISPATCH_HOPS
elif track.pattern == "handoff":
_, trace = result
calls = sum(count_model_calls(step["history"]) for step in trace)
tool_calls = sum(count_tool_calls(step["history"]) for step in trace)
hops = DISPATCH_HOPS + 1
else:
raise ValueError(f"patrón desconocido: {track.pattern!r}")
return calls, tool_calls, hops
def coordination_cost(plan, track_results):
per_track = {}
for t in plan:
calls, tool_calls, hops = track_calls_and_hops(t, track_results[t.key])
per_track[t.key] = {"pattern": t.pattern, "calls": calls, "tools": tool_calls, "hops": hops}
call_counts = [v["calls"] for v in per_track.values()]
total_specialist_calls = sum(call_counts)
total_tool_calls = sum(v["tools"] for v in per_track.values())
total_hops = sum(v["hops"] for v in per_track.values())
total_model_calls = PLAN_CALLS + total_specialist_calls + COMPOSE_CALLS
surrounding = PLAN_CALLS + COMPOSE_CALLS
sequential_rounds = sum(call_counts) + surrounding
parallel_rounds = (max(call_counts) if call_counts else 0) + surrounding
return {
"per_track": per_track, "total_model_calls": total_model_calls,
"total_tool_calls": total_tool_calls, "total_hops": total_hops,
"sequential_rounds": sequential_rounds, "parallel_rounds": parallel_rounds,
}
def print_cost_report(label, plan, cost):
print(f"--- costo de coordinación: {label} ---")
for t in plan:
d = cost["per_track"][t.key]
print(f" {t.key:<18} patrón={d['pattern']:<9} "
f"llamadas={d['calls']:<3} tools={d['tools']:<3} hops={d['hops']}")
rounds_saved = cost["sequential_rounds"] - cost["parallel_rounds"]
pct = round(100 * rounds_saved / cost["sequential_rounds"]) if cost["sequential_rounds"] else 0
print(f" TOTAL llamadas al modelo: {cost['total_model_calls']}")
print(f" TOTAL tool calls: {cost['total_tool_calls']}")
print(f" TOTAL hops entre agentes: {cost['total_hops']}")
print(f" rondas secuencial={cost['sequential_rounds']} vs. paralelo={cost['parallel_rounds']} "
f"({rounds_saved} rondas menos, {pct}% menos, con {len(plan)} track(s) corriendo a la vez)")
print()
Hasta acá, ni una sola línea distinta de lo que ya ejecutaste en las lecciones 02 a 07. Lo que sigue son las tres demos.
Demo A y Demo B: recapitulando lo ya construido
print("========== DEMO A: Luis (pipeline) ==========")
bb_a = Blackboard()
PLAN_A = [Track(key="book_studio", pattern="pipeline",
description="cotizar Studio pro 3h, validar política de cancelación, confirmar")]
bb_a.write("supervisor", member="Luis")
PIPELINE_STAGES = [
PipelineStage(kind="quote", name="booking_agent", label="cotizar"),
PipelineStage(kind="validate_policy", name="policy_agent", label="validar la política de cancelación"),
PipelineStage(kind="confirm", name="booking_agent", label="confirmar la reserva"),
]
INITIAL_PAYLOAD_A = {"room": "Studio", "tier": "pro", "hours": 3, "member": "Luis"}
script_quote_a = [
{"stop_reason": "tool_use", "content": [{"type": "tool_use", "id": "toolu_01", "name": "get_quote",
"input": {"room": "Studio", "tier": "pro", "hours": 3}}]},
{"stop_reason": "end_turn", "content": [{"type": "text", "text": "Studio pro 3h cuesta 9600 centavos."}]},
]
script_policy_a = [
{"stop_reason": "tool_use", "content": [{"type": "tool_use", "id": "toolu_01", "name": "search_docs",
"input": {"query": "política de cancelación"}}]},
{"stop_reason": "end_turn", "content": [{"type": "text", "text": (
"Puedes cancelar sin cargo hasta 2 horas antes del horario reservado. "
"No hay ningún impedimento para confirmar.")}]},
]
script_confirm_a = [
{"stop_reason": "tool_use", "content": [{"type": "tool_use", "id": "toolu_01", "name": "book_room",
"input": {"room": "Studio", "tier": "pro", "hours": 3, "member": "Luis"}}]},
{"stop_reason": "end_turn", "content": [{"type": "text", "text": "Reservé Studio pro 3h para Luis (confirmación #1)."}]},
]
def job_a_book_studio():
return run_pipeline(PIPELINE_STAGES, [script_quote_a, script_policy_a, script_confirm_a], INITIAL_PAYLOAD_A)
results_a = run_tracks_parallel({"book_studio": job_a_book_studio})
payload_a, trace_a = results_a["book_studio"]
bb_a.write("booking_agent", room=payload_a["room"], tier=payload_a["tier"],
hours=payload_a["hours"], price_cents=payload_a["price_cents"])
bb_a.write("booking_agent", booking_id=payload_a["booking_id"])
print("Blackboard final:", bb_a)
cost_a = coordination_cost(PLAN_A, results_a)
print_cost_report("Demo A (Luis, 1 track, pipeline)", PLAN_A, cost_a)
print("========== DEMO B: Marta (fan-out) ==========")
bb_b = Blackboard()
PLAN_B = [
Track(key="compare_rooms", pattern="fanout", description="comparar Studio y Boardroom pro 2h"),
Track(key="no_show_policy", pattern="fanout", description="pregunta general de no-presentación"),
]
bb_b.write("supervisor", member="Marta")
script_pricing_b = [
{"stop_reason": "tool_use", "content": [
{"type": "tool_use", "id": "toolu_01", "name": "get_quote", "input": {"room": "Studio", "tier": "pro", "hours": 2}},
{"type": "tool_use", "id": "toolu_02", "name": "get_quote", "input": {"room": "Boardroom", "tier": "pro", "hours": 2}},
]},
{"stop_reason": "end_turn", "content": [{"type": "text", "text": (
"Studio pro 2h: 6400 centavos. Boardroom pro 2h: 12800 centavos. Studio es la opción más barata de las dos.")}]},
]
script_no_show_b = [
{"stop_reason": "tool_use", "content": [{"type": "tool_use", "id": "toolu_01", "name": "search_docs",
"input": {"query": "qué pasa si un socio no se presenta a una reserva"}}]},
{"stop_reason": "end_turn", "content": [{"type": "text", "text": (
"Si no te presentas a una reserva confirmada y no cancelas con al menos 2 horas de anticipación, "
"Reservo cobra el 50% del precio cotizado como cargo por no-presentación.")}]},
]
def job_b_compare_rooms():
return run_specialist("pricing_agent", "Compara Studio y Boardroom pro 2h.", script_pricing_b)
def job_b_no_show_policy():
return run_specialist("policy_agent", "¿Qué pasa si un socio no se presenta a una reserva confirmada?", script_no_show_b)
results_b = run_tracks_parallel({"compare_rooms": job_b_compare_rooms, "no_show_policy": job_b_no_show_policy})
print("Blackboard final:", bb_b, "(ningún track escribió -- ninguno reservó nada)")
cost_b = coordination_cost(PLAN_B, results_b)
print_cost_report("Demo B (Marta, 2 tracks, fan-out)", PLAN_B, cost_b)
Qué esperar (idéntico a lo ya visto en las lecciones 03, 04, 06 y 07 de este módulo -- acá corren, por primera vez, en el mismo proceso, una después de la otra):
========== DEMO A: Luis (pipeline) ==========
Blackboard final: Blackboard(member='Luis', room='Studio', tier='pro', hours=3, price_cents=9600, booking_id=1, log=[WriteLogEntry(seq=1, writer='supervisor', field='member', value='Luis'), WriteLogEntry(seq=2, writer='booking_agent', field='room', value='Studio'), WriteLogEntry(seq=3, writer='booking_agent', field='tier', value='pro'), WriteLogEntry(seq=4, writer='booking_agent', field='hours', value=3), WriteLogEntry(seq=5, writer='booking_agent', field='price_cents', value=9600), WriteLogEntry(seq=6, writer='booking_agent', field='booking_id', value=1)])
--- costo de coordinación: Demo A (Luis, 1 track, pipeline) ---
book_studio patrón=pipeline llamadas=6 tools=3 hops=4
TOTAL llamadas al modelo: 8
TOTAL tool calls: 3
TOTAL hops entre agentes: 4
rondas secuencial=8 vs. paralelo=8 (0 rondas menos, 0% menos, con 1 track(s) corriendo a la vez)
========== DEMO B: Marta (fan-out) ==========
Blackboard final: Blackboard(member='Marta', room=None, tier=None, hours=None, price_cents=None, booking_id=None, log=[WriteLogEntry(seq=7, writer='supervisor', field='member', value='Marta')]) (ningún track escribió -- ninguno reservó nada)
--- costo de coordinación: Demo B (Marta, 2 tracks, fan-out) ---
compare_rooms patrón=fanout llamadas=2 tools=2 hops=2
no_show_policy patrón=fanout llamadas=2 tools=1 hops=2
TOTAL llamadas al modelo: 6
TOTAL tool calls: 3
TOTAL hops entre agentes: 4
rondas secuencial=6 vs. paralelo=4 (2 rondas menos, 33% menos, con 2 track(s) corriendo a la vez)
Fíjate en el seq del WriteLogEntry de Marta: seq=7, no seq=1 — el WRITE_SEQ es un
contador de proceso, y ya llevaba 6 escrituras acumuladas de la Demo A cuando empezó la Demo
B, exactamente la misma continuidad que ya confirmó M7 L07 Ejercicio 2.
Demo C: Valentina — pipeline, fan-out y handoff, los tres a la vez
La pieza nueva de esta lección. Valentina hace una petición compuesta que, aplicando el criterio de las tres preguntas de M7 L02, se descompone en tres tracks: una reserva con orden fijo (pipeline), una comparación independiente (fan-out), y una cotización que se topa con una pregunta de política a mitad de camino (handoff).
print()
print("========== DEMO C: Valentina (pipeline + fan-out + handoff) ==========")
REQUEST_C = (
"Valentina (equipo de producto): cotiza y reserva Focus pro 2h para la "
"revisión de diseño de esta tarde, validando la política de cancelación "
"antes de confirmar. Aparte, compara Studio y Boardroom pro 2h por si "
"necesitamos más espacio, y cotiza el Studio pro 4h para el taller del "
"viernes -- dime qué pasa si alguien no llega a ese taller."
)
print(REQUEST_C)
bb_c = Blackboard()
PLAN_C = [
Track(key="book_focus", pattern="pipeline",
description="cotizar Focus pro 2h, validar política de cancelación, confirmar"),
Track(key="compare_rooms", pattern="fanout",
description="comparar Studio y Boardroom pro 2h -- sub-pregunta independiente"),
Track(key="workshop_no_show", pattern="handoff",
description="cotizar Studio pro 4h; ceder el turno si aparece la pregunta de no-presentación"),
]
print("plan del supervisor (concepto):")
for t in PLAN_C:
print(f" [{t.key}] patrón={t.pattern} -- {t.description}")
bb_c.write("supervisor", member="Valentina")
INITIAL_PAYLOAD_C = {"room": "Focus", "tier": "pro", "hours": 2, "member": "Valentina"}
script_quote_c = [
{"stop_reason": "tool_use", "content": [{"type": "tool_use", "id": "toolu_01", "name": "get_quote",
"input": {"room": "Focus", "tier": "pro", "hours": 2}}]},
{"stop_reason": "end_turn", "content": [{"type": "text", "text": "Focus pro 2h cuesta 4000 centavos."}]},
]
script_policy_c = [
{"stop_reason": "tool_use", "content": [{"type": "tool_use", "id": "toolu_01", "name": "search_docs",
"input": {"query": "política de cancelación"}}]},
{"stop_reason": "end_turn", "content": [{"type": "text", "text": (
"Puedes cancelar sin cargo hasta 2 horas antes del horario reservado. "
"No hay ningún impedimento para confirmar.")}]},
]
script_confirm_c = [
{"stop_reason": "tool_use", "content": [{"type": "tool_use", "id": "toolu_01", "name": "book_room",
"input": {"room": "Focus", "tier": "pro", "hours": 2, "member": "Valentina"}}]},
{"stop_reason": "end_turn", "content": [{"type": "text", "text": "Reservé Focus pro 2h para Valentina (confirmación #2)."}]},
]
def job_c_book_focus():
return run_pipeline(PIPELINE_STAGES, [script_quote_c, script_policy_c, script_confirm_c], INITIAL_PAYLOAD_C)
script_pricing_c = [
{"stop_reason": "tool_use", "content": [
{"type": "tool_use", "id": "toolu_01", "name": "get_quote", "input": {"room": "Studio", "tier": "pro", "hours": 2}},
{"type": "tool_use", "id": "toolu_02", "name": "get_quote", "input": {"room": "Boardroom", "tier": "pro", "hours": 2}},
]},
{"stop_reason": "end_turn", "content": [{"type": "text", "text": (
"Studio pro 2h: 6400 centavos. Boardroom pro 2h: 12800 centavos. Studio es la opción más barata de las dos.")}]},
]
def job_c_compare_rooms():
return run_specialist("pricing_agent", "Compara Studio y Boardroom pro 2h.", script_pricing_c)
script_workshop_booking = [
{"stop_reason": "tool_use", "content": [{"type": "tool_use", "id": "toolu_01", "name": "get_quote",
"input": {"room": "Studio", "tier": "pro", "hours": 4}}]},
{"stop_reason": "tool_use", "content": [{"type": "tool_use", "id": "toolu_02", "name": HANDOFF_TOOL_NAME,
"input": {"receiver": "policy_agent", "reason": "la pregunta de no-presentación es política, fuera de mi expertise",
"task": "¿qué pasa si un miembro no se presenta a una reserva confirmada?",
"context": {"room": "Studio", "tier": "pro", "hours": 4, "price_cents": 12800}}}]},
]
script_workshop_policy = [
{"stop_reason": "tool_use", "content": [{"type": "tool_use", "id": "toolu_01", "name": "search_docs",
"input": {"query": "qué pasa si no me presento a mi reserva"}}]},
{"stop_reason": "end_turn", "content": [{"type": "text", "text": (
"Si no te presentas a una reserva confirmada y no cancelas con al menos 2 horas de anticipación, "
"Reservo cobra el 50% del precio cotizado como cargo por no-presentación.")}]},
]
model_scripts_workshop = {"booking_agent": script_workshop_booking, "policy_agent": script_workshop_policy}
def job_c_workshop_no_show():
return run_with_handoff("booking_agent", "Cotiza Studio pro 4h. ¿Qué pasa si no llego?", model_scripts_workshop)
jobs_c = {"book_focus": job_c_book_focus, "compare_rooms": job_c_compare_rooms, "workshop_no_show": job_c_workshop_no_show}
results_c = run_tracks_parallel(jobs_c)
for key in sorted(results_c):
print(f"--- track {key!r} ({[t.pattern for t in PLAN_C if t.key == key][0]}) ---")
if key == "book_focus":
payload, trace = results_c[key]
for step in trace:
print(f" etapa {step['stage']} ({step['label']}) -> {step['agent']}: {step['output']}")
elif key == "compare_rooms":
final, _ = results_c[key]
print(f" respuesta: {final['content'][0]['text']}")
elif key == "workshop_no_show":
final, trace = results_c[key]
print(f" handoff: {trace[0]['agent']} -> {trace[0]['package'].receiver}")
print(f" respuesta final: {final['content'][0]['text']}")
payload_focus_c, _ = results_c["book_focus"]
bb_c.write("booking_agent", room=payload_focus_c["room"], tier=payload_focus_c["tier"],
hours=payload_focus_c["hours"], price_cents=payload_focus_c["price_cents"])
bb_c.write("booking_agent", booking_id=payload_focus_c["booking_id"])
print()
print("Blackboard final:", bb_c)
compare_final_c, _ = results_c["compare_rooms"]
no_show_final_c, no_show_trace_c = results_c["workshop_no_show"]
no_show_ctx_c = no_show_trace_c[0]["package"].context
no_show_charge_c = no_show_ctx_c["price_cents"] // 2
final_response_c = (
f"Reservé Focus pro {bb_c.hours}h para {bb_c.member} ({bb_c.price_cents} "
f"centavos, confirmación #{bb_c.booking_id}) -- ya validé la política de "
f"cancelación. {compare_final_c['content'][0]['text']} Para el taller del "
f"viernes, el Studio pro 4h sale {no_show_ctx_c['price_cents']} centavos; "
f"si alguien no llega, el cargo de no-presentación sería de {no_show_charge_c} centavos."
)
print("respuesta final para Valentina:", final_response_c)
print()
cost_c = coordination_cost(PLAN_C, results_c)
print_cost_report("Demo C (Valentina, 3 tracks, pipeline + fan-out + handoff)", PLAN_C, cost_c)
Qué esperar:
========== DEMO C: Valentina (pipeline + fan-out + handoff) ==========
Valentina (equipo de producto): cotiza y reserva Focus pro 2h para la revisión de diseño de esta tarde, validando la política de cancelación antes de confirmar. Aparte, compara Studio y Boardroom pro 2h por si necesitamos más espacio, y cotiza el Studio pro 4h para el taller del viernes -- dime qué pasa si alguien no llega a ese taller.
plan del supervisor (concepto):
[book_focus] patrón=pipeline -- cotizar Focus pro 2h, validar política de cancelación, confirmar
[compare_rooms] patrón=fanout -- comparar Studio y Boardroom pro 2h -- sub-pregunta independiente
[workshop_no_show] patrón=handoff -- cotizar Studio pro 4h; ceder el turno si aparece la pregunta de no-presentación
--- track 'book_focus' (pipeline) ---
etapa 1 (cotizar) -> booking_agent: Focus pro 2h cuesta 4000 centavos.
etapa 2 (validar la política de cancelación) -> policy_agent: Puedes cancelar sin cargo hasta 2 horas antes del horario reservado. No hay ningún impedimento para confirmar.
etapa 3 (confirmar la reserva) -> booking_agent: Reservé Focus pro 2h para Valentina (confirmación #2).
--- track 'compare_rooms' (fanout) ---
respuesta: Studio pro 2h: 6400 centavos. Boardroom pro 2h: 12800 centavos. Studio es la opción más barata de las dos.
--- track 'workshop_no_show' (handoff) ---
handoff: booking_agent -> policy_agent
respuesta final: Si no te presentas a una reserva confirmada y no cancelas con al menos 2 horas de anticipación, Reservo cobra el 50% del precio cotizado como cargo por no-presentación.
Blackboard final: Blackboard(member='Valentina', room='Focus', tier='pro', hours=2, price_cents=4000, booking_id=2, log=[WriteLogEntry(seq=8, writer='supervisor', field='member', value='Valentina'), WriteLogEntry(seq=9, writer='booking_agent', field='room', value='Focus'), WriteLogEntry(seq=10, writer='booking_agent', field='tier', value='pro'), WriteLogEntry(seq=11, writer='booking_agent', field='hours', value=2), WriteLogEntry(seq=12, writer='booking_agent', field='price_cents', value=4000), WriteLogEntry(seq=13, writer='booking_agent', field='booking_id', value=2)])
respuesta final para Valentina: Reservé Focus pro 2h para Valentina (4000 centavos, confirmación #2) -- ya validé la política de cancelación. Studio pro 2h: 6400 centavos. Boardroom pro 2h: 12800 centavos. Studio es la opción más barata de las dos. Para el taller del viernes, el Studio pro 4h sale 12800 centavos; si alguien no llega, el cargo de no-presentación sería de 6400 centavos.
--- costo de coordinación: Demo C (Valentina, 3 tracks, pipeline + fan-out + handoff) ---
book_focus patrón=pipeline llamadas=6 tools=3 hops=4
compare_rooms patrón=fanout llamadas=2 tools=2 hops=2
workshop_no_show patrón=handoff llamadas=4 tools=3 hops=3
TOTAL llamadas al modelo: 14
TOTAL tool calls: 8
TOTAL hops entre agentes: 9
rondas secuencial=14 vs. paralelo=8 (6 rondas menos, 43% menos, con 3 track(s) corriendo a la vez)
Verifica los tres números de la respuesta final a mano: Focus pro 2h = 2500 * 2 * 80 // 100 = 4000, Studio pro 4h = 4000 * 4 * 80 // 100 = 12800, cargo de no-presentación = 12800 // 2 = 6400. booking_id: 2 porque este proceso ya había reservado el Studio de Luis (Demo A,
booking_id: 1) antes de llegar a la Demo C — Marta (Demo B) nunca reservó nada, así que no
consumió ningún booking_id.
El resumen final: las tres demos, lado a lado
print("========== RESUMEN: costo de coordinación de las tres demos ==========")
print(f"{'demo':8}{'tracks':8}{'llamadas':10}{'tools':8}{'hops':6}{'rondas seq':12}{'rondas par':12}")
for label, plan, cost in [("A", PLAN_A, cost_a), ("B", PLAN_B, cost_b), ("C", PLAN_C, cost_c)]:
print(f"{label:8}{len(plan):<8}{cost['total_model_calls']:<10}"
f"{cost['total_tool_calls']:<8}{cost['total_hops']:<6}"
f"{cost['sequential_rounds']:<12}{cost['parallel_rounds']:<12}")
print()
print("reservas totales en BOOKINGS al cerrar el proceso:", len(rt.BOOKINGS))
Qué esperar:
========== RESUMEN: costo de coordinación de las tres demos ==========
demo tracks llamadas tools hops rondas seq rondas par
A 1 8 3 4 8 8
B 2 6 3 4 6 4
C 3 14 8 9 14 8
reservas totales en BOOKINGS al cerrar el proceso: 2
Tres peticiones, tres formas distintas, tres costos de coordinación distintos — medidos, no
estimados. La Demo A (un solo track de pipeline) no ahorra ninguna ronda porque no tiene con quién
compartir el tiempo de coordinación. La Demo B (dos tracks de fan-out) ahorra un 33%, el mismo
número que M4 L07 ya había medido con la misma estructura. La Demo C (tres tracks, los tres
patrones combinados) ahorra un 43% — el ahorro crece con más tracks corriendo a la vez, siempre
que uno de ellos domine el max() sin que los demás sumen tiempo extra. 2 reservas reales en
todo el proceso (Luis y Valentina) — Marta nunca reservó nada, exactamente como confirmó la
lección 06.
Errores comunes
-
Pensar que la Demo C necesita algún mecanismo nuevo. No lo necesita — es exactamente
run_pipeline,run_specialistyrun_with_handoff, cada uno resolviendo su track, despachados juntos porrun_tracks_parallelsin ningún cambio de firma. -
Ejecutar las tres demos en un orden distinto y esperar los mismos
booking_id. Como en cada mini-proyecto de esta guía, el orden de ejecución determina la numeración — si la Demo C corriera antes que la Demo A, Valentina se quedaría conbooking_id: 1. -
Construir la respuesta final de Valentina antes de que Paso de escritura al Blackboard termine. El
booking_idy elprice_centsde la respuesta final vienen debb_c, DESPUÉS de sus doswrite()— intentar componerla antes usaría datos parciales o unAttributeError. -
Olvidar que el ahorro de rondas no es gratis en llamadas. Las tres demos tienen el MISMO número de llamadas al modelo con o sin concurrencia — el fan-out ahorra tiempo de coordinación (rondas), nunca trabajo real (llamadas, tool calls). Confundir estos dos números fue exactamente el error que M4 L07 advirtió desde su primera lección.
Ejercicios
Ejercicio 1: Confirma tu propia ejecución (Fácil)
Ejecuta el sistema completo de esta lección, las tres demos en el mismo proceso, y confirma línea
por línea que tu salida coincide con la de esta lección — presta especial atención a los
booking_id (1 para Luis, 2 para Valentina) y al seq de la primera escritura de cada
Blackboard (1, 7, 8).
Ver solución
No hay una única "solución de código" para este ejercicio — es una verificación: si tu salida coincide exactamente con el "Qué esperar" de cada sección (salvo cualquier línea informativa de orden de llegada, que puede variar), tu Reservo desechable arrancó limpio y el sistema completo funcionó de punta a punta, con las tres demos compartiendo el mismo proceso.
Ejercicio 2: Agrega una cuarta demo, con un solo track de handoff puro (Medio)
Diseña una cuarta demo —un socio nuevo— cuya petición completa se resuelva con un solo track
de patrón handoff, sin ningún pipeline ni fan-out adicional (como el caso de Camila en M7 L08).
Ejecútala como parte del mismo proceso y mide su costo de coordinación con coordination_cost.
Ver solución
print()
print("========== DEMO D: Rodrigo (SOLO handoff) ==========")
bb_d = Blackboard()
PLAN_D = [Track(key="quote_no_show", pattern="handoff",
description="cotizar Boardroom pro 1h; ceder el turno si aparece la pregunta de no-presentación")]
bb_d.write("supervisor", member="Rodrigo")
script_rodrigo_booking = [
{"stop_reason": "tool_use", "content": [{"type": "tool_use", "id": "toolu_01", "name": "get_quote",
"input": {"room": "Boardroom", "tier": "pro", "hours": 1}}]},
{"stop_reason": "tool_use", "content": [{"type": "tool_use", "id": "toolu_02", "name": HANDOFF_TOOL_NAME,
"input": {"receiver": "policy_agent", "reason": "pregunta de no-presentación, fuera de mi expertise",
"task": "¿qué pasa si un miembro no se presenta a una reserva confirmada?",
"context": {"room": "Boardroom", "tier": "pro", "hours": 1, "price_cents": 6400}}}]},
]
model_scripts_rodrigo = {"booking_agent": script_rodrigo_booking, "policy_agent": script_workshop_policy}
def job_d_quote_no_show():
return run_with_handoff("booking_agent", "Cotiza Boardroom pro 1h. ¿Qué pasa si no llego?", model_scripts_rodrigo)
results_d = run_tracks_parallel({"quote_no_show": job_d_quote_no_show})
cost_d = coordination_cost(PLAN_D, results_d)
print_cost_report("Demo D (Rodrigo, 1 track, handoff puro)", PLAN_D, cost_d)
Salida esperada (continuando el mismo proceso, después de las Demos A, B y C):
========== DEMO D: Rodrigo (SOLO handoff) ==========
--- costo de coordinación: Demo D (Rodrigo, 1 track, handoff puro) ---
quote_no_show patrón=handoff llamadas=4 tools=3 hops=3
TOTAL llamadas al modelo: 6
TOTAL tool calls: 3
TOTAL hops entre agentes: 3
rondas secuencial=6 vs. paralelo=6 (0 rondas menos, 0% menos, con 1 track(s) corriendo a la vez)
Explicación: Boardroom pro 1h = 8000 * 1 * 80 // 100 = 6400 centavos. Con un solo track, otra
vez sin ningún ahorro de rondas (la misma conclusión de la Demo A) — pero con hops=3 en vez de
hops=4, porque un handoff (DISPATCH_HOPS + 1) es un hop más barato que un pipeline de tres
etapas (DISPATCH_HOPS + 2), la diferencia exacta que predijo la tabla de la lección 07.
Ejercicio 3: ¿Por qué la Demo C ahorra más porcentaje de rondas que la Demo B? (Difícil)
La Demo B ahorra 33% de rondas; la Demo C ahorra 43%. Sin ejecutar código nuevo, explica —usando
los números ya calculados de call_counts de cada demo ([6] para A, [2, 2] para B, [6, 2, 4]
para C)— por qué agregar un tercer track (con 4 llamadas internas) a la Demo C aumenta el
porcentaje de ahorro en vez de diluirlo.
Ver solución
El porcentaje de ahorro depende de qué tan lejos está el sum() del max(), relativo al sum()
—no del número de tracks en sí—. Para la Demo B: sum([2,2])=4, max([2,2])=2 — el paralelo
"absorbe" la mitad del trabajo secuencial de los tracks (sin contar surrounding). Para la Demo C:
sum([6,2,4])=12, max([6,2,4])=6 — el paralelo también absorbe la mitad del trabajo de los
tracks. La diferencia real está en el surrounding (PLAN_CALLS + COMPOSE_CALLS = 2, fijo,
siempre): para la Demo B, ese 2 fijo es una porción más grande del total secuencial (6 de
8 iniciales, antes de sumar surrounding → sequential=6+2=8... pero cuidado, hay que
recalcular con los números reales impresos: sequential_B=6, parallel_B=4, ahorro 2/6=33%;
sequential_C=14, parallel_C=8, ahorro 6/14=43%). Cuantos más tracks corren a la vez, y cuanto
más grande es el track dominante relativo a los demás, más rondas "se esconden" detrás del max()
sin sumar al secuencial — la Demo C tiene un track de 6 llamadas que domina sobre dos tracks más
chicos (2 y 4), y esos dos tracks más chicos son pura ganancia de paralelismo, sin costar nada
extra en rondas. Esto confirma el principio general de M4 L07: el ahorro de fan-out crece con más
sub-tareas independientes, siempre que ninguna sea más grande que la que ya domina el max().
Cierre de la guía
Ocho módulos, un solo sistema construido de punta a punta, sin ningún framework. Vale la pena mirar hacia atrás antes de mirar hacia adelante.
Lo que construiste
M1 -- El framework de decisión: cuándo multi-agente vale la pena, medido
con números reales (3 vs. 5 llamadas para una tarea que no lo necesitaba).
M2 -- El supervisor/router: un coordinador que decide a qué especialista
delegar -- ruteo determinista y ruteo por decisión del modelo.
M3 -- El pipeline: orden fijo, sin decisión de ruteo en cada paso, cuando
la tarea SIEMPRE necesita los mismos pasos en el mismo orden.
M4 -- El fan-out: sub-tareas independientes, corriendo a la vez, con
agregación determinista de resultados -- sin importar el orden de
finalización de los hilos.
M5 -- El handoff: un agente que cede el turno directamente a mitad de
tarea, con un paquete mínimo de transferencia -- sin volver a un
supervisor externo.
M6 -- El blackboard: estado compartido de proceso, con un log de quién
escribió qué -- la alternativa a pasar mensajes punto a punto.
M7 -- La composición: los cinco patrones anteriores, combinados sobre una
petición real, con el criterio de las tres preguntas para decidir
cuándo combinar cuáles.
M8 -- El capstone: el sistema completo, entregado, con el conteo de costo
de coordinación que M1 prometió medir -- generalizado a cualquier
combinación de patrones.
Cada patrón se construyó desde cero, en Python 3.14 puro, sin LangChain, sin LangGraph, sin
ningún framework de orquestación — la orquestación real (el router, el paso de mensajes, el
blackboard, el fan-out y su agregación, el conteo de costo) se ejecutó de verdad en cada lección,
y su salida se citó. Las decisiones de cada agente fueron siempre concepto, con guiones realistas
de claude-sonnet-5 — nunca una llamada real a una API. Entiendes ahora, desde adentro, qué hace
un framework de orquestación por debajo antes de decidir si adoptar uno.
A dónde seguir
Este sistema, tal como quedó construido, tiene límites deliberados — cada uno apunta a la guía
correcta del ecosistema agentic-engineering-ecosystem:
-
El
Blackboardvive en memoria y se pierde cuando el proceso termina. Si Reservo necesitara que un miembro nuevo del equipo, mañana, en una sesión completamente distinta, pudiera consultar qué se decidió hoy, hace falta un mecanismo de persistencia real —un store en disco, escribir, leer, expirar—. Ese mecanismo es el terreno deagent-memory-and-state-guide. -
Cada agente de este sistema corrió con un guion corto, sin preocuparse por el tamaño de su ventana de contexto. Un sistema de producción, con conversaciones más largas y más tools por agente, necesita presupuestar y comprimir qué entra en el
system/messagesde cada llamada — compaction, few-shot, context rot. Eso escontext-engineering-guide. -
Ningún agente de esta guía desconfió nunca de otro. El
Blackboardque cualquier especialista puede escribir, elcontextde un handoff que el receptor acepta sin verificar — toda esa superficie asume buena fe. Un sistema real necesita defenderse de un agente que miente, de un blackboard envenenado, de una instrucción maliciosa incrustada en un handoff. Eso esagent-security-and-sandboxing-guide. -
El conteo de costo de esta guía es simple, a propósito —llamadas, hops, rondas, sin trazas, sin rollbacks, sin costos reales de tokens—. Medir un sistema multi-agente en producción, con datos reales, evaluaciones automatizadas y observabilidad, es el terreno de
evaluation-frameworks-guideyagents-in-production-guide. -
Y si este sistema tuviera que escalar a producción real, con más agentes, más estado, y equipos distintos manteniendo cada especialista, hay frameworks que resuelven buena parte de esta orquestación por ti —LangGraph y CrewAI son los más conocidos— y un protocolo emergente para que agentes de organizaciones distintas se comuniquen entre sí, A2A (Agent2Agent protocol). Ninguno de los dos se construyó en esta guía —ninguno se necesita para entender los patrones que ya construiste a mano—, pero ahora que sabes qué hacen por debajo, adoptarlos es una decisión informada, no un salto a ciegas.
No hace falta memorizar cuál guía resuelve cuál límite — alcanza con saber que existen, y que cada límite de este sistema tiene un nombre y un lugar dentro del ecosistema. Construiste, con tus propias manos, cinco patrones de coordinación multi-agente, un sistema completo que los combina, y la disciplina de medir su costo antes de asumir que valía la pena. Eso es lo que se lleva de acá.
Recursos adicionales
- Anthropic — Building effective agents — El principio que sostuvo toda la guía: empezar con el mecanismo más simple que resuelva la tarea, y solo agregar coordinación cuando el trabajo genuinamente la necesita.
- Anthropic — Multi-agent research system — Un sistema de producción real de Anthropic donde los cinco patrones de esta guía —o su equivalente— conviven sobre tareas complejas, la referencia que acompañó cada módulo.
- Anthropic — Agent SDK overview — Cómo se ve, en código de producción con llamadas reales, un sistema que aplica los mismos patrones que construiste a mano en esta guía.
- Anthropic — Messages API reference — La forma exacta de
tool_use/tool_result/stop_reasonque cada agente de este sistema completo siguió respetando, sin cambios, desde la primera lección deagent-fundamentals-and-tool-calling-guidehasta la última de esta guía.