Módulo 7: Orchestrating The Full Reservo System
Mini-proyecto: tres peticiones compuestas para Reservo
Descripción
Seis lecciones te dejaron con el criterio completo de composición: las tres preguntas que deciden
qué patrón le corresponde a cada parte de una petición (02), el pipeline como una pieza (03), el
fan-out generalizado a cualquier callable (04), un handoff dentro de una rama del fan-out (05), el
blackboard escrito solo fuera de la sección paralela (06), y la corrida completa de la petición de
Ana, de punta a punta (07). Este mini-proyecto no agrega ningún concepto nuevo — te da tres
peticiones de Reservo que nunca viste, con formas genuinamente distintas entre sí, y te pide aplicar
el criterio completo del módulo: cuántos tracks necesita cada una, con qué patrón se resuelve cada
uno, y qué queda escrito en el Blackboard al final.
El entregable de esta lección es el sistema de composición completo —Track, run_pipeline,
run_tracks_parallel, run_with_handoff, Blackboard— corriendo contra los tres escenarios del
encargo, más el juicio para reconocer que no todas las peticiones compuestas necesitan los tres
patrones a la vez — una lección que ya adelantó el Ejercicio 2 de la lección 02, ahora confirmada
sobre dos casos nuevos, ejecutados.
Conexión con el módulo
Este mini-proyecto es la síntesis de las seis lecciones anteriores, no una lección nueva. De la 02
usas el criterio de las tres preguntas. De la 03, run_pipeline sin ningún cambio. De la 04,
run_tracks_parallel. De la 05, run_with_handoff dentro de una rama del fan-out. De la 06, la
disciplina de escribir al Blackboard solo fuera de la sección paralela. De la 07, la forma
completa de una corrida de punta a punta con su respuesta final compuesta. Cuando termines, el
Módulo 8 toma este mismo sistema y lo entrega como el capstone completo de la guía, con su
propia demo de al menos dos peticiones de forma distinta y el conteo de costo de coordinación de
cada una.
El encargo
Reservo te pasa tres peticiones que llegaron la misma semana, de tres socios distintos:
Escenario 1 (Sofía): "Cotiza y reserva Boardroom pro 4h para la reunión trimestral, validando
la política de cancelación antes de confirmar. Aparte, compara Focus y Studio pro 4h por si
el equipo crece, y cotiza el Focus pro 2h para la sesión de feedback -- avísame qué pasa si
alguien no llega a esa sesión."
Escenario 2 (Diego): "Reserva el Focus pro 2h para la entrevista de mañana, validando la
política de cancelación antes de confirmar. Aparte, ¿qué pasa si un candidato no llega a una
entrevista reservada?"
Escenario 3 (Camila): "Cotiza el Studio pro 3h para el webinar. Y aparte, ¿qué pasa si no
llego a esa reserva?"
Tu encargo tiene dos entregables, para cada escenario:
a) Aplica el criterio de la lección 02 y decide cuántos tracks necesita cada petición, y con qué patrón se resuelve cada uno — antes de escribir ningún código de ejecución.
b) Corre el sistema completo sobre los tres escenarios, en el mismo proceso (uno después del
otro), y confirma el estado final del Blackboard para cada uno que involucre una reserva real.
La solución completa (el entregable)
Ver la solución completa
import ast
import concurrent.futures
import itertools
from dataclasses import dataclass, field
import reservo_tools as rt
def dispatch_parallel(tool_use_blocks, tools):
with concurrent.futures.ThreadPoolExecutor(max_workers=len(tool_use_blocks)) as pool:
futures = [pool.submit(tools[b["name"]], **b["input"]) for b in tool_use_blocks]
results = [f.result() for f in futures]
return [
{"type": "tool_result", "tool_use_id": b["id"], "content": str(r)}
for b, r in zip(tool_use_blocks, results)
]
def run_agent_parallel(question, model_script, tools, max_iterations=10):
messages = [{"role": "user", "content": question}]
for step in range(max_iterations):
turn = model_script[step]
messages.append({"role": "assistant", "content": turn["content"]})
if turn["stop_reason"] != "tool_use":
return turn, messages
tool_result_blocks = dispatch_parallel(turn["content"], tools)
messages.append({"role": "user", "content": tool_result_blocks})
raise RuntimeError(f"max_iterations alcanzado ({max_iterations})")
POLICY_DOCS = {
"no-show-policy": (
"Si un miembro no se presenta a una reserva confirmada y no cancela "
"con al menos 2 horas de anticipación, Reservo cobra el 50% del "
"precio cotizado como cargo por no-presentación."
),
"cancellation-policy": (
"Las reservas se pueden cancelar sin cargo hasta 2 horas antes del "
"horario reservado. Cancelaciones dentro de esas 2 horas aplican "
"el cargo de no-presentación."
),
}
def search_docs(query):
q = query.lower()
if "cancela" in q:
return f"[cancellation-policy] {POLICY_DOCS['cancellation-policy']}"
if "no" in q and ("present" in q or "show" in q):
return f"[no-show-policy] {POLICY_DOCS['no-show-policy']}"
return "No se encontró una política relevante para esa pregunta."
SPECIALISTS = {
"booking_agent": {
"tools": {
"list_rooms": rt.list_rooms, "get_quote": rt.get_quote,
"book_room": rt.book_room, "cancel_booking": rt.cancel_booking,
},
},
"policy_agent": {"tools": {"search_docs": search_docs}},
"pricing_agent": {"tools": {"get_quote": rt.get_quote}},
}
def run_specialist(name, task, model_script):
tools = SPECIALISTS[name]["tools"]
return run_agent_parallel(task, model_script, tools)
@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
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
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
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)
)
# ======================================================================
# Escenario 1: Sofía -- pipeline + fan-out + handoff (los tres patrones)
# ======================================================================
print("========== ESCENARIO 1 ==========")
REQUEST_1 = (
"Sofía: cotiza y reserva Boardroom pro 4h para la reunión trimestral, "
"validando la política de cancelación antes de confirmar. Aparte, compara "
"Focus y Studio pro 4h por si el equipo crece, y cotiza el Focus pro 2h "
"para la sesión de feedback -- avísame qué pasa si alguien no llega a esa sesión."
)
print(REQUEST_1)
bb1 = Blackboard()
bb1.write("supervisor", member="Sofía")
PIPELINE_STAGES_BR = [
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_BR = {"room": "Boardroom", "tier": "pro", "hours": 4, "member": "Sofía"}
script_quote_br = [
{"stop_reason": "tool_use", "content": [
{"type": "tool_use", "id": "toolu_01", "name": "get_quote",
"input": {"room": "Boardroom", "tier": "pro", "hours": 4}}]},
{"stop_reason": "end_turn", "content": [
{"type": "text", "text": "Boardroom pro 4h cuesta 25600 centavos."}]},
]
script_policy_br = [
{"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_br = [
{"stop_reason": "tool_use", "content": [
{"type": "tool_use", "id": "toolu_01", "name": "book_room",
"input": {"room": "Boardroom", "tier": "pro", "hours": 4, "member": "Sofía"}}]},
{"stop_reason": "end_turn", "content": [
{"type": "text", "text": "Reservé Boardroom pro 4h para Sofía (confirmación #1)."}]},
]
def job1_book_boardroom():
return run_pipeline(PIPELINE_STAGES_BR, [script_quote_br, script_policy_br, script_confirm_br], INITIAL_PAYLOAD_BR)
script_pricing_1 = [
{"stop_reason": "tool_use", "content": [
{"type": "tool_use", "id": "toolu_01", "name": "get_quote",
"input": {"room": "Focus", "tier": "pro", "hours": 4}},
{"type": "tool_use", "id": "toolu_02", "name": "get_quote",
"input": {"room": "Studio", "tier": "pro", "hours": 4}},
]},
{"stop_reason": "end_turn", "content": [
{"type": "text", "text": (
"Focus pro 4h: 8000 centavos. Studio pro 4h: 12800 centavos. "
"Focus es la opción más barata de las dos."
)}]},
]
def job1_compare_rooms():
return run_specialist("pricing_agent", "Compara Focus y Studio pro 4h.", script_pricing_1)
script_feedback_booking = [
{"stop_reason": "tool_use", "content": [
{"type": "tool_use", "id": "toolu_01", "name": "get_quote",
"input": {"room": "Focus", "tier": "pro", "hours": 2}}]},
{"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": "Focus", "tier": "pro", "hours": 2, "price_cents": 4000},
}}]},
]
script_feedback_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_feedback = {"booking_agent": script_feedback_booking, "policy_agent": script_feedback_policy}
def job1_feedback_no_show():
return run_with_handoff("booking_agent", "Cotiza Focus pro 2h. ¿Qué pasa si no llego?", model_scripts_feedback)
jobs1 = {
"book_boardroom": job1_book_boardroom,
"compare_rooms": job1_compare_rooms,
"feedback_no_show": job1_feedback_no_show,
}
results1 = run_tracks_parallel(jobs1)
for key in sorted(results1):
print(f"--- {key} ---")
if key == "book_boardroom":
payload, _ = results1[key]
print(" payload final:", payload)
elif key == "compare_rooms":
final, _ = results1[key]
print(" respuesta:", final["content"][0]["text"])
elif key == "feedback_no_show":
final, trace = results1[key]
print(f" {trace[0]['agent']} cedió el turno a {trace[0]['package'].receiver}")
print(" respuesta final:", final["content"][0]["text"])
payload_br, _ = results1["book_boardroom"]
bb1.write("booking_agent", room=payload_br["room"], tier=payload_br["tier"],
hours=payload_br["hours"], price_cents=payload_br["price_cents"])
bb1.write("booking_agent", booking_id=payload_br["booking_id"])
print()
print("Blackboard final escenario 1:", bb1)
no_show_ctx_1 = results1["feedback_no_show"][1][0]["package"].context
print("cargo no-presentación sesión de feedback:", no_show_ctx_1["price_cents"] // 2)
# ======================================================================
# Escenario 2: Diego -- solo pipeline + fan-out (SIN handoff: la pregunta
# de política ya nace en policy_agent, ningún agente necesita ceder el
# turno a mitad de camino)
# ======================================================================
print()
print("========== ESCENARIO 2 ==========")
REQUEST_2 = (
"Diego: reserva el Focus pro 2h para la entrevista de mañana, validando la "
"política de cancelación antes de confirmar. Aparte, ¿qué pasa si un "
"candidato no llega a una entrevista reservada?"
)
print(REQUEST_2)
bb2 = Blackboard()
bb2.write("supervisor", member="Diego")
INITIAL_PAYLOAD_D = {"room": "Focus", "tier": "pro", "hours": 2, "member": "Diego"}
script_quote_d = [
{"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_d = [
{"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_d = [
{"stop_reason": "tool_use", "content": [
{"type": "tool_use", "id": "toolu_01", "name": "book_room",
"input": {"room": "Focus", "tier": "pro", "hours": 2, "member": "Diego"}}]},
{"stop_reason": "end_turn", "content": [
{"type": "text", "text": "Reservé Focus pro 2h para Diego (confirmación #2)."}]},
]
def job2_book_focus():
return run_pipeline(PIPELINE_STAGES_BR, [script_quote_d, script_policy_d, script_confirm_d], INITIAL_PAYLOAD_D)
script_no_show_general = [
{"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."
)}]},
]
def job2_no_show_policy():
return run_specialist("policy_agent", "¿Qué pasa si un candidato no llega a una entrevista reservada?", script_no_show_general)
jobs2 = {"book_focus": job2_book_focus, "no_show_policy": job2_no_show_policy}
results2 = run_tracks_parallel(jobs2)
for key in sorted(results2):
print(f"--- {key} ---")
if key == "book_focus":
payload, _ = results2[key]
print(" payload final:", payload)
else:
final, _ = results2[key]
print(" respuesta:", final["content"][0]["text"])
payload_d, _ = results2["book_focus"]
bb2.write("booking_agent", room=payload_d["room"], tier=payload_d["tier"],
hours=payload_d["hours"], price_cents=payload_d["price_cents"])
bb2.write("booking_agent", booking_id=payload_d["booking_id"])
print()
print("Blackboard final escenario 2:", bb2)
# ======================================================================
# Escenario 3: Camila -- SOLO handoff (ni pipeline ni un segundo track de
# fan-out: la petición completa vive en una sola cadena emisor->receptor)
# ======================================================================
print()
print("========== ESCENARIO 3 ==========")
REQUEST_3 = "Camila: cotiza el Studio pro 3h para el webinar. Y aparte, ¿qué pasa si no llego a esa reserva?"
print(REQUEST_3)
script_camila_booking = [
{"stop_reason": "tool_use", "content": [
{"type": "tool_use", "id": "toolu_01", "name": "get_quote",
"input": {"room": "Studio", "tier": "pro", "hours": 3}}]},
{"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": "Studio", "tier": "pro", "hours": 3, "price_cents": 9600},
}}]},
]
script_camila_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."
)}]},
]
final_c, trace_c = run_with_handoff(
"booking_agent", "Cotiza Studio pro 3h. ¿Qué pasa si no llego?",
{"booking_agent": script_camila_booking, "policy_agent": script_camila_policy},
)
print(f"{trace_c[0]['agent']} cedió el turno a {trace_c[0]['package'].receiver}")
print("respuesta final:", final_c["content"][0]["text"])
ctx_c = trace_c[0]["package"].context
print("cargo no-presentación:", ctx_c["price_cents"] // 2)
print("reservas totales en BOOKINGS tras el Escenario 3 (sigue en 2 -- Camila solo cotizó,")
print(" el guion nunca llamó a book_room):", len(rt.BOOKINGS))
Qué esperar:
========== ESCENARIO 1 ==========
Sofía: cotiza y reserva Boardroom pro 4h para la reunión trimestral, validando la política de cancelación antes de confirmar. Aparte, compara Focus y Studio pro 4h por si el equipo crece, y cotiza el Focus pro 2h para la sesión de feedback -- avísame qué pasa si alguien no llega a esa sesión.
--- book_boardroom ---
payload final: {'room': 'Boardroom', 'tier': 'pro', 'hours': 4, 'member': 'Sofía', 'price_cents': 25600, '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}
--- compare_rooms ---
respuesta: Focus pro 4h: 8000 centavos. Studio pro 4h: 12800 centavos. Focus es la opción más barata de las dos.
--- feedback_no_show ---
booking_agent cedió el turno a 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 escenario 1: Blackboard(member='Sofía', room='Boardroom', tier='pro', hours=4, price_cents=25600, booking_id=1, log=[WriteLogEntry(seq=1, writer='supervisor', field='member', value='Sofía'), WriteLogEntry(seq=2, writer='booking_agent', field='room', value='Boardroom'), WriteLogEntry(seq=3, writer='booking_agent', field='tier', value='pro'), WriteLogEntry(seq=4, writer='booking_agent', field='hours', value=4), WriteLogEntry(seq=5, writer='booking_agent', field='price_cents', value=25600), WriteLogEntry(seq=6, writer='booking_agent', field='booking_id', value=1)])
cargo no-presentación sesión de feedback: 2000
========== ESCENARIO 2 ==========
Diego: reserva el Focus pro 2h para la entrevista de mañana, validando la política de cancelación antes de confirmar. Aparte, ¿qué pasa si un candidato no llega a una entrevista reservada?
--- book_focus ---
payload final: {'room': 'Focus', 'tier': 'pro', 'hours': 2, 'member': 'Diego', 'price_cents': 4000, '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': 2, 'confirmed': True}
--- no_show_policy ---
respuesta: 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 escenario 2: Blackboard(member='Diego', room='Focus', tier='pro', hours=2, price_cents=4000, booking_id=2, log=[WriteLogEntry(seq=7, writer='supervisor', field='member', value='Diego'), WriteLogEntry(seq=8, writer='booking_agent', field='room', value='Focus'), WriteLogEntry(seq=9, writer='booking_agent', field='tier', value='pro'), WriteLogEntry(seq=10, writer='booking_agent', field='hours', value=2), WriteLogEntry(seq=11, writer='booking_agent', field='price_cents', value=4000), WriteLogEntry(seq=12, writer='booking_agent', field='booking_id', value=2)])
========== ESCENARIO 3 ==========
Camila: cotiza el Studio pro 3h para el webinar. Y aparte, ¿qué pasa si no llego a esa reserva?
booking_agent cedió el turno a 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.
cargo no-presentación: 4800
reservas totales en BOOKINGS tras el Escenario 3 (sigue en 2 -- Camila solo cotizó,
el guion nunca llamó a book_room): 2
El razonamiento por escenario:
Escenario 1 (Sofía) — los tres patrones, igual que la petición de Ana. book_boardroom
necesita orden fijo (cotizar → validar → confirmar) → pipeline. compare_rooms no depende de
nada más → fan-out plano. feedback_no_show empieza como una cotización de booking_agent y
se topa, a mitad de camino, con una pregunta de política → handoff, dentro de la tercera rama
del fan-out. booking_id: 1 porque es la primera reserva real del proceso (Sofía corre primero en
este script); el cargo de no-presentación de la sesión de feedback (4000 // 2 = 2000) sale del
context del handoff, no de ningún cálculo dentro de policy_agent.
Escenario 2 (Diego) — solo dos patrones: pipeline + fan-out, SIN handoff. book_focus sigue
necesitando el mismo pipeline de tres etapas. Pero la pregunta de no-presentación de Diego —"¿qué
pasa si un candidato no llega a una entrevista reservada?"— es una pregunta de política general,
sin ningún dato de una reserva puntual: el supervisor la rutea directamente a policy_agent desde
el principio, como un segundo track de fan-out plano. Ningún agente necesita ceder el turno a mitad
de camino, porque nadie empezó a trabajar esta pregunta pensando que era su propio dominio.
booking_id: 2 porque este script ya había reservado el Boardroom de Sofía antes.
Escenario 3 (Camila) — solo handoff, ni pipeline ni un segundo track. A diferencia de los otros
dos, esta petición no tiene ninguna palabra sobre "reservar" ni "validar antes de confirmar" — solo
pide cotizar, y la segunda mitad de la frase depende directamente de esa cotización (el cargo de
no-presentación necesita el price_cents que booking_agent ya calculó). No hace falta un
pipeline (no hay ninguna etapa de confirmación) ni un segundo track de fan-out (no hay ninguna
sub-pregunta independiente) — la petición completa vive en una sola cadena booking_agent →
policy_agent, resuelta con run_with_handoff solo. reservo_tools.BOOKINGS sigue teniendo
exactamente 2 reservas (Boardroom y Focus) después de este escenario, porque Camila nunca pidió
reservar — solo cotizar.
Errores comunes
-
Forzar un pipeline o un fan-out adicional en el Escenario 3, "para que se parezca más" a los otros dos. El punto de este escenario es justamente reconocer que una petición compuesta puede necesitar solo un patrón — agregar estructura que la petición no pide es exactamente el tipo de sobre-ingeniería que el Módulo 1 de esta guía ya advirtió contra, desde el principio del ecosistema.
-
Buscar un handoff en el Escenario 2 porque "también hay una pregunta de no-presentación", como en el Escenario 1. Son dos preguntas con la misma superficie de vocabulario pero de naturaleza distinta: la de Sofía depende de una reserva puntual que
booking_agentya estaba cotizando (por eso hace falta ceder el turno); la de Diego es una pregunta general que nace, desde el principio, en el dominio depolicy_agent(por eso no hace falta ningún handoff). -
Confundir
booking_id: 1del Escenario 1 conbooking_id: 1de otras lecciones del módulo. Este mini-proyecto corre los tres escenarios en el mismo proceso, uno después del otro — elbooking_idde Sofía es1porque es la primera reserva de ESTE script, no porque coincida por casualidad con el de la petición de Ana en la lección 07 (un proceso distinto). -
Ejecutar los tres escenarios en un orden distinto y esperar los mismos
booking_id. Como en todos los mini-proyectos de esta guía, el orden de ejecución determina la numeración — si corrieras el Escenario 2 antes que el 1, Diego se quedaría conbooking_id: 1y Sofía con2.
Ejercicios
Ejercicio 1: Confirma tu propia ejecución (Fácil)
Ejecuta los tres escenarios del encargo tú mismo y confirma, línea por línea, que tu salida coincide
con la solución completa. Presta especial atención a los booking_id de los Escenarios 1 y 2 —
deben salir 1 y 2 respectivamente, en ese orden, si corriste el script completo de principio a
fin.
Ver solución
No hay una única "solución de código" para esta parte — es una verificación: si tu salida coincide con la de la solución completa, tu Reservo desechable arrancó limpio y los tres escenarios corrieron en el orden esperado.
Ejercicio 2: Agrega un cuarto escenario, con los tres patrones en un orden distinto (Medio)
Diseña un cuarto escenario donde el fan-out sea la parte "principal" (dos sub-preguntas independientes) y el handoff aparezca dentro de la parte del pipeline en vez de dentro del fan-out —por ejemplo: "Marta: reserva el Studio pro 3h para el taller, validando la política antes de confirmar; si te preguntan por el cargo de no-presentación en esa etapa, resuélvelo directo con policy_agent. Aparte, compara Focus y Boardroom pro 3h, y dime cuál sala tiene más capacidad." Decide, sin ejecutar código todavía, cuántos tracks necesita y con qué patrón se resuelve cada uno.
Ver solución
Esta petición necesita, otra vez, tres tracks: un pipeline para la reserva del Studio (cotizar →
validar política → confirmar), y dos tracks de fan-out plano para las dos preguntas de
comparación ("compara Focus y Boardroom" y "cuál sala tiene más capacidad" — esta última respondida
con list_rooms, no con get_quote, ya que pregunta por capacidad, no por precio). A diferencia de
la petición de Ana o del Escenario 1 de este mini-proyecto, no hay ningún handoff en esta
versión: la frase "si te preguntan por el cargo... resuélvelo directo con policy_agent" es, en los
hechos, una instrucción para que el pipeline incluya una etapa de política —exactamente la etapa
validate_policy que ya construyó el Módulo 3—, no una señal de que un agente vaya a ceder el turno
a mitad de camino. Esto confirma un punto importante del criterio de la lección 02: una pregunta de
política DENTRO de un flujo con orden fijo se resuelve con una etapa de pipeline, no con un handoff
— el handoff es específicamente para cuando la necesidad de otro especialista se descubre a
mitad de una tarea que no lo anticipaba, no para cuando ya se sabía de antemano que hacía falta
validar una política en un punto fijo del flujo.
Ejercicio 3: Mide cuántos tracks necesitó cada escenario del encargo (Difícil)
Sin volver a leer las peticiones, construye una tabla —a partir de los PLAN que aplicaste en la
parte (a) del encargo— que cuente, por escenario, cuántos tracks de cada patrón (pipeline,
fanout, handoff) usó cada uno. ¿Qué proporción de los tres escenarios necesitó los tres patrones
a la vez?
Ver solución
from collections import Counter
ESCENARIOS = {
"Sofía": ["pipeline", "fanout", "handoff"],
"Diego": ["pipeline", "fanout"],
"Camila": ["handoff"],
}
for nombre, patrones in ESCENARIOS.items():
print(f"{nombre}: {len(patrones)} track(s) -- {Counter(patrones)}")
full_combo = sum(1 for patrones in ESCENARIOS.values() if set(patrones) == {"pipeline", "fanout", "handoff"})
print(f"\nescenarios con los tres patrones a la vez: {full_combo}/{len(ESCENARIOS)}")
Salida esperada:
Sofía: 3 track(s) -- Counter({'pipeline': 1, 'fanout': 1, 'handoff': 1})
Diego: 2 track(s) -- Counter({'pipeline': 1, 'fanout': 1})
Camila: 1 track(s) -- Counter({'pipeline': 0, 'fanout': 0, 'handoff': 1})
escenarios con los tres patrones a la vez: 1/3
Explicación: solo 1 de 3 escenarios de este encargo necesitó los tres patrones a la vez — la misma proporción, aproximadamente, que ya viste en otros módulos de esta guía sobre "cuántas peticiones reales necesitan el mecanismo más completo, contra cuántas se resuelven con menos". El punto central de este mini-proyecto, y de todo el Módulo 7, no es que toda petición compuesta necesite los cinco patrones — es tener el criterio para reconocer, caso por caso, exactamente cuántos hacen falta y cuáles.
Resumen y siguiente paso
- El mini-proyecto no agregó ningún concepto nuevo: aplicó las seis lecciones anteriores —el criterio de las tres preguntas, el pipeline como pieza, el fan-out generalizado, el handoff dentro de una rama, el blackboard escrito fuera de la sección paralela, la corrida completa— sobre tres peticiones de Reservo nuevas.
- Los tres escenarios tuvieron formas distintas: Sofía necesitó los tres patrones (como Ana, en la lección 07); Diego necesitó dos (pipeline + fan-out, sin handoff); Camila necesitó solo uno (handoff puro, sin pipeline ni un segundo track).
- Solo 1 de los 3 escenarios usó los tres patrones a la vez — confirmando, con números, que "petición compuesta" no significa automáticamente "los cinco patrones a la vez".
- El criterio completo de este módulo —las tres preguntas de la lección 02, aplicadas con disciplina— es lo que distingue estos tres casos entre sí, sin necesitar ningún mecanismo nuevo para ninguno.
Con esto termina el Módulo 7. Combinaste los cinco patrones de los Módulos 2 a 6 sobre peticiones reales de Reservo —una que necesita los tres a la vez, una que necesita dos, una que necesita solo uno—, con el criterio para decidir cuál le corresponde a cada parte, sin inventar ningún mecanismo de coordinación nuevo. En el Módulo 8 construimos el capstone de la guía: el sistema multi-agente completo de Reservo —supervisor + los tres especialistas, sobre un blackboard compartido— entregado como proyecto final, con una demo de al menos dos peticiones de forma distinta y el conteo de costo de coordinación de cada una.
Recursos adicionales
- Anthropic — Building effective agents — El principio de usar el mecanismo más simple que resuelva cada parte de la tarea, ahora aplicado tres veces sobre peticiones de forma distinta.
- Anthropic — Multi-agent research system — Un caso real donde reconocer cuánta estructura necesita CADA tarea —ni más ni menos— determinó el diseño final del sistema.
- Anthropic — Agent SDK overview — Cómo se ve, en código de producción, un sistema que aplica este mismo criterio de composición — el destino final del Módulo 8 de esta guía.
- Python —
collections.Counter— La estructura usada en el Ejercicio 3 para contar cuántos tracks de cada patrón usó cada escenario.