1. Descripción general
El título del ADK 2 es tres patrones de organización. En este codelab, se enseñan los tres conceptos a través de la compilación de una app, un entrenador para el día de la carrera de maratón, una etapa ejecutable a la vez. Cada nivel responde una sola pregunta, agrega una idea y se ejecuta por sí solo.
Qué aprenderás
- Flujos de trabajo de gráficos (pilar 1): Cuando puedes dibujar el flujo antes de que llegue la entrada.
- Agentes colaborativos (pilar 2): Cuando conoces al equipo, pero la solicitud elige el subconjunto, y los tres modos de colaboración (
chat,taskysingle_turn), cada uno se ejecuta en vivo. - Flujos de trabajo dinámicos (pilar 3): Cuando la forma del trabajo en sí depende de la entrada.
- Cómo elegir: Un árbol de decisión de una sola pregunta y cómo se componen los patrones.
El hilo conductor
Estructura conocida → subconjunto de variables o equipo conocido → forma desconocida → elige la correcta

Qué compilarás
Una app, Marathon Race Day Coach, ensambló un nivel ejecutable a la vez. Cada nivel es un módulo de Python simple que ejecutas desde la terminal. En el L5, las siguientes partes son todas tuyas.
La imagen se extrae del código en ejecución: cada línea continua se leyó de Workflow.graph.edges. Esta es la primera lección: Las partes que se pueden dibujar con anticipación son exactamente el Pilar 1, y las partes que no se pueden dibujar son la razón por la que existen los Pilares 2 y 3.

