Módulo 5: Handoff y delegación
Mini-proyecto: handoffs en Reservo
Descripción
Siete lecciones te dejaron con el patrón handoff completo: por qué hace falta (02), qué va en el paquete y qué no (03), el mecanismo que interrumpe el loop (04), el handoff de punta a punta (05), las guardas contra cadenas sin fin (06), y el costo medido frente a la vuelta obligada a un supervisor (07). Este mini-proyecto no agrega ningún concepto nuevo — te da tres escenarios de Reservo que nunca viste y te pide el juicio completo: no solo ejecutar un handoff cuando hace falta, sino reconocer los dos casos en los que no corresponde — uno donde ni siquiera hace falta transferir nada, y uno donde el handoff funciona, pero un patrón distinto (fan-out, Módulo 4) hubiera sido el diseño correcto desde el principio.
El entregable de esta lección es el patrón completo de handoff corriendo contra los tres escenarios del encargo, más el razonamiento —no solo el código— que distingue cuándo SÍ, cuándo NO, y cuándo "funciona pero no es el patrón correcto".
Conexión con el módulo
Este mini-proyecto es la síntesis de las siete lecciones anteriores, no una lección nueva. De la 03
usas HandoffPackage. De la 04, run_agent_with_handoff y run_specialist_with_handoff. De la 05,
run_with_handoff. De la 02 y la 07, el criterio para reconocer cuándo un handoff aporta algo real
frente a las alternativas. De la 01 y el Módulo 4, la frontera entre handoff y fan-out. Cuando
termines, el Módulo 6 toma este mismo Reservo y construye la forma en que los agentes comparten
estado sin pasarse paquetes punto a punto: un blackboard común.
El encargo
Reservo te pasa tres escenarios que llegaron la misma semana:
Escenario A: "Necesito reservar el Boardroom pro 2h para Marta. Si cancelo el mismo
día, ¿me cobran algo?"
Escenario B: "Cotiza Studio pro 2h. Después cancela mi reserva número 5."
Escenario C: "Compara Focus y Studio, tier pro, 2 horas cada una, y dime la política
de cancelación."
Tu encargo tiene dos entregables:
a) Para cada escenario, decide si corresponde handoff — y si no corresponde, explica en una frase por qué (¿el propio especialista ya resuelve todo? ¿la necesidad del segundo especialista era obvia desde el principio, y un patrón distinto hubiera sido mejor?).
b) Para el escenario que sí necesita handoff, ejecútalo de punta a punta con
run_with_handoff, citando el historial completo de los dos agentes y la respuesta final.
La solución completa (el entregable)
Ver la solución completa
import concurrent.futures
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}},
}
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 print_history(history):
for i, m in enumerate(history):
role, content = m["role"], m["content"]
if isinstance(content, str):
print(f" [{i}] {role:<9} pregunta: {content!r}")
continue
for block in content:
if block["type"] == "tool_use":
print(f" [{i}] {role:<9} tool_use({block['name']}): {block['input']}")
elif block["type"] == "tool_result":
print(f" [{i}] {role:<9} tool_result: {block['content']}")
elif block["type"] == "text":
print(f" [{i}] {role:<9} texto final: {block['text']!r}")
# ============================================================
# Escenario A: handoff real -- booking_agent reserva, y a mitad
# de tarea, la pregunta de cancelación same-day excede su expertise.
# ============================================================
print("=== Escenario A ===")
task_a = "Necesito reservar el Boardroom pro 2h para Marta. Si cancelo el mismo día, ¿me cobran algo?"
script_a_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": "book_room",
"input": {"room": "Boardroom", "tier": "pro", "hours": 2, "member": "Marta"}}]},
{"stop_reason": "tool_use", "content": [
{"type": "tool_use", "id": "toolu_03", "name": HANDOFF_TOOL_NAME,
"input": {"receiver": "policy_agent",
"reason": "la pregunta de cancelación same-day no vive en mi expertise",
"task": "¿qué cargo aplica si cancelo una reserva el mismo día?",
"context": {"room": "Boardroom", "tier": "pro", "hours": 2,
"price_cents": 12800, "booking_id": 1}}}]},
]
script_a_policy = [
{"stop_reason": "tool_use", "content": [
{"type": "tool_use", "id": "toolu_01", "name": "search_docs",
"input": {"query": "cancela el mismo día"}}]},
{"stop_reason": "end_turn", "content": [
{"type": "text", "text": (
"Puedes cancelar sin cargo hasta 2 horas antes del horario reservado. "
"Si cancelas dentro de esas 2 horas, aplica el cargo de no-presentación."
)}]},
]
final_a1, hist_a1, package_a = run_specialist_with_handoff("booking_agent", task_a, script_a_booking)
print("--- booking_agent (hasta el handoff) ---")
print_history(hist_a1)
print(f" handoff -> {package_a.receiver} | task={package_a.task!r} | context={package_a.context}")
final_a2, hist_a2, _ = run_specialist_with_handoff(package_a.receiver, package_a.task, script_a_policy)
print("--- policy_agent ---")
print_history(hist_a2)
no_show_a = package_a.context["price_cents"] // 2
final_response_a = (
f"Reservé {package_a.context['room']} {package_a.context['tier']} "
f"{package_a.context['hours']}h para Marta (confirmación #{package_a.context['booking_id']}), "
f"{package_a.context['price_cents']} centavos. Sobre cancelar el mismo día: "
f"{final_a2['content'][0]['text']} Si esa cancelación cuenta como no-presentación, "
f"el cargo para esta reserva sería de {no_show_a} centavos."
)
print("respuesta final:", final_response_a)
print()
# ============================================================
# Escenario B: NO hace falta handoff -- las dos partes viven en
# booking_agent.
# ============================================================
print("=== Escenario B ===")
task_b = "Cotiza Studio pro 2h. Después cancela mi reserva número 5."
script_b = [
{"stop_reason": "tool_use", "content": [
{"type": "tool_use", "id": "toolu_01", "name": "get_quote",
"input": {"room": "Studio", "tier": "pro", "hours": 2}}]},
{"stop_reason": "tool_use", "content": [
{"type": "tool_use", "id": "toolu_02", "name": "cancel_booking", "input": {"id": 5}}]},
{"stop_reason": "end_turn", "content": [
{"type": "text", "text": (
"Studio pro 2h cuesta 6400 centavos. No encontré ninguna reserva "
"activa con el número 5 -- revisa el número e intenta de nuevo."
)}]},
]
final_b, hist_b, package_b = run_specialist_with_handoff("booking_agent", task_b, script_b)
print_history(hist_b)
print("handoff:", package_b)
print("respuesta final:", final_b["content"][0]["text"])
print()
# ============================================================
# Escenario C: pricing_agent compara, y termina haciendo handoff
# a policy_agent -- pero las dos partes eran independientes desde
# el principio (debería haber sido fan-out, M4, no handoff).
# ============================================================
print("=== Escenario C ===")
task_c = "Compara Focus y Studio, tier pro, 2 horas cada una, y dime la política de cancelación."
script_c_pricing = [
{"stop_reason": "tool_use", "content": [
{"type": "tool_use", "id": "toolu_01", "name": "get_quote",
"input": {"room": "Focus", "tier": "pro", "hours": 2}},
{"type": "tool_use", "id": "toolu_02", "name": "get_quote",
"input": {"room": "Studio", "tier": "pro", "hours": 2}},
]},
{"stop_reason": "tool_use", "content": [
{"type": "tool_use", "id": "toolu_03", "name": HANDOFF_TOOL_NAME,
"input": {"receiver": "policy_agent",
"reason": "la política de cancelación no vive en mi expertise",
"task": "¿cuál es la política de cancelación?",
"context": {}}}]},
]
script_c_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."}]},
]
final_c1, hist_c1, package_c = run_specialist_with_handoff("pricing_agent", task_c, script_c_pricing)
print("--- pricing_agent (hasta el handoff) ---")
print_history(hist_c1)
print(f" handoff -> {package_c.receiver} | task={package_c.task!r} | context={package_c.context}")
final_c2, hist_c2, _ = run_specialist_with_handoff(package_c.receiver, package_c.task, script_c_policy)
print("--- policy_agent ---")
print_history(hist_c2)
Qué esperar:
=== Escenario A ===
--- booking_agent (hasta el handoff) ---
[0] user pregunta: 'Necesito reservar el Boardroom pro 2h para Marta. Si cancelo el mismo día, ¿me cobran algo?'
[1] assistant tool_use(get_quote): {'room': 'Boardroom', 'tier': 'pro', 'hours': 2}
[2] user tool_result: {'price_cents': 12800}
[3] assistant tool_use(book_room): {'room': 'Boardroom', 'tier': 'pro', 'hours': 2, 'member': 'Marta'}
[4] user tool_result: {'booking_id': 1, 'confirmed': True}
[5] assistant tool_use(handoff_to_specialist): {'receiver': 'policy_agent', 'reason': 'la pregunta de cancelación same-day no vive en mi expertise', 'task': '¿qué cargo aplica si cancelo una reserva el mismo día?', 'context': {'room': 'Boardroom', 'tier': 'pro', 'hours': 2, 'price_cents': 12800, 'booking_id': 1}}
handoff -> policy_agent | task='¿qué cargo aplica si cancelo una reserva el mismo día?' | context={'room': 'Boardroom', 'tier': 'pro', 'hours': 2, 'price_cents': 12800, 'booking_id': 1}
--- policy_agent ---
[0] user pregunta: '¿qué cargo aplica si cancelo una reserva el mismo día?'
[1] assistant tool_use(search_docs): {'query': 'cancela el mismo día'}
[2] user tool_result: [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.
[3] assistant texto final: 'Puedes cancelar sin cargo hasta 2 horas antes del horario reservado. Si cancelas dentro de esas 2 horas, aplica el cargo de no-presentación.'
respuesta final: Reservé Boardroom pro 2h para Marta (confirmación #1), 12800 centavos. Sobre cancelar el mismo día: Puedes cancelar sin cargo hasta 2 horas antes del horario reservado. Si cancelas dentro de esas 2 horas, aplica el cargo de no-presentación. Si esa cancelación cuenta como no-presentación, el cargo para esta reserva sería de 6400 centavos.
=== Escenario B ===
[0] user pregunta: 'Cotiza Studio pro 2h. Después cancela mi reserva número 5.'
[1] assistant tool_use(get_quote): {'room': 'Studio', 'tier': 'pro', 'hours': 2}
[2] user tool_result: {'price_cents': 6400}
[3] assistant tool_use(cancel_booking): {'id': 5}
[4] user tool_result: {'cancelled': False}
[5] assistant texto final: 'Studio pro 2h cuesta 6400 centavos. No encontré ninguna reserva activa con el número 5 -- revisa el número e intenta de nuevo.'
handoff: None
respuesta final: Studio pro 2h cuesta 6400 centavos. No encontré ninguna reserva activa con el número 5 -- revisa el número e intenta de nuevo.
=== Escenario C ===
--- pricing_agent (hasta el handoff) ---
[0] user pregunta: 'Compara Focus y Studio, tier pro, 2 horas cada una, y dime la política de cancelación.'
[1] assistant tool_use(get_quote): {'room': 'Focus', 'tier': 'pro', 'hours': 2}
[1] assistant tool_use(get_quote): {'room': 'Studio', 'tier': 'pro', 'hours': 2}
[2] user tool_result: {'price_cents': 4000}
[2] user tool_result: {'price_cents': 6400}
[3] assistant tool_use(handoff_to_specialist): {'receiver': 'policy_agent', 'reason': 'la política de cancelación no vive en mi expertise', 'task': '¿cuál es la política de cancelación?', 'context': {}}
handoff -> policy_agent | task='¿cuál es la política de cancelación?' | context={}
--- policy_agent ---
[0] user pregunta: '¿cuál es la política de cancelación?'
[1] assistant tool_use(search_docs): {'query': 'política de cancelación'}
[2] user tool_result: [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.
[3] assistant texto final: 'Puedes cancelar sin cargo hasta 2 horas antes del horario reservado.'
El razonamiento por escenario:
Escenario A — handoff real, y bien diseñado. booking_agent ya completó una tarea real —cotizar
y reservar el Boardroom— antes de toparse con la pregunta de cancelación same-day. La necesidad de
policy_agent no era obvia desde el principio de la conversación: apareció recién en la segunda
mitad de la frase, después de que el trabajo de reserva ya estaba en curso. El context que viaja
(price_cents=12800, booking_id=1) es exactamente lo que permite personalizar la respuesta final
con el cargo específico de esta reserva (6400, la mitad de 12800) — sin ese contexto,
policy_agent solo podría citar la política general.
Escenario B — ningún handoff, y está bien así. "Cotiza Studio pro 2h" y "cancela mi reserva
número 5" viven, las dos, completas dentro de la expertise de booking_agent — get_quote y
cancel_booking son dos de sus cuatro tools canónicas. No hay ningún momento en el que
booking_agent se tope con un límite real: forzar un handoff acá sería el mismo error que ya
advirtió el Módulo 1 —sumar coordinación donde un solo especialista ya resuelve todo—. El resultado
cancelled: False para la reserva 5 es correcto y válido (no existe esa reserva en un Reservo
recién iniciado), no una falla que necesite intervención de nadie más.
Escenario C — el handoff FUNCIONA, pero no es el patrón correcto. Fíjate en algo importante:
esta petición es, palabra por palabra, muy parecida al ejemplo central del Módulo 4 ("cotiza Focus
pro 3h y dime la política de cancelación") — dos sub-preguntas que son independientes desde
que llega la petición, no algo que pricing_agent descubre a mitad de camino. El código de arriba
ejecuta el handoff igual, y produce una respuesta correcta — pero el diseño correcto para esta
petición era reconocer, antes de que pricing_agent empezara a trabajar, que las dos partes se
podían repartir con fan-out (Módulo 4): las dos preguntas hubieran corrido en el mismo costo, sin que
pricing_agent tuviera que "descubrir" la necesidad de política a mitad de su propia comparación.
El handoff no es incorrecto acá — es una solución que funciona para un problema que, bien
diagnosticado desde el principio, no necesitaba esperar a que alguien empezara a trabajar para
repartirse.
Errores comunes
-
Forzar un handoff en el Escenario B "porque el encargo tiene dos partes". El punto de este escenario es justamente reconocer que dos partes conectadas por "después" no siempre necesitan coordinación entre especialistas — si las dos viven en el mismo registro de tools, es una tarea de varios pasos para UN agente, no un caso de handoff ni de ningún otro patrón multi-agente.
-
Confundir el Escenario A (handoff bien diseñado) con el Escenario C (handoff que funciona, pero no es el patrón correcto). Los dos ejecutan exactamente el mismo mecanismo con el mismo código — la diferencia está en el diagnóstico previo: en A, la necesidad de
policy_agentaparece genuinamente durante el trabajo; en C, era reconocible desde el principio, y el Módulo 4 ya construyó el patrón correcto para ese caso. -
Pensar que el Escenario C "está mal" y hay que rehacerlo con fan-out para que este mini-proyecto esté completo. No — el ejercicio es reconocer la distinción, no necesariamente reescribir el escenario. Un sistema real con este historial de peticiones sería la señal de rediseñar cómo se enruta este tipo de pregunta compuesta, pero eso es una decisión de arquitectura más amplia, no algo que el mini-proyecto exige corregir.
-
Ejecutar los tres escenarios en el mismo proceso sin reiniciar el estado. Si corres este mini-proyecto después de otro ejemplo de la guía en la misma sesión, los
booking_idy el estado deBOOKINGSno van a coincidir con el "Qué esperar" — cada escenario asume un Reservo desechable, recién iniciado.
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. Para el Escenario A, verifica a mano que 12800 // 2 == 6400.
Ver solución
No hay una única "solución de código" para este ejercicio — es una verificación: si tu salida coincide con la de la solución completa, tu Reservo desechable arrancó limpio.
Ejercicio 2: Agrega un cuarto escenario, D (Medio)
Diseña un cuarto escenario: policy_agent responde una pregunta de política, y a mitad de esa
respuesta el miembro pregunta "¿y me conviene más Focus o Studio para esto?" — una pregunta de
comparación de precios, fuera de la expertise de policy_agent. Diseña el handoff completo
(policy_agent -> pricing_agent) y ejecútalo.
Ver solución
task_d = "¿Qué pasa si cancelo con poca anticipación? Y de paso, ¿me conviene más Focus o Studio para una reunión de 2h?"
script_d_policy = [
{"stop_reason": "tool_use", "content": [
{"type": "tool_use", "id": "toolu_01", "name": "search_docs",
"input": {"query": "cancela con poca anticipación"}}]},
{"stop_reason": "tool_use", "content": [
{"type": "tool_use", "id": "toolu_02", "name": HANDOFF_TOOL_NAME,
"input": {"receiver": "pricing_agent",
"reason": "la comparación de precios no vive en mi expertise",
"task": "¿qué sala conviene más entre Focus y Studio, tier pro, 2h?",
"context": {}}}]},
]
script_d_pricing = [
{"stop_reason": "tool_use", "content": [
{"type": "tool_use", "id": "toolu_01", "name": "get_quote",
"input": {"room": "Focus", "tier": "pro", "hours": 2}},
{"type": "tool_use", "id": "toolu_02", "name": "get_quote",
"input": {"room": "Studio", "tier": "pro", "hours": 2}},
]},
{"stop_reason": "end_turn", "content": [
{"type": "text", "text": "Focus pro 2h: 4000 centavos. Studio pro 2h: 6400 centavos. Focus es más barata."}]},
]
final_d1, hist_d1, package_d = run_specialist_with_handoff("policy_agent", task_d, script_d_policy)
print(f"handoff -> {package_d.receiver} | task={package_d.task!r}")
final_d2, hist_d2, _ = run_specialist_with_handoff(package_d.receiver, package_d.task, script_d_pricing)
print("respuesta final:", final_d2["content"][0]["text"])
Salida esperada:
handoff -> pricing_agent | task='¿qué sala conviene más entre Focus y Studio, tier pro, 2h?'
respuesta final: Focus pro 2h: 4000 centavos. Studio pro 2h: 6400 centavos. Focus es más barata.
Explicación: este escenario confirma que el handoff no es exclusivo de booking_agent como
emisor — cualquier especialista, al toparse con una pregunta fuera de su propia expertise, puede
ceder el turno con el mismo mecanismo. policy_agent ya había resuelto su propia parte
(search_docs sobre la pregunta de cancelación) antes de reconocer que la segunda pregunta —una
comparación de precios— pertenece a pricing_agent, no a él.
Ejercicio 3: Clasifica cinco peticiones nuevas: ¿handoff, no-handoff, o "debería ser otro
patrón"? (Difícil)
Sin ejecutar código, clasifica cada una de estas cinco peticiones con el criterio completo del módulo: (a) "Resérvame el Focus básico 2h para Luis"; (b) "¿Cuál es la política de cancelación? Y si aplica, ¿la política de no-presentación es la misma?"; (c) "Cotiza Boardroom pro 1h, y si el precio te parece razonable, resérvalo"; (d) "Compara las tres salas, pro, 2h cada una, y dime cuál tiene mejor política de cancelación"; (e) "Necesito cancelar mi reserva 3, y de paso pregúntame — ah espera, ¿ustedes tienen alguna promoción para reservas de más de 5 horas?".
Ver solución
(a) No-handoff. Todo vive en booking_agent (book_room sobre una sola sala) — una sola
tarea, un solo especialista, sin ningún límite de expertise involucrado.
(b) No-handoff (dentro de policy_agent). Las dos preguntas —cancelación y
no-presentación— viven completas en la expertise de policy_agent: dos consultas a search_docs,
sin necesidad de involucrar a nadie más. No es fan-out entre agentes ni handoff — es, como mucho,
fan-out DENTRO de un agente (la variante del Módulo 4, lección 05), si las dos consultas corrieran
en el mismo turno.
(c) No-handoff. "Cotiza, y si el precio te parece razonable, resérvalo" es una tarea
condicional pero de UN solo dominio: cotizar y reservar son las dos tools canónicas de
booking_agent. No hay ningún segundo especialista involucrado en ningún punto.
(d) Debería ser fan-out (M4), no handoff — como el Escenario C de este mini-proyecto. "Cuál
tiene mejor política de cancelación" depende de comparar precios (pricing_agent) Y de conocer la
política (policy_agent) — dos sub-preguntas identificables desde el principio, sin que ninguna
dependa del trabajo de la otra. Un handoff FUNCIONARÍA (uno de los dos especialistas podría cederle
el turno al otro a mitad de camino), pero el diagnóstico correcto, hecho ANTES de que nadie
trabajara, es fan-out.
(e) Handoff real — el caso genuino del módulo. El miembro empieza pidiendo cancelar una
reserva (booking_agent, cancel_booking), y a mitad de esa misma interacción —de forma
literalmente espontánea, marcada por el "ah espera"— surge una pregunta sobre promociones que no
vive en ninguna tool de booking_agent ni de los otros dos especialistas existentes. Este es
exactamente el patrón de la lección 01: la necesidad de otro tipo de respuesta aparece durante
el trabajo, no antes — aunque en este caso puntual, ningún especialista de Reservo tiene la
expertise de promociones, así que el handoff terminaría en un límite distinto (ningún receptor
disponible), un caso que queda fuera del alcance de los tres especialistas que construye esta guía.
Resumen y siguiente paso
- El mini-proyecto no agregó ningún concepto nuevo: aplicó las siete lecciones anteriores —por qué hace falta el handoff, el paquete mínimo, el mecanismo, el flujo completo, las guardas, el costo medido— sobre tres escenarios de Reservo nuevos.
- El Escenario A confirmó un handoff genuino: la necesidad de
policy_agentapareció durante el trabajo debooking_agent, y elcontextdel paquete permitió una respuesta personalizada (6400centavos, no solo "el 50%"). - El Escenario B confirmó que no todo lo que suena a "dos partes" necesita coordinación entre agentes — a veces, un solo especialista con las tools correctas ya resuelve todo.
- El Escenario C fue el más importante del lote: un handoff que funciona, ejecutado con el mismo código de siempre, pero que revela una necesidad reconocible desde el principio — el tipo de caso que el Módulo 4 (fan-out) hubiera resuelto mejor desde el diseño.
Con esto termina el Módulo 5. Construiste el patrón handoff completo: por qué un agente en curso necesita ceder el turno sin volver a un supervisor externo, el paquete mínimo que decide qué viaja y qué no, el mecanismo que interrumpe el loop, las guardas contra cadenas sin fin, y el costo medido frente a la alternativa. En el Módulo 6 construimos el sexto patrón: el blackboard — cómo los agentes comparten un estado común, sin pasarse paquetes punto a punto entre ellos.
Recursos adicionales
- Anthropic — Building effective agents — El principio de reconocer, con criterio y no por hábito, cuándo un mecanismo de coordinación aporta algo real — el eje completo de este mini-proyecto.
- Anthropic — Multi-agent research system — Un caso real donde distinguir el patrón correcto de coordinación —no solo "algo que funciona"— determinó el diseño final de un sistema multi-agente.
- Anthropic — Agent SDK overview — Cómo se ve, en código real, un mecanismo de transferencia de control entre agentes — el destino del Módulo 8 de esta guía.
- Python —
dataclasses— El módulo detrás deHandoffPackage, la base de todo el handoff de este módulo.