۱. مرور کلی
عنوان ADK 2 سه الگوی ارکستراسیون است. این آزمایشگاه کد، هر سه مورد را با ساخت یک اپلیکیشن - یک مربی روز مسابقه ماراتن - آموزش میدهد. هر مرحله به یک سوال پاسخ میدهد، یک ایده اضافه میکند و به صورت مستقل اجرا میشود.
آنچه یاد خواهید گرفت
- نمودار گردشهای کاری (رکن ۱) - زمانی که میتوانید جریان را قبل از رسیدن ورودی رسم کنید .
- عوامل مشارکتی (رکن ۲) - وقتی تیم را میشناسید اما درخواست زیرمجموعه را انتخاب میکند - و هر سه حالت همکاری (
chat/task/single_turn)، هر کدام به صورت زنده اجرا میشوند. - گردشهای کاری پویا (رکن ۳) - زمانی که شکل خود کار به ورودی بستگی دارد.
- چگونه انتخاب کنیم - یک درخت تصمیم تک سوالی، و نحوه ترکیب الگوها.
خط عبور
ساختار شناخته شده → تیم/زیرمجموعه متغیر شناخته شده → شکل ناشناخته → مورد مناسب را انتخاب کنید

آنچه خواهید ساخت
یک برنامه - یک مربی روز مسابقه ماراتن - هر بار یک سطح قابل اجرا را مونتاژ میکرد. هر سطح یک ماژول پایتون ساده است که شما از ترمینال اجرا میکنید؛ تا سطح ۵، قطعات زیر همگی متعلق به شما هستند.
تصویر از کد در حال اجرا رسم شده است: هر خط ممتد از Workflow.graph.edges خوانده شده است. که اولین درس است - بخشهایی که میتوانند از قبل رسم شوند دقیقاً ستون ۱ هستند و بخشهایی که نمیتوانند رسم شوند دلیل وجود ستونهای ۲ و ۳ هستند.