Requisitos
- Una Cuenta de Google (para Colab): No se requiere configuración local.
- Aproximadamente 50 minutos (los dos niveles de L4 son los más largos, así que tenlos en cuenta en el presupuesto).
- Es una de las dos formas de acceder a un modelo de Gemini. Elige tu carril: Ejecuta un paso de configuración y omite el otro:
🎓 Taller | 🏠 Para llevar a casa | |
Quiénes | Estás en un taller en vivo y el instructor te dio un vínculo para reclamar créditos. | Todos los demás, incluidos los asistentes al taller, después |
Necesitas lo siguiente | El vínculo de reclamo y una Cuenta de Google que pueda crear un proyecto de Cloud | Una clave de API de AI Studio gratuita |
Se ejecuta en | Vertex AI, en un proyecto facturado a tu crédito del taller | Google AI Studio |
Costo | Cobertura del crédito | Nivel gratuito |
Paso de configuración | Configuración del taller (próximo paso) | Configuración para llevar a casa (el paso siguiente) |
Todo lo que sigue al prólogo es idéntico de cualquier manera: el carril solo decide con qué extremo del modelo se comunica el notebook.
Dos formas de seguir el curso
Cada paso que se indica a continuación se asigna a una celda del notebook de Colab y a una carpeta del repositorio de GitHub. Elige una de las siguientes opciones:
- ▶ Colab (recomendado): Abre el notebook → ejecuta las celdas de arriba abajo.
- 💻 Local:
git cloneel repo,./setup_venv.shy, luego, ejecuta cada nivel como un módulo (python -m ...) o explora todos con./run.sh(adk web).
2. Configuración del taller: Reclama tu crédito y cambia a Vertex AI
En el taller, se te proporcionará crédito de Google Cloud. Reclamarás el crédito, crearás un proyecto facturado a ese crédito y dirigirás el notebook a Vertex AI en lugar de a AI Studio. Una celda hace todo después del reclamo.
1 · Reclama tu crédito (aprox. 1 min)
- Abre el vínculo de reclamo que compartió tu instructor. Se verá de esta forma
https://me.developers.google.com/benefits/claim/your-workshop-name. - Accede y sigue los pasos de la página para aceptar el crédito.
- Anota la Cuenta de Google que usaste. Cada paso que se indica a continuación se debe ejecutar con la misma cuenta.
2 · Abre el notebook y, luego, instala el ADK 2 (aproximadamente 1 min)
Haz clic en Abrir en Colab ▶ y, luego, ejecuta la primera celda de código. Fija la versión exacta del ADK 2 con la que se verificó este codelab y muestra ✓ installed.
3 · Ejecuta la celda "Configuración del taller" (alrededor de 3 min)
Esta es la celda titulada 🎓 Ruta A · Taller. Ejecútalo y Colab te pedirá que autorices la misma Cuenta de Google con la que acabas de reclamar el crédito y permitas el acceso.
Hace cuatro cosas: crea un proyecto llamado adk-2-tutorial-XXXX en tu crédito, habilita la API de Vertex AI en él, establece las cuatro variables de entorno que lee cada celda posterior y, luego, realiza una llamada de prueba a Vertex y espera hasta que responda, de modo que la configuración finaliza o te indica por qué, en lugar de fallar más adelante dentro de un nivel.
Resultado esperado: La última línea es la que importa:
Signed in as: you@example.com
...
Successfully created GCP project 'adk-2-tutorial-4817'.
Successfully linked 'adk-2-tutorial-4817' to billing account '01ABCD-...'.
waiting for Vertex AI to come up on the new project... (10s)
waiting for Vertex AI to come up on the new project... (20s)
✅ Vertex AI on adk-2-tutorial-4817 · us-central1 · gemini-2.5-flash — answered a test call
4 · Omite el paso "Configuración para llevar a casa"
No ejecutes la celda de la clave de AI Studio, ya que el notebook volvería a AI Studio y se desharía lo que acabas de hacer. (La celda se protege contra esto y se negará a ejecutarse, pero la acción más ordenada es simplemente omitirla). Ve directamente desde aquí a la celda Shared building blocks.
5 · Ejecuta la celda "Bloques de creación compartidos".
Ejecútalo una vez. Define los esquemas de Pydantic y las situaciones de maratón predefinidas que reutilizan todos los niveles a partir del L2. Verás ✓ schemas + scenarios ready.
Después del taller
Tu crédito y el proyecto que creaste no durarán para siempre. Para seguir ejecutando estos niveles de forma gratuita una vez que finalice el taller, ejecuta el paso Configuración para llevar a casa, que incluye una clave gratuita de AI Studio, sin proyecto de Cloud ni facturación. Esa celda es lo único que cambia.
Para realizar la limpieza antes, abre la consola de Cloud, selecciona adk-2-tutorial-XXXX y bórralo. Ningún otro elemento de este codelab crea recursos facturables.
3. Configuración para llevar a casa: Clave de API de AI Studio
Todo lo que se encuentra en esta ruta de aprendizaje se ejecuta con una clave de API gratuita de Google AI Studio, sin necesidad de un proyecto de Google Cloud, facturación ni instalación local. Todo este paso lleva alrededor de 3 minutos.
1 · Abre el notebook
Haz clic en Abrir en Colab ▶. Se abrirá el notebook, que incluye una introducción en Markdown y una celda ejecutable por nivel. Ejecutas las celdas de arriba hacia abajo, y cada una imprime su propio resultado justo debajo.
2 · Instala el ADK 2 (alrededor de 1 min)
Ejecuta la primera celda de código. Fija la versión exacta en la que se verificó este codelab:
%pip install -q "google-adk==2.3.0" python-dotenv pydantic nest_asyncio
Espera a que termine. Verás ✓ installed. (La instalación tarda entre 30 y 60 s la primera vez; después, se almacena en caché).
3 · Obtén tu clave de API de Gemini desde AI Studio (aprox. 1 min)
- Abre aistudio.google.com/app/apikey en una pestaña nueva del navegador.
- Accede con tu Cuenta de Google.
- Haz clic en Crear clave de API (en la esquina superior derecha).
- Elige un proyecto de Google existente o permite que se cree uno.
- Copia la clave, que comienza con
AIza...y tiene alrededor de 40 caracteres.
4 · Agrega tu clave a Colab (alrededor de 1 min)
Opción A: Colab Secrets (recomendada; la clave permanece oculta):
- Haz clic en el ícono de llave 🔑 en la barra lateral izquierda de Colab.
- Haz clic en + Agregar nuevo secreto.
- Establece el Nombre exactamente como
GOOGLE_API_KEY. - Pega la clave en Value.
- Cambia Acceso al notebook a ACTIVADO.
Opción B: Pegar cuando se solicite (rápido): Omite el secreto. Cuando ejecutes la siguiente celda, se mostrará un mensaje oculto 🔑 Enter your Google AI Studio API key:. Pega y presiona Intro.
5 · Ejecuta la celda clave
Lee el secreto (o recurre a la instrucción de pegado) y, luego, dirige el ADK a AI Studio (no a Vertex AI):
import os
# 🏠 TAKE-HOME ONLY — if you ran the Workshop setup cell, skip this one.
if os.environ.get("GOOGLE_GENAI_USE_VERTEXAI") == "True":
raise SystemExit("✋ You're set up on the workshop path (Vertex AI). Skip this cell.")
# Google AI Studio API key — add GOOGLE_API_KEY in the 🔑 Secrets panel (or paste when prompted).
try:
from google.colab import userdata
key = userdata.get("GOOGLE_API_KEY")
except Exception:
import getpass
key = getpass.getpass("Enter your Google AI Studio API key: ")
os.environ["GOOGLE_API_KEY"] = "".join(key.split()) # drop any stray whitespace/newlines
os.environ["GOOGLE_GENAI_USE_VERTEXAI"] = "False" # use AI Studio, not Vertex AI
print("✅ API key set — using Google AI Studio.")
Resultado esperado: ✅ API key set — using Google AI Studio.
6 · Ejecuta la celda "Bloques de creación compartidos"
Ejecuta la celda Shared building blocks una vez. Define los esquemas de Pydantic y las situaciones de maratón predefinidas que reutilizan todos los niveles a partir del L2. Verás ✓ schemas + scenarios ready.
¡Todo listo! 🎽 Un pequeño desvío antes de L0, la versión que todos compilan primero.
4. Prólogo: ¿Por qué no usar una sola instrucción grande?
⚡ Antes de ejecutarlo, define UNA cosa para mirar: ¿De dónde proviene cada número específico? Ese es todo el ejercicio; todo lo demás es decoración.
Antes de la escalera, ejecuta lo que reemplaza la escalera: un agente cuyo mensaje promete todo: buscar el clima, analizar el curso, leer el registro de entrenamiento, enrutar según las condiciones y generar el plan.
Lo que verás: Una estrategia segura, específica y bien formateada… cuyos números son inventados. En una ejecución en vivo, se abrió con "Recopilé las métricas del clima de hoy" y registró 11 °C, un viento de 14 km/h y un análisis de un registro de entrenamiento que nunca había visto. Aquí no hay una API del clima, ni datos de cursos, ni registros: una llamada opaca al modelo fabrica sus entradas o las convierte en algo inútil.
Esa es la enfermedad, y tiene cuatro síntomas que vale la pena mencionar:
- No puedes confiar en ella: Los datos se inventan con fluidez.
- No puedes probarlo: El enrutamiento del paso 4 se encuentra dentro del texto, por lo que no hay ningún
ifpara realizar pruebas de unidades. - No puedes intercambiar un paso: No hay ninguna unión en la que se pueda conectar una API del clima real.
- Pagas por todo, siempre: Cinco pasos, una llamada gigante, sin almacenamiento en caché de una parte determinística.
Mantén ese sentimiento. Los siguientes nueve niveles quitan esos pasos de la instrucción, uno a la vez: las funciones recuperan (N1 a N2a), una instrucción if dirige (N2b), los especialistas dividen el trabajo (N3a a N3b) y el código limita la forma (N4a a N4b).

💻 Local: python -m shared.prologue
5. Nivel 0: Tu primer agente de ADK 2

