Módulo 7: Orchestrating The Full Reservo System

Anatomía de una petición compuesta

Descripción

Antes de ejecutar un solo agente, esta lección construye el criterio que decide qué patrón le corresponde a cada parte de una petición compuesta — el paso que, en toda esta guía, hasta ahora solo se había hecho de forma implícita, dentro de la cabeza de quien escribía cada ejemplo. Vas a formalizar ese criterio en tres preguntas simples, aplicarlas a la petición de Ana de la lección 01, y construir Track — una estructura de datos mínima que anota la decisión del supervisor, sin resolver todavía ninguna sub-tarea.

No hay ningún agente corriendo en esta lección. Es, a propósito, la lección más "de lápiz y papel" del módulo — porque decidir bien el plan, antes de ejecutar nada, es lo que hace que las lecciones 03 a 07 no tengan que volver a preguntarse "¿esto va en pipeline o en fan-out?" en medio del código.

Conexión con el módulo

Track es la única pieza nueva de esta lección — y es deliberadamente delgada: tres campos, ningún comportamiento propio. Las lecciones 03 a 07 la usan para ETIQUETAR sus jobs, nunca para ejecutarlos — la ejecución sigue siendo, sin excepción, run_pipeline (M3), run_specialist (M2) o run_with_handoff (M5), exactamente como ya los conoces.


Analogía: la lista del maître, antes de que nadie cocine nada

Antes de que un solo plato salga de la cocina, el maître de la analogía del Módulo 1 hace algo que no involucra ni ollas ni mozos: mira el pedido completo de la mesa grande y, en su cabeza (o en una libreta), separa "esto va a la parrilla", "esto lo puede responder cualquier mozo sin consultar a nadie", "esto probablemente termine necesitando al sommelier". Ese paso —clasificar antes de actuar— no cocina nada ni sirve nada. Es exactamente lo que hace esta lección: antes de que booking_agent, policy_agent o pricing_agent reciban una sola tarea, alguien tiene que decidir qué le corresponde a quién, y con qué mecanismo.


Las tres preguntas del criterio

Con los cinco patrones ya construidos (M2-M6), decidir cuál le toca a una parte de una petición se reduce a tres preguntas, en este orden:

1. ¿Esta sub-tarea depende del RESULTADO de otra sub-tarea de la misma petición?
   SÍ, en un orden fijo y conocido de antemano -> PIPELINE (M3)
   SÍ, pero el orden se descubre A MITAD de resolver la sub-tarea -> HANDOFF (M5)
   NO, es completamente independiente -> sigue a la pregunta 2

2. (si la respuesta a 1 fue "NO") ¿Hay OTRA sub-tarea, también independiente,
   que podría resolverse al mismo tiempo?
   SÍ -> FAN-OUT (M4)
   NO (es la única sub-tarea de la petición) -> un solo run_specialist alcanza,
                                                  como en el Módulo 2 aislado

3. (siempre, sin importar la respuesta a 1 o 2) ¿Algún dato que esta sub-tarea
   produce hace falta MÁS ADELANTE en la misma corrida, para otra parte del
   sistema que todavía no sabemos quién va a leerlo?
   SÍ -> ese dato se escribe al BLACKBOARD (M6), sin importar qué patrón
         resolvió la sub-tarea que lo produjo
   NO -> el resultado se usa solo para responder, y no se escribe a ningún lado

Fíjate en algo importante de la pregunta 3: no es excluyente con las otras dos. El Blackboard no compite con el pipeline, el fan-out o el handoff por resolver la sub-tarea — se pregunta después de que ya se decidió cómo resolverla, y su respuesta puede ser "sí" o "no" sin cambiar en nada la decisión de las preguntas 1 y 2.


Ejemplo trabajado: aplicando el criterio a la petición de Ana

from dataclasses import dataclass


@dataclass
class Track:
    """La anotación del supervisor sobre CADA sub-tarea de una petición
    compuesta: no un patrón nuevo -- una etiqueta que dice qué patrón
    (de los ya construidos en M2-M6) le corresponde a esta parte."""
    key: str
    pattern: str  # "pipeline" | "fanout" | "handoff"
    description: str


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("--- petición compuesta ---")
print(REQUEST)

