Módulo 8: Project The Reservo Multi Agent System

Midiendo el costo de coordinación del sistema completo

Descripción

Cuatro lecciones de conteo, cuatro fórmulas distintas: M1 L05 midió una tarea resuelta por un agente único contra un supervisor con un solo especialista (3 vs. 5 llamadas, 0 vs. 2 hops). M3 L05 midió un pipeline de tres etapas contra un supervisor que decidiera en cada una (7 vs. 10 llamadas, 2 vs. 6 hops). M4 L07 midió un fan-out secuencial contra uno paralelo (6 = 6 llamadas, 4 = 4 hops, pero 6 vs. 4 rondas). M5 L07 midió un handoff directo contra una vuelta al supervisor (4 vs. 6 llamadas, 1 vs. 3 hops). Cada una midió su propio patrón, aislado.

Esta lección construye la pieza que falta: coordination_cost, una función que generaliza las tres convenciones —N - 1 para un pipeline (M3), HOPS_PER_SPECIALIST = 2 para un track consultado por el supervisor (M4), el hop directo de un handoff (M5)— en una sola fórmula que funciona sobre cualquier PLAN de tracks, sin importar cuántos ni de qué patrón. Es la única pieza de código genuinamente nueva de todo este módulo, y la vas a probar contra las Demos A y B ya construidas —confirmando, con una comparación honesta, en qué se parece y en qué se diferencia de las mediciones aisladas de M3 y M4.

Conexión con el módulo

Esta lección reusa, sin cambios, count_model_calls y count_tool_calls de M1 L05, y las convenciones de hops de M2 L06/M4 L07 (HOPS_PER_SPECIALIST = 2) y M5 L07 (el hop directo de un handoff). Lo único nuevo es track_calls_and_hops y coordination_cost, que aplican esas convenciones ya conocidas sobre un PLAN con varios tracks a la vez — el conteo que el Módulo 1 prometió medir para un sistema completo, y que M7 L01 explícitamente dejó pendiente para acá. La lección 08 reusa esta misma función, sin cambios, sobre la Demo C.


Analogía: el libro de cuentas, no solo la cocina

Las lecciones 03, 04, 05 y 06 de este módulo mostraron que la cocina funciona —cada mecanismo resuelve su parte—. Pero un dueño de restaurante que quiere vender el negocio no entrega solo una cocina que funciona: entrega un libro de cuentas que dice, con números, cuánto cuesta atender cada tipo de mesa. Esta lección es ese libro de cuentas, generalizado: no un renglón distinto por cada tipo de mesa (como hicieron M3, M4 y M5, cada uno con su propia hoja de cálculo), sino una sola fórmula que funciona sin importar cuántos platos —o patrones— tenga la mesa que estás cobrando.


Ejemplo trabajado: coordination_cost, generalizando M3, M4 y M5

import ast
import concurrent.futures
from dataclasses import dataclass
import reservo_tools as rt

# ---- SPECIALISTS, run_specialist, run_pipeline, run_tracks_parallel,
# Track: idénticos a los Módulos 2, 3, 4/7 -- las mismas funciones que ya
# probaste en las lecciones 02, 03 y 04 de este módulo ----


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})")


def search_docs(query):
    q = query.lower()
    if "cancela" in q:
        return "[cancellation-policy] Puedes cancelar sin cargo hasta 2 horas antes del horario reservado."
    if "no" in q and ("present" in q or "show" in q or "llega" in q):
        return ("[no-show-policy] Si no te presentas a una reserva confirmada y no cancelas "
                 "con al menos 2 horas de anticipación, Reservo cobra el 50% del precio "
                 "cotizado como cargo por no-presentación.")
    return "No se encontró una política relevante para esa pregunta."