⚡ Resumen: Un agente es un modelo + una instrucción + herramientas a las que puede llamar; un Runner lo ejecuta. Todo lo que hay después de este nivel son más agentes, dispuestos de mejores formas.
La pregunta: ¿Puedes hacer que un modelo responda y recurra a código real cuando la aritmética es importante?
La idea única (tres partes):
Agent: Es lo que razona (un modelo de Gemini más una instrucción).Runner: Es el elemento que ejecuta un agente dentro de una sesión y transmite eventos.- una herramienta: Una función de Python simple (
pace_splits) a la que el modelo decide llamar. El ADK lee la firma y la cadena de documentación, y le entrega al modelo una declaración; no es necesario escribir un esquema.
Después del prólogo, esta es la primera corrección: un LLM que hace aritmética de ritmo mentalmente se equivocará con gusto. pace_splits es Python determinístico, por lo que los números de la respuesta se calculan, no se improvisan.
▶ Colab: Ejecuta la celda L0 · 📁 GitHub: L0_first_agent/ · 💻 Local: python -m L0_first_agent.agent

def pace_splits(target_finish: str) -> dict:
"""Convert a goal time like '3:30:00' into exact per-mile / per-km paces."""
... # deterministic Python — no LLM
pace_coach = Agent(
name="pace_coach", model=MODEL,
tools=[pace_splits], # the model may call it; ADK reads the signature
instruction="You are a friendly, concise marathon coach. ... If the runner "
"mentions a goal time, call pace_splits — never do arithmetic yourself.",
)
runner = Runner(node=pace_coach, session_service=InMemorySessionService(), auto_create_session=True)
async for event in runner.run_async(user_id="u1", session_id="s1", new_message=msg):
... # events carry the model's text
🔍 Los marcadores: Agent(...) · tools=[pace_splits] · Runner(...). Y, en el resultado, la línea 🔧, que es el modelo que decide, a mitad de la respuesta, llamar a tu código.
Qué verás:
🔧 model called tool → pace_splits({'target_finish': '3:30:00'})
🔧 tool returned → {'per_mile': '8:00', 'per_km': '4:58', ...}
🧠 Coach: To finish in 3:30:00, you need an average pace of 8:00 per mile...
Las líneas 🔧 son la lección: A mitad de la respuesta, el modelo eligió llamar a tu función, y el 8:00/mile exacto en su respuesta provino de tu código, no de las estadísticas de tokens.
❓ Es posible que te preguntes: ¿El modelo siempre llama a la herramienta? No, se decide por pregunta. Pregunta algo que no incluya números y las líneas de 🔧 desaparecerán (el playground te pide que pruebes exactamente esto).
👀 Lee: pace_splits (una función simple) y la línea tools=[pace_splits]. · ▶ Ejecútalo. · ✏️ Cambio: Haz la pregunta general (sin tiempo objetivo). Observa que desaparecen las líneas de 🔧: el modelo decide cuándo vale la pena llamar a una herramienta. Luego, vuelve a escribir el instruction y vuelve a ejecutarlo. La instrucción es el resto del programa.
6. L1: Tu primer flujo de trabajo

⚡ Resumen: Una función simple y un agente de LLM son el mismo tipo de nodo. Trabajo predecible → función (0 LLM, determinístico); razonamiento → agente.
La pregunta: ¿cómo se combina código simple y un LLM en un solo flujo, sin pagar por una llamada al modelo en las partes que son solo código?
La idea principal: En un Workflow, una función simple de Python y un agente de LLM son solo nodos en la misma lista de edges.
START ──► fetch_conditions (function, 0 LLM) ──► advise (agent, 1 LLM)
▶ Colab: Ejecuta la celda L1 · 📁 GitHub: L1_graph_basics/ · 💻 Local: python -m L1_graph_basics.workflow

El nodo de función imprime los datos que produjo (sin llamada al modelo) y, luego, el agente brinda asesoramiento que hace referencia a la temperatura y el viento reales que recibió:
def fetch_conditions(node_input): # function node — 0 LLM
return Event(output=Conditions(temp_f=78, wind_mph=12, conditions="sunny").model_dump())
advise = Agent(name="advise", model=MODEL, mode="single_turn",
input_schema=Conditions, instruction="...give pacing + gear advice...")
workflow = Workflow(edges=[(START, fetch_conditions, advise)])
🔍 Los marcadores: Una tupla de borde ((START, fetch_conditions, advise)) con una función de Python simple en el medio y input_schema= que valida la transferencia.
Novedades en comparación con el nivel 0: Workflow(edges=[...]), START (donde ingresa la entrada), un nodo de función que devuelve Event(output=...) y input_schema=Conditions para que la salida de la función se valide según ese esquema antes de que el agente la vea (como texto JSON: input_schema valida el límite, no le entrega al agente un objeto de Python).
❓ Es posible que te preguntes: ¿El orden de función y luego agente es el requerido? No. Puedes hacer el pedido que quieras, con la combinación y la cantidad que desees. advise se ejecuta en segundo lugar solo porque necesita los datos de fetch_conditions. La lección es la nobleza, no la secuencia.
👀 Lectura: fetch_conditions devuelve datos sin llamada al modelo; advise tiene input_schema=Conditions. · ▶ Ejecútalo. · ✏️ Cambio: Establece temp_f=30 en la función y vuelve a ejecutarla. La sugerencia cambia y la función sigue costando 0 llamadas al LLM.
7. L2a: Fan-out paralelo + JoinNode (pilar 1a)

⚡ Resumen: Realiza la expansión en paralelo (gratis), espera todo, agrupa y entrega a un agente la imagen completa.
La pregunta: Puedes dibujar el flujo antes de que llegue la entrada. Comienza con el esqueleto: recopila datos en paralelo, agrúpalos y entrégalos a un agente.
La forma:
START ──► fetch_weather ──┐
START ──► analyze_course ─┼─► JoinNode ─► strategy (1 agent)
START ──► pull_fitness ───┘ (bundles)
▶ Colab: Ejecuta la celda L2a · 📁 GitHub: L2a_parallel_join/ · 💻 Local: python -m L2a_parallel_join.workflow