آنچه نیاز دارید
- یک حساب گوگل (برای Colab) - نیازی به تنظیمات محلی نیست .
- حدود ۵۰ دقیقه (دو مرحلهی L4 طولانیترین مراحل هستند - برای آنها برنامهریزی کنید).
- یکی از دو راه برای رسیدن به مدل Gemini. مسیر خود را انتخاب کنید - شما یک مرحله راهاندازی را اجرا میکنید و مرحله دیگر را رد میکنید:
🎓 کارگاه آموزشی | 🏠 غذای بیرونبر | |
سازمان بهداشت جهانی | شما در یک کارگاه حضوری هستید و مدرس به شما یک لینک برای دریافت اعتبار داده است. | بقیه افراد - از جمله شرکتکنندگان در کارگاه، پس از آن |
شما نیاز دارید | لینک درخواست و یک حساب گوگل که میتواند یک پروژه ابری ایجاد کند | |
اجرا میشود | هوش مصنوعی ورتکس، در پروژهای که صورتحساب آن به حساب کارگاه شما منظور شده است | استودیوی هوش مصنوعی گوگل |
هزینه | تحت پوشش اعتبار | سطح رایگان |
مرحله راهاندازی | راه اندازی کارگاه (مرحله بعدی) | آمادهسازی برای بردن به خانه (مرحله بعد) |
از Prologue به بعد، همه چیز در هر صورت یکسان است - خط ارتباطی فقط تصمیم میگیرد که نوتبوک با کدام نقطه پایانی مدل ارتباط برقرار کند.
دو راه برای دنبال کردن
هر مرحله زیر به یک سلول در دفترچه یادداشت Colab و یک پوشه در مخزن GitHub نگاشت میشود. یکی از موارد زیر را انتخاب کنید:
- ▶ Colab (توصیه میشود): دفترچه یادداشت را باز کنید → سلولها را از بالا به پایین مرور کنید.
- 💻 محلی: مخزن
git clone،./setup_venv.sh، سپس هر سطح را به عنوان یک ماژول اجرا کنید (python -m ...) یا همه آنها را با./run.sh(adk web) مرور کنید.
۲. راهاندازی کارگاه · اعتبار خود را مطالبه کنید و به Vertex AI بروید
در کارگاه به شما اعتبار گوگل کلود داده میشود. شما آن را مطالبه میکنید، یک پروژه مرتبط با آن ایجاد میکنید و نوتبوک را به جای AI Studio به سمت Vertex AI نشانه میگیرید. پس از مطالبه، یک سلول همه کارها را انجام میدهد.
۱ · اعتبار خود را مطالبه کنید (~۱ دقیقه)
- لینک درخواستی که مربیتان به اشتراک گذاشته را باز کنید. چیزی شبیه به
https://me.developers.google.com/benefits/claim/your-workshop-nameاست. - برای پذیرش اعتبار، وارد سیستم شوید و صفحه را تا انتها دنبال کنید.
- توجه داشته باشید که از کدام حساب گوگل استفاده کردهاید. هر مرحله زیر باید با همان حساب اجرا شود.
۲ · نوتبوک را باز کنید و ADK 2 را نصب کنید (تقریباً ۱ دقیقه)
روی «باز کردن در Colab ▶» کلیک کنید، سپس اولین سلول کد را اجرا کنید. این کد دقیقاً همان نسخه ADK 2 را که این codelab روی آن تأیید شده است، پین میکند و ✓ installed را چاپ میکند.
۳ · اجرای سلول «راهاندازی کارگاه» (حدود ۳ دقیقه)
این سلولی با عنوان 🎓 مسیر A · کارگاه است. آن را اجرا کنید و Colab از شما میخواهد که مجوز دهید - همان حساب گوگلی را که اخیراً اعتبار را با آن دریافت کردهاید انتخاب کنید و اجازه دسترسی دهید.
این برنامه چهار کار انجام میدهد: پروژهای به نام adk-2-tutorial-XXXX با اعتبار شما ایجاد میکند، API هوش مصنوعی Vertex را روی آن فعال میکند، چهار متغیر محیطی را برای هر بار خواندن سلول بعدی تنظیم میکند، و سپس یک فراخوانی آزمایشی به Vertex انجام میدهد و منتظر میماند تا پاسخ دهد - بنابراین راهاندازی یا کار خود را تمام میکند یا دلیل آن را به شما میگوید، به جای اینکه بعداً در یک مرحله از کار بیفتد.
خروجی مورد انتظار - آخرین خط مهم است:
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
۴ · از مرحله «راهاندازی برای بردن به خانه» صرف نظر کنید
سلول کلید AI Studio را اجرا نکنید - این کار نوتبوک را به AI Studio برمیگرداند و کاری را که انجام دادهاید خنثی میکند. (سلول در برابر این کار محافظت میکند و از اجرا خودداری میکند، اما حرکت درستتر این است که از آن صرف نظر کنید.) مستقیماً از اینجا به سلول بلوکهای سازنده مشترک بروید.
۵ · سلول «بلوکهای سازنده مشترک» را اجرا کنید
یک بار آن را اجرا کنید. این برنامه طرحوارههای Pydantic + سناریوهای ماراتن از پیش تعیینشده را تعریف میکند که هر سطح از L2 به بعد دوباره از آنها استفاده میکند. خواهید دید ✓ schemas + scenarios ready .
بعد از کارگاه
اعتبار شما و پروژهای که ایجاد کردهاید برای همیشه دوام نخواهد داشت. برای اینکه بتوانید پس از پایان کارگاه، این مراحل را به صورت رایگان دوباره اجرا کنید، به جای آن مرحله راهاندازی خانگی را اجرا کنید - یک کلید رایگان استودیوی هوش مصنوعی، بدون پروژه ابری، بدون پرداخت صورتحساب. آن یک سلول تنها چیزی است که تغییر میکند.
برای پاکسازی سریعتر: کنسول Cloud را باز کنید، adk-2-tutorial-XXXX را انتخاب کنید و آن را حذف کنید. هیچ چیز دیگری در این codelab منابع قابل پرداخت ایجاد نمیکند.
۳. راهاندازی در منزل · کلید API استودیو هوش مصنوعی
همه چیز در این مسیر با یک کلید API رایگان Google AI Studio اجرا میشود - بدون پروژه Google Cloud، بدون صورتحساب، بدون نصب محلی. کل این مرحله حدود ۳ دقیقه طول میکشد.
۱ · دفترچه یادداشت را باز کنید
روی «باز کردن در Colab ▶» کلیک کنید. به دفترچه یادداشت میرسید - یک مقدمهی نشانهگذاری، سپس یک سلول قابل اجرا برای هر سطح. سلولها را از بالا به پایین اجرا میکنید؛ هر کدام خروجی خود را درست زیر آن چاپ میکنند.
۲ · نصب ADK 2 (تقریباً ۱ دقیقه)
اولین سلول کد را اجرا کنید. این کد دقیقاً همان نسخهای را که این آزمایشگاه کد روی آن تأیید شده است، پین میکند:
%pip install -q "google-adk==2.3.0" python-dotenv pydantic nest_asyncio
صبر کنید تا تمام شود - ✓ installed خواهید دید. (نصب برای اولین بار حدود ۳۰ تا ۶۰ ثانیه طول میکشد؛ پس از آن در حافظه پنهان (cache) ذخیره میشود.)
۳ · کلید API جمینی خود را از AI Studio دریافت کنید (تقریباً ۱ دقیقه)
- aistudio.google.com/app/apikey را در یک تب جدید مرورگر باز کنید.
- با حساب گوگل خود وارد شوید.
- روی ایجاد کلید API (بالا سمت راست) کلیک کنید.
- یک پروژه موجود گوگل را انتخاب کنید یا بگذارید خودش یکی ایجاد کند.
- کلید را کپی کنید - با
AIza...شروع میشود و حدود ۴۰ کاراکتر دارد.
۴ · کلید خود را به Colab اضافه کنید (تقریباً ۱ دقیقه)
گزینه الف - Colab Secrets (توصیه میشود؛ کلید مخفی میماند):
- روی آیکون کلید 🔑 در نوار کناری سمت چپ Colab کلیک کنید.
- روی + افزودن راز جدید کلیک کنید.
- نام را دقیقاً روی
GOOGLE_API_KEYتنظیم کنید. - کلید خود را در Value قرار دهید.
- دسترسی به دفترچه یادداشت را روی روشن (ON) قرار دهید.
گزینه ب — چسباندن در صورت درخواست (سریع): از مرحله مخفی صرف نظر کنید؛ وقتی سلول بعدی را اجرا میکنید، یک اعلان مخفی نشان داده میشود 🔑 Enter your Google AI Studio API key: — چسباندن و Enter را فشار دهید.
۵ · اجرای سلول کلید
این کد مخفی را میخواند (یا به اعلان چسباندن برمیگردد)، سپس ADK را به سمت AI Studio (نه 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.")
خروجی مورد انتظار: ✅ API key set — using Google AI Studio.
۶ · سلول «بلوکهای سازنده مشترک» را اجرا کنید
سلول بلوکهای سازندهی مشترک را یک بار اجرا کنید. این سلول، طرحوارههای Pydantic + سناریوهای ماراتن از پیش تعیینشده را تعریف میکند که هر سطح از سطح ۲ به بعد دوباره از آنها استفاده میکند. خواهید دید که ✓ schemas + scenarios ready .
آمادهاید! 🎽 یک تغییر مسیر سریع قبل از L0 - نسخهای که همه اول میسازند.
۴. پیشگفتار · چرا یک نکتهی مهم مطرح نشود؟
⚡ قبل از اینکه آن را اجرا کنید، یک چیز را که باید مراقب آن باشید، در نظر بگیرید: هر عدد خاص از کجا آمده است؟ کل تمرین همین است - هر چیز دیگری تزئینی است.
قبل از نردبان، چیزی را که نردبان جایگزین آن میشود اجرا کنید: یک عامل که دستورش همه چیز را تضمین میکند - آب و هوا را دریافت کند، مسیر را تجزیه و تحلیل کند، گزارش تمرین را بخواند، بر اساس شرایط مسیریابی کند، طرح را خروجی دهد.
آنچه خواهید دید: یک استراتژی مطمئن، خاص و خوشفرم... که اعداد آن اختراع شدهاند . در یک اجرای زنده، با عبارت «من معیارهای آب و هوای امروز را استخراج کردهام» شروع شد و دمای ۵۲ درجه فارنهایت، باد ۹ مایل در ساعت و تحلیلی از یک گزارش تمرینی که هرگز ندیده بود را گزارش کرد. در اینجا هیچ API آب و هوایی، هیچ داده دورهای، هیچ گزارشی وجود ندارد - یک فراخوانی مدل مبهم یا ورودیهای آن را جعل میکند یا آنها را به بیفایده بودن سوق میدهد.
این بیماری است و چهار علامت دارد که ارزش نام بردن دارند:
- شما نمیتوانید به آن اعتماد کنید - دادهها به طور روان و ساختگی ساخته شدهاند.
- شما نمیتوانید آن را آزمایش کنید - مسیریابی مرحله ۴ درون نثر وجود دارد؛ هیچ
ifبرای آزمایش واحد وجود ندارد. - شما نمیتوانید یک مرحله را جابجا کنید - هیچ درزی وجود ندارد که یک API آب و هوای واقعی بتواند به آن وصل شود.
- شما برای همه چیز، هر بار هزینه پرداخت میکنید - پنج مرحله، یک تماس بزرگ، بدون ذخیره یک بخش قطعی.
این حس را داشته باشید. نه سطح بعدی، این مراحل را یکی یکی از اعلان خارج میکنند: توابع واکشی میکنند (L1-L2a)، یک if مسیرها را تعیین میکند (L2b)، متخصصان کار را تقسیم میکنند (L3a-L3b) و کد، شکل را محدود میکند (L4a-L4b).

💻 محلی: python -m shared.prologue
۵. L0 · اولین مامور ADK 2 شما

خلاصه کلام: یک عامل (agent) شامل یک مدل + یک دستورالعمل + ابزارهایی است که میتواند فراخوانی کند ؛ یک Runner آن را اجرا میکند. هر چیزی پس از این سطح، فقط عاملهای بیشتری هستند که در شکلهای بهتری چیده شدهاند.
سوال این است: آیا میتوانید مدلی برای پاسخ دادن پیدا کنید - و وقتی محاسبات مهم است، به کد واقعی دسترسی پیدا کنید؟
یک ایده - سه بخش:
-
Agent- چیزی که استدلال میکند (یک مدل Gemini + یک دستورالعمل). -
Runner- چیزی که یک عامل را درون یک جلسه اجرا میکند و رویدادها را پخش میکند. - یک ابزار - یک تابع ساده پایتون (
pace_splits) که مدل تصمیم به فراخوانی آن میگیرد. ADK امضا + رشته سند را میخواند و یک اعلان به مدل میدهد؛ بدون نوشتن طرحواره.
بعد از مقدمه، این اولین نکتهی اصلاحی است: یک LLM که محاسبات سرعت را در ذهن خود انجام میدهد، خوشبختانه اشتباه خواهد کرد - pace_splits یک پایتون قطعی است، بنابراین اعداد موجود در پاسخ محاسبه میشوند، نه بداهه .
▶ همکاری: اجرای سلول L0 · 📁 گیتهاب: L0_first_agent/ · 💻 محلی: 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
🔍 نشانگرها: Agent(...) · tools=[pace_splits] · Runner(...) . و در خروجی، خط 🔧 — این مدل است که در اواسط پاسخ، تصمیم میگیرد کد شما را فراخوانی کند.
آنچه خواهید دید:
🔧 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...
خطوط 🔧 درس عبرتی هستند: در اواسط پاسخ، مدل تصمیم گرفت تابع شما را فراخوانی کند، و دقیقاً 8:00/mile در پاسخش از کد شما آمده است - نه از آمار توکن.
❓ ممکن است از خود بپرسید: آیا مدل همیشه ابزار را فراخوانی میکند؟ خیر - در هر سوال تصمیم میگیرد. چیزی بپرسید که هیچ عددی در آن نباشد و خطوط 🔧 ناپدید میشوند (این زمین بازی دقیقاً شما را وادار میکند همین کار را امتحان کنید).
👀 بخوانید: pace_splits (یک تابع ساده) و خط tools=[pace_splits] . · ▶ آن را اجرا کنید . · ✏️ تغییر: سوال کلی را بپرسید (بدون زمان هدف) - توجه کنید که خطوط 🔧 ناپدید میشوند: مدل تصمیم میگیرد چه زمانی یک ابزار ارزش فراخوانی دارد. سپس instruction را بازنویسی کرده و دوباره اجرا کنید - دستورالعمل، بقیه برنامه است.
۶. L1 · اولین گردش کار شما

⚡ خلاصه: یک تابع ساده و یک عامل LLM از یک نوع گره هستند. کار قابل پیشبینی → تابع (0 LLM، قطعی)؛ استدلال → عامل.
سوال این است: چگونه میتوان کد ساده و یک LLM را در یک جریان با هم ترکیب کرد، بدون اینکه برای بخشهایی که فقط کد هستند، هزینه مدلسازی پرداخت کنیم؟
یک ایده: در یک Workflow ، یک تابع پایتون ساده و یک عامل LLM هر دو فقط گرههایی در یک لیست edges یکسان هستند.
START ──► fetch_conditions (function, 0 LLM) ──► advise (agent, 1 LLM)
▶ همکاری: اجرای سلول L1 · 📁 گیتهاب: L1_graph_basics/ · 💻 محلی: python -m L1_graph_basics.workflow

گره تابع، دادههای تولید شده را چاپ میکند (بدون فراخوانی مدل)، سپس عامل توصیهای ارائه میدهد که به دما و باد واقعی دریافتی اشاره دارد:
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)])
🔍 نشانگرها: یک تاپل لبهای - (START, fetch_conditions, advise) - با یک تابع پایتون ساده که در وسط آن قرار دارد، و input_schema= که اعتبارسنجی تحویل را انجام میدهد.
چه چیزهایی در مقایسه با L0 جدید است: Workflow(edges=[...]) ، START (جایی که ورودی وارد میشود)، یک گره تابع که Event(output=...) برمیگرداند، و input_schema=Conditions ، بنابراین خروجی تابع قبل از اینکه عامل آن را ببیند، در برابر آن طرحواره اعتبارسنجی میشود (مانند متن JSON - input_schema مرز را اعتبارسنجی میکند، یک شیء پایتون به عامل نمیدهد).
❓ ممکن است از خود بپرسید: آیا تابع-سپس-عامل ترتیب مورد نیاز است؟ خیر - هر ترتیبی، هر ترکیبی، هر تعداد. advise فقط به این دلیل در مرحله دوم اجرا میشود که به دادههای fetch_conditions نیاز دارد. درس این است که همترازی وجود دارد، نه توالی.
👀 بخوانید: fetch_conditions دادهها را بدون فراخوانی مدل برمیگرداند؛ advise دارای input_schema=Conditions است. · ▶ آن را اجرا کنید . · ✏️ تغییر: temp_f=30 را در تابع تنظیم کنید و دوباره اجرا کنید - advice برعکس میشود و تابع همچنان 0 فراخوانی LLM انجام میدهد.
7. L2a · فن خروجی موازی + JoinNode (ستون 1a)