# Paso 1 (concepto, claude-sonnet-5): el supervisor lee la petición completa
# y la descompone en sub-tareas, decidiendo QUÉ PATRÓN resuelve cada una --
# no solo A QUIÉN delegarla (eso ya lo hacía el Módulo 2 por sí solo).
print()
print("=== el supervisor (concepto) descompone la petición en tracks ===")
PLAN = [
    Track(key="book_focus", pattern="pipeline",
          description="cotizar Focus pro 3h, validar política de cancelación, confirmar "
                       "-- pasos fijos, en orden, cada uno depende del anterior"),
    Track(key="compare_rooms", pattern="fanout",
          description="comparar Studio y Boardroom pro 3h -- no depende de nada más "
                       "en la petición, puede resolverse en paralelo"),
    Track(key="boardroom_no_show", pattern="handoff",
          description="cotizar Boardroom pro 2h; la pregunta de no-presentación aparece "
                       "A MITAD de esa sub-tarea, no se sabe de antemano"),
]
for t in PLAN:
    print(f"[{t.key}]")
    print(f"  patrón: {t.pattern}")
    print(f"  por qué: {t.description}")

print()
print("=== ningún patrón nuevo: los tres ya existen, PLAN solo los etiqueta ===")
by_pattern = {}
for t in PLAN:
    by_pattern.setdefault(t.pattern, []).append(t.key)
for pattern in sorted(by_pattern):
    print(f"  {pattern:<9} -> {by_pattern[pattern]}")

Qué esperar:

--- 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.

=== el supervisor (concepto) descompone la petición en tracks ===
[book_focus]
  patrón: pipeline
  por qué: cotizar Focus pro 3h, validar política de cancelación, confirmar -- pasos fijos, en orden, cada uno depende del anterior
[compare_rooms]
  patrón: fanout
  por qué: comparar Studio y Boardroom pro 3h -- no depende de nada más en la petición, puede resolverse en paralelo
[boardroom_no_show]
  patrón: handoff
  por qué: cotizar Boardroom pro 2h; la pregunta de no-presentación aparece A MITAD de esa sub-tarea, no se sabe de antemano

=== ningún patrón nuevo: los tres ya existen, PLAN solo los etiqueta ===
  fanout    -> ['compare_rooms']
  handoff   -> ['boardroom_no_show']
  pipeline  -> ['book_focus']

PLAN es, deliberadamente, la única salida de esta lección — tres objetos Track, sin un solo tool_use, sin un solo run_specialist invocado. Las lecciones 03 a 05 van a tomar, una por una, cada Track de esta lista y resolverla con el mecanismo real que le corresponde.


Por qué Track no reemplaza a RoutingDecision (Módulo 2)

RoutingDecision (M2) responde a quién delegar una petición completa — un target, un solo especialista. Track responde una pregunta distinta y más específica: con qué patrón resolver una parte de una petición que ya se sabe compuesta. No son la misma decisión a distinta escala —son dos decisiones de naturaleza distinta—: RoutingDecision elige un agente; Track elige un mecanismo de coordinación, que puede terminar involucrando a uno, dos o tres agentes por dentro. De hecho, vas a ver en la lección 03 que el propio mecanismo de pipeline (el Track book_focus) internamente sigue usando RoutingDecision-como-concepto en ningún punto — usa PipelineStage, que ya trae el nombre del agente fijo en cada etapa, sin necesidad de decidir nada en tiempo de ejecución.


Errores comunes

  1. Aplicar la pregunta 1 del criterio mirando solo palabras clave, como el router determinista del Módulo 2. El criterio de esta lección no es un cartel de reglas de vocabulario — es una pregunta sobre dependencia de datos: ¿el resultado de una sub-tarea alimenta la entrada de otra? Dos sub-tareas pueden compartir vocabulario (las dos mencionan "Boardroom", por ejemplo) y seguir siendo completamente independientes entre sí.

  2. Confundir "puede resolverse al mismo tiempo" (fan-out) con "no importa en qué orden se lea la respuesta" (cualquier patrón). El fan-out exige que la sub-tarea NO dependa del resultado de ninguna otra — no solo que el orden de lectura final no importe.

  3. Decidir el patrón mirando el AGENTE en vez de la ESTRUCTURA de la sub-tarea. Que dos sub-tareas usen el mismo agente (booking_agent, en la petición de Ana, aparece en dos tracks distintos: book_focus y boardroom_no_show) no dice nada por sí solo sobre qué patrón les corresponde — lo que importa es si sus resultados dependen entre sí, no quién las resuelve.

  4. Pensar que la pregunta 3 (blackboard) es obligatoria para toda sub-tarea. No lo es — la lección 06 confirma, ejecutando, que dos de los tres tracks de esta petición (compare_rooms y boardroom_no_show) terminan sin escribir nada al Blackboard, porque ninguno produce un hecho de registro que otra parte del sistema necesite leer después.


Ejercicios

Ejercicio 1: Confirma tu propia ejecución (Fácil)

Ejecuta el ejemplo trabajado de esta lección tú mismo y confirma que tu salida coincide, línea por línea, con el bloque "Qué esperar". No hay ningún estado de Reservo involucrado todavía —esta lección no toca reservo_tools— así que no hace falta preocuparte por reiniciar nada entre corridas.

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, tu PLAN se construyó sin desvíos.