🔍 Los marcadores: Tres bordes que comienzan en START (ese es el punto de expansión) y JoinNode, el punto de encuentro.
- Las tres recuperaciones son funciones que se ejecutan en paralelo, con 0 llamadas a LLM.
JoinNodeespera los tres y los agrupa en una carga útil con tipo (BundledRunData) con la clave del nombre de la función.- Un agente de
strategylee el paquete y escribe unRaceStrategy.
Qué verás: Cada recuperación imprimirá una marca de tiempo started / finished. Los tres comienzan en 0.0 s y el fan-out finaliza en 2.0 s, la recuperación más lenta, no los 4.5 s a los que se sumarían sus duraciones. Esa superposición es el paralelismo. (El tiempo total de total que se imprime al final es de aproximadamente 8 s porque también contiene la llamada al LLM del agente de estrategia. Lee las marcas de tiempo de recuperación para el reclamo paralelo, no el total).
💡 Devolución de llamada de prólogo: El megaprompt inventó el clima. Aquí, la temperatura proviene de una función de recuperación: código real, unión real. Intercambia el diccionario predefinido por una API de clima real y no cambiará nada más.
❓ Tal vez te preguntes: ¿Cuánto
JoinNode
¿Qué debo comprender? En una oración: Espera hasta que se completen todas las ramas paralelas, empaqueta los resultados en un diccionario con la clave del nombre de la función upstream y no calcula nada por sí misma. Ese diccionario es exactamente la razón por la que el router de L2b puede escribir node_input["fetch_weather"]["temp_f"].
👀 Lectura: Tres bordes se extienden desde START; JoinNode los agrupa para un agente. · ▶ Ejecútalo y lee las marcas de tiempo, no el total. · ✏️ Cambio: Haz que una recuperación duerma 3.0: Primero predice la nueva hora de finalización del fan-out y, luego, verifica.
8. L2b: Agrega el router determinístico (pilar 1b)

⚡ Resumen: L2a sin modificar + un if simple decide qué agente uno se ejecuta. Ramificación, sin preguntarle al modelo
La pregunta: El plan debe ser diferente para el clima cálido y el frío. ¿Cómo creas una rama sin pedirle al modelo que decida?
La forma (L2a + un router):
... JoinNode ─► route_by_weather ─► hot_strategy
(if-statement) ─► normal_strategy
─► cold_strategy
▶ Colab: Ejecuta la celda L2b. Prueba run("NORMAL") o run("COLD"). · 📁 GitHub: L2b_router/ · 💻 Local: python -m L2b_router.workflow COLD

def route_by_weather(node_input): # an if-statement, 0 LLM
temp = node_input["fetch_weather"]["temp_f"]
route = "HOT" if temp >= 70 else "COLD" if temp <= 40 else "NORMAL"
return Event(output=node_input, route=route)
(route_by_weather, {"HOT": hot_strategy, "NORMAL": normal_strategy, "COLD": cold_strategy})
🔍 Los marcadores: Event(output=..., route=...), un nodo de función que nombra la ruta, y el borde de diccionario {"HOT": ..., "NORMAL": ..., "COLD": ...} que asigna nombres a nodos.
Conclusión: Tres tipos de trabajo, tres hogares:
- Trabajo predecible → funciones (las 3 recuperaciones paralelas)
- Una regla clara → enrutamiento explícito (
route_by_weatheres una instrucciónif, no una decisión del modelo) - Reasoning → El modelo (se ejecuta exactamente un agente de estrategia)
Qué verás: temp=78F -> route=HOT y, luego, un RaceStrategy estructurado. Costo neto: 1 llamada al LLM.
⚠️ Si agregas una cuarta rama, también debes agregar una entrada DEFAULT_ROUTE al diccionario de rutas. Una ruta que no coincide con el diccionario no es un error: la rama simplemente termina y el programa sale con 0 sin resultado, lo que es un callejón sin salida confuso para depurar.
❓ Quizás te preguntes: ¿Entonces, el L2b es literalmente el L2a más un router? Sí, las recuperaciones y la unión no se modifican, y sigue siendo exactamente 1 llamada al LLM. Qué cambió: "Siempre el mismo agente" se convirtió en "Uno de tres, elegido por los datos".
👀 Lee: route_by_weather: El router es una instrucción if, no un agente. · ▶ También ejecuta run("COLD"). · ✏️ Cambio: Agrega una rama WINDY con un cuarto agente y lee la advertencia de DEFAULT_ROUTE que se encuentra arriba antes de hacerlo.
9. L3a, Collaborative agents: one flag, two worlds (Agentes colaborativos: una marca, dos mundos) — Pilar 2

⚡ Resumen: El mismo equipo, una sola marca. chat le entrega la conversación completa a un especialista y nunca regresa; single_turn convierte a cada especialista en una herramienta: subconjunto paralelo, devolución automática y una síntesis.
La pregunta: Conoces al equipo, pero la solicitud decide qué miembros deben responder. ¿Cómo permites que un LLM elija el subconjunto y los ejecute de forma simultánea?
La forma: Un coordinador de seis especialistas (médico, clima, ritmo, equipo, nutrición y mental). En este nivel, se ejecuta el mismo equipo dos veces: la misma instrucción del coordinador y los mismos seis especialistas. La única diferencia es una marca en los subagentes. El contraste es la lección.
▶ Colab: Ejecuta la celda L3a · 📁 GitHub: L3a_collaborative/ · 💻 Local: python -m L3a_collaborative.concierge --mode chat "What about fueling?"