⚡ خلاصه: به صورت موازی (رایگان) پخش کنید، منتظر همه باشید، بسته را تهیه کنید، تصویر کامل را به یک اپراتور بدهید.
سوال: شما میتوانید جریان را قبل از رسیدن ورودی رسم کنید . با اسکلت شروع کنید: دادهها را به صورت موازی جمعآوری کنید، آنها را بستهبندی کنید، و به یک عامل بدهید.
شکل:
START ──► fetch_weather ──┐
START ──► analyze_course ─┼─► JoinNode ─► strategy (1 agent)
START ──► pull_fitness ───┘ (bundles)
▶ همکاری: اجرای سلول L2a · 📁 گیتهاب: L2a_parallel_join/ · 💻 محلی: python -m L2a_parallel_join.workflow

🔍 نشانگرها: سه یال که همگی از START شروع میشوند - که همان fan-out است - و JoinNode ، نقطه تلاقی.
- سه واکشی که انجام شد، توابع هستند - آنها به صورت موازی اجرا میشوند و هیچ فراخوانی LLM ندارند.
-
JoinNodeمنتظر هر سه میماند و آنها را در یک payload نوعدار (BundledRunData) که با نام تابع کلیدگذاری شده است، دستهبندی میکند. - یک عامل
strategyبسته را میخواند و یکRaceStrategyمینویسد.
آنچه خواهید دید: هر واکشی یک مهر زمانی started / finished را چاپ میکند. هر سه از 0.0 ثانیه شروع میشوند و fan-out در 2.0 ثانیه پایان مییابد - کندترین واکشی، نه 4.5 ثانیهای که مجموع مدت زمان آنها میشود. این همپوشانی، موازی بودن است. ( کل زمان دیوار چاپ شده در انتها تقریباً 8 ثانیه است زیرا شامل فراخوانی LLM عامل استراتژی نیز میشود - مهرهای زمانی واکشی را برای ادعای موازی بخوانید، نه کل.)
💡 فراخوانی مقدماتی: تابع اعلان بزرگ، آب و هوای خود را اختراع کرد . در اینجا دما از یک تابع واکشی (fetch) خارج میشود - کد واقعی، درز واقعی. کد از پیش تعیینشده را با یک API آب و هوای واقعی جایگزین کنید و هیچ چیز دیگری تغییر نمیکند.
❓ شاید از خود بپرسید: چقدر
JoinNode
آیا باید بفهمم؟ یک جمله: منتظر میماند تا هر شاخه موازی اجرا شود، خروجیها را در یک dict که با نام تابع بالادستی کلیدگذاری شده است ، بستهبندی میکند و خودش چیزی را محاسبه نمیکند. دقیقاً به همین دلیل است که روتر L2b میتواند node_input["fetch_weather"]["temp_f"] را بنویسد.
👀 بخوانید: سه یال از START به صورت fan-out منتشر میشوند؛ JoinNode آنها را برای یک عامل دسته بندی میکند. · ▶ آن را اجرا کنید و مهرهای زمانی را بخوانید، نه کل را. · ✏️ تغییر: یک fetch sleep 3.0 ایجاد کنید - ابتدا زمان پایان fan-out جدید را پیشبینی کنید، سپس تأیید کنید.
۸. L2b · اضافه کردن روتر قطعی (رکن ۱ب)