Ejercicio 2: Aplica el criterio a una petición nueva, sin ejecutar nada (Medio)

Sin escribir ningún Track todavía, aplica las tres preguntas del criterio a esta petición: "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?". ¿Cuántos tracks necesita? ¿Hace falta un handoff en algún punto?

Ver solución

Esta petición necesita dos tracks, no tres: un Track pipeline (cotizar → validar política → confirmar Focus pro 2h) y un Track fanout (la pregunta de no-presentación). No hace falta ningún handoff — a diferencia del boardroom_no_show de la petición de Ana, la pregunta de no-presentación de Diego nunca empieza en el dominio de booking_agent: es una pregunta de política general, sin ningún dato de reserva puntual, que el supervisor puede rutear directamente a policy_agent desde el principio, sin que nadie tenga que ceder el turno a mitad de camino. Este es exactamente el mismo escenario que resuelve el Ejercicio equivalente del mini-proyecto (lección 08) — confírmalo ahí, ejecutado.

Ejercicio 3: ¿Qué pasa si dos tracks dependen entre sí, pero el criterio los etiqueta como fan-out por error? (Difícil)

Imagina que alguien, aplicando mal la pregunta 1 del criterio, etiqueta como fanout dos sub-tareas donde la segunda en realidad necesita el price_cents que produce la primera —como si fueran compare_rooms y boardroom_no_show, pero conectadas—. Sin ejecutar código, explica qué pasaría si esas dos sub-tareas corrieran de verdad con run_tracks_parallel (la generalización de fan-out que construye la lección 04), y por qué el error no se vería en la salida de esa función, sino en el resultado final.

Ver solución

run_tracks_parallel no sabe nada sobre dependencias de datos — solo sabe que le dieron un diccionario de {key: callable} y que tiene que correr cada callable y devolver sus resultados, sorted por clave. Si la segunda sub-tarea necesitara el price_cents de la primera pero se etiquetó (por error) como independiente, el mecanismo de fan-out no fallaría — ambos callables correrían igual, cada uno en su propio hilo, y run_tracks_parallel devolvería sus dos resultados sin ninguna queja. El problema aparecería DESPUÉS, al construir la respuesta final: la segunda sub-tarea habría tenido que inventar, adivinar o dejar en blanco el price_cents que en realidad necesitaba, porque en el momento en que corrió, la primera sub-tarea —corriendo en paralelo, en otro hilo— todavía no había terminado de calcularlo. Este es exactamente el tipo de error que la lección 01 del criterio busca evitar desde el diseño: la pregunta 1 (¿depende del resultado de otra sub-tarea?) tiene que responderse ANTES de decidir el patrón, no después de ver que algo salió mal en la ejecución.


Resumen y siguiente paso

  • El criterio de esta lección tiene tres preguntas: ¿depende de otra sub-tarea? (pipeline si el orden es fijo desde el inicio, handoff si se descubre a mitad de camino), ¿hay otra sub-tarea independiente al mismo tiempo? (fan-out), ¿algún dato hace falta más adelante en la corrida? (blackboard, sin importar qué patrón resolvió esa parte).
  • Track anota la decisión del supervisor —tres campos, ningún comportamiento propio— sin ejecutar ninguna sub-tarea todavía.
  • Aplicado a la petición de Ana, el criterio produjo tres tracks: book_focus (pipeline), compare_rooms (fanout), boardroom_no_show (handoff) — sin que ningún patrón nuevo hiciera falta.
  • Track no reemplaza a RoutingDecision (M2): responden preguntas de naturaleza distinta —a quién delegar, contra con qué mecanismo resolver.

Siguiente lección: 03 — El supervisor decide, el pipeline valida antes de reservar. Tomamos el primer Track de PLANbook_focus— y lo resolvemos con run_pipeline, sin ningún cambio respecto al Módulo 3.


Recursos adicionales

  1. Anthropic — Building effective agents — El principio de decidir la forma de la orquestación ANTES de ejecutarla, en vez de descubrirla sobre la marcha — el espíritu completo de esta lección.
  2. Python — dataclasses — El módulo detrás de Track, la misma herramienta que ya usaste para PipelineStage (M3), SubTask (M4), HandoffPackage (M5) y Blackboard (M6).
  3. Anthropic — Multi-agent research system — Un orquestador real que descompone una tarea compleja en sub-tareas antes de despachar ninguna, la misma disciplina que aplica el criterio de esta lección.
  4. Python — Diccionarios: setdefault — El método usado para agrupar PLAN por patrón al final del ejemplo trabajado.