🔍 Los marcadores: mode="single_turn" en la fábrica y en el resultado, TRANSFER → (tiempo 1) en comparación con una ráfaga de líneas DISPATCH → que comparten una marca de tiempo (tiempo 2).
Tiempo 1: Ejecuta el valor predeterminado primero y observa cómo falla el trabajo
No se escribió ningún mode= → los subagentes se establecen de forma predeterminada en chat. Verás lo siguiente:
TRANSFER → nutrition_specialist (transfer_to_agent — the only tool chat subagents provide)
Final speaker: nutrition_specialist
El coordinador no tiene herramientas de delegación: los subagentes de chat solo le dan transfer_to_agent, una transferencia en serie de toda la conversación a un especialista. Ese especialista le responde directamente al usuario, y la ejecución finaliza allí. No hay envíos paralelos. No se puede devolver. No hay síntesis. Si haces la pregunta amplia, la situación empeora: seis especialistas, una transferencia.
No es un error, sino que el modo de chat está haciendo su trabajo. La conversación pertenece a quien la sostiene, hasta que alguien la transfiere explícitamente. Es correcto para un asistente de respuesta abierta, pero incorrecto para un paso de canalización.
Momento 2: Una bandera, dos mundos
La única diferencia es mode="single_turn" en cada especialista. La misma pregunta, vuelve a ejecutarla:
[t= 7.8s] DISPATCH → medical_specialist ← same timestamp =
[t= 7.8s] DISPATCH → weather_specialist one turn, many calls
[t=14.5s] ↩ medical_specialist replied ← replies land inside
[t=14.5s] ↩ weather_specialist replied one short window
🧠 Concierge (synthesized): <one answer>
Ahora, el ADK inserta una herramienta de delegación por especialista, que lleva el nombre del subagente y se describe con su description= (ese texto es lo que lee el coordinador cuando elige el subconjunto; omítelo y solo enrutarás por nombres). El coordinador emite varias llamadas en un turno, el ADK las ejecuta en paralelo, cada una devuelve automáticamente su resultado y el coordinador sintetiza.
Pregunta | Especialistas que disparan |
"¿Qué sucede con la carga de combustible?" | solo nutrición |
“Me duele la rodilla en el kilómetro 29”. | Solo para uso médico |
"¿Debería competir hoy?" | médico + clima + ritmo |
"¿Hay algo de lo que deba preocuparme?" | todos los 6 |
Por qué cada especialista recibe la instrucción completa: Cada subagente de single_turn se ejecuta en su propia rama de sesión aislada, por lo que no puede ver la conversación ni a sus pares. Nada es ambiental: el coordinador debe reenviar la totalidad de SpecialistInput (pregunta + estrategia + datos del ejecutor) por separado en cada llamada paralela.
💡 Dónde ADK 2 le da un lugar directo: Un LLM elige un subconjunto por solicitud Y lo ejecuta en paralelo, declarado a través de sub_agents + mode="single_turn". En la versión 1.x, podrías ensamblar la misma forma envolviendo cada especialista en AgentTool. Lo que cambia es que ahora es una declaración en lugar de una conexión. (ParallelAgent siempre es todo y transfer_to_agent es serial).
⚠️ Dos advertencias honestas: (1) El modelo elige el subconjunto, por lo que es menos determinístico que el router codificado de forma rígida de L2. El subconjunto exacto puede variar de una ejecución a otra. (2) En ocasiones, verás una línea Error validating input: ... para un especialista. Casi nunca es el resultado del especialista. output_schema hace que Gemini aplique eso del lado del servidor. Es la entrada: El coordinador debe reproducir todo el SpecialistInput anidado de forma literal para cada llamada paralela y, a veces, se equivoca en alguna. El ADK devuelve el error como resultado de esa herramienta, el coordinador se recupera y la síntesis sigue funcionando.
❓ Tal vez te preguntes: ¿
chat
¿Solo delegación de estilo 1.x, un agente a la vez? Básicamente, sí: es el comportamiento predeterminado de la versión 1.x, ahora con un nombre. La brecha con single_turn es tridimensional: lo que tiene el coordinador (un transfer_to_agent frente a una herramienta por especialista), cuántos pueden trabajar (uno, que posee la conversación, frente a N en paralelo) y si se recupera el control (nunca frente a automáticamente, con resultados). Y sobre el código: La rama if mode == de la fábrica existe solo para que un equipo pueda compilarse de ambas maneras para este contraste: una app real codifica un modo y desaparece if.
👀 Lectura: La fábrica _specialist: El parámetro mode es el nivel completo. · ▶ Ejecuta ambos ritmos. · ✏️ Cambio: Pregunta "Me duele la rodilla en el kilómetro 29": Primero predice el subconjunto y, luego, verifica las líneas de DISPATCH.
# The factory's mode parameter is THE variable this level teaches:
def _specialist(name, domain, focus, mode):
kwargs = {}
if mode == "single_turn": # the structured contract only makes sense for a TOOL
kwargs = dict(mode="single_turn",
input_schema=SpecialistInput, output_schema=SpecialistResponse)
return Agent(name=name, model=MODEL,
description=f"Marathon {domain} specialist. Consult for: {focus}.",
instruction=..., **kwargs)
race_concierge = Agent(name="race_concierge", model=MODEL,
sub_agents=[...six specialists...], # NOTE: no `mode` on the coordinator
instruction="...DECIDE which specialists are relevant... call them IN PARALLEL... SYNTHESIZE...")
10. L3b · Modo de tarea: Una conversación con una línea de llegada (Pilar 2)

⚡ Resumen: El modo intermedio: Habla con el usuario hasta que se recopilen los campos y, luego, regresa automáticamente con un objeto validado.
La pregunta: L3a dejó un espacio. chat es el propietario de toda la conversación; single_turn nunca habla con el usuario. Pero el trabajo real de admisión se encuentra en el medio: "Habla con el usuario HASTA que hayas recopilado X y, luego, regresa con un objeto validado". ¿Qué modo es ese?
La forma:
race_desk (coordinator)
└─ gear_fitter (mode="task", output_schema=GearOrder)
▶ Colab: Ejecuta la celda L3b · 📁 GitHub: L3b_task_desk/ · 💻 Local: python -m L3b_task_desk.desk