⚡ خلاصه: L2a دست نخورده + یک تابع if ساده تصمیم میگیرد کدام عامل را اجرا کند. شاخهبندی، بدون پرسیدن از مدل.
سوال: طرح باید برای هوای گرم و سرد متفاوت باشد. چگونه میتوانید بدون اینکه از مدل بخواهید تصمیم بگیرد، شاخهبندی کنید؟
شکل (L2a + a روتر):
... JoinNode ─► route_by_weather ─► hot_strategy
(if-statement) ─► normal_strategy
─► cold_strategy
▶ همکاری: اجرای سلول L2b — امتحان کنید run("NORMAL") / run("COLD") · 📁 گیتهاب: L2b_router/ · 💻 محلی: 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})
🔍 نشانگرها: Event(output=..., route=...) — یک گره تابع که مسیر را نامگذاری میکند — و dict-edge {"HOT": ..., "NORMAL": ..., "COLD": ...} که نامها را به گرهها نگاشت میکند.
خلاصه کلام - سه نوع کار، سه خانه:
- کار قابل پیشبینی → توابع (سه واکشی موازی)
- یک قانون واضح → مسیریابی صریح (
route_by_weatherیک دستور شرطیifاست، نه یک تصمیم مدل) - استدلال → مدل (دقیقاً یک عامل استراتژی اجرا میشود)
چیزی که خواهید دید: temp=78F -> route=HOT ، سپس یک RaceStrategy ساختاریافته. هزینه خالص: ۱ تماس LLM.
⚠️ اگر شاخه چهارمی اضافه کردید، به route-dict یک ورودی DEFAULT_ROUTE نیز بدهید. مسیری که dict با آن مطابقت ندارد، خطا نیست - شاخه به سادگی پایان مییابد و برنامه با مقدار 0 و بدون خروجی خارج میشود، که یک بنبست گیجکننده برای اشکالزدایی است.
❓ شاید از خود بپرسید: پس L2b به معنای واقعی کلمه همان L2a به علاوه یک روتر است؟ بله - واکشیها و اتصال دست نخورده باقی ماندهاند و هنوز دقیقاً همان فراخوانی LLM است. چه چیزی تغییر کرده است: «همیشه همان عامل» به «یکی از سه عاملی که توسط دادهها انتخاب میشوند» تبدیل شده است.
👀 بخوانید: route_by_weather — روتر یک دستور if است، نه یک عامل. · ▶ همچنین run("COLD") اجرا کنید . · ✏️ تغییر: یک شاخه WINDY با یک عامل چهارم اضافه کنید — و قبل از انجام این کار، هشدار DEFAULT_ROUTE بالا را بخوانید.
۹. L3a · عوامل مشارکتی: یک پرچم، دو جهان - رکن ۲

