Módulo 7: Orchestrating The Full Reservo System
La corrida completa, ejecutada de punta a punta
Descripción
Las seis lecciones anteriores construyeron cada pieza de la composición por separado: el criterio de decisión (02), el pipeline como una pieza (03), el fan-out generalizado a callables (04), el handoff adentro de una rama del fan-out (05), y el blackboard escrito solo fuera de la sección paralela (06). Esta lección las junta todas, de una sola vez, sobre la petición completa de Ana: los tres patrones corriendo juntos, el blackboard compartido, y —lo único genuinamente nuevo de esta lección— la respuesta final compuesta que Ana recibe, construida combinando lo que produjeron los tres tracks.
No hay ninguna pieza de bajo nivel nueva en esta lección. Es la primera vez que ves el sistema completo funcionando de principio a fin: supervisor, pipeline, fan-out, handoff y blackboard, sobre una sola petición, 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 06:
Track, run_pipeline, run_tracks_parallel, run_with_handoff, Blackboard. Lo único nuevo es
la composición de la respuesta final —que combina el payload del pipeline, la respuesta de
pricing_agent, y el context del handoff— y el resumen de cierre que confirma, con la corrida ya
terminada, qué patrón resolvió cada parte de la petición original. El Módulo 8 retoma exactamente
esta misma corrida como la base del sistema completo que entrega como capstone.
Analogía: el pedido completo, servido
Cierra la escena del restaurante que abrió este módulo. El maître ya decidió el plan (lección 02). La línea de montaje ya cotizó, validó y confirmó la reserva principal (lección 03). Los dos mozos ya resolvieron sus preguntas independientes a la vez (lección 04), y uno de ellos ya llamó al sommelier a mitad de camino (lección 05). La pizarra de la cocina ya tiene anotado lo que importa (lección 06). Lo único que falta —y es lo que hace esta lección— es que alguien junte todo eso en una sola respuesta y se la lleve a la mesa: no tres papeles sueltos, una sola frase coherente que responde exactamente lo que Ana preguntó, en el mismo orden en que lo preguntó.
Ejemplo trabajado: la corrida 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)
# ---- M3: pipeline (sin cambios) ----
@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 (sin cambios) ----
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 (sin cambios) ----
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)
)
# ---- Módulo 7: la capa de composición (lección 02 + 04) ----
@dataclass
class Track:
key: str
pattern: str # "pipeline" | "fanout" | "handoff" -- ningún patrón nuevo, solo la etiqueta
description: str
def run_tracks_parallel(jobs):
"""jobs: dict {track_key: callable sin argumentos}. Mismo mecanismo de
ThreadPoolExecutor + as_completed que run_fanout_parallel (M4), pero
despachando un callable arbitrario en vez de siempre run_specialist."""
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
print("--- petición compuesta ---")
REQUEST = (
"Ana (equipo de diseño): cotiza y reserva Focus pro 3h para el lanzamiento, "
"validando la política de cancelación antes de confirmar. Aparte, compara "
"Studio y Boardroom pro 3h por si necesitamos más espacio, y de paso cotiza "
"el Boardroom pro 2h para la reunión de cierre -- dime también qué pasa si "
"alguien del equipo no llega a esa reunión."
)
print(REQUEST)
bb = Blackboard()
print()
print("=== Paso 1 (concepto, supervisor): decide el plan de tres tracks ===")
PLAN = [
Track(key="book_focus", pattern="pipeline",
description="cotizar Focus pro 3h, validar política de cancelación, confirmar"),
Track(key="compare_rooms", pattern="fanout",
description="comparar Studio y Boardroom pro 3h -- sub-pregunta independiente"),
Track(key="boardroom_no_show", pattern="handoff",
description="cotizar Boardroom pro 2h; si aparece la pregunta de no-presentación, ceder el turno"),
]
for t in PLAN:
print(f" [{t.key:<18}] patrón={t.pattern:<9} -- {t.description}")
bb.write("supervisor", member="Ana")
print(f"supervisor escribe member={bb.member!r} (antes de abrir los tracks en paralelo)")
# --- Track 1: pipeline (M3, sin cambios) ---
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 = {"room": "Focus", "tier": "pro", "hours": 3, "member": "Ana"}
model_script_quote = [
{"stop_reason": "tool_use", "content": [
{"type": "tool_use", "id": "toolu_01", "name": "get_quote",
"input": {"room": "Focus", "tier": "pro", "hours": 3}}]},
{"stop_reason": "end_turn", "content": [
{"type": "text", "text": "Focus pro 3h cuesta 6000 centavos."}]},
]
model_script_policy = [
{"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."
)}]},
]
model_script_confirm = [
{"stop_reason": "tool_use", "content": [
{"type": "tool_use", "id": "toolu_01", "name": "book_room",
"input": {"room": "Focus", "tier": "pro", "hours": 3, "member": "Ana"}}]},
{"stop_reason": "end_turn", "content": [
{"type": "text", "text": "Reservé Focus pro 3h para Ana (confirmación #1)."}]},
]
def job_book_focus():
return run_pipeline(
PIPELINE_STAGES,
[model_script_quote, model_script_policy, model_script_confirm],
INITIAL_PAYLOAD,
)
# --- Track 2: fan-out plano (M4, un solo especialista) ---
model_script_pricing = [
{"stop_reason": "tool_use", "content": [
{"type": "tool_use", "id": "toolu_01", "name": "get_quote",
"input": {"room": "Studio", "tier": "pro", "hours": 3}},
{"type": "tool_use", "id": "toolu_02", "name": "get_quote",
"input": {"room": "Boardroom", "tier": "pro", "hours": 3}},
]},
{"stop_reason": "end_turn", "content": [
{"type": "text", "text": (
"Studio pro 3h: 9600 centavos. Boardroom pro 3h: 19200 centavos. "
"Studio es la opción más barata de las dos."
)}]},
]
def job_compare_rooms():
return run_specialist("pricing_agent", "Compara Studio y Boardroom pro 3h.", model_script_pricing)
# --- Track 3: handoff (M5, sin cambios) dentro de una rama del fan-out ---
model_script_boardroom_booking = [
{"stop_reason": "tool_use", "content": [
{"type": "tool_use", "id": "toolu_01", "name": "get_quote",
"input": {"room": "Boardroom", "tier": "pro", "hours": 2}}]},
{"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": "Boardroom", "tier": "pro", "hours": 2, "price_cents": 12800},
}}]},
]
model_script_boardroom_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_boardroom = {
"booking_agent": model_script_boardroom_booking,
"policy_agent": model_script_boardroom_policy,
}
def job_boardroom_no_show():
return run_with_handoff("booking_agent", "Cotiza Boardroom pro 2h. ¿Qué pasa si no llego?", model_scripts_boardroom)
print()
print("=== Paso 2 (ejecutado): los tres tracks corren a la vez, con ThreadPoolExecutor ===")
jobs = {
"book_focus": job_book_focus,
"compare_rooms": job_compare_rooms,
"boardroom_no_show": job_boardroom_no_show,
}
raw_results = run_tracks_parallel(jobs)
print("claves según llegaron (informativo, NO se usa para imprimir):", list(raw_results.keys()))
print()
print("=== Paso 3: resultado de cada track, SIEMPRE impreso en orden fijo (sorted por key) ===")
for key in sorted(raw_results):
print(f"--- track {key!r} ({[t.pattern for t in PLAN if t.key == key][0]}) ---")
if key == "book_focus":
payload, trace = raw_results[key]
for step in trace:
print(f" etapa {step['stage']} ({step['label']}) -> {step['agent']}: {step['output']}")
print(f" payload final: {payload}")
elif key == "compare_rooms":
final, history = raw_results[key]
print(f" respuesta: {final['content'][0]['text']}")
elif key == "boardroom_no_show":
final, trace = raw_results[key]
print(f" handoff: {trace[0]['agent']} -> {trace[0]['package'].receiver}")
print(f" respuesta final (de {trace[-1]['agent']}): {final['content'][0]['text']}")
print()
print("=== Paso 4 (ejecutado, tras cerrar el pool -- sin escrituras concurrentes): "
"booking_agent escribe la reserva real al Blackboard ===")
payload_focus, _ = raw_results["book_focus"]
bb.write("booking_agent", room=payload_focus["room"], tier=payload_focus["tier"],
hours=payload_focus["hours"], price_cents=payload_focus["price_cents"])
bb.write("booking_agent", booking_id=payload_focus["booking_id"])
print(f"booking_agent escribió room={bb.room!r} tier={bb.tier!r} hours={bb.hours!r} "
f"price_cents={bb.price_cents!r} booking_id={bb.booking_id!r}")
print("compare_rooms y boardroom_no_show NO escriben al Blackboard -- son cotizaciones de "
"comparación, no la reserva de registro (misma regla que pricing_agent en el Módulo 6).")
print()
print("--- estado final del Blackboard ---")
print(bb)
print()
print("--- el log completo de writes (todos ocurrieron FUERA de la sección paralela) ---")
for entry in bb.log:
print(f" #{entry.seq} {entry.writer:<14} escribió {entry.field}={entry.value!r}")
print()
print("--- respuesta final compuesta para Ana (concepto, grounded en los tres tracks) ---")
compare_final, _ = raw_results["compare_rooms"]
no_show_final, no_show_trace = raw_results["boardroom_no_show"]
no_show_context = no_show_trace[0]["package"].context
no_show_charge = no_show_context["price_cents"] // 2
final_response = (
f"Reservé Focus pro {bb.hours}h para {bb.member} "
f"({bb.price_cents} centavos, confirmación #{bb.booking_id}) -- ya validé "
f"la política de cancelación. {compare_final['content'][0]['text']} Para la "
f"reunión de cierre, el Boardroom pro 2h sale {no_show_context['price_cents']} "
f"centavos; si alguien no llega, el cargo de no-presentación sería de "
f"{no_show_charge} centavos."
)
print(final_response)
print()
print("--- resumen: qué patrón resolvió cada parte de la petición ---")
for t in PLAN:
print(f" {t.key:<18} -> {t.pattern:<9} -- {t.description}")
Qué esperar (corrida real; la línea de "claves según llegaron" puede variar entre ejecuciones — el resto, no):
--- petición compuesta ---
Ana (equipo de diseño): cotiza y reserva Focus pro 3h para el lanzamiento, validando la política de cancelación antes de confirmar. Aparte, compara Studio y Boardroom pro 3h por si necesitamos más espacio, y de paso cotiza el Boardroom pro 2h para la reunión de cierre -- dime también qué pasa si alguien del equipo no llega a esa reunión.
=== Paso 1 (concepto, supervisor): decide el plan de tres tracks ===
[book_focus ] patrón=pipeline -- cotizar Focus pro 3h, validar política de cancelación, confirmar
[compare_rooms ] patrón=fanout -- comparar Studio y Boardroom pro 3h -- sub-pregunta independiente
[boardroom_no_show ] patrón=handoff -- cotizar Boardroom pro 2h; si aparece la pregunta de no-presentación, ceder el turno
supervisor escribe member='Ana' (antes de abrir los tracks en paralelo)
=== Paso 2 (ejecutado): los tres tracks corren a la vez, con ThreadPoolExecutor ===
claves según llegaron (informativo, NO se usa para imprimir): ['boardroom_no_show', 'book_focus', 'compare_rooms']
=== Paso 3: resultado de cada track, SIEMPRE impreso en orden fijo (sorted por key) ===
--- track 'boardroom_no_show' (handoff) ---
handoff: booking_agent -> policy_agent
respuesta final (de policy_agent): 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.
--- track 'book_focus' (pipeline) ---
etapa 1 (cotizar) -> booking_agent: Focus pro 3h cuesta 6000 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 3h para Ana (confirmación #1).
payload final: {'room': 'Focus', 'tier': 'pro', 'hours': 3, 'member': 'Ana', 'price_cents': 6000, 'cancellation_policy': '[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.', 'cleared_to_book': True, 'booking_id': 1, 'confirmed': True}
--- track 'compare_rooms' (fanout) ---
respuesta: Studio pro 3h: 9600 centavos. Boardroom pro 3h: 19200 centavos. Studio es la opción más barata de las dos.
=== Paso 4 (ejecutado, tras cerrar el pool -- sin escrituras concurrentes): booking_agent escribe la reserva real al Blackboard ===
booking_agent escribió room='Focus' tier='pro' hours=3 price_cents=6000 booking_id=1
compare_rooms y boardroom_no_show NO escriben al Blackboard -- son cotizaciones de comparación, no la reserva de registro (misma regla que pricing_agent en el Módulo 6).
--- estado final del Blackboard ---
Blackboard(member='Ana', room='Focus', tier='pro', hours=3, price_cents=6000, booking_id=1, log=[WriteLogEntry(seq=1, writer='supervisor', field='member', value='Ana'), WriteLogEntry(seq=2, writer='booking_agent', field='room', value='Focus'), 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=6000), WriteLogEntry(seq=6, writer='booking_agent', field='booking_id', value=1)])
--- el log completo de writes (todos ocurrieron FUERA de la sección paralela) ---
#1 supervisor escribió member='Ana'
#2 booking_agent escribió room='Focus'
#3 booking_agent escribió tier='pro'
#4 booking_agent escribió hours=3
#5 booking_agent escribió price_cents=6000
#6 booking_agent escribió booking_id=1
--- respuesta final compuesta para Ana (concepto, grounded en los tres tracks) ---
Reservé Focus pro 3h para Ana (6000 centavos, confirmación #1) -- ya validé la política de cancelación. Studio pro 3h: 9600 centavos. Boardroom pro 3h: 19200 centavos. Studio es la opción más barata de las dos. Para la reunión de cierre, el Boardroom pro 2h sale 12800 centavos; si alguien no llega, el cargo de no-presentación sería de 6400 centavos.
--- resumen: qué patrón resolvió cada parte de la petición ---
book_focus -> pipeline -- cotizar Focus pro 3h, validar política de cancelación, confirmar
compare_rooms -> fanout -- comparar Studio y Boardroom pro 3h -- sub-pregunta independiente
boardroom_no_show -> handoff -- cotizar Boardroom pro 2h; si aparece la pregunta de no-presentación, ceder el turno
Verifica los tres números de la respuesta final a mano: Focus pro 3h = 2500 * 3 * 80 // 100 = 6000, Boardroom pro 2h = 8000 * 2 * 80 // 100 = 12800, cargo de no-presentación = 12800 // 2 = 6400. Los tres salen de tools reales (get_quote, ejecutado), no de texto libre inventado por
ningún guion — la única parte "concepto" de todo este párrafo es la decisión del supervisor de
armar el plan de tres tracks (Paso 1) y la redacción final que los combina.
Lo que esta corrida demuestra, junto
Una sola petición de Reservo, resuelta en una sola llamada a un puñado de funciones ya construidas, sin ningún patrón nuevo:
book_focus -> PIPELINE (M3): 3 etapas, orden fijo, la política se valida
antes de confirmar -- run_pipeline, sin cambios.
compare_rooms -> FAN-OUT (M4): una sub-pregunta independiente, resuelta con
una sola llamada, corriendo A LA VEZ que las otras dos --
run_tracks_parallel, generalización mínima de M4.
boardroom_no_show -> HANDOFF (M5), DENTRO de una rama del fan-out: booking_agent
cotiza, se topa con una pregunta de política, cede el turno
a policy_agent -- run_with_handoff, sin cambios.
(sobre todo lo anterior) BLACKBOARD (M6): el supervisor escribe member ANTES de
abrir los tracks; booking_agent escribe los hechos reales
de la reserva DESPUÉS de que el pool se cierra -- nunca
durante la sección paralela.
Y la respuesta final —una sola frase para Ana— combina información de los tres tracks: el
precio y la confirmación de book_focus, la comparación de compare_rooms, el precio y el cargo
de no-presentación de boardroom_no_show. Ningún track por sí solo tenía toda esa información — la
composición final es lo que la junta, igual que ya viste en el Módulo 3 (la síntesis del pipeline) y
el Módulo 5 (la síntesis grounded en el context del handoff), ahora combinando tres fuentes en
vez de una o dos.
Errores comunes
-
Pensar que esta lección introduce algún mecanismo nuevo de coordinación. No lo hace — cada pieza de este script es una función ya construida en un módulo anterior, sin modificar. Lo único nuevo es el orden en que se llaman juntas y la respuesta final que las combina.
-
Construir la respuesta final ANTES del Paso 4. El cargo de no-presentación y el precio de Focus vienen de
bb(después de las escrituras del Paso 4) y deraw_results(después del Paso 2) — intentar componer la respuesta antes de que ambos existan fallaría con unNameErroro, peor, usaría datos parciales. -
Olvidar verificar los tres números a mano.
6000,12800,6400no son literales sueltos en el guion — cada uno sale de una tool real (get_quote) ejecutada, y se puede recalcular con la fórmula de descuento pro (base * 80 // 100) del Módulo 1. -
Ejecutar esta lección en un proceso que ya tenía reservas. Si
booking_idno sale1, el proceso ya corrió otro ejemplo antes — esta lección, como todas, asume un Reservo desechable, recién iniciado.
Ejercicios
Ejercicio 1: Confirma tu propia ejecución (Fácil)
Ejecuta la corrida completa de esta lección tú mismo y confirma, línea por línea, que tu salida
coincide con el "Qué esperar". Presta especial atención a que el log del Blackboard salga
idéntico al de arriba, aunque la línea de "claves según llegaron" pueda variar.
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" del ejemplo trabajado (salvo la línea informativa de orden de llegada), tu Reservo desechable arrancó limpio y la corrida completa funcionó de punta a punta.
Ejercicio 2: Corre la misma petición dos veces seguidas, en el mismo proceso (Medio)
Ejecuta el script completo de esta lección dos veces, dentro del mismo proceso (por ejemplo, dentro
de una función run_scenario() que llamas dos veces). Confirma qué cambia entre la primera y la
segunda corrida, y qué se mantiene igual.
Ver solución
def run_scenario():
bb_local = Blackboard()
bb_local.write("supervisor", member="Ana")
jobs_local = {
"book_focus": job_book_focus,
"compare_rooms": job_compare_rooms,
"boardroom_no_show": job_boardroom_no_show,
}
results_local = run_tracks_parallel(jobs_local)
payload_local, _ = results_local["book_focus"]
bb_local.write("booking_agent", room=payload_local["room"], tier=payload_local["tier"],
hours=payload_local["hours"], price_cents=payload_local["price_cents"])
bb_local.write("booking_agent", booking_id=payload_local["booking_id"])
return bb_local
bb_run1 = run_scenario()
bb_run2 = run_scenario()
print("booking_id corrida 1:", bb_run1.booking_id)
print("booking_id corrida 2:", bb_run2.booking_id)
print("seq del primer write de la corrida 2:", bb_run2.log[0].seq)
Salida esperada (continuando sobre el proceso ya usado en el ejemplo trabajado, así que el conteo sigue desde donde estaba):
booking_id corrida 1: 2
booking_id corrida 2: 3
seq del primer write de la corrida 2: 7
Explicación: booking_id y WRITE_SEQ son contadores (itertools.count) a nivel de módulo
—reservo_tools._booking_ids y el WRITE_SEQ de este script—, no a nivel de corrida individual.
Cada Blackboard() nuevo empieza con log=[] vacío, pero el seq de su primera entrada CONTINÚA
desde donde se quedó el contador global, no reinicia en 1. Esto confirma, ejecutando, que "un
Reservo desechable, recién iniciado" significa un proceso nuevo — no simplemente una instancia
nueva de Blackboard() dentro del mismo proceso.
Ejercicio 3: ¿Qué pasaría si Ana solo pidiera dos de las tres partes? (Difícil)
Sin ejecutar código todavía, imagina que la petición de Ana solo tuviera las primeras dos partes
—book_focus y compare_rooms, sin la pregunta del Boardroom para el cierre—. Explica qué cambiaría
en PLAN, en jobs, en la respuesta final compuesta, y qué se mantendría exactamente igual en
run_tracks_parallel y en el Blackboard.
Ver solución
PLAN tendría dos Track en vez de tres —sin boardroom_no_show—; jobs tendría dos entradas en
vez de tres. La respuesta final compuesta perdería la última oración (la del Boardroom y el cargo de
no-presentación), porque no habría ningún no_show_context del cual leer price_cents. Lo que no
cambiaría en absoluto: run_tracks_parallel sigue funcionando exactamente igual con dos jobs que con
tres —su firma no depende de ningún número fijo de tracks—, y el Blackboard seguiría escribiéndose
de la misma forma (member antes, los hechos de la reserva después), porque ninguna de esas dos
escrituras depende de que exista el tercer track. Esto confirma, sin necesidad de ejecutar nada, que
el mecanismo de composición de este módulo escala hacia abajo (menos tracks) tan bien como escala
hacia arriba (más tracks, como en el Ejercicio 2 de la lección 04) — la generalización de la lección
04 no tiene ningún número de tracks hardcodeado en ningún punto.
Resumen y siguiente paso
- Los tres
TrackdePLANcorrieron juntos, conrun_tracks_parallel, sin que ningún mecanismo necesitara modificarse:run_pipeline(M3) resolvióbook_focus,run_specialist(M2) resolviócompare_rooms,run_with_handoff(M5) resolvióboardroom_no_show. - Los números reales:
Focus pro 3h = 6000centavos,Boardroom pro 2h = 12800centavos, cargo de no-presentación= 6400centavos — los tres verificables a mano con la fórmula de descuento pro del Módulo 1. - El
Blackboardse escribió dos veces —antes y después de la sección paralela, nunca durante— y sulogsalió idéntico en cada corrida, confirmando la garantía de la lección 06 sobre la petición completa. - La respuesta final combinó las tres fuentes en una sola frase — ninguna de las tres, por separado, tenía toda la información que Ana necesitaba.
- Ningún patrón nuevo se usó en ningún momento de este módulo. Supervisor, pipeline, fan-out, handoff, blackboard — los cinco de los Módulos 2 a 6, combinados sobre una sola petición real.
Siguiente lección: 08 — Mini-proyecto: tres peticiones compuestas para Reservo. Aplicas exactamente este mismo criterio a tres peticiones nuevas — una que necesita los tres patrones, como la de Ana; una que necesita solo dos; y una que necesita solo uno.
Recursos adicionales
- Anthropic — Building effective agents — El principio de componer mecanismos simples ya probados, en vez de diseñar uno nuevo para cada petición — el resultado completo de este módulo.
- Anthropic — Multi-agent research system — Un orquestador real donde varios mecanismos de coordinación —secuencial, paralelo, delegación, estado compartido— conviven sobre la misma tarea compleja, exactamente como en esta corrida.
- Anthropic — Messages API reference — La forma exacta de
tool_use/tool_result/stop_reasonque cada agente de esta corrida sigue respetando, sin cambios, en los tres patrones a la vez. - Python —
concurrent.futures— El módulo detrás derun_tracks_parallel, el único mecanismo genuinamente nuevo de todo el módulo, y aun así una generalización mínima de algo que ya conocías.