🔍 Los marcadores: mode="task" + output_schema= en el mismo agente, y, en el resultado, la pausa ⏸ y la llamada finish_task.
Qué verás:
━━ TURN 1 ━━ user: 'I need shoes for the marathon.'
race_desk → delegate: gear_fitter
gear_fitter: What is your shoe size?
⏸ The run ENDED — but nothing failed. This is a PAUSED task.
━━ TURN 2 ━━ user: 'Size 9, wide.' (same session → resumes the task)
gear_fitter → finish_task (payload validates as GearOrder)
race_desk: Your order ... in size 9 Wide has been confirmed.
Sucedieron tres cosas que el modo L3a no puede hacer:
- La ejecución se detuvo genuinamente a mitad de la tarea: Una tarea pausada, no un bloqueo ni una falla. El agente hizo su pregunta aclaratoria y mantiene la tarea abierta. (En
adk web, solo escribirías la respuesta; los scripts de la plataforma la mostrarían como un segundo mensaje en la misma sesión). - El siguiente mensaje reanudó el MISMO agente de tareas: No hubo redireccionamiento ni nueva delegación. La sesión sabe quién estaba esperando.
finish_taskla finalizó: Se inyectó un ADK de herramientas porque demode="task". El agente debe llamarlo para finalizar, y su carga útil debe validarse enoutput_schema. Una conversación con una línea de meta escrita: Luego, el control vuelve automáticamente al coordinador y se adjunta el resultado.
La regla de una pregunta para elegir un modo
💡 Chat de "¿El usuario necesita hablar con él? ¿Y HASTA CUÁNDO?" = indefinidamente · tarea = hasta que se recopilen los campos · un solo turno = nunca.
Modo | Con interacción humana | ¿Paralelo? | Regresa al elemento superior |
| Conversación completa | no | manual (a través de transferencia) |
| Solo preguntas de aclaración | no | Automática (a través de |
| ninguno | yes | automática (con su resultado) |
mode se aplica solo a los subagentes, nunca al coordinador. Además, los nodos de flujo de trabajo tienen el valor predeterminado single_turn (por eso, los niveles L1 y L2b nunca lo escribieron), mientras que los subagentes tienen el valor predeterminado chat (por eso, el nivel L3a tuvo que hacerlo).
⚠️ Dos notas de versión antes de que compiles sobre esta: (1) task
como nodo de gráfico estático depende de la versión: En las versiones 2.0.0b1 a 2.3.0 (la versión fijada de este codelab), Workflow(...) genera un error en la construcción. Usa exactamente lo que hace este nivel (un coordinador de chat con subagentes de tareas) o envía a través de ctx.run_node. Se corrigió en la versión 2.5.0. (2) "Los agentes de tareas deben ser agentes hoja" (no tienen subagentes propios) es una limitación documentada del ADK, pero un contrato, no una protección en el tiempo de ejecución: ni la versión 2.3.0 ni la 2.5.0 te detendrán. No interpretes la ausencia de un error como permiso.
💡 Profundiza: Un agente task integrado en un flujo de trabajo de grafo (la forma 2.5.0+), con un enrutamiento que puede volver a iniciar la conversación para un reintento: repo complementario 22_agent_in_workflow · guía de modo completo: docs/agent-modes.md.
❓ Tal vez te preguntes: ¿qué significa
task
¿Qué me ofrece este que los otros dos no? Tres cosas: devolución automática (el chat se lleva la conversación), una línea de meta escrita (la carga útil de finish_task debe validarse según el esquema; recibes datos, no una transcripción) y pausar/reanudar (el símbolo ⏸ es una tarea en espera de un humano, no un cuelgue).
👀 Lectura: gear_fitter: mode="task" + output_schema es el contrato completo. · ▶ Ejecútalo. · ✏️ Cambio: run_desk("I need a hydration vest", "2 liters, medium"): La pregunta aclaratoria se adapta, la línea de llegada permanece escrita.
11. L4a: Fan-out paralelo con tamaño de tiempo de ejecución (pilar 3a)

⚡ Resumen: El esqueleto sigue siendo de tres pasos estáticos. El dinámico se oculta dentro del paso central, en el que los datos deciden el ancho en el tiempo de ejecución.
⚠️ Atención: Este es el paso más difícil de la escalera. El nivel anterior tenía 44 líneas; este tiene alrededor de 120, con tres agentes y dos nodos de flujo de trabajo, y nada de eso es relleno. Dedica unos 15 minutos y apóyate en la línea Leer/Ejecutar/Cambiar al final: no es necesario que comprendas cada línea en la primera lectura.
La pregunta: La forma del trabajo depende de la entrada. No puedes dibujar el gráfico con anticipación. Comienza con el ancho del tiempo de ejecución: Deja que el LLM decida cuántas preguntas secundarias.
La forma (un nivel de profundidad):
START ─► decompose ─► research_topic (parallel_worker) ─► synthesize
│ │ │
└──┴──┴─ (flat: no children yet)
Una pregunta abierta se descompone en N subpreguntas (N es elegido por el LLM en el tiempo de ejecución, de 3 a 7), cada una de las cuales se investiga en paralelo y, luego, se sintetiza en un informe.
▶ Colab: Ejecuta la celda L4a · 📁 GitHub: L4a_flat_research/ · 💻 Local: python -m L4a_flat_research.deep_research

🔍 Los marcadores: No hay
dynamic=True
interruptor. Dynamic es una forma de escribir, no una configuración. Dos marcadores y solo dos: @node(parallel_worker=True) (toma una lista de tamaño de tiempo de ejecución y ejecuta un trabajador por elemento) y ctx.run_node(...) (nodos de programación de código directamente). Si ves cualquiera de las dos flechas, significa que estás en la versión dinámica.
Lo que verás: El descompositor imprimirá, p.ej., 5 subpreguntas, investigará en paralelo y, luego, generará un resumen sintetizado. El número difiere en cada ejecución, algo que el gráfico fijo no podía hacer.
Dos marcas del trabajador que vale la pena comprender:
rerun_on_resume=Truees obligatorio en cualquier nodo que llame actx.run_node. El ADK genera unValueErrorsin él. Cuando se reanuda, debe volver a ejecutar el nodo de envío para volver a compilar los elementos secundarios que generó, ya que estos no se encuentran en el gráfico estático.retry_config=limita cómo FALLA esto. Un trabajador paralelo cancela cada elemento del mismo nivel y vuelve a generar el instantáneo si falla un elemento secundario, por lo que, sin un reintento, un solo error 429 transitorio descarta toda la ejecución, incluidas todas las llamadas por las que ya se pagó. El reintento se realiza en el nodo interno por elemento, por lo que cada rama se reintenta de forma independiente.
❓ Es posible que te preguntes: ¿cómo sabe el ADK que esto es dinámico? No es necesario, ya que no se declara nada en ningún lugar. El descompositor produce una lista en el tiempo de ejecución; el trabajador paralelo se ajusta a lo que llega. El dinamismo es una propiedad del flujo de datos que escribiste, no un modo que activaste.
👀 Lee: Las dos marcas en research_topic: parallel_worker y rerun_on_resume. · ▶ Ejecútalo. · ✏️ Cambio: Intercambia tu propia pregunta abierta. N cambia porque la entrada decidió el ancho.
12. L4b · Add recursive spawning (Pillar 3b)

⚡ TL;DR: La recursión se escribe, no se da: el trabajador se llama a sí mismo a través de ctx.run_node, Python común, por lo que el freno también debe escribirse. Es MAX_DEPTH.
La pregunta: A veces, un hallazgo de investigación revela un subtema específico que merece su propia investigación. ¿Cómo permites que una rama genere más trabajo paralelo y lo mantienes dentro de límites?
La forma (ahora recursiva):
START ─► decompose ─► research_topic (parallel_worker, recursive) ─► synthesize
│ │ │
│ │ └─ research(q3) ─► maybe spawn children
│ └─── research(q2) ─► maybe spawn children
└────── research(q1) ─► maybe spawn children
▶ Colab: Ejecuta la celda L4b · 📁 GitHub: L4b_recursion/ · 💻 Local: python -m L4b_recursion.deep_research

@node(parallel_worker=True, rerun_on_resume=True)
async def research_topic(ctx, node_input):
finding = coerce(await ctx.run_node(research_agent, node_input=...), ResearchFinding)
if finding.needs_deeper and finding.deeper_questions and depth < MAX_DEPTH: # boundary in CODE
children = await ctx.run_node(research_topic, node_input=deeper) # recursive fan-out
yield Event(output={..., "children": children})
🔍 Los marcadores: ctx.run_node(research_topic, ...) dentro de research_topic en sí (la autorreferencia es la recursión) y el protector depth < MAX_DEPTH una línea más arriba.
Qué verás: Nodos de investigación que imprimen spawning N deeper (recursión en vivo) y, luego, una forma de árbol de tiempo de ejecución (p.ej., 5 top-level + 10 recursive children). El árbol difiere en cada ejecución.
⚠️ Antes de aumentar el valor: El límite superior crece rápidamente: MAX_DEPTH=3 toma el peor caso de alrededor de 30 llamadas a alrededor de 93. Y, al final de una ejecución, es posible que veas una línea de registro cancelling N leftover tasks, que indica que el ADK está desglosando su grupo de tareas paralelas después de que el resultado ya se haya completado. Es inofensivo y, según tu configuración de registro, es posible que nunca lo veas.
❓ Quizás te preguntes: ¿no es recursivo el método dinámico de forma predeterminada? No, L4a es completamente dinámica y no tiene recursión. Dynamic solo te ofrece un flujo de control de Python común y corriente; L4b elige escribir recursión con él. Y como tú escribiste la recursión, tú debes escribir su límite. Aquí es donde "deja que el LLM dé forma al trabajo, mantén los límites en el código" deja de ser un eslogan.
👀 Lee: El guardia: if finding.needs_deeper and depth < MAX_DEPTH. · ▶ Ejecútalo. · ✏️ Cambio: Establece MAX_DEPTH = 1 y vuelve a ejecutar. El árbol se aplana (y la ejecución se vuelve más económica). El límite es TUYO, en el código.
13. Nivel 5: ¿Qué patrón deberías usar?

⚡ Resumen: Un eje decide todo: quién elige el siguiente paso: el gráfico que dibujaste, el LLM o tu código.
Ya construiste los tres. Este es el modelo que los hace útiles: adapta el patrón a la forma de tu problema.
El eje: ¿Quién decide qué se ejecutará a continuación?
Pilar | Quién decide qué se ejecutará a continuación | Integrada |
1 · Gráfico | el gráfico que dibujaste | L2a o L2b |
2 · Colaborativo | el LLM | L3a y L3b |
3 · Dinámico | tu código de Python, en el tiempo de ejecución | L4a y L4b |
Paso 0: ¿Realmente necesitas un gráfico?
El ADK incluye agentes de flujo de trabajo prediseñados: SequentialAgent, ParallelAgent y LoopAgent. En el caso de una cadena simple de agentes, esas son las respuestas correctas más económicas y no hay ningún gráfico que ensamblar. Supera a los agentes cuando necesites enrutamiento explícito (el router de L2b), una unión (el JoinNode de L2a) o nodos que no son agentes (una función simple, sin llamadas a LLM). Por lo general, esta última es la razón.
Would a prebuilt SequentialAgent / ParallelAgent / LoopAgent do?
│
├─ YES ──────────────────────────────► use it; stop here
│
└─ NO — I need routing, a join, or non-agent nodes
│
Can you draw the workflow before the input arrives?
│
├─ YES ───────────────────────────► Pillar 1 · Graph workflow (L2a/L2b)
│
└─ NO
├─ Known team, request picks the subset? ─► Pillar 2 · Collaborative (L3a/L3b)
└─ Does the shape depend on the input? ──► Pillar 3 · Dynamic (L4a/L4b)

El encuadre honesto de la versión 1.x frente a la versión 2
Esto no significa que "la versión 2.0 puede hacer cosas que la versión 1.x no podía", ya que la versión 1.x podía compilar todo. El cambio es que la versión 2.0 le da a cada forma un lugar más directo, por lo que el flujo de control conocido abandona la instrucción y se convierte en una estructura que puedes ver y probar.
Patrón | El costo de la versión 1.x | Página principal del ADK 2 |
Gráfico | 4 llamadas al LLM en la compilación común; el enrutamiento está oculto en una instrucción | Nodos de función y de agente como pares → 1 llamada, router de declaraciones |
Colaborativo | Se puede compilar a través de la canalización de | Un equipo declarado: |
Dinámico | La recursión te saca del marco. |
|
Toda la app y lo que un gráfico no puede mostrarte
Ya creaste cada una de las piezas que se muestran a continuación. Workflow expone su estructura en graph.edges, por lo que esta imagen se genera a partir del código en lugar de dibujarse a mano. Lo que encuentra la introspección es el resumen de este lab:
Pilar | Qué contiene | Por qué |
1 · Gráfico (L2b) | 10 tramos, rutas y todo lo demás | Lo dibujaste antes de que llegara cualquier entrada. |
2 · Colaborativo (L3a) | 0 aristas: Solo | El LLM elige el subconjunto por solicitud. |
3 · Dynamic (L4a/L4b) | 3 bordes: Idénticos en ambos | La recursión está escrita en Python, no conectada en el gráfico. |
Esa última fila es la prueba de la pregunta que responde L4b: L4a y L4b tienen el mismo gráfico, y solo uno de ellos es recursivo.
Qué puedes compilar ahora
Cada patrón que acabas de ejecutar es una forma de producto real:
Practicaste lo siguiente: | En la naturaleza, eso es | Salir de |
Gráfico y router (L2a/L2b) | Canalizaciones de documentos, ETL con pasos de LLM, cadenas de revisión y aprobación, plataformas de evaluación | L2b de este repo |
Coordinador y equipo de | Un copiloto de asistencia con equipos de especialistas, mesas de triage y revisión con múltiples enfoques | Modo 2 de demostración de maratón |
Agentes de | Formularios de admisión, flujos de reserva, incorporación, KYC: cualquier "recopilación y acción" | |
Ancho y profundidad dinámicos (L4a/L4b) | Agentes de investigación, generadores de informes y análisis de auditoría en entradas de tamaño desconocido | Modo 3 de demostración de maratón |
Componen
Los tres patrones no son mutuamente excluyentes. Un nodo de gráfico puede llamar a un coordinador colaborativo, y un especialista puede iniciar un flujo de trabajo dinámico. Elige el patrón adecuado para cada parte del problema. Así evitarás convertir cada sistema de agentes en una instrucción gigante.

💡 Pruébalo en tu propio flujo de trabajo: La secuencia de comandos que generó este gráfico es scripts/graph_dump.py. Apúntala a cualquier Workflow y se imprimirán los bordes reales, un diagrama estructural gratuito de todo lo que construyas.
14. Felicitaciones

Creaste un entrenador para el día de la carrera de maratón y, en el proceso, los tres patrones de orquestación del ADK 2.
Qué aprendiste
- Prólogo: La mega instrucción que inventó su propio clima: Por qué existe la estructura.
- L0 a L1:
Agent,Runner, una herramienta real que el modelo elige llamar y tu primerWorkflow(nodos de función + nodos de agente como pares). - L2a / L2b: Flujos de trabajo de gráficos: Fan-out paralelo +
JoinNodey, luego, enrutamiento determinístico, una llamada a LLM. - L3a: Agentes colaborativos: El mismo equipo ejecuta
chat(varado) y, luego,single_turn(subconjunto paralelo + síntesis): Una marca, dos mundos. - L3b: Modo
task: Una pregunta aclaratoria en pausa, un resumen con guion,finish_taskque devuelve un objeto validado. - L4a / L4b: Flujos de trabajo dinámicos: ancho de tiempo de ejecución (fan-out) y, luego, profundidad de tiempo de ejecución (recursión) con límites en el código.
- L5: El árbol de decisión y cómo se componen los patrones.
Líneas que vale la pena conservar
Las funciones preparan el contexto. Los bordes definen el flujo de trabajo. El router elige la ruta. El modelo escribe la respuesta.
Permite que el LLM dé forma al trabajo, pero mantén los límites en el código.
Haz coincidir el patrón con la forma de tu problema.
Próximos pasos
- Ejecuta la app completa a partir de la cual se extrajeron estos niveles: Marathon Race Day Coach, una compilación de FastAPI + SSE con una IU del navegador que muestra los tres modos en vivo: github.com/cuppibla/adk-2-marathon-demo.
- Amplía tu alcance: adk-workflows-compared: Los 23 ejemplos de flujos de trabajo oficiales del ADK 2, cada uno con un puerto 1.x y orientación sobre cuándo usarlo. Comienza con
docs/three-pillars.mdy, luego, los elementos que se omitieron en este codelab:07_loop,17_request_inputy22_agent_in_workflow. - Porta tu propio problema: ¿qué partes tienen estructura conocida (L2), equipo conocido (L3a/L3b) y forma desconocida (L4)?
- Explora el código: github.com/cuppibla/adk2-tutorial.
- ¿Pasó por el taller? Tu crédito y el proyecto que creaste no durarán para siempre. Para seguir ejecutando estos niveles de forma gratuita, realiza el paso de configuración para llevar a casa: una clave gratuita de AI Studio, sin proyecto de Cloud ni facturación. El único cambio es intercambiar esa celda.