⚡ خلاصه: همان تیم، یک پرچم. chat کل مکالمه را به یک متخصص میدهد و دیگر هرگز برنمیگردد؛ single_turn هر متخصص را به یک ابزار تبدیل میکند - زیرمجموعه موازی، بازگشت خودکار، یک سنتز.
سوال: شما تیم را میشناسید، اما درخواست تصمیم میگیرد که کدام اعضا باید پاسخ دهند. چگونه به یک LLM اجازه میدهید زیرمجموعه را انتخاب کند - و آنها را همزمان اجرا کند؟
شکل: یک هماهنگکننده بر فراز شش متخصص (پزشکی، آب و هوا، سرعت، تجهیزات، تغذیه، روان). این سطح ، همان تیم را دو بار هدایت میکند - همان هماهنگکنندهی راهنما، همان شش متخصص. تنها تفاوت، یک پرچم روی زیرمأمورها است. این تضاد، درس عبرتی است.
▶ همکاری: اجرای سلول L3a · 📁 گیتهاب: L3a_collaborative/ · 💻 محلی: python -m L3a_collaborative.concierge --mode chat "What about fueling?"

🔍 نشانگرها: mode="single_turn" در factory — و در خروجی ، TRANSFER → (مرحله ۱) در مقابل مجموعهای از خطوط DISPATCH → که یک مهر زمانی مشترک دارند (مرحله ۲).
ضرب اول · ابتدا پیشفرض را اجرا کنید — و شاهد شکست آن باشید
بدون mode= نوشته شده → زیرعاملها بهطور پیشفرض chat میکنند. آنچه خواهید دید:
TRANSFER → nutrition_specialist (transfer_to_agent — the only tool chat subagents provide)
Final speaker: nutrition_specialist
هماهنگکننده هیچ ابزار تفویض اختیار نداشت - زیرعاملهای چت فقط transfer_to_agent به آن میدادند، یک انتقال سریالی از کل مکالمه به یک متخصص. آن متخصص مستقیماً به کاربر پاسخ میدهد و اجرا همانجا تمام میشود. بدون ارسال موازی. بدون بازگشت. بدون ترکیب. سوال کلی را بپرسید و اوضاع بدتر میشود: شش متخصص، یک انتقال.
این یک اشکال نیست - این حالت چت است که کار خود را انجام میدهد. مکالمه متعلق به هر کسی است که آن را در دست دارد، تا زمانی که کسی صراحتاً آن را منتقل کند. برای یک دستیار با پایان باز مناسب است؛ برای یک مرحلهی خط لولهای نامناسب است.
بیت ۲ · یک پرچم، دو جهان
تنها تفاوت: mode="single_turn" روی هر متخصص. همان سوال، دوباره اجرا کنید:
[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>
حالا ADK به ازای هر متخصص یک ابزار واگذاری اختیار تزریق میکند - که از روی زیرعامل نامگذاری شده و با description= (این متن همان چیزی است که هماهنگکننده هنگام انتخاب زیرمجموعه میخواند؛ اگر از آن صرفنظر کنید، فقط روی نامها مسیریابی میکنید). هماهنگکننده چندین فراخوانی را در یک نوبت انجام میدهد، ADK آنها را به صورت موازی اجرا میکند، هر کدام نتیجه خود را به طور خودکار برمیگرداند و هماهنگکننده آنها را ترکیب میکند.
سوال | متخصصانی که آتش نشان می دهند |
«سوختگیری چطور؟» | فقط تغذیه |
«زانوم تو مایل ۱۸ درد میکنه» | فقط پزشکی |
«امروز باید مسابقه بدم؟» | پزشکی + آب و هوا + سرعت |
«چیزی هست که باید نگرانش باشم؟» | هر 6 |
چرا هر متخصص کل جلسه توجیهی را دریافت میکند: هر زیرعامل single_turn در شاخه جلسه جداگانه خود اجرا میشود - نمیتواند مکالمه یا همسالان خود را ببیند. هیچ چیز محیطی نیست: هماهنگکننده باید کل SpecialistInput (سوال + استراتژی + دادههای اجراکننده) را جداگانه به هر تماس موازی ارسال کند.
💡 جایی که ADK 2 به این موضوع جایگاه مستقیم میدهد: یک LLM یک زیرمجموعه به ازای هر درخواست را انتخاب میکند و آن را به صورت موازی اجرا میکند - که از طریق sub_agents + mode="single_turn" تعریف میشود . شما میتوانستید در نسخه ۱.x با قرار دادن هر متخصص در AgentTool ، شکل یکسانی را مونتاژ کنید؛ چیزی که تغییر میکند این است که اکنون به جای piping، یک اعلان است. ( ParallelAgent همیشه-همه است و transfer_to_agent سریال است.)
⚠️ دو نکتهی صادقانه: (1) مدل زیرمجموعه را انتخاب میکند، بنابراین نسبت به روتر کدنویسی شدهی L2 قطعیت کمتری دارد - زیرمجموعهی دقیق میتواند از یک اجرا به اجرای دیگر متفاوت باشد. (2) گاهی اوقات برای یک متخصص، خط Error validating input: ... مشاهده خواهید کرد. تقریباً هرگز خروجی متخصص نیست - output_schema باعث میشود Gemini آن را در سمت سرور اعمال کند. ورودی است: هماهنگکننده باید کل SpecialistInput تو در تو را کلمه به کلمه برای هر فراخوانی موازی بازتولید کند، و گاهی اوقات یکی را اشتباه انجام میدهد. ADK خطا را به عنوان نتیجهی آن ابزار برمیگرداند، هماهنگکننده بازیابی میشود و سنتز همچنان انجام میشود.
❓ شاید از خود بپرسید: آیا
chat
واگذاری فقط به سبک ۱.x - یک عامل در هر زمان؟ اساساً بله: این رفتار پیشفرض ۱.x است، حالا با یک نام. شکاف single_turn سهبعدی است: آنچه هماهنگکننده در اختیار دارد (یک transfer_to_agent در مقابل یک ابزار برای هر متخصص ) · چند نفر میتوانند کار کنند (یک، مالکیت مکالمه در مقابل N به صورت موازی) · آیا کنترل برمیگردد (هرگز در مقابل به طور خودکار، با نتایج). و در مورد کد: شاخه if mode == کارخانه فقط وجود دارد، بنابراین میتوان یک تیم را به هر دو روش برای این تضاد ساخت - یک برنامه واقعی یک حالت را به صورت سخت کد میکند و if ناپدید میشود.
👀 بخوانید: کارخانه _specialist — پارامتر mode کل سطح است. · ▶ هر دو بیت را اجرا کنید . · ✏️ تغییر: بپرسید «زانوی من در مایل ۱۸ درد میکند» — ابتدا زیرمجموعه را پیشبینی کنید ، سپس خطوط 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...")
۱۰. L3b · حالت وظیفه: مکالمهای با خط پایان — رکن ۲

خلاصه: حالت میانی - تا زمانی که فیلدها جمعآوری شوند با کاربر صحبت میکند، سپس به صورت خودکار یک شیء اعتبارسنجی شده را برمیگرداند.
سوال: L3a یک فاصله ایجاد کرد. chat کل مکالمه را در اختیار دارد؛ single_turn هرگز با کاربر صحبت نمیکند. اما کار واقعی دریافت اطلاعات بین این دو حالت انجام میشود: «با کاربر صحبت کن تا زمانی که X را جمعآوری کنی - سپس با یک شیء معتبر برگرد.» این کدام حالت است؟
شکل:
race_desk (coordinator)
└─ gear_fitter (mode="task", output_schema=GearOrder)
▶ همکاری: اجرای سلول L3b · 📁 گیتهاب: L3b_task_desk/ · 💻 محلی: python -m L3b_task_desk.desk


🔍 نشانگرها: mode="task" + output_schema= روی همان عامل — و در خروجی، فراخوانی ⏸ pause و finish_task .
آنچه خواهید دید:
━━ 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.
سه اتفاق افتاده است که هیچ یک از حالتهای L3a قادر به انجام آنها نیستند:
- اجرا واقعاً در میانه کار متوقف شد - یک کار متوقف شده ، نه هنگ و نه شکست. عامل سوال روشن کننده خود را پرسید و کار را باز نگه داشته است. (در
adk webشما فقط پاسخ را تایپ میکنید؛ مهار آن را به عنوان یک پیام دوم در همان جلسه اسکریپت میکند.) - پیام بعدی همان عامل وظیفه را از سر گرفت - بدون تغییر مسیر، بدون تفویض اختیار مجدد. جلسه میداند چه کسی منتظر بوده است.
-
finish_taskآن را به پایان رساند - ابزاری که ADK به دلیلmode="task"تزریق کرد. عامل باید آن را برای اتمام فراخوانی کند و بار داده آن باید در برابرoutput_schemaاعتبارسنجی شود. مکالمهای با یک خط پایان تایپ شده - سپس کنترل به طور خودکار به هماهنگکننده برمیگرداند و نتیجه پیوست میشود.
قانون تک سوالی برای انتخاب یک حالت
💡 «آیا کاربر نیاز دارد با آن صحبت کند — و تا چه زمانی؟» چت = بینهایت · وظیفه = تا زمانی که فیلدها جمعآوری شوند · تک نوبت = هرگز.
حالت | انسان در حلقه | موازی؟ | به والد برمیگردد |
| مکالمه کامل | خیر | دستی (از طریق انتقال) |
| فقط سوالات روشنگرانه | خیر | خودکار (از طریق |
| هیچ کدام | بله | خودکار (با نتیجهاش) |
mode فقط روی زیرعاملها (subagents) اجرا میشود - هرگز روی هماهنگکننده (coordinator) اجرا نمیشود. و گرههای گردش کار به طور پیشفرض روی single_turn هستند (به همین دلیل است که L1-L2b هرگز آن را ننوشت)، در حالی که زیرعاملها به طور پیشفرض روی chat ) هستند (به همین دلیل است که L3a مجبور بود این کار را بکند).
⚠️ دو نکتهی مهم قبل از اینکه بر اساس این موارد کار را پیش ببرید: (1) task
از آنجایی که یک گره گراف استاتیک وابسته به نسخه است - در نسخههای 2.0.0b1 تا 2.3.0 (پین این آزمایشگاه کد)، Workflow(...) در زمان ساخت مطرح میشود؛ دقیقاً از کاری که این سطح انجام میدهد استفاده کنید (یک هماهنگکننده چت با زیرعاملهای وظیفه) یا از طریق ctx.run_node ارسال کنید. در نسخه 2.5.0 برطرف شد. (2) «عاملهای وظیفه باید عاملهای برگ باشند» (هیچ زیرعاملی از خودشان ندارند) یک محدودیت ADK مستند است - اما یک قرارداد است ، نه یک محافظ زمان اجرا: نه 2.3.0 و نه 2.5.0 شما را متوقف نمیکنند. عدم وجود خطا را به عنوان مجوز تلقی نکنید.
💡 عمیقتر بروید: یک عامل task که در یک گردش کار گراف (شکل ۲.۵.۰+) تعبیه شده است، با مسیریابی که میتواند مکالمه را برای تلاش مجدد به عقب برگرداند: مخزن همراه 22_agent_in_workflow · راهنمای حالت کامل: docs/agent-modes.md .
❓ شاید از خود بپرسید: چه چیزی
task
چیزی که آن دو نفر دیگر نمیتوانند را به من بفروش؟ سه چیز: بازگشت خودکار (چت به جای آن مکالمه را ادامه میدهد) · یک خط پایان تایپ شده (حجم داده finish_task باید در برابر طرحواره اعتبارسنجی شود - شما دادهها را برمیگردانید، نه یک رونوشت) · مکث/از سرگیری (⏸ یک وظیفه نگهداشته شده منتظر یک انسان است، نه یک وقفه).
👀 بخوانید: gear_fitter — mode="task" + output_schema کل قرارداد است. · ▶ آن را اجرا کنید . · ✏️ تغییر دهید: run_desk("I need a hydration vest", "2 liters, medium") — سوال توضیحی تطبیق داده میشود، خط پایان تایپ شده باقی میماند.
۱۱. L4a · خروجی موازی به اندازه زمان اجرا (رکن ۳a)

خلاصه کلام: اسکلت هنوز از سه مرحله استاتیک تشکیل شده است - مراحل دینامیک در مرحله میانی پنهان شدهاند، جایی که عرض توسط دادهها در زمان اجرا تعیین میشود.
⚠️ هشدار: این تندترین مرحلهی نردبان است. مرحلهی قبلی ۴۴ خط بود؛ این مرحله حدود ۱۲۰ خط دارد - سه عامل و دو گرهی گردش کار، و هیچکدام از آنها اضافی نیستند. حدود ۱۵ دقیقه زمان در نظر بگیرید و به خط «خواندن/اجرا/تغییر» در انتها تکیه کنید: لازم نیست در همان دور اول همهی خطوط را یاد بگیرید.
سوال: شکل کار به ورودی بستگی دارد. شما نمیتوانید نمودار را از قبل رسم کنید. با عرض زمان اجرا شروع کنید: بگذارید LLM تصمیم بگیرد که چند زیرسوال وجود دارد.
شکل (به عمق یک سطح):
START ─► decompose ─► research_topic (parallel_worker) ─► synthesize
│ │ │
└──┴──┴─ (flat: no children yet)
یک سوال باز به N زیرسوال تجزیه میشود - N زیرسوال توسط LLM در زمان اجرا (3-7) انتخاب میشود - هر کدام به صورت موازی بررسی میشوند ، سپس در یک خلاصه خلاصه ترکیب میشوند .
▶ همکاری: اجرای سلول L4a · 📁 گیتهاب: L4a_flat_research/ · 💻 محلی: python -m L4a_flat_research.deep_research

🔍 نشانگرها — وجود ندارند
dynamic=True
سوئیچ. پویا یک روش نوشتن است، نه یک پیکربندی. دو نشانگر و فقط دو: @node(parallel_worker=True) (یک لیست به اندازه زمان اجرا میگیرد، به ازای هر آیتم یک worker اجرا میکند) و ctx.run_node(...) (زمانبندی کد گرهها مستقیماً). هر کدام را که ببینید → شما در حالت پویا هستید.
What you'll see: the decomposer prints eg 5 sub-questions, they research in parallel, then a synthesized briefing. The number differs on every run — the fixed graph couldn't do that.
Two flags on the worker worth understanding:
-
rerun_on_resume=Trueis mandatory on any node that callsctx.run_node— ADK raises aValueErrorwithout it. On resume it must re-execute the dispatching node to rebuild the children it spawned, since those aren't in the static graph. -
retry_config=bounds how this FAILS. A parallel worker cancels every sibling and re-raises the instant one child fails — so without a retry, a single transient 429 discards the whole run, including every call already paid for. The retry lands on the inner per-item node, so each branch retries independently.
❓ You might be wondering: where does ADK "know" this is dynamic? It doesn't need to — nothing is declared anywhere. The decomposer produces a list at runtime; the parallel worker sizes itself to whatever arrives. The dynamism is a property of the data flow you wrote, not a mode you switched on.
👀 Read: the two flags on research_topic — parallel_worker and rerun_on_resume . · ▶ Run it. · ✏️ Change: swap in your own open question — N changes because the input decided the width.
12. L4b · Add recursive spawning (Pillar 3b)

⚡ TL;DR: recursion is written, not given — the worker calls itself through ctx.run_node , ordinary Python — so the brake must be written too. That's MAX_DEPTH .
The question: one research finding sometimes surfaces a narrow sub-topic worth its own investigation. How do you let a branch spawn more parallel work — and keep it bounded?
The shape (now recursive):
START ─► decompose ─► research_topic (parallel_worker, recursive) ─► synthesize
│ │ │
│ │ └─ research(q3) ─► maybe spawn children
│ └─── research(q2) ─► maybe spawn children
└────── research(q1) ─► maybe spawn children
▶ Colab: run the L4b cell · 📁 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})
🔍 The markers: ctx.run_node(research_topic, ...) inside research_topic itself — self-reference is the recursion — and the guard depth < MAX_DEPTH one line above it.
What you'll see: research nodes printing spawning N deeper — recursion happening live — then a runtime tree shape (eg 5 top-level + 10 recursive children ). The tree differs on every run.
⚠️ Before you raise the knob: the ceiling grows fast — MAX_DEPTH=3 takes the worst case from ~30 calls to ~93. And at the very end of a run you may see a cancelling N leftover tasks log line: that's ADK tearing down its parallel task group after the result is already complete. Harmless — and depending on your logging config you may never see it.
❓ You might be wondering: isn't dynamic recursive by default? No — L4a is fully dynamic with zero recursion. Dynamic only hands you ordinary Python control flow; L4b chooses to write recursion with it. And because you wrote the recursion, you must write its boundary — this is where "let the LLM shape the work, keep the boundaries in code" stops being a slogan.
👀 Read: the guard: if finding.needs_deeper and depth < MAX_DEPTH . · ▶ Run it. · ✏️ Change: set MAX_DEPTH = 1 and re-run — the tree flattens (and the run gets cheaper). The boundary is YOURS, in code.
13. L5 · Which pattern should you use?