SPECIALISTS = {
    "booking_agent": {"tools": {"get_quote": rt.get_quote, "book_room": rt.book_room}},
    "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, "label": stage.label, "history": history})
    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


@dataclass
class Track:
    key: str
    pattern: str  # "pipeline" | "fanout" | "handoff"
    description: str


def count_model_calls(history):
    """M1 L05, sin cambios: cada turno 'assistant' es una llamada al
    modelo consumida del guion (concepto) -- pedir una tool, o el texto final."""
    return sum(1 for m in history if m["role"] == "assistant")


def count_tool_calls(history):
    """M1 L05, sin cambios: cuenta los bloques tool_use reales despachados."""
    total = 0
    for m in history:
        if isinstance(m["content"], list):
            total += sum(1 for b in m["content"] if b["type"] == "tool_use")
    return total


PLAN_CALLS = 1     # concepto: el supervisor lee la petición y arma el PLAN de tracks
COMPOSE_CALLS = 1  # concepto: la síntesis final para el socio, combinando todos los tracks
DISPATCH_HOPS = 2  # convención M2 L06 / M4 L07: ida y vuelta del supervisor a CADA track


def track_calls_and_hops(track, result):
    """Generaliza, por patrón, cómo se cuentan las llamadas y los hops
    INTERNOS de un track -- reusando exactamente las fórmulas ya medidas
    en M3 (pipeline_hops = N - 1), M4 (HOPS_PER_SPECIALIST = 2) y M5 (el
    hop directo de un handoff = 1)."""
    if track.pattern == "pipeline":
        _, trace = result
        calls = sum(count_model_calls(s["history"]) for s in trace)
        tool_calls = sum(count_tool_calls(s["history"]) for s in trace)
        hops = DISPATCH_HOPS + (len(trace) - 1)
    elif track.pattern == "fanout":
        _, history = result
        calls = count_model_calls(history)
        tool_calls = count_tool_calls(history)
        hops = DISPATCH_HOPS
    elif track.pattern == "handoff":
        _, trace = result
        calls = sum(count_model_calls(step["history"]) for step in trace)
        tool_calls = sum(count_tool_calls(step["history"]) for step in trace)
        hops = DISPATCH_HOPS + 1
    else:
        raise ValueError(f"patrón desconocido: {track.pattern!r}")
    return calls, tool_calls, hops


def coordination_cost(plan, track_results):
    """El conteo prometido en el Módulo 1, generalizado a una petición con
    N tracks de patrones distintos. Devuelve llamadas al modelo, tool
    calls, hops entre agentes, y rondas de coordinación (secuencial vs.
    paralelo -- max() en vez de sum(), la fórmula de M4 L07, porque los
    tracks corren a la vez con run_tracks_parallel)."""
    per_track = {}
    for t in plan:
        calls, tool_calls, hops = track_calls_and_hops(t, track_results[t.key])
        per_track[t.key] = {"pattern": t.pattern, "calls": calls, "tools": tool_calls, "hops": hops}

    call_counts = [v["calls"] for v in per_track.values()]
    total_specialist_calls = sum(call_counts)
    total_tool_calls = sum(v["tools"] for v in per_track.values())
    total_hops = sum(v["hops"] for v in per_track.values())
    total_model_calls = PLAN_CALLS + total_specialist_calls + COMPOSE_CALLS

    surrounding = PLAN_CALLS + COMPOSE_CALLS
    sequential_rounds = sum(call_counts) + surrounding
    parallel_rounds = (max(call_counts) if call_counts else 0) + surrounding

    return {
        "per_track": per_track, "total_model_calls": total_model_calls,
        "total_tool_calls": total_tool_calls, "total_hops": total_hops,
        "sequential_rounds": sequential_rounds, "parallel_rounds": parallel_rounds,
    }


# --- Reconstruir los jobs de la Demo A (Luis, lección 03) y la Demo B
# (Marta, lección 04) -- los mismos guiones, sin ningún cambio ---
PIPELINE_STAGES = [
    PipelineStage(kind="quote", name="booking_agent", label="cotizar"),
    PipelineStage(kind="validate_policy", name="policy_agent", label="validar la política de cancelación"),
    PipelineStage(kind="confirm", name="booking_agent", label="confirmar la reserva"),
]
INITIAL_PAYLOAD_A = {"room": "Studio", "tier": "pro", "hours": 3, "member": "Luis"}
script_quote_a = [
    {"stop_reason": "tool_use", "content": [{"type": "tool_use", "id": "toolu_01", "name": "get_quote",
     "input": {"room": "Studio", "tier": "pro", "hours": 3}}]},
    {"stop_reason": "end_turn", "content": [{"type": "text", "text": "Studio pro 3h cuesta 9600 centavos."}]},
]
script_policy_a = [
    {"stop_reason": "tool_use", "content": [{"type": "tool_use", "id": "toolu_01", "name": "search_docs",
     "input": {"query": "política de cancelación"}}]},
    {"stop_reason": "end_turn", "content": [{"type": "text", "text": (
        "Puedes cancelar sin cargo hasta 2 horas antes del horario reservado. "
        "No hay ningún impedimento para confirmar.")}]},
]
script_confirm_a = [
    {"stop_reason": "tool_use", "content": [{"type": "tool_use", "id": "toolu_01", "name": "book_room",
     "input": {"room": "Studio", "tier": "pro", "hours": 3, "member": "Luis"}}]},
    {"stop_reason": "end_turn", "content": [{"type": "text", "text": "Reservé Studio pro 3h para Luis (confirmación #1)."}]},
]


def job_book_studio():
    return run_pipeline(PIPELINE_STAGES, [script_quote_a, script_policy_a, script_confirm_a], INITIAL_PAYLOAD_A)


script_pricing_b = [
    {"stop_reason": "tool_use", "content": [
        {"type": "tool_use", "id": "toolu_01", "name": "get_quote", "input": {"room": "Studio", "tier": "pro", "hours": 2}},
        {"type": "tool_use", "id": "toolu_02", "name": "get_quote", "input": {"room": "Boardroom", "tier": "pro", "hours": 2}},
    ]},
    {"stop_reason": "end_turn", "content": [{"type": "text", "text": (
        "Studio pro 2h: 6400 centavos. Boardroom pro 2h: 12800 centavos. Studio es la opción más barata de las dos.")}]},
]
script_no_show_b = [
    {"stop_reason": "tool_use", "content": [{"type": "tool_use", "id": "toolu_01", "name": "search_docs",
     "input": {"query": "qué pasa si un socio no se presenta a una reserva"}}]},
    {"stop_reason": "end_turn", "content": [{"type": "text", "text": (
        "Si no te presentas a una reserva confirmada y no cancelas con al menos 2 horas de anticipación, "
        "Reservo cobra el 50% del precio cotizado como cargo por no-presentación.")}]},
]


def job_compare_rooms():
    return run_specialist("pricing_agent", "Compara Studio y Boardroom pro 2h.", script_pricing_b)


def job_no_show_policy():
    return run_specialist("policy_agent", "¿Qué pasa si un socio no se presenta a una reserva confirmada?", script_no_show_b)


# --- Probar la función contra la Demo A (Luis, 1 track, pipeline) ---
PLAN_A = [Track(key="book_studio", pattern="pipeline", description="cotizar, validar, confirmar")]
results_a = run_tracks_parallel({"book_studio": job_book_studio})
cost_a = coordination_cost(PLAN_A, results_a)

print("=== costo de coordinación: Demo A (Luis, 1 track, pipeline) ===")
for t in PLAN_A:
    d = cost_a["per_track"][t.key]
    print(f"  {t.key:<14} patrón={d['pattern']:<9} llamadas={d['calls']} tools={d['tools']} hops={d['hops']}")
print(f"  TOTAL llamadas al modelo: {cost_a['total_model_calls']}  "
      f"(1 PLAN + {cost_a['per_track']['book_studio']['calls']} especialistas + 1 síntesis)")
print(f"  TOTAL hops: {cost_a['total_hops']}")
print(f"  rondas secuencial={cost_a['sequential_rounds']} vs. paralelo={cost_a['parallel_rounds']}")

# --- Probar la función contra la Demo B (Marta, 2 tracks, fan-out) ---
PLAN_B = [
    Track(key="compare_rooms", pattern="fanout", description="comparar Studio y Boardroom pro 2h"),
    Track(key="no_show_policy", pattern="fanout", description="pregunta general de no-presentación"),
]
results_b = run_tracks_parallel({"compare_rooms": job_compare_rooms, "no_show_policy": job_no_show_policy})
cost_b = coordination_cost(PLAN_B, results_b)

print()
print("=== costo de coordinación: Demo B (Marta, 2 tracks, fan-out) ===")
for t in PLAN_B:
    d = cost_b["per_track"][t.key]
    print(f"  {t.key:<14} patrón={d['pattern']:<9} llamadas={d['calls']} tools={d['tools']} hops={d['hops']}")
print(f"  TOTAL llamadas al modelo: {cost_b['total_model_calls']}")
print(f"  TOTAL hops: {cost_b['total_hops']}")
rounds_saved = cost_b['sequential_rounds'] - cost_b['parallel_rounds']
pct = round(100 * rounds_saved / cost_b['sequential_rounds'])
print(f"  rondas secuencial={cost_b['sequential_rounds']} vs. paralelo={cost_b['parallel_rounds']} "
      f"({rounds_saved} menos, {pct}% menos)")

Qué esperar:

=== costo de coordinación: Demo A (Luis, 1 track, pipeline) ===
  book_studio    patrón=pipeline  llamadas=6 tools=3 hops=4
  TOTAL llamadas al modelo: 8  (1 PLAN + 6 especialistas + 1 síntesis)
  TOTAL hops: 4
  rondas secuencial=8 vs. paralelo=8

=== costo de coordinación: Demo B (Marta, 2 tracks, fan-out) ===
  compare_rooms  patrón=fanout    llamadas=2 tools=2 hops=2
  no_show_policy patrón=fanout    llamadas=2 tools=1 hops=2
  TOTAL llamadas al modelo: 6
  TOTAL hops: 4
  rondas secuencial=6 vs. paralelo=4 (2 menos, 33% menos)

Dos resultados para leer con cuidado. La Demo A, con un solo track, no ahorra nada de rondas (8 = 8) — tiene sentido: max() y sum() de una lista de un solo elemento dan el mismo número, así que correr un único track "en paralelo" no tiene ningún otro track con el que compartir el tiempo de coordinación. La Demo B, con dos tracks, sí ahorra 2 rondas (33% menos) — el mismo 33% que ya viste en el ejemplo canónico de M4 L07, porque la estructura es idéntica: dos tracks, dos llamadas internas cada uno.


Una comparación honesta: esta función NO reduce exactamente a M3 L05

Vale la pena ser preciso acá, porque sería fácil (y engañoso) afirmar que coordination_cost "reproduce" los números de M3 L05 cuando se le pasa un solo track de pipeline. No lo hace, y hay una razón real:

m3_pipeline_total = 0 + cost_a["per_track"]["book_studio"]["calls"] + 1  # 0 ruteo (M3) + especialistas + 1 síntesis
m3_pipeline_hops = 3 - 1  # N - 1, la fórmula original de M3 L05

print(f"M3 aislado:  {m3_pipeline_total} llamadas, {m3_pipeline_hops} hops "
      f"(sin PLAN_CALLS -- el patrón ya se sabía de antemano)")
print(f"M8 sistema:  {cost_a['total_model_calls']} llamadas, {cost_a['total_hops']} hops "
      f"(+1 PLAN_CALLS, +2 DISPATCH_HOPS: el supervisor SÍ tuvo que decidir el plan)")
M3 aislado:  7 llamadas, 2 hops (sin PLAN_CALLS -- el patrón ya se sabía de antemano)
M8 sistema:  8 llamadas, 4 hops (+1 PLAN_CALLS, +2 DISPATCH_HOPS: el supervisor SÍ tuvo que decidir el plan)

La diferencia es real, no un error de conteo: la lección de M3 midió un pipeline aislado, donde el patrón ya se sabía de antemano —nadie tuvo que decidir "¿esto es un pipeline o alguna otra cosa?"—. El sistema completo de este módulo es distinto: recibe una petición arbitraria y tiene que decidir, primero, cuántos tracks tiene y con qué patrón se resuelve cada uno —eso es exactamente PLAN_CALLS—, y tiene que despachar y recibir de vuelta cada track, aunque sea uno solo —eso es DISPATCH_HOPS—. Ese costo de "decidir el plan" no existía en la lección aislada de M3 porque, en esa lección, el plan ya venía dado. Es la misma clase de hallazgo que ya viste en M1 L05 (coordinar cuesta, incluso cuando el trabajo real es idéntico) — ahora aplicado al costo de decidir qué mecanismo usar, no solo al de ejecutarlo.


De dónde sale cada pieza de la fórmula

PLAN_CALLS = 1        -- el supervisor SIEMPRE arma un PLAN, sin importar
                          cuántos tracks resulten (0, 1, 2, o más).
COMPOSE_CALLS = 1      -- la síntesis final SIEMPRE combina lo que
                          produjeron los tracks, sin importar cuántos haya.
DISPATCH_HOPS = 2      -- CADA track, sin importar su patrón interno, es
                          despachado por el supervisor y le devuelve un
                          resultado -- ida y vuelta, la convención de M4 L07.

Hops INTERNOS por patrón (adentro de DISPATCH_HOPS, no en vez de):
  pipeline (N etapas)  -> N - 1   (traspaso directo etapa a etapa, M3 L05)
  fanout (1 especialista) -> 0    (nada que traspasar puertas adentro)
  handoff              -> 1       (el traspaso directo emisor -> receptor, M5 L07)

Con esta tabla, verificar hops=4 de book_studio en la Demo A es directo: DISPATCH_HOPS(2) + (3 etapas - 1) = 2 + 2 = 4. Y verificar hops=2 de cada track de la Demo B: DISPATCH_HOPS(2) + 0 = 2, por track — 2 + 2 = 4 en total para los dos tracks de Marta.


Errores comunes

  1. Esperar que coordination_cost de exactamente los mismos números que M3/M4/M5 aislados. No lo hace, a propósito — la sección anterior de esta lección lo explica: el sistema completo paga un costo real de decisión (PLAN_CALLS) que un patrón aislado, con el mecanismo ya decidido de antemano, no pagaba.

  2. Olvidar sumar DISPATCH_HOPS a los hops internos de cada track. Un error común es calcular solo N - 1 para un pipeline y olvidar los 2 hops de despacho/retorno del supervisor — el resultado saldría 2 en vez de 4 para la Demo A, un número que no coincide con la salida real ejecutada.

  3. Aplicar max() en vez de sum() para las rondas secuenciales, o viceversa. La convención de M4 L07 es clara: secuencial suma (sum(), cada track espera al anterior), paralelo acota al más largo (max(), todos corren a la vez). Invertirlas produciría un ahorro de rondas negativo, una señal inmediata de que algo está mal.


Ejercicios

Ejercicio 1: Calcula a mano el costo de un PLAN de 3 tracks, antes de ejecutar (Fácil)

Sin ejecutar código todavía, calcula a mano total_model_calls y total_hops para un PLAN con tres tracks: uno pipeline de 4 etapas, uno fanout, y uno handoff, donde las llamadas internas de cada uno son 8, 2 y 3 respectivamente. Después, verifica tu cálculo con código.

Ver solución

A mano: total_model_calls = PLAN_CALLS(1) + (8 + 2 + 3) + COMPOSE_CALLS(1) = 1 + 13 + 1 = 15. total_hops = [DISPATCH_HOPS(2) + (4-1)] + [DISPATCH_HOPS(2) + 0] + [DISPATCH_HOPS(2) + 1] = 5 + 2 + 3 = 10.

per_track_hardcoded = {
    "pipeline_track": {"calls": 8, "hops": 2 + (4 - 1)},
    "fanout_track": {"calls": 2, "hops": 2 + 0},
    "handoff_track": {"calls": 3, "hops": 2 + 1},
}
total_calls = PLAN_CALLS + sum(v["calls"] for v in per_track_hardcoded.values()) + COMPOSE_CALLS
total_hops = sum(v["hops"] for v in per_track_hardcoded.values())
print("total_model_calls:", total_calls)
print("total_hops:", total_hops)

Salida esperada:

total_model_calls: 15
total_hops: 10

Explicación: coincide con el cálculo a mano — la fórmula no hace nada distinto de sumar las piezas ya conocidas de cada patrón, más los dos costos fijos del sistema (PLAN_CALLS, COMPOSE_CALLS).

Ejercicio 2: ¿Cuántas rondas ahorra un PLAN de 3 tracks desparejos? (Medio)

Usando los mismos tres tracks del Ejercicio 1 (llamadas internas 8, 2 y 3), calcula las rondas secuencial y paralelo, y el porcentaje de ahorro.

Ver solución
call_counts = [8, 2, 3]
surrounding = PLAN_CALLS + COMPOSE_CALLS
sequential_rounds = sum(call_counts) + surrounding
parallel_rounds = max(call_counts) + surrounding
saved = sequential_rounds - parallel_rounds
pct = round(100 * saved / sequential_rounds)
print(f"secuencial={sequential_rounds} paralelo={parallel_rounds} ahorro={saved} ({pct}%)")

Salida esperada:

secuencial=15 paralelo=10 ahorro=5 (33%)

Explicación: con tracks de tamaños muy desparejos (8 llamadas contra 2 y 3), el ahorro de rondas depende del track MÁS GRANDE —el que domina el max()—. Acá, aunque hay tres tracks, el ahorro real viene de que los tracks de 2 y 3 llamadas "se esconden" detrás del de 8, sin sumar tiempo de coordinación extra — el mismo principio de M4 L07, ahora con tres tracks en vez de dos.

Ejercicio 3: ¿Por qué coordination_cost no necesita saber CUÁNTOS especialistas distintos hay? (Difícil)

Sin ejecutar código, explica por qué coordination_cost calcula el mismo DISPATCH_HOPS = 2 para un track sin importar si su especialista interno es booking_agent, policy_agent o pricing_agent — y por qué eso es una ventaja de diseño, no una limitación.

Ver solución

Porque el costo que DISPATCH_HOPS mide no depende de QUIÉN resuelve el track, depende de que hay un track que el supervisor tuvo que despachar y recibir de vuelta — el mismo costo de coordinación existe sin importar si el especialista detrás es simple o complejo. Es una ventaja de diseño, no una limitación, por la misma razón que run_tracks_parallel (M4/M7) no necesita saber qué hace cada job por dentro: la función de costo, igual que la función de despacho, opera al nivel de "cuántos tracks hay y qué patrón resuelve cada uno" —información que SIEMPRE está disponible en el PLAN— sin necesitar bajar al detalle de qué tools tiene cada especialista. Si coordination_cost tuviera que conocer el registro interno de cada especialista para calcular hops, dejaría de generalizar: cada especialista nuevo que Reservo agregara en el futuro obligaría a tocar la función de costo, exactamente el tipo de acoplamiento que esta guía evitó desde que M2 separó el registro (SPECIALISTS) del mecanismo de despacho (run_specialist).


Resumen y siguiente paso

  • coordination_cost generaliza las tres fórmulas de costo de M3, M4 y M5 en una sola función que funciona sobre cualquier PLAN de tracks, sin importar cuántos ni de qué patrón.
  • Probada contra la Demo A: 8 llamadas, 4 hops, 0 rondas ahorradas (un solo track no tiene con quién compartir tiempo de coordinación).
  • Probada contra la Demo B: 6 llamadas, 4 hops, 2 rondas ahorradas (33% menos, el mismo patrón que M4 L07 ya midió).
  • El sistema completo paga un costo real —PLAN_CALLS y DISPATCH_HOPS— que las lecciones aisladas de M3/M4/M5 no pagaban, porque en el sistema completo el patrón correcto se decide, no se asume de antemano.

Siguiente lección: 08 — Proyecto: el sistema multi-agente de Reservo. El capstone: las seis piezas de este módulo, ensambladas en un solo sistema, con la tercera demo (Valentina, los tres patrones combinados) y el cierre de toda la guía.


Recursos adicionales

  1. Anthropic — Multi-agent research system — Anthropic documenta ahí mismo que sus sistemas multi-agente consumen considerablemente más tokens que una conversación de un solo agente — la misma clase de costo de coordinación que esta lección cuantifica, a escala de tokens en vez de llamadas.
  2. Python — Funciones integradas sum() y max() — Las dos funciones detrás de sequential_rounds y parallel_rounds, sin cambios desde M4 L07.
  3. Python — Dataclasses — La estructura detrás de Track, el tipo que coordination_cost recibe como su PLAN.
  4. Anthropic — Building effective agents — La advertencia explícita de Anthropic sobre el costo de latencia y llamadas de los sistemas multi-agente — el mismo principio que esta lección convierte en una función reusable.