⚡ TL;DR: one axis decides everything — who picks the next step: the graph you drew, the LLM, or your code.
You've built all three. This is the model that makes them useful: match the pattern to the shape of your problem.
The axis: who decides what runs next?
Pillar | Who decides what runs next | Built in |
1 · Graph | the graph you drew | L2a / L2b |
2 · Collaborative | the LLM | L3a / L3b |
3 · Dynamic | your Python code, at runtime | L4a / L4b |
Step 0: do you even need a graph?
ADK ships prebuilt workflow agents — SequentialAgent , ParallelAgent , LoopAgent . For a plain chain of agents, those are the cheapest correct answer and there's no graph to assemble. Reach past them when you need explicit routing (L2b's router), a join (L2a's JoinNode ), or nodes that aren't agents (a plain function, zero LLM calls) — that last one is usually the reason.
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)

The honest 1.x-vs-2 framing
This is not "2.0 can do things 1.x couldn't" — 1.x could build all of it. The shift is that 2.0 gives each shape a more direct home , so known control flow leaves the prompt and becomes structure you can see and test.
الگو | The 1.x cost | The ADK 2 home |
نمودار | 4 LLM calls in the common build; routing hidden in a prompt | function + agent nodes as peers → 1 call, |
Collaborative | buildable via | a declared team: |
پویا | recursion drops you out of the framework | |
The whole app, and what a graph can't show you
You have now built every piece below. Workflow exposes its structure at graph.edges , so this picture is generated from the code rather than drawn by hand — and what the introspection finds is the summary of this lab:
Pillar | What | چرا |
1 · Graph (L2b) | 10 edges , routes and all | you drew it before any input arrived |
2 · Collaborative (L3a) | 0 edges — only | the LLM picks the subset per request |
3 · Dynamic (L4a/L4b) | 3 edges — identical in both | the recursion is written in Python, not wired in the graph |
That last row is the proof for the question L4b answers: L4a and L4b have the same graph, and only one of them recurses.
What you can build now
Each pattern you just ran is a real product shape:
You practiced | In the wild, that's | شروع از |
Graph + router (L2a/L2b) | document pipelines, ETL-with-LLM-steps, review/approval chains, eval harnesses | this repo's L2b |
Coordinator + | a support copilot with specialist teams, triage desks, multi-lens review | marathon demo mode 2 |
| intake forms, booking flows, onboarding, KYC — any "collect then act" | |
Dynamic width/depth (L4a/L4b) | research agents, report generators, audit sweeps over unknown-sized inputs | marathon demo mode 3 |
They compose
The three patterns are not mutually exclusive . A graph node can call a collaborative coordinator; a specialist can launch a dynamic workflow. Choose the right pattern per part of the problem — that's how you avoid turning every agent system into one giant prompt.

💡 Try it on your own workflow: the script that drew this is scripts/graph_dump.py . Point it at any Workflow and it will print the real edges — a free structural diagram of anything you build.
14. Congratulations

You built a Marathon Race Day Coach and, along the way, all three of ADK 2's orchestration patterns.
What you learned
- Prologue — the mega-prompt that invented its own weather : why structure exists at all.
- L0–L1 —
Agent,Runner, a real tool the model chooses to call, and your firstWorkflow(function nodes + agent nodes as peers). - L2a / L2b — graph workflows: parallel fan-out +
JoinNode, then deterministic routing — one LLM call. - L3a — collaborative agents: the same team run in
chat(stranded) thensingle_turn(parallel subset + synthesis) — one flag, two worlds. - L3b —
taskmode: a paused clarifying question, a scripted resume,finish_taskreturning a validated object. - L4a / L4b — dynamic workflows: runtime width (fan-out), then runtime depth (recursion) with boundaries in code.
- L5 — the decision tree, and how the patterns compose.
Lines worth keeping
Functions prepare the context. Edges define the workflow. The router chooses the path. The model writes the answer.
Let the LLM shape the work, but keep the boundaries in code.
Match the pattern to the shape of your problem.
مراحل بعدی
- Run the full app these levels were distilled from — the Marathon Race Day Coach , a FastAPI + SSE build with a browser UI showing all three modes live: github.com/cuppibla/adk-2-marathon-demo .
- Go wider: adk-workflows-compared — all 23 official ADK 2 workflow samples, each with a 1.x port and when-to-use guidance. Start with
docs/three-pillars.md, then the things this codelab skipped:07_loop,17_request_input,22_agent_in_workflow. - Port your own problem: which parts are known-structure (L2), known-team (L3a/L3b), unknown-shape (L4)?
- Explore the code: github.com/cuppibla/adk2-tutorial .
- Came through the workshop? Your credit — and the project it created — won't last forever. To keep re-running these levels for free, do the Take-home setup step instead: a free AI Studio key, no Cloud project, no billing. Swapping that one cell is the only change.