1. לפני שמתחילים
ברוב אתרי הכרטיסים, הקונים מוכנסים לתור כדי למנוע קריסות של השרתים וכדי לנהל את הביקוש הגבוה. התחלתם את הרכישה ואתם נמצאים מאחורי 14,000 אנשים. התור זז במשך ארבעים דקות. כשהתור שלכם מגיע, יש לכם חלון זמן קצר לבחור מושבים ולשלם. אם לא תהיה לכם אפשרות להצטרף בפרק הזמן הזה, המיקום שלכם בתור יאבד.
הרבה משימות בעולם האמיתי מתבצעות לפי הדפוס הזה: משימה שנמשכת הרבה יותר זמן משיחה אחת, ומתבצעת מול מערכת חיצונית שאין לכם שליטה עליה. לדוגמה, המתנה להשלמת מלאי של ספק, המתנה לאישור תביעה או המתנה בתור.
נציגי AI רגילים פועלים בתוך תור אחד של צ'אט, שנמשך שניות והוא ריאקטיבי בלבד. הם לא יכולים לחכות 40 דקות או לפעול באופן אוטונומי. כדי לפתור את הבעיה הזו, צריך סוכן למשימות מתמשכות: מערכת שנשארת במצב לא פעיל בזמן המתנה ארוך, שומרת את ההקשר בזמן הפעלה מחדש של התהליך וממשיכה לפעול באופן אוטומטי כשהיא מופעלת על ידי טריגר חיצוני.
בסדנה הזו נסביר איך ליצור סוכן לניהול תור לרכישת כרטיסים להופעה, ונסקור את דפוסי הארכיטקטורה העיקריים שנדרשים לסוכנים שפועלים לאורך זמן ב-Google Cloud.
מה תפַתחו

בסדנה הזו תבנו סוכן להזמנת כרטיסים שפועל לאורך זמן, ותלמדו על דפוסי תוכנה מרכזיים של סוכנים:
- השהיה והפעלה מחדש של תהליך: השהיית הפעלת סוכן במהלך תקופות המתנה ארוכות והמשך ההרצה באמצעות טריגרים חיצוניים.
- אחסון נתונים עמיד: שמירת מצב השיחה וארטיפקטים בין הפעלות מחדש של התהליך באמצעות SQLite/Cloud SQL ו-Cloud Storage.
- אמצעי הגנה מפני נתונים לא עדכניים ואידמפוטנטיות: אימות נתונים דינמיים (כמו זמינות מושבים) לפני ביצוע פעולה, ושימוש במפתחות אידמפוטנטיות כדי למנוע עסקאות כפולות.
- תרשימי זרימה של ADK: מבנה הביצוע באמצעות צמתי קוד דטרמיניסטיים לכללים פרוצדורליים וצמתי סוכנים לקבלת החלטות הקשריות.
- אישור עם מעורבות אנושית: השהיית הביצוע כדי לאסוף ולשמור מגבלות הוצאה מפורשות מהמשתמש.
- פריסה בענן: יצירת קונטיינר ופריסת הסוכן ב-Cloud Run באמצעות טריגרים של Cloud Scheduler ו-Pub/Sub.
להגדרה
קודם כל, צריך להכין את הסביבה ולוודא שיש לכם גישה ל-Google Cloud. מומלץ להריץ את הפקודה הזו ב-Cloud Shell, שבו כבר יש את כל הכלים שצריך. פשוט בודקים שיש לכם מספיק מקום, ושאפשר ליצור פרויקטים בחשבון.
👈💻 משכפלים את המאגר ומריצים את ההגדרה:
cd ~
git clone https://github.com/gca-americas/monstertix
cd ~/monstertix
./setup.sh
תתבקשו לעשות את זה רק פעם אחת:
Which Google Cloud project should this workshop use?
Find yours at https://console.cloud.google.com (top-left picker)
Project id [YOUR_PROJECT_ID]:
לאחר מכן, המערכת מבצעת את הפעולות הבאות, לפי הסדר:
virtualenv | יוצרת את | |
כניסה | | |
project | מצביע | |
APIs | מאפשרת את עשרת המשתתפים שנדרשים לסדנה. | |
| כותב את הפרויקט, האזור ומזהה המודל, כך שכל סקריפט ו- | |
בדיקת המודל | מבצע קריאה אמיתית אחת ל-Gemini, כך שמזהה פרויקט או מודל שגוי ייכשל כאן | |
seed | יוצר סשן שכבר עברו יומיים מאז שהוא התחיל, שנפתח בשלב 3 | |
Cloud SQL | מתחיל מופע של Postgres ברקע | |
מקום השיפוט | פורס את המוכר של כרטיסי הדמה שלכם ב-Cloud Run | |
👈💻 מפעילים את הסביבה:
cd ~/monstertix
source .venv/bin/activate
הפרומפט שלך מתחיל עכשיו במילים (.venv).
סקירה כללית של התרחיש
בתרחיש הסדנה הזה, הסוכן שלכם מזמין שני כרטיסים להופעה ("The Midnight Signal" באמסטרדם). העדפות המשתמש שנשמרות בזיכרון ההיסטורי כוללות:
- ההצגה חייבת להיות בסוף שבוע (סם לא יכול להגיע להצגות באמצע השבוע).
- מומלץ להימנע ממושבים ביציע העליון בזיגו דום (הראות לא טובה).
- מחיר היעד הוא בערך 200 $לכרטיס, עם מגבלה מקסימלית של 250$.

אימות ההתקנה
👉💻 פקודה אחת, שמונה תקתוקים:
./verify.sh
Checking the rig
✓ adk installed adk, version 2.6.2
✓ adk 2 apis App + ResumabilityConfig
✓ workflow graphs Workflow + node
✓ async sqlite drivers sqlalchemy + aiosqlite + greenlet
✓ project your-project-id
✓ vertex ai no API keys anywhere
✓ seeded session 13 events, two days old
✓ venue https://venue-you-xxxx.us-central1.run.app
Ready. Cloud SQL is still building in the background and nothing
before step 10 needs it.
👉 פתיחת כרטיסיות שנייה בדפדפן:
איפה | מה זה |
| לוח הבקרה, הכפתורים. |
2. האם הנחיות יכולות לעזור לכם להגיע לאוטונומיה?
הנחיות לא יכולות להתחיל את עצמן
הנחיה היא פשוט טקסט שמועבר למודל AI כשמתבצעת שיחה. הוא לא יכול לפעול בעצמו, להתעורר או להתחיל שיחה חדשה.
אם תגידו לסוכן בהנחיה שלו "תחזור אליי בעוד 10 דקות", שום דבר לא יקרה אחרי שהתור הנוכחי יסתיים. למודל אין שעון פנימי או לולאה. כדי להפוך סוכן לאוטונומי, משהו מחוץ לסוכן צריך להפעיל אותו ולקרוא לו.

הפעלת הציוד
נתחיל בבדיקת כלי הפיתוח. adk web מפעיל את ממשק האינטרנט של ADK, ממשק הפיתוח שמגיע עם ADK. הכלי מאפשר לכם לדבר עם הסוכן שאתם בונים בלי לכתוב קוד frontend קודם, והוא מציג לכם מידע שלא יכול להופיע בחלון צ'אט, כמו כל קריאה לכלי והארגומנטים שלה, מצב הסשן כשהוא משתנה, הארטיפקטים שהסוכן שמר והאירועים הגולמיים שמתרחשים מתחת לפני השטח. לא כדאי לחשוף אותו בסביבת הייצור. זהו החלון הטוב ביותר להצצה לסוכן פעיל בזמן שאתם לומדים. בשלב 3 בונים את חזית האתר שרוצים לחשוף, ובשלב האחרון פורסים אותה.
👈🔴 בחלונית המקום, לוחצים על איפוס המקום. כל שלב מתחיל באותו עולם: 8 מופעים, כל המושבים זמינים, השעון על מהירות x1.
👈💻 Terminal 1 – טוענים את הקוד של השלב הזה ומפעילים את ממשק האינטרנט של ADK:
cd ~/monstertix
. ./set_env.sh
./use-solution.sh 2 --force
adk web agent --port 8000 --allow_origins "*"
use-solution.sh מעתיק את הקוד שנוצר בשלב מסוים אל agent/, שבו מתבצעת העבודה. כל שלב מתחיל עם הפקודה הזו, והיא מדפיסה את הקבצים שהיא שינתה כדי שתדעו מה לקרוא.
איך מתחילים:
קובץ | מה זה |
| הסוכן – שם, מודל, הוראה, רשימת כלים |
|
|
| לקוח HTTP למקום האירוע. אין כאן משהו מעניין |
| איזה מודל להשתמש, ובדיקה שפרטי הכניסה שלכם פועלים |
. ./set_env.sh מפעיל את הסביבה הווירטואלית, טוען את הפרויקט והמודל ומייצא את $WORKSHOP – הנתיב המוחלט אל ~/monstertix. מריצים אותו בכל חלון Terminal שפותחים. הוא מדפיס את מה שהוא הגדיר:
folder /home/you/monstertix
project your project id
model gemini-2.5-flash (We'll avoid using 3.x for now to avoid high usage demands)
venue https://venue-yourname-xxxx.run.app
👈 פותחים את הכרטיסיות הראשונות בדפדפן:
איפה | מה זה |
ממשק משתמש באינטרנט של ADK | |
לדבר איתו
👉✨ בממשק המשתמש האינטרנטי של ADK, מקלידים:
I want to see The Midnight Signal. I'm in Amsterdam, going with Sam, budget around $200 each.
הוא מחפש את הסיור, קורא את מפות המושבים ומסיק מסקנות לגבי התקציב והחברה שלכם. זהו עוזר אישי עם יכולות גבוהות. עם זאת, כשהמכירה המוקדמת נפתחת בשעה 10:00, הסוכן לא מופעל באופן אוטומטי כי לא פועל תהליך או טריגר ברקע.
קריאת מה שקרה עכשיו
אמצע המסך הוא לא רק יומן צ'אט. כל שלב בתור ממוספר, כולל קריאות לכלים:
▸ I want to see The Midnight Signal. I'm in Amsterdam... ← user input
⚡ search_events({"city": "Amsterdam"}) ← tool execution
✓ search_events ← venue API response
Two Amsterdam shows: Tuesday 10 Nov and Saturday 14 ... ← model response
👈 לוחצים על שורה עם הסמל ⚡. בחלונית הימנית מוצגים הארגומנטים שהמודל בחר. לוחצים על ✓ מתחת לקובץ ה-JSON שהתקבל.
המידע על הסיור לא מופיע בנתוני האימון או בפרומפט של המודל. הסוכן אחזר את הנתונים האלה על ידי הפעלת הכלי search_events, שביצע שאילתה ב-API של המקום.
פתיחת הקוד שהרצתם
ראיתם את הפעולה ואת העקבות. עכשיו נסתכל על הקוד עצמו.
agent/concert/
├── agent.py the agent — about twenty lines
├── tools.py what it can do
├── venue.py an httpx wrapper, nothing interesting
└── config.py which model, and a credentials check
👉💻 פותחים את agent/concert/agent.py. הסוכן כולו נמצא בחלק התחתון של הקובץ:
INSTRUCTION = """
You help someone plan a trip to see a band on tour.
You can search tour dates and read seat maps. Be concrete: name the show, the
city, the section, and the price...
"""
root_agent = Agent(
name="concert",
model=MODEL,
instruction=INSTRUCTION,
tools=[search_events, get_seatmap],
)
שם, מודל, תיאור של אופן הפעולה של הסוכן ורשימה של פונקציות Python. זה כל מה ש-Agent עושה. הוא מצא את adk web כי שם הקובץ הוא agent.py ושם המשתנה הוא root_agent.
👉💻 עכשיו agent/concert/tools.py:
def search_events(city: str = "", weekday: str = "") -> dict:
"""List tour dates. Call with no arguments to see the whole tour.
This venue sells one artist and one tour, so there is nothing to search by
name — never ask the user which artist they mean.
Args:
city: Optional. Filter to one city, e.g. "Amsterdam".
weekday: Optional. Filter to one weekday, e.g. "Saturday".
"""
return venue.get("/events", city=city, weekday=weekday)
פונקציה רגילה. אין דקורטור, אין רישום ואין סכימה לכתיבה. ה-ADK קורא את החתימה ואת מחרוזת התיעוד ויוצר את הגדרת הכלי שהמודל רואה. לכן מחרוזת התיעוד הזו חשובה מאוד, ולכן מחיקת ארגומנט אחד שינתה את התנהגות המודל.
המודל אף פעם לא מריץ את הקוד שלכם. כשראית את search_events({"city": "Amsterdam"}), המודל ביקש את השיחה הזו. ה-ADK הפעיל את הפונקציה, החזיר את התוצאה והמודל כתב משפט על כך. הלולאה היא:
👈 הסמלים בקצה הימני של המסך מאפשרים לשנות את מה שמוצג בחלונית. הכלי השני הוא גרף הסוכן – נסו אותו ותראו שני כלים:

אז אומרים לו לא להירדם
מה היית עושה כדי לפתור את הבעיה? כדאי לנסות את מה שרוב האנשים מנסים קודם. נזכרים בטקסט INSTRUCTION משלב 1, הטקסט החופשי שהועבר למודל בכל תור, וכותבים אותו מחדש.
👉💻 פותחים את agent/concert/agent.py. מתחת ל-INSTRUCTION יש עוד אחד, שכבר נכתב בשבילכם. אפשר לנסות לשנות את ההגדרה בעצמכם:
PROACTIVE_INSTRUCTION = """
You are a proactive ticket-buying assistant.
Monitor the presale for The Midnight Signal. The moment it opens at 10:00 on
Tuesday, buy two tickets for the Amsterdam Saturday show — do not wait to be
asked, and do not wait for me to say anything. Act on your own.
Keep checking until the tickets are bought.
"""
# agent/concert/agent.py
root_agent = Agent(
name="concert",
model=MODEL,
instruction=PROACTIVE_INSTRUCTION, # ← was INSTRUCTION
tools=[search_events, get_seatmap],
)
👉💻 בטרמינל 1, מפעילים מחדש:
cd ~/monstertix
adk web agent --port 8000 --allow_origins "*"
👉✨ מסבירים לו את התוכנית, בדיוק כמו שמסבירים לאדם אחר:
I want to see The Midnight Signal in Amsterdam with Sam. The presale opens at 10:00 on Tuesday. Grab us two tickets the moment it opens.
הוא יסכים, וזה יישמע כאילו הוא מתכוון לזה.
👈 עכשיו מחכים. אל תקליד שום דבר אחר. צופים בפאנל.
מחכים דקה. לא קורה כלום!
למה ההנחיה הזו לא יכולה לעבוד
כדאי לקרוא אותו שוב, הפעם עם נקודת מבט חדשה. מעקב. ברגע שהיא נפתחת. חשוב להמשיך לבדוק. כל אחד מהם מתאר משהו שקורה במהלך תקופה מסוימת.
עכשיו נזכיר מה instruction= בעצם. זהו מחרוזת, שמועברת למודל כחלק מהבקשה, ברגע שמישהו שולח הודעה. אף אחד לא קורא את זה בין התורות.
# agent/concert/agent.py
root_agent = Agent(
instruction=PROACTIVE_INSTRUCTION, # read on invocation
... # ...and there was no invocation
)
הוראה לא יכולה להפעיל את הפונקציה שבה היא נכתבה.
גרסאות של פרומפטים שנכשלות
ניסיון לאכוף ביצוע אוטונומי באמצעות הוראות בפרומפט נכשל ללא קשר לניסוח. ניסיונות נפוצים ליצירת הנחיות – למשל:
- "תגדיר טיימר ליום שלישי בשעה 10:00 ותקנה את הכרטיסים".
- "בדוק כל חמש דקות אם נפתחה מכירה מוקדמת".
- "פועל באופן אוטונומי בלי לחכות לקלט של משתמשים".
כל ההנחיות נכשלות מאותה סיבה: הנחיות הן לקריאה בלבד כשמתבצעת הפעלה. אם אין מתקשר חיצוני, לא מבוצע קוד בין התורות.
3. חוסר פעילות מבוסס-אירועים
הסוכנים לא נשארים פעילים לנצח
השארת תהליך שפועל באופן רציף בזמן שמתבצעת בדיקה של מצב המערכת מבזבזת זיכרון, גורמת להוצאות מיותרות ומובילה לקריסה אם המחשב הנייד עובר למצב שינה או מופעל מחדש.
במקום להשאיר סוכן פעיל ללא הפסקה בזמן שהוא ממתין, סוכן למשימות מתמשכות צריך להיות במצב שינה מלא. אירוע חיצוני (כמו טיימר, webhook או הודעה) מעיר את הנציג רק כשיש עבודה בפועל לבצע.
👈🔴 בחלונית המקום, לוחצים על איפוס המקום. כל שלב מתחיל באותו עולם: 8 מופעים, כל המושבים זמינים, השעון על מהירות x1.
👉💻 טוענים את הקוד של השלב הזה:
cd ~/monstertix
. ./set_env.sh
./use-solution.sh 3 --force
שימו לב מה הותקן: monstertix/, השרת, השעון ועוד משהו שלא ראיתם עדיין. בודקים שהיא לא שינתה את agent/, כי בשלב הזה לא מתבצעים שינויים בנציג. אל תשכח את זה, זה העיקר.
קובץ | מה זה |
| חדש. שרת אינטרנט עם Runner מאחוריו |
| חדש. משהו שיודע את השעה ומתקשר לשרת |
הוא מחולק לשני חלקים, והם נשמרים בשני קבצים עד שהם מגיעים לענן: 
הופך ל-, בשלב 10 | |||
הטריגר | נקודת קצה שבה אפשר להריץ את הסוכן | Cloud Run | |
הגורם להפעלת הטריגר | משהו עם שעון, שקורא לו | Cloud Scheduler | |
הטריגר – שרת האינטרנט שלכם
כן, שרת אינטרנט. כדי שמישהו יוכל להתקשר למספר, צריך שיהיה משהו שמקשיב לו. ממשק המשתמש באינטרנט של ADK מהשלבים הקודמים הוא ממשק פיתוח שיכול להריץ סוכן.
👉💻 פותחים את monstertix/server.py:
from google.adk.runners import Runner
from google.adk.sessions import InMemorySessionService
from concert.agent import root_agent
session_service = InMemorySessionService()
runner = Runner(
agent=root_agent,
app_name="concert",
session_service=session_service,
)
@app.post("/wake")
async def wake(payload: dict = Body(default={})):
session_id = f"wake-{uuid.uuid4().hex[:8]}" # brand new, every time
await session_service.create_session(...)
async for event in runner.run_async(
user_id=USER_ID, session_id=session_id,
new_message=types.Content(role="user",
parts=[types.Part(text=payload["message"])]),
):
...
Agent לא מופעלת מעצמה. זהו תיאור: שם, דגם, הוראה, רשימת כלים. משהו צריך לקבל הודעה נכנסת, למצוא את הסשן המתאים, להעביר למודל את השיחה עד עכשיו, להפעיל את הכלים שהוא מבקש, להחזיר את התוצאות, לחזור על הפעולה עד שהוא מפסיק להפעיל כלים, ולכתוב כל שלב כזה באחסון. הדבר הזה הוא Runner.

בממשק האינטרנט של ADK יש הגדרה כזו שמוגדרת כברירת מחדל. כאן אתם בונים את האפליקציה בעצמכם, וזה ההבדל בין ממשק המשתמש האינטרנטי של ADK לבין אפליקציה שאפשר לפרוס.
👉💻 Terminal 2 — start it:
מפעילים את Runner,
cd ~/monstertix
. ./set_env.sh
python -m monstertix.server
[server] agent concert · gemini-x-flash
[server] listening on http://xxxx_URL/wake
[server] nothing will happen until something calls it. Ctrl-C to stop.
תקרא את השורה האחרונה. הטריגר פועל והסוכן נטען, אבל לא קורה כלום. צריך להפריד בין שני הרעיונות: האפשרות לקבל שיחות והאפשרות להתקשר.
ואם כבר מדברים על דלתות, יש גם דלת כניסה
למרות ש-adk web הוא כלי פיתוח מצוין, לא כדאי להשתמש בו במוצר שאתם משיקים. לא מומלץ להפעיל את האפשרות הזו כשפורסים לסביבת ייצור, ואם היא מופעלת, לא מומלץ להפנות אליה לקוחות. לכן אותו שרת גם מציג דף.
👈 פותחים את הכתובת http://127.0.0.1:8090 בדפדפן – אותה כתובת.
הכירו את MonsterTix. חלון צ'אט, שם בפינה, שני לחצנים. הוא שולח בקשה ל-/wake ומציג את התשובה שמתקבלת, והוא יעבוד ללא שינוי מול כל נקודת קצה שמחזירה תשובה באותו אופן.
בדף | מה יש מתחת |
השיחה |
|
+ סשן חדש | מזהה סשן חדש – שיחה חדשה, ולאחר מכן תקציב חדש |
איפוס הזיכרון | מוחק כל סשן ואת קובץ הזיכרון. הוא שואל קודם, ומראה בדיוק איפה המדינה הזו נמצאת |
👉✨ אומרים לו משהו – What Amsterdam dates are there? – וצופים במסוף 2. אותו Runner, אותו סוכן, אותן שורות יומן. רק המתקשר השתנה.
הגורם המפעיל – משהו שיודע מה השעה
👉💻 פותחים את monstertix/clock.py. בודקים את הקוד.
time.sleep(args.delay) # wait
while True:
client.post(f"{TRIGGER_URL}/wake", # knock
json={"message": args.message})
if not args.every:
return # once, unless told otherwise
time.sleep(args.every) # or again, forever
זהו מנגנון פשוט מאוד: שינה, POST, אולי חזרה על הפעולה. כל מתזמן שבו השתמשתם עושה את אותו הדבר, בנוסף לניסיונות חוזרים, אזורי זמן והבטחה שהוא ימשיך לפעול אחרי הפעלה מחדש.
👈💻 Terminal 3 — fire once, 10 seconds from now:
cd ~/monstertix
. ./set_env.sh
python -m monstertix.clock --in 10
[clock] target http://127.0.0.1:8090/wake
[clock] first fire in 60s, once only
[clock] go and watch the other terminal — do not touch the keyboard
👈 עכשיו מפסיקים להקליד. אין ספק. צופים בטרמינל 2.
דקה לאחר מכן, כשאין אף אחד ליד המקלדת:
[clock] firing → POST /wake
[server] woken · session wake-23930983
[server] called search_events
[server] called purchase
[server] said I've purchased 2 tickets for The Midnight Signal in
Amsterdam on Saturday, section A. Your total is 420.
[clock] done. the agent ran and you were not involved.
זו התשובה לשלב 2. משהו מחוץ לסוכן קרא לו, והמשהו הזה היה sleep ובקשת HTTP.
👉💻 Ctrl-C גם אחרי שצפיתם בו כמה פעמים. אם רוצים לראות את ההודעה שוב, צריך להשתמש קודם ב---every 30.
עכשיו תקרא מה כתוב
"I've bought two tickets for The Midnight Signal in Amsterdam, section A, $420."
👉🔴 בודקים את החלונית של המקום ורואים שהכרטיס נרכש.
אז מה עדיין לא בסדר
הוא קנה משהו. כדאי לבדוק מה המערכת ניחשה. הוא בחר באמסטרדם ובקטע א' כי אלה היו האפשרויות הראשונות שהוא מצא, ואמרת לו לקנות אותן ישירות. הוא לא יודע שסאם לא יכול להגיע בימי חול. סוכן שמבצע פעולות על סמך ניחושים בשעה 3 לפנות בוקר גרוע יותר מסוכן שלא עושה כלום.
ההתעוררות היא החלק הקל. סוכן שמתעורר בלי כלום – בלי היסטוריה, בלי העדפות, בלי אף אחד שמקשיב – לא שווה עדיין להעיר אותו.
4. ניהול משך החיים של ההקשר
המידע נשמר במקומות שונים למשך תקופות שונות
הסוכן מאחסן עובדות בכמה מקומות: בשיחה בצ'אט, במצב הסשן, בקבצים (ארטיפקטים) או בזיכרון המשתמש לטווח ארוך. כל מקום נשאר למשך פרק זמן שונה.
אם הוספתם מידע במקום הלא נכון:
- הוא נעלם מהר מדי: פרטים חשובים נמחקים כשהצ'אט מסתיים.
- העובדות נשארות בזיכרון יותר מדי זמן: עובדות זמניות מזהמות את הזיכרון לטווח ארוך וגורמות לבלבול בשיחות עתידיות.
צריך להתאים כל פריט מידע למקום האחסון הנכון ולמשך החיים הנכון.
טעינת הקוד
👈🔴 בחלונית המקום, לוחצים על איפוס המקום.
👉💻 בטרמינל 1, מריצים את הפקודה:
cd ~/monstertix
. ./set_env.sh
./use-solution.sh 4 --force
מה השתנה מאז השלב הקודם:
קובץ | מה השתנה |
| הוספת |
| חדש. חנות הזיכרונות שלנו |
| עוד שלושה כלים ברשימה, וההוראה אומרת לסוכן להשתמש בהם |
👈💻 מפעילים מחדש את הסוכן בטרמינל 1:
cd ~/monstertix
. ./set_env.sh
adk web agent --port 8000 --allow_origins "*" \
--session_service_uri="sqlite+aiosqlite:///$WORKSHOP/sessions.db" \
--artifact_service_uri="file://$WORKSHOP/artifacts"
שני דגלים שלא השתמשתם בהם בעבר:
דגל | מה זה |
| ADK כותב כל הודעה, קריאה לכלי ושינוי מצב בשורות (מסד נתונים SQLite). |
| קבצים שהסוכן שומר בהם (קבצים) |
בלעדיהם, ADK שומר את כל הנתונים בזיכרון וזורק אותם כשמפסיקים את התהליך. שתי ההגדרות האלה הן שגורמות לכך שההגדרה נשמרת.
מסד הנתונים sessions.db מכיל שיחה קודמת (two-days-ago) עם 13 אירועים מתועדים:
you We're thinking about seeing The Midnight Signal. Me, Sam, and maybe Priya.
you Sam can't do weeknights though, bailed on every single one.
you Priya's out, she's away that weekend. Just me and Sam.
agent Two it is. Still Saturday the 14th, still Amsterdam.
you You said the upper bowl at Ziggo was bad last time?
קובץ זיכרון משלוש הזמנות קודמות, memory/userx.md, שאפשר לפתוח בעורך:
- Sam bails on weeknights. Every weeknight show we booked, Sam cancelled.
- Hates the upper bowl at Ziggo Dome, "couldn't see a thing" from section B.
- Comfortable spend is around $200 per ticket. $250 is the hard ceiling.
- Always books two seats, always together.
לכן חשובות השורות בצבע הענבר בשעות הערב המוקדמות. המופע של יום חול בערב זול ב-10 $למושב, יש בו מושבים טובים יותר והוא מופיע בראש הרשימה, אבל הוא לא התשובה הנכונה לאדם הזה. הסיבה היחידה לכך שהסוכן יכול לדעת את זה היא שזה קובץ שהוא כתב אחרי ההזמנה האחרונה. בשלבים 4 ו-5 נבדוק אם העובדה הזו נכונה.
שלושת הכלים שהוספנו

note_companion מתעד מי מגיע ואילו מגבלות חלות עליו:
# agent/concert/tools.py
def note_companion(name: str, constraint: str, tool_context: ToolContext) -> dict:
prefs = dict(tool_context.state.get("user:prefs", {}))
prefs[name] = constraint
tool_context.state["user:prefs"] = prefs
return {"companions": prefs}
להוסיף אותו כי המשפט "Sam can't do weeknights" צריך להיות נכון גם בשבוע הבא. כדי לעשות את זה, כותבים את זה ב-user:prefs ורואים את החלק user: של השם עושה את העבודה. בהמשך המאמר מוסבר איך עושים את זה.
recall
ו-
remember לקרוא ולכתוב קובץ טקסט, memory/userx.md. מחזיקים אותם רגע.
👉 עכשיו נסתכל על שני המקומות שבהם הסוכן שומר את ההקשר:
location | מה הוא מכיל |
מדינה | הזיכרון של הסשן הזה, כצמדי מפתח/ערך – אתם נמצאים כאן בשלב 4 |
Artifacts | קבצים שהסוכן שמר, שלא נכללים בפרומפט |
מה השתנה ב-
get_seatmap
. בעבר היא החזירה את כל מפת המושבים. עכשיו המערכת שומרת את מפת המושבים בקובץ ומחזירה סיכום קצר:
# agent/concert/tools.py
async def get_seatmap(event_id: str, tool_context: ToolContext) -> dict:
seatmap = venue.get(f"/events/{event_id}/seatmap") # about 6 KB of JSON
part = types.Part(inline_data=types.Blob(
mime_type="application/json",
data=json.dumps(seatmap, indent=2).encode()))
await tool_context.save_artifact(f"seatmap_{event_id}.json", part)
summary = {"event_id": ..., "sections": [...], "captured_at": ...}
tool_context.state["temp:seatmap"] = summary
return summary
למה השינוי הזה חשוב?
כל מטען ייעודי (payload) שמוחזר על ידי כלי מצורף להיסטוריית השיחות ונשלח מחדש למודל בכל התורות הבאות. החזרה של אובייקטים גדולים בפורמט JSON (כמו מפת מושבים בגודל 6KB) מגדילה את צריכת הטוקנים בכל תור עוקב.
במקום זאת, שומרים אותו בקובץ ומשלמים על שם קובץ אחד. אם הסוכן צריך את הפרט הזה, הוא יבקש אותו שוב באמצעות load_artifact. ברוב המקרים הוא לא יצטרך אותו.
לאן הקובץ מועבר: שירות הארטיפקטים
תסתכל שוב על השורה ב-get_seatmap, ב-agent/concert/tools.py:
await tool_context.save_artifact(f"seatmap_{event_id}.json", part)
tool_context מגיע מ-ADK, שמעביר אותו לכלי שלכם כארגומנט, ו-save_artifact היא שיטה שמוגדרת בו. השורה הזו היא המקום שבו הכלי מעביר את הבייטים ל-ADK.
לאחר מכן, ה-ADK כותב אותם באמצעות שירות הארטיפקטים שאיתו הוא הופעל. בחרת את האפשרות הזו עם הסימון:
--artifact_service_uri="file://$WORKSHOP/artifacts"
ב-ADK יש שלושה מקומות לאחסון שלהם:
URI | לאן מועברים הבייטים |
| RAM. הם נעלמים כשמפסיקים את התהליך. זו ברירת המחדל אם לא מעבירים דגל |
| תיקייה בכונן. מה נמצא בשימוש |
| Cloud Storage. מה הסוכן שנפרס משתמש בשלב האחרון |
👉💻 כאן בוחרים קבצים:
ls -R ~/monstertix/artifacts
כדי לעבור מהמחשב הנייד ל-Cloud Storage, משנים את file:// ל-gs://. (נראה את זה בשלב 10)
מאגר הזיכרונות שלנו

ADK מספק ממשק לזיכרון לטווח ארוך שנקרא BaseMemoryService. יש לו שתי שיטות:
# google.adk.memory.BaseMemoryService — ADK's interface, not your file
async def add_session_to_memory(session) # take a conversation in
async def search_memory(*, app_name, user_id, query) # hand memories back
ה-ADK כולל שלוש הטמעות של התכונה:
מחלקה | URI | מה צריך |
|
| שום דבר. אם מפסיקים את התהליך, מאבדים את כל הנתונים |
|
| מאגר מידע של Vertex AI RAG |
|
| משאב Agent Engine |
אנחנו נכתוב תיאור משלנו לסדנה הזו. לא כדאי להשתמש ב-InMemoryMemoryService, כי הוא שוכח הכול כשמפסיקים את התהליך, ולא כדאי להשתמש בשני האחרים, כי הם צריכים משאבי ענן שלוקח כמה דקות ליצור אותם. אפשר לבחור קובץ אחר, כי אפשר להכין אותו מראש ולקרוא אותו בכלי עריכה. לכן, אם ניקח את memory.py כהטמעה משלנו, נאחסן את הכול בקובץ Markdown:
class MarkdownMemoryService(BaseMemoryService):
async def search_memory(self, *, app_name, user_id, query) -> SearchMemoryResponse:
path = self._path(user_id) # memory/userx.md
return SearchMemoryResponse(memories=[
MemoryEntry(content=types.Content(
role="user", parts=[types.Part(text=path.read_text())]))])
SearchMemoryResponse ו-MemoryEntry הם הסוגים של ADK, ו-Memory Bank מחזיר את אותם אובייקטים. אחד מהם הוא שירות מנוהל. שלנו כולל קובץ.
לבחור קובץ מסיבה אחת: אתם יכולים לפתוח אותו ולקרוא אותו. הזיכרון לטווח ארוך של הסוכן הוא דף טקסט.
אז לאן מכוונים אותו?
לסיכום, בסדנה הזו נבחרים שניים מתוך שלושת השירותים בשורת הפקודה. את השלישי יישמנו בעצמנו מהסיבה שצוינה למעלה:
שירות | איפה מגדירים את זה | הסדנה הזו |
ביקור |
|
|
ארטיפקט |
|
|
זיכרון | בשום מקום בשורת הפקודה | |
לכן, memory.py בונה את השירות עצמו, ממשתנה סביבה:
# agent/concert/memory.py
MEMORY_DIR = pathlib.Path(os.environ.get("MEMORY_DIR", "./memory"))
MEMORY_USER = os.environ.get("MEMORY_USER", "userx")
memory_service = MarkdownMemoryService(MEMORY_DIR)
. ./set_env.sh מייצא את שניהם. הפונקציה Get MEMORY_DIR מגדירה את המקום שבו הסוכן קורא את הזיכרון שלו, והפונקציה MEMORY_USER קובעת את הקובץ.
איפה הדברים נמצאים?
עכשיו יש ארבעה מקומות שבהם אפשר להציג עובדה. בודקים את כל הארבע.
קודם, הסשן שלכם
👈 לוחצים על סבב חדש בסרגל העליון.
👉✨ אומרים שלושה דברים, אחד אחרי השני:
Sam can't do weeknights, by the way.

Show me the seat map for the Amsterdam Saturday show.

Actually, hold that thought — what else is on in Amsterdam?
ההודעה השלישית הזו חשובה. הפעולה הזו מסיימת את התור שלכם לקריאת מפת המושבים, וסיום התור הוא מה שגורם לסמל temp: להיעלם.
שלושה דברים נשמרו באחסון, בשלושה מקומות שונים:
מה נשמר | איזה כלי ביצע את הפעולה | לאן היא נעלמה | |
1 | סם לא יכול להגיע בימי חול |
| |
2 | מפת המושבים המלאה, 6KB |
| שירות הארטיפקטים |
3 | סיכום של מפת המושבים בשורה אחת | | אף מקום |
👈 עכשיו בודקים. הכרטיסייה State (מצב) והכרטיסייה Artifacts (פריטי מידע) נמצאות בצד ימין של adk web.


בכרטיסייה מצב, שימו לב שהמדיניות temp:seatmap לא מופיעה. מפתחות מצב עם הקידומת temp: קיימים רק במהלך ההפעלה הפעילה, והם מוסרים אוטומטית על ידי ADK לפני שמצב הסשן נשמר במסד הנתונים.
ואז השיחה הישנה יותר
👈 עוברים אל two-days-ago בתפריט הנפתח של הסשן בחלק העליון.
הסשן הזה מכיל היסטוריית שיחות שמולאה מראש במסד הנתונים.

👈 בודקים את רצף האירועים (העמודה האמצעית). שימו לב שהתורות הישנות הוחלפו בבלוק אירועים מסוכם.

👈 בודקים את הכרטיסייה State (מצב). שלושה סוגים של מפתחות מונחים שם יחד:
target_event_id ms-ams-01 ← plain: this booking only
party_size 2 ← plain: this booking only
user:prefs {"excluded_weekdays": [...]} ← the person, every session
הקידומת user: (למשל, user:prefs) מגדירה את היקף המצב בכל הסשנים של משתמש נתון. לעומת זאת, מפתחות ללא קידומת (
target_event_id
,
party_size
) מייצג מצב סשן רגיל, שמוגבל לסשן הפעיל ונמחק כשהסשן מסתיים.
👈 עכשיו משווים בין שני הכרטיסיות 'מצב'. הפריטים user:prefs והפריטים המאוחסנים נשמרים, אבל הפריט temp:seatmap לא מופיע באף אחד מהסשנים. מפתחות מצב עם הקידומת temp: הם זמניים בלבד ואף פעם לא נשמרים במסד הנתונים.
אז הקובץ
👈💻 פותחים אותו בעורך:
cloudshell edit ~/monstertix/memory/userx.md
שלוש הזמנות, החל מינואר. הסוכן קורא את ההודעה כשהוא מתקשר אל recall(), והיא תישאר שם גם אם איבדנו את נתוני הסשן.
ארבעת המקומות
איפה הוא נמצא | האם הוא שורד את ההודעה הבאה? | האם הוא שורד הפעלה מחדש? | האם הוא שורד מחיקה של הסשן? |
| לא | לא | לא |
| כן | כן | לא |
| כן | כן | yes |
| כן | כן | כן, והוא נשמר גם אחרי מחיקת מסד הנתונים |
ארטיפקט | כן | כן | תלוי בשירות |
צריך לקרוא את הטבלה הזו כטבלה על הקונטיינרים, ולא על מה שיש בתוכם.memory/userx.md שורד הכול, והוא מכיל רק את מה שremember() בחר להכניס אליו. השיחה הנוכחית לא נשמרת. לכן, העובדה שהקובץ ממשיך להתקיים אחרי סיום הסשן לא אומרת שהסוכן זוכר על מה דיברתם, וההבדל הזה משמעותי בשלב 5.

מה המצב של
אפשר לחשוב על מצב כעל מילון שמועבר עם הסשן. לקרוא ולכתוב אותו מהכלים שלכם דרך tool_context.state, ולאפשר ל-ADK לשמור אותו לצד השיחה.
המשמעות של הקידומות נקבעת על ידי ADK. לא הגדרתם אותם בשום מקום. ערכת ה-ADK קוראת את החלק הראשון של שם המפתח ומשתמשת בו כדי להחליט לאן הערך יועבר וכמה זמן הוא יישמר:
מפתח שנקרא | ADK שומר את הנתונים | משך |
| עם הסשן | עד שהפעילות תימחק |
| נגד המשתמש | בכל סשן של המשתמש |
| נגד האפליקציה | לכל משתמש |
| בשום מקום | הפעלה אחת |
קריאה היא מחזור יחיד של בקשה ותגובה, כולל כל הנימוקים של המודל, הפעלת הכלי והתשובה הסופית.
כשקריאה מסתיימת, ADK כותב את המצב החדש למסד הנתונים ומסיר כל מפתח temp: בדרך.
דבר אחד לא מוסבר
ב-two-days-ago ראיתם סיכום במקום שבו היו מופיעות כמה מהתגובות הראשונות של מישהו. ה-ADK כתב אותו באופן אוטומטי אחרי שהשיחה התארכה מספיק.
לכל עובדה יש חיי מדף
5. הידרדרות ההקשר ודחיסה
סיכומים בלי פרטים
כשהשיחה ארוכה מדי, המערכת מסכמת את ההודעות הישנות כדי לחסוך מקום ואסימונים.
הסיכומים טובים כדי להבין את הרעיון הכללי, וכדאי לשמור אותם כדי לחסוך בשימוש באסימונים.

👈🔴 בחלונית המקום, לוחצים על איפוס המקום. כל שלב מתחיל באותו עולם: 8 מופעים, כל המושבים זמינים, השעון על מהירות x1.
👉💻 מעבר לקוד של השלב הזה:
cd ~/monstertix
. ./set_env.sh
./use-solution.sh 5 --force
מה השתנה מאז השלב הקודם:
קובץ | מה השתנה |
| הסוכן עטוף ב- |
כל השאר | ללא שינוי |
agent.py לא מסתיים יותר ב-root_agent. הוא מסתיים במילים הבאות:
app = App(
name="concert",
root_agent=root_agent,
events_compaction_config=EventsCompactionConfig(
compaction_interval=3, # summarise after every 3 turns
overlap_size=1, # re-read 1 turn either side when summarising
),
)
חשוב לדעת שהפונקציה adk web מחפשת קודם משתנה בשם app ואם היא לא מוצאת אותו, היא מחפשת משתנה בשם root_agent. עוטפים את הסוכן ב-App כשצריך משהו שרלוונטי לכל האפליקציה ולא לסוכן אחד: דחיסה כאן, ואפשרות להמשך הפעולה בשלבים הבאים.
compaction_interval=3 אומר ש-ADK מסכם אחרי כל שלושה תורים. תור אחד הוא מהרגע שבו אתם אומרים משהו ועד שהסוכן מסיים את התשובה שלו, לא משנה כמה כלים הוא הפעיל במהלך התור. המספר שלוש הוא קטן ולכן הוא מופעל במהלך סדנה. באפליקציה אמיתית, המספר יהיה הרבה יותר גדול.
overlap_size=1 אומר שכל סיכום מתחיל תור אחד מוקדם יותר ממה שנדרש.
ADK מסכם את תורות 1, 2 ו-3. שלוש תורות אחר כך, הוא מסכם את המקבץ הבא. עם overlap_size=1, הסיכום השני מתחיל שוב בתור 3, ולא בתור 4:

שימו לב שהתור השלישי מסוכם פעמיים. זה קורה בכוונה. אם שאלתם שאלה בתור 3 והנציג ענה בתור 4, פיצול נקי יציב את השאלה בסיכום אחד ואת התשובה בסיכום השני, וישאיר את שניהם מבלבלים. חזרה על תור היא ביטוח זול נגד קטיעת מחשבה באמצע.
מגדירים את שני השדות. ל-ADK אין ברירת מחדל לאף אחת מהאפשרויות.
הדחיסה ושירות הזיכרון שלכם אף פעם לא נפגשים
חשוב להבהיר את ההבדל, כי שתי המילים נשמעות קשורות אבל המערכות לא.
האירוע EventsCompactionConfig מתקיים בApp בagent.py, ליד root_agent. הוא מאפיין של האפליקציה, ולא של שירות כלשהו:
# agent/concert/agent.py
app = App(
name="concert",
root_agent=root_agent,
events_compaction_config=EventsCompactionConfig(
compaction_interval=3,
overlap_size=1,
),
)
סיכום השיחה. הוא לא משכתב את הקובץ.
זו הסיבה היחידה לכך שהפונקציה remember() קיימת כקריאה מכוונת. כל מה שנשאר בתמליל ייקרא בסופו של דבר כפרפרזה שמישהו אחר כתב. כל מה שנכתב בקובץ ייקרא בדיוק כמו שהוא נשמר, מחר ובשבוע הבא. בסוף השלב הזה תוכלו לראות את ההבדל.
איפה הסיכום נשמר
ב-sessions.db, בטבלה events, הסוכן כותב רשומה של דחיסה בשורת אירוע, עם חותמת זמן כמו כל אירועי התורות האחרים.
לאירוע רגיל יש שדה content והסיכום נמצא בקטע actions.compaction.
👉💻 אפשר לעיין בנתונים במסד הנתונים שלכם:
cd ~/monstertix
. ./set_env.sh
sqlite3 sessions.db \
"select json_extract(event_data,'\$.actions.compaction.compacted_content.parts[0].text')
from events where event_data like '%compaction%';"
כך נראה הצורה של האירוע:
events row
└── event_data (JSON)
├── id, author, timestamp, invocation_id ← like every other event
├── content ← ABSENT on this one
└── actions
└── compaction
├── start_timestamp ← the window it replaced
├── end_timestamp
└── compacted_content.parts[0].text ← the summary itself
כל אירוע בזרם האירועים נלקח מהתוכן שלו. הסיכום במסד הנתונים שנשלח למודל בכל תור.
הנציג/ה השני/ה
budget_split הוא סוכן נפרד לחלוטין, ולא כלי:
# agent/concert/agent.py
budget_split = Agent(
name="budget_split",
model=MODEL,
include_contents="none", # sees none of the conversation
instruction="You do ticket arithmetic and nothing else...", #just the budget
output_key="budget_plan", # its answer goes into state
)
root_agent = Agent(
...,
tools=[..., AgentTool(agent=budget_split)],
)
למה סוכן ולא פונקציה רגילה? כי צריך מודל כדי לבצע את העבודה. הקריאה "איזה מהחלקים האלה מתאים לתקציב של 200 $לארבעה אנשים, ומה הסכום הכולל" היא פעולה אריתמטית שמשולבת בה שיפוט, והתשובה צריכה להיות קריאה לאנשים. תכפיל פונקציית Python, ואל תבקש ממנה להחליט אילו אפשרויות כדאי לציין.
אז למה לא לתת לסוכן הראשי לעשות את זה? הסתכלות על השורה הבאה include_contents="none" פירושה שהשיחה לא מועברת לסוכן הזה. לא צ'אט קבוצתי, לא העדפות, לא ארבעים תאריכים לסיור. הוא מקבל את הבקשה וזהו. הדגל הזה קיים רק ב-Agent, וזו הסיבה האמיתית לכך שמדובר בסוכן ולא בפונקציה.

עדיפות להנחיה קצרה: זולה יותר, מהירה יותר וקשה יותר להסיט אותה מהנושא. הסתמכו על כך שהחישובים לא מושפעים ממשהו שנאמר לפני עשרים תורות, כי הם אף פעם לא רואים את זה.
למה
AgentTool
? עוטפים את הסוכן כדי שהסוכן הראשי יוכל להפעיל אותו כמו כל כלי אחר. מבחינת המודל, budget_split זהה ל-search_events.
וגם
output_key="budget_plan" מכניס את התשובה למצב הסשן בשם הזה במקום להשאיר אותה חופשית בשיחה. למה זה חשוב?
מפעילים פתרונות חכמים
👈💻 בטרמינל 1, מקישים על Ctrl-C ומפעילים מחדש עם אותם דגלים:
cd ~/monstertix
adk web agent --port 8000 --allow_origins "*" \
--session_service_uri="sqlite+aiosqlite:///$WORKSHOP/sessions.db" \
--artifact_service_uri="file://$WORKSHOP/artifacts"
👉 מתחילים הפעלה חדשה ומנהלים שיחה אמיתית. חמש או שש פניות. הדוגמה הזו עובדת:
hello
what's on in Amsterdam?
Sam is coming with us, and he can't do weeknights
there are 4 of us and I can spend about $200 a head. What fits?
👈 פותחים את הכרטיסייה State (מצב).
התשובה המלאה של הסוכן לחישובים נמצאת שם, ב-budget_plan.

👉✨ עכשיו משנים את המספרים:
what if 10 people come and I can only do $120 each?
👈 כדאי לעיין שוב ב-budget_plan. היא נכתבה מחדש, ולא נוספה. הסיבה לכך היא ש-output_key מכיל את הגרסה האחרונה.

מה אי אפשר לראות: הסיכום
באחת מהשיחות האלה, ADK סיכם את השיחות הקודמות.
👈💻 בטרמינל חדש, מריצים את הפקודה הבאה כדי לקרוא את הקובץ ישירות:
cd ~/monstertix
. ./set_env.sh
./show-compaction.sh
קריאת מה שנכתב
הנה דוגמה אמיתית, בגרסה מקוצרת:
── summary 1 ──────────────────────────────────────────────────────
Conversation Language: English
**User Request:**
The user initially wanted to know what concerts are playing in Amsterdam.
Subsequently, the user provided a constraint regarding a companion, Sam.
**Context Summary:**
The user is planning a concert trip. The agent identified two events in
Amsterdam: "The Midnight Signal" at the Ziggo Dome on Tuesday, November
10 and Saturday, November 14, 2026. The user then informed the agent that a
companion named "Sam" will be joining, but "he can't do weeknights." The
agent has successfully noted this constraint for Sam.
**Key Decisions/Information Obtained:**
* **City of interest:** Amsterdam
* **Events found:** "The Midnight Signal" at the Ziggo Dome, Tuesday
10 November and Saturday 14 November 2026.
* **Companion:** Sam
* **Companion constraint:** Sam cannot attend concerts on weeknights.
**Unresolved Questions/Tasks:**
* The agent has not explicitly confirmed if the found event (which is
on a Saturday) is suitable given Sam's constraint, nor has it asked the
user for the next steps regarding this event or further event searches.
**Tools Used:**
* `recall`
* `search_events`
* `note_companion`
הוא שמר על המחירים, הקטעים, הערים וההעדפות, וציין בצורה נכונה שעדיין לא התקבלה החלטה.
מכאן נובעים שלושה דברים:
זהו ניסוח מחדש. אף אחד לא אמר את מה שכתוב כאן. זהו תיאור של מה שנאמר, שנוצר על ידי אותו סוג של תהליך שיוצר את כל שאר הדברים שהמודל טועה בהם מדי פעם.
הערה: מחרוזות הסיכום מאוחסנות ברשומות פנימיות של אירועים בסשן, ולא מוצגות ישירות ככרטיסיות נפרדות בממשק המשתמש.
כל שלוש תגובות, בשיחה שכוללת עכשיו חלקים מסיכומים קודמים.
לנסות להזמין בלי לחזור על עצמכם
בסשן חדש אל תתחילו סשן חדש – הסיכום שקראתם עכשיו נמצא באירועים של הסשן הזה, וזו הסיבה היחידה לכך שהסוכן עדיין יודע משהו על אמסטרדם.
👉✨ לא לתת לו שום דבר חדש:
Book us something.
צופים בסוכן שואל איזו תוכנית, איזה חלק וכמה אנשים. מסמנים את התשובה כנכונה: בסיכום עצמו מצוין שהמשתמש לא אישר שום דבר, ולכן הנציג שאל. הקרדיט על הדחיסה מגיע ל-Compaction.
גם אם הדחיסה מצליחה, סיכום של תפניות קודמות עלול ליצור עמימות שמחייבת את הסוכן לאשר מחדש את האילוצים עם המשתמש.
👈✨ עונים על השאלה ומאשרים את ההזמנה:
Amsterdam Saturday, section C, 8 people
הוא קורא את ההזמנה לפני שהוא מוציא כסף:
"You want to book 8 tickets in Section C for The Midnight Signal in Amsterdam on Saturday, November 14th. העלות הכוללת תהיה 760$. לקנות את הכרטיסים האלה?"
👉✨ מאשרים את הפעולה:
yes
עכשיו מופיעה הודעה שהפעולה הצליחה:
"הרכישה שלך של 8 כרטיסים בקטע C למופע The Midnight Signal באמסטרדם ביום שבת, 14 בנובמבר, בסכום כולל של 760$, הושלמה. מספר ההזמנה שלך הוא ord_227877fe.'
בודקים אם
👈 עוברים אל החלונית של המקום. אל תסתמכו על מה שהסוכן אומר.

בחלונית | מה אמור להופיע |
כרטיסים שנרכשו |
|
ההזמנה שמתחת |
|
הסיור, אמסטרדם שבת | החלק C ירד ב-8 |
פעילות |
|
בודקים בחלונית את מכירת הכרטיסים.
למה user:prefs הייתה הבחירה הנכונה
תזכור note_companion לכתוב את האילוץ של סם בשלב האחרון user:prefs, ואת ההתנגדות הברורה: למה להשתמש בכלי אם הסוכן יכול פשוט לזכור את זה.
הנה הסיבה. לא לכלול את הסטטוס בשיחה, ולא לכלול אותו בסיכום. בודקים את user:prefs ואת budget_plan ביחס לערכים שהוזנו, ומגלים שהם לא השתנו. לאחר מכן בודקים את התמליל.
סיכום שומר על הצורה הכללית ומאבד את הפרטים. כדאי לשמור כל דבר שאתם לא רוצים לאבד במקום אחר ולא בשיחה.
6. המשך התהליך
המתנה של 40 דקות בלי להישאר אונליין
כשנציג צריך להמתין בתור של כרטיסים למשך 40 דקות, שמירה על תהליך Python פעיל בהמתנה היא מסוכנת ויקרה.
כדי לטפל בהמתנה ארוכה בצורה נכונה:
- שמירת המצב במסד נתונים: אחסון המיקום שבו הסוכן הפסיק את הפעולה, כדי שהתהליך יוכל להיסגר בצורה בטוחה.
- הפעלה לפי דרישה: כשההמתנה מסתיימת, אות חיצוני מפעיל מחדש את הסוכן וממשיך את הביצוע בדיוק מהמקום שבו הוא הפסיק.
👈🔴 בחלונית המקום, לוחצים על איפוס המקום. כל שלב מתחיל באותו עולם: 8 מופעים, כל המושבים זמינים, השעון על מהירות x1.
👉💻 מעבר לקוד של השלב הזה:
cd ~/monstertix
. ./set_env.sh
./use-solution.sh 6 --force
מפעילים פתרונות חכמים
מה השתנה מאז השלב הקודם:
קובץ | מה השתנה |
| הוסיף/ה את |
| |
join_queue מחזירה כרטיס מיד במקום לחסום, ו-ResumabilityConfig מאפשרת להשהות את ההרצה ולהמשיך אותה אחר כך.

👈💻 Terminal 1 — הסוכן, ללא שינוי:
cd ~/monstertix
. ./set_env.sh
adk web agent --port 8000 --allow_origins "*" \
--session_service_uri="sqlite+aiosqlite:///$WORKSHOP/sessions.db" \
--artifact_service_uri="file://$WORKSHOP/artifacts"
👉✨ בקשה מהסוכן לקנות:
Get us two tickets to the Amsterdam show.
השיחה מתבצעת אל recall, אחר כך אל search_events, ואז אל join_queue, והתשובה היא משהו דומה לזה:
You are in the queue for The Midnight Signal at Ziggo Dome, Amsterdam, on Saturday, November 14. Your position is 14203.
שימו לב שהתאריך שנבחר הוא שבת בלי שנשאלתם. יש שתי הופעות באמסטרדם, ה-17 הוא יום שלישי, ובקובץ הזיכרון מהשלב הקודם כתוב שסם לא מופיע בלילות באמצע השבוע. זהו נתון מהזמנה קודמת שמשפיע על שאלה בהזמנה הנוכחית.
שימו לב גם שהיא הצטרפה לתור לפני שהיא בדקה מושב או מחיר ספציפיים, ולא אמרה דבר על אף אחד מהם. זה הכלל של המקום, קונים את הכרטיסים עד שמגיעים לחזית, אז הדבר היחיד שכדאי להשיג מוקדם הוא מקום בתור. איפה נשב? נשאל את השאלה הזו בעוד ארבעים דקות.
👈 מסתכלים על לוח הבקרה. הטקסט שמופיע בבאנר:

Agent is waiting in line — #14,203
About 2400s at 1×. Safe to kill the agent right now — it will still be here.
התור ארוך באמת, והוא לא יתקצר בזמן שאתם עובדים.
👈💻 משביתים את הסוכן:
Ctrl-C
👉💻 מתחילים שוב בטרמינל 1:
cd ~/monstertix
. ./set_env.sh
adk web agent --port 8000 --allow_origins "*" \
--session_service_uri="sqlite+aiosqlite:///$WORKSHOP/sessions.db" \
--artifact_service_uri="file://$WORKSHOP/artifacts"
👉✨ באותו סשן, שואלים את הסוכן איפה הוא בתור:
Where am I in line?
You are currently at position 12982 in the queue. It is not your turn yet.

משווים את הכרטיס ורואים שהמיקום ממשיך לרדת בזמן שלא הופעל שום דבר. שימו לב שהסוכן לא ידע שהוא הופסק. הוא קרא ל-check_queue עם הכרטיס שהוא קיבל לפני ההפעלה מחדש, כי הכרטיס הזה היה בדיסק.
👉💻 רוצה לדעת למה?
cd ~/monstertix
sqlite3 sessions.db "select name from pragma_table_info('events');"
id
app_name
user_id
session_id
invocation_id
timestamp
event_data
שימו לב שכל תור בשיחה מופיע בשורה נפרדת, וכולל את התור עצמו, מי אמר אותו, לאיזה כלי הוא התקשר, מה חזר והערה שהוא JSON בתוך event_data.
👉💻 מחפשים את הכרטיס לתור:
sqlite3 sessions.db "select event_data from events;" \
| grep -o '"ticket": "q_[a-z0-9]*", "position": [0-9]*' | tail -1
"ticket": "q_8cc2c6f9", "position": 14203
כש-join_queue חוזר, הכרטיס שלו נשמר ישירות במסד הנתונים. במהלך ההפעלה מחדש, ה-ADK טוען מחדש מהדיסק את התפניות הקודמות ואת תוצאות הכלי, וכך הסוכן יכול להמשיך מהמקום שבו הוא הפסיק.
שימו לב מה לא התחיל מחדש. הסוכן ימשיך את השיחה במקום להפעיל מחדש את תהליך העבודה מההתחלה. מכיוון ש-LongRunningFunctionTool ו-ResumabilityConfig שומרים את מצב ההפעלה בדיסק, ADK יודע שהסוכן כבר ממתין בתור, ולכן הוא לא יחפש שוב תאריכים לסיור או יבקש כרטיס כפול.

למה join_queue ולא check_queue
שתי הפונקציות מחזירות ערך באלפיות השנייה. ההבדל הוא אם העבודה הבסיסית הושלמה כשהפונקציה מחזירה ערך.
מה היא מחזירה | האם העבודה הושלמה? | |
| כרטיס בתור והמיקום הראשוני | לא. העיבוד ממשיך באופן אסינכרוני במערכת החיצונית. |
| הסטטוס הנוכחי של התור | כן. מחזירה תשובה מיידית מנקודה מסוימת בזמן. |
LongRunningFunctionTool מיועדת לפעולות שמחזירות נקודת אחיזה לעבודה בהמתנה. ההגדרה 'המתנה' join_queue מאפשרת לסוכן להשהות את הביצוע בזמן שהתור החיצוני מתקדם.
לעומת זאת, check_queue הוא סינכרוני ושלם. אם נעטוף את הקוד, הסוכן ישעה את הביצוע באופן שגוי בכל פעם שהוא יבדוק את הסטטוס שלו – גם כשהוא יגיע לראש התור ויצטרך להשלים את הרכישה באופן מיידי.

מה גרם לזה לעבוד
שלושה רכיבים מאפשרים השעיה ושחזור של ביצוע:
# agent/concert/agent.py
LongRunningFunctionTool(func=join_queue) # the tool returns without finishing
ResumabilityConfig(is_resumable=True) # the run may pause and be picked up
--session_service_uri="sqlite+aiosqlite:///$WORKSHOP/sessions.db"
-
LongRunningFunctionTool: מסמן כלי כלא חוסם, ומחזיר נקודת אחיזה פעילה כדי שהסוכן יוכל לחכות בלי להשהות את השרשור. -
ResumabilityConfig: מורה לסביבת זמן הריצה להשהות את מחזור החיים של ההפעלה במשימה שלא הושלמה, ולחדש את ההפעלה מהמצב האחרון שנרשם. -
Session Storage URI (--session_service_uri): מפנה את שמירת המצב לאחסון חיצוני (למשל, SQLite, Cloud SQL) במקום לזיכרון התהליך, וכך מונע אובדן נתונים במהלך הפעלה מחדש.
7. פעולה על נתונים לא פעילים
המידע לא מעודכן בזמן ההמתנה
היסטוריית שיחות שנשמרת היא לא מצב בזמן אמת. במהלך פרקי זמן ארוכים של המתנה, נתונים חיצוניים – כמו זמינות כרטיסים או תמחור – יכולים להשתנות בלי שהנציג ידע על כך.
סוכן עם מידע חסר ישהה את הפעולה כדי לבקש הבהרה, אבל סוכן עם נתונים לא עדכניים יפעל באופן מיידי על סמך הנחות מיושנות. כדי למנוע פעולות לא חוקיות, צריך לאמת מחדש נתונים רגישים לזמן מיד לפני ביצוע פעולות קריטיות.
אל תטען קוד חדש עדיין. להישאר בסוכן של השלב הקודם (הוא יכול לקנות כרטיסים, אבל כרגע אין לו אמצעי הגנה מפני נתונים לא עדכניים או אידמפוטנטיות).
👈🔴 לוחצים על איפוס המקום ואז מתחילים סשן חדש ב-adk web – הלחצן + סשן חדש נמצא מעל הצ'אט.
כדי לנקות את כרטיס התור הקודם ואת סטטוס הרכישה בהמתנה, צריך להתחיל סשן חדש. בשלב הזה נדרש הקשר ביצוע נקי כדי להדגים טיפול נכון בנתונים לא עדכניים.
תמונת המצב משקרת
👉✨ מבקשים לראות את אותה תוכנית כמו קודם:
Get us two tickets to the Amsterdam show.
👈✨ עכשיו, בזמן ההמתנה בתור, שואלים על מקומות ישיבה:
How much are the seats, and what's still available?
Section A: $210 per ticket, with 400 seats available.
Section B: $145 per ticket, with 900 seats available.
Section C: $95 per ticket, with 1200 seats available.
👈🔴 לוחצים על מכירת הכרטיסים הטובים. הערך של קטע א' יורד לאפס.
👈🔴 לוחצים על דילוג על ההמתנה כדי להעביר את הנציג/ה לראש התור.

👉✨ אומרים לו לקנות:
Buy the two section A seats.
הוא מופעל purchase(section="A") בלי לבדוק מחדש את הזמינות של המושבים, ולכן Venue API מחזיר תגובת שגיאה:
{"error": "sold_out", "available": 0,
"message": "those seats are gone — re-fetch the seatmap before buying"}
Oh no! It looks like the Section A tickets just sold out. Let me get an
updated seat map to see what's still available.

שימו לב שהנציג שולח שאילתה חוזרת למפת המושבים אחרי שהוא מקבל תגובת שגיאה מה-API. הסיבה העיקרית היא הקשר לא עדכני: בשלב מוקדם יותר בסשן, הנציג רשם "חלק א': 400 זמינים", והתור הזה נשאר בהיסטוריית השיחות שהועברה למודל.
חשוב לציין בצורה ברורה איפה המידע הישן מאוחסן: הוא נשמר בהיסטוריית השיחות, וקידומות מצב כמו temp: לא יכולות לבטל אותו.

הפתרון
👈🔴 בחלונית המקום, לוחצים על איפוס המקום. כל שלב מתחיל באותו עולם: 8 מופעים, כל המושבים זמינים, השעון על מהירות x1.
👉💻 עכשיו טוענים את הקוד של השלב הזה:
cd ~/monstertix
. ./set_env.sh
./use-solution.sh 7 --force
👈💻 בטרמינל 1, לוחצים על Ctrl-C ומפעילים מחדש:
cd ~/monstertix
. ./set_env.sh
adk web agent --port 8000 --allow_origins "*" \
--session_service_uri="sqlite+aiosqlite:///$WORKSHOP/sessions.db" \
--artifact_service_uri="file://$WORKSHOP/artifacts"
מה השתנה:
קובץ | מה השתנה |
| חדש. מופעל מיד לפני רכישה ויכול לחסום אותה |
|
|
| |
fence.py היא קריאה חוזרת ש-ADK מפעיל ברגע שלפני כל קריאה לכלי:
root_agent = Agent(
...,
before_tool_callback=refresh_before_purchase, # re-read inventory, then decide
)
הוכחה
👉🔴 איפוס המקום מחכים בתור, לוחצים על SELL THE GOOD SEATS (מכירת המושבים הטובים) בזמן ההמתנה, ואז אומרים לו לקנות כרטיסים למקטע A.
הפעם היא תיפסק מעצמה. צופים בסוכן אומר משהו כמו:
"נראה שהכרטיסים לקטע א' נמכרו. אני צריך לקרוא שוב את מפת המושבים".
אחר כך תראו שהיא מתקשרת שוב אל get_seatmap, בלי שתתבקש, וחוזרת עם הצעה אחרת, של סעיף ג'.
👈🔴 חשוב לקרוא בעיון את פיד הפעילות של המקום, כי הוא מציג את שני החלקים:
error purchase failed: Section A has 0 seats left, 2 requested. Re-read the
seat map and pick again.
agent calling purchase

במרכז השליטה מופיעה רכישה של שיחות, אבל מספר הכרטיסים נשאר אפס. זה קורה כי fence.py פועל כמאבטח לפני שהכלי מופעל:
- הסוכן מנסה להתקשר לכלי הרכישה.
-
fence.pyמקבל את הבקשה ראשון ובודק את הזמינות של המושבים בזמן אמת. - אם הכרטיסים אזלו,
fence.pyמחזיר שגיאה ישירות ומבטל את הרכישה לפני שמתבצעת קריאה ל-API.
הנחיות לא יכולות להבטיח שמודל שפה גדול לא ינסה לקנות כרטיסים שנגמרו. אמצעי הגנה על קוד (before_tool_callback) מבטיחים שהרכישה הבעייתית לא תגיע לעולם האמיתי.
👈🔴 לוחצים על איפוס המקום ואז על ביטול הרכישה הבאה.
👉✨ מבקשים ממנו לקנות, ומאפשרים לו לנסות שוב כמה פעמים שרוצים.

מספר ההזמנות נשאר 1 כי המקום קיבל את אותו מפתח אידמפוטנטיות בניסיון החוזר, והחזיר את האישור המקורי במקום ליצור הזמנה כפולה.
8. תהליך עבודה אוטונומי
מודלים גדולים של שפה (LLM) לא מצליחים לפעול לפי כללים נוקשים בכל פעם
מודלים גדולים של שפה (LLM) מצטיינים בקבלת החלטות (למשל, בחירת תוכנית על סמך העדפות המשתמש). אבל אם מסתמכים על מודל LLM שיפעל לפי כללים קפדניים שלב אחר שלב (למשל: "תמיד צריך להצטרף לתור לפני שבודקים את המושבים"), מדי פעם הוא ידלג על שלבים או יבלבל את הסדר.
כדי לפתור את הבעיה, צריך לפצל את תהליך העבודה לשני סוגים של שלבים:
- כללים קבועים (פונקציות קוד): שלבים שחייבים להתבצע בדיוק באותו אופן בכל פעם (למשל, קבלת כרטיס תור, בדיקת המיקום) נכתבים כצמתי קוד רגילים.
- החלטות (צמתי סוכן): שלבים שדורשים שיקול דעת (בחירת אזורים ביציע, בדיקת העדפות המשתמש) מועברים לסוכן ה-AI.
כדאי לבדוק מה באמת כלול בתהליך הזה. חלק מההערות לא כוללות שיפוט בכלל:
שלב | פסק דין? |
לקחת כרטיס תור אחד בלבד | לא. תמיד אחת, תמיד לפני הקנייה |
בודקים אם הגיע התור שלכם | לא. שואלים, קוראים את התשובה |
לכתוב את התקציר לקונה | לא. אותו משפט, אותה צורה, בכל פעם |
באיזה חלק של המסך הן מוצגות | כן. מחיר, יום, מי מגיע, מה הם שונאים |
העדפות ומגבלות של משתמשים | כן. |
אסור להסתמך על מודל שפה גדול (LLM) בהליכים מחמירים, כי מודלים הסתברותיים לא יצליחו בסופו של דבר לפעול לפי רצפים קשיחים אם לא יתבצע מעקב אחריהם.
ADK Workflow מפריד בין ביצוע דטרמיניסטי לבין נימוק מבוסס-AI:
- צמתי פונקציה אוכפים כללים קבועים (כמו הצטרפות לתור או בדיקת המיקום) באופן עקבי בכל פעם.
- צמתי סוכנים מטפלים בהחלטות לפי הקשר (למשל, בחירת מושבים על סמך העדפות המשתמשים).
- צמתי המתנה משעים את הביצוע בלי להשאיר תהליך פעיל בזיכרון.
הסוכן המקורי לא מוחלף. הוא הופך לצומת הסופי בתרשים של תהליך העבודה, ושומר על הכלים, הבדיקות לגבי נתונים לא עדכניים והאמצעים להבטחת אידמפוטנטיות. הגרף קובע את סדר הביצוע, והסוכן מבצע את החלטת הרכישה.
👈🔴 בחלונית המקום, לוחצים על איפוס המקום.
👉💻 מעבר לקוד של השלב הזה:
cd ~/monstertix
. ./set_env.sh
./use-solution.sh 8 --force
מפעילים פתרונות חכמים
👈💻 בטרמינל 1, מקישים על Ctrl-C ומפעילים מחדש. אותה פקודה כמו תמיד:
cd ~/monstertix
. ./set_env.sh
adk web agent --port 8000 --allow_origins "*" \
--session_service_uri="sqlite+aiosqlite:///$WORKSHOP/sessions.db" \
--artifact_service_uri="file://$WORKSHOP/artifacts"
מה השתנה מאז השלב הקודם:
קובץ | מה השתנה |
| חדש. ההרצה ללא השגחה כתרשים ADK 2 |
|
|
| הוא גם איבד |
הפעלת התרשים – שלוש הודעות, לפי הסדר
מריצים את כל הפעולות הבאות ב-Python הלילי (כאן נמצא הגרף). שולחים את הקלט הזהה בכל תור כדי לראות את המעבר של תרשים תהליך העבודה בין המצבים השונים.
1. שימו לב למי אתם מדברים עכשיו
👈 טוענים מחדש את ממשק האינטרנט של ADK.
# concert/agent.py, at the bottom
buyer_agent = Agent(name="concert", ...) # ← everything you built, renamed
from .nightly import nightly
root_agent = nightly # ← what adk web actually runs
שימו לב: הפונקציה adk web מבצעת את האובייקט שמוקצה ל-root_agent. כאן, root_agent מצביע על nightly תרשים זרימת העבודה, כאשר buyer_agent מוגדר כצומת הסופי בתרשים.
2. התחלת השיחה
👉✨ מקלידים את הטקסט הבא ומקישים על Enter:
go
הערה: בתרשים של תהליך עבודה אוטומטי, הרצה ראשונית מתחילה בלי לחכות לקלט של משתמשים. שליחת הודעה ב-adk web מפעילה את ההרצה הראשונית.
👈 עכשיו מסתכלים על דפדפן האינטרנט. בחלונית הימנית מוצג הגרף והצמתים נצבעים בזמן שהם פועלים. החלונית השמאלית היא זרם האירועים, שבו אפשר לראות איך מתקבלות ההחלטות:

הצפייה בqueue_up כתיבה של queue_ticket ושל queue_event_id למצב הסשן.

אמור להופיע קריאה לפונקציה בשם adk_request_input, שממתינה לתשובה. אחרי זה, הכל נהיה שקט והריצה מסתיימת. התרשים הגיע ל-check_front, מצא אותך במקום 14,203 ועצר.
👈 בודקים את חלונית המקום: כרטיס אחד בתור, ולא מופעל שום דבר.

3. הפעלת המחשב – וחשוב להקפיד להקליד בתיבה הנכונה
גוללים בזרם האירועים עד לאירוע adk_request_input ומאתרים את תיבת הקלט שמשויכת לאירוע הספציפי הזה:

עכשיו יש שני מקומות שבהם אפשר להקליד, והם משמשים למטרות שונות.
איפה מקלידים | מה קורה בעקבות ההשעיה |
התיבה | עונה על ההתפרצות. ההרצה שהושהתה תימשך מ- |
תיבת הצ'אט הראשית בתחתית | מתחילים הפעלה חדשה. הגרף מופעל מחדש מ- |
👉✨ מקלידים משהו בתיבה Enter your response... ולוחצים על החץ.
👉 טרמינל 1:
[check_front] #14,197 — not yet. Pausing.
שורה אחת. לא נבחרה תוכנית שנייה ולא נלקח כרטיס תור שני, מה שהיה מציב אותך בסוף תור של 14,203 אנשים.
אם רואים את הסמלים [open] ו-[pick_show] רצים שוב, סימן שהקלדתם בתיבת הצ'אט. לוחצים על איפוס המקום ומתחילים את השלב מחדש.
👈 בודקים שוב את הכרטיס של המקום: עדיין כרטיס אחד, והמיקום ירד קצת כי השעון של המקום המשיך לפעול בזמן שהשעון שלכם לא פעל.

4. להגיע לחזית ואז לסיים
👈🔴 לוחצים על דילוג על ההמתנה בחלונית המקום.

👉✨ עונים על האירוע החדש ביותר adk_request_input בפעם האחרונה – שוב בתיבה שלו, ולא בתיבת הצ'אט. 
הפעם, תראו את check_front מגיע אליכם ונופל. צופים בהדלקה של החלונית הימנית brief ושל concert, שני הצמתים האחרונים.

👈 ובודקים את הדפדפן. זהו סיום הריצה, ויש בה ארבעה דברים שכדאי לקרוא.

הגרף ירוק לגמרי, והקצוות נספרים. האפליקציותbrief ו-concert מוארות, ובקצוות של pick_show, queue_up ו-check_front מופיע עכשיו תג.
2x
הוא מספר הפעמים שהגרף עבר את הקצה הזה, פעם אחת בכל התעוררות. המספר 2 בחלק העליון הוא המחיר של המשך ההפעלה: המתזמן נכנס מחדש לתרשים מההתחלה בכל פעם. זה לא אומר שהעבודה בוצעה שלוש פעמים, וההוכחה לכך היא חלונית המקום: כרטיס תור אחד, לא שניים. @node(rerun_on_resume=False) היא הסיבה.
התרשים

# agent/concert/nightly.py
nightly = Workflow(
name="concert_nightly",
edges=[(START, open_the_night, pick_show, queue_up,
check_front, brief, buyer_agent)],
)
שימו לב ששני צמתים הם סוכנים ושניים הם פונקציות, ומתייחסים לכל אחד מהם כאל שאלת העיצוב המלאה.
צומת | למה דווקא הצורה הזו |
| agent. בחירת תוכנית מתוך ההיסטוריה של מישהו היא שיפוט. הפונקציה מתקשרת אל |
| function. הצטרפות לתור היא כלל. כלל לא יכול להיות הטלת מטבע בשעה 3 לפנות בוקר |
| function. השאלה 'Am I at the front' (האם אני בחזית) כוללת תשובה נכונה אחת, והמקום כולל אותה |
| agent. הוא |
מסתכלים על הצומת האחרון ומאתרים את הסוכן שיצרתם. לא העתקה, לא שכתוב:
# agent/concert/nightly.py
from .agent import buyer_agent
הכל מגיע ביחד: כל הכלים שיש לו, קריאה חוזרת של מפת המושבים ב-fence.py, מפתח האידמפוטנטיות ב-purchase. יש רק אחד כזה.
למה היא הפסיקה במקום לחכות
זו הגרסה שאף אחד לא צריך לכתוב:
# what NOT to write — this is nobody's file
while not ready: # never do this
time.sleep(1)
הוא משאיר תהליך פעיל למשך ארבעים דקות בלי להשיג כלום, מפסיק לפעול עם הטרמינל ולא ניתן להמשיך אותו. מה קורה במקום זאת כשלוחצים על check_front:
# agent/concert/nightly.py — check_front
if not status["ready"]:
return RequestInput(message=f"Still at #{position}. Wake me and I'll check.")
RequestInput הוא הפרעה. הגרף נעצר במקום שבו הוא נמצא, הקריאה עוברת למאגר הסשנים והתהליך יכול לצאת.
הריצה לא עולה כלום בזמן ההמתנה, והיא מסתיימת במקום אחד בדיוק: כרטיסים שנרכשו. יכול להיות שייקח לכם כל הלילה וכמה התעוררויות כדי להגיע לשם.
9. עבודה עם נציג
אל תוציאו כסף בלי לבדוק שוב עם אדם
סוכן אוטונומי שפועל במהלך הלילה לא צריך לקבל החלטות פיננסיות יקרות על סמך העדפות משוערות או לא ברורות מתוך היסטוריית הצ'אט.
לפני ביצוע פעולות חשובות (כמו הוצאת כסף על כרטיסים), הסוכן צריך להשהות את הפעולה, לבקש מהמשתמש לאשר בקול רם את התקציב שלו ולשמור את האישור המפורש הזה לפני ביצוע רכישות.
👈🔴 בחלונית המקום, לוחצים על איפוס המקום. כל שלב מתחיל באותו עולם: 8 מופעים, כל המושבים זמינים, השעון על מהירות x1.
👉💻 מעבר לקוד של השלב הזה:
cd ~/monstertix
. ./set_env.sh
./use-solution.sh 9 --force
מפעילים פתרונות חכמים
👈💻 בטרמינל 1, מקישים על Ctrl-C ומפעילים מחדש. אותה פקודה כמו קודם:
cd ~/monstertix
. ./set_env.sh
adk web agent --port 8000 --allow_origins "*" \
--session_service_uri="sqlite+aiosqlite:///$WORKSHOP/sessions.db" \
--artifact_service_uri="file://$WORKSHOP/artifacts"
מה השתנה מאז השלב הקודם:
קובץ | מה השתנה |
| ההסכמה של האדם, כפי שנאמרה על ידו. |
| הוספת |
| צומת ראשון חדש, |
אל תדגמו אותו. מאחסנים את המשפט.
פותחים את agent/concert/budget.py וקוראים את כל המאמר:
def load(state) -> str:
"""What they agreed, verbatim. Empty string means nobody has said."""
return str(state.get("budget") or "")
state["budget"] = "up to $100 for the upper bowl or general admission, up to
$250 for the lower bowl, and up to $300 for a Saturday show"
הצומת בודק שני דברים: שהאדם אמר את זה ושהוא אישר את זה.
ואז הוא שומר את המשפט במצב סשן ולא ב-user:, בכוונה. התקציב מוסכם להזמנה אחת. המשפט "250$ אם הם טובים" נאמר על הלהקה הזו, באותו ערב, ואין לו קשר להזמנת כרטיסים.
הדבר נכון גם לגבי כל מה שהם אמרו
לא מפסיקים כשהתקציב מוצה. אנשים משנים את דעתם בזמן שהם מחליטים, ולכן חשוב לתעד כאן את כל מה שהם מבקשים, במקום להסתמך על מה שהנציג זוכר.
"I live in NYC, I can only do weekends, I have 10 people coming with me"
# agent/concert/nightly.py
def what_they_asked_for(ctx) -> str:
"""Everything this person typed, joined, in their own words."""
said = []
for event in (ctx.session.events or []):
if event.author != "user":
continue
for part in (event.content.parts if event.content else []) or []:
text = (getattr(part, "text", None) or "").strip()
if text and text not in said:
said.append(text)
return "\n".join(said)
קריאה רק אירועים שהמשתמש יצר, הוא בודק רק
text
חלקים מתוך function_response, ולכן השאלה והתשובה לגבי התקציב לא מופיעות כאן. אין כפילויות.
הערה: הפונקציה agree_budget מופעלת בכל הרצה, ולא רק בהרצה הראשונה, כך שגם תיקון שהוקלד בזמן שהסוכן נמצא בתור יתקבל. לאחר מכן, open_the_night מעבירים את שני המשפטים, הבקשה והתקציב, לכלי לבחירת תמונות, והוא קורא אותם.
הגרף, צומת אחד ארוך יותר
נתחיל את השיחה הזו:
# agent/concert/nightly.py
nightly = Workflow(
name="concert_nightly",
edges=[(START, agree_budget, open_the_night, pick_show, queue_up,
check_front, brief, buyer_agent)],
)

שני צמתים עוצרים וממתינים, וזהו אותו מנגנון. check_front מחכה למקום ונענה על ידי שעון. agree_budget מחכה לאדם, ורק אדם יכול לענות לשיחה. ההבדל הזה הוא כל מה שקורה בשלב 10.
הפעלה – שלב אחר שלב
1. התחלת השיחה
👈 טוענים מחדש את ממשק האינטרנט של ADK ובוחרים את סוכן concert.
👉✨ בקשו את מה שאתם באמת רוצים:
Get us two tickets to the Amsterdam show.

בשלב 8 לא היה משתמש בתרשים, ולכן כל מה שהקלדתם נמחק ו-go עשה את מה שהוא עושה בדרך כלל. כאן, הפונקציה agree_budget קוראת לפונקציה what_they_asked_for(ctx) בשורה הראשונה שלה, שאוספת את כל מה שהקלדתם ומכניסה אותו ל-state["request"]. אחר כך open_the_night מעביר אותו ללקט לצד התקציב.
לא ציינתם יום, ויש שתי הופעות באמסטרדם. אז צופים בבוחר התאריכים חוזר ל-recall(), מחפשים את Sam bails on weeknights ובוחרים את יום שבת.
בדפדפן, ההרצה נעצרת באירוע adk_request_input:
התרשים שאל אותך שאלה. אותה הפרעה כמו ההמתנה בתור בשלב 8, אבל עכשיו היא מופנית לאדם שיענה על השאלה.
2. תענו כמו שאנשים עונים בפועל
👉✨ בתיבה Enter your response... – לא בתיבת הצ'אט שבתחתית המסך:
About a 100 for the cheap seats. 250 if they're the good ones.
צופים בהפסקת ההפעלה ומאשרים:
[agree_budget] reading back: About a hundred for the cheap seats. Two-fifty
if they're the good ones.
⚡ adk_request_input
So: About a hundred for the cheap seats. Two-fifty if they're the good
ones. Have I got that right?

3. אישור
👉✨ באותה תיבה:
Yes, that's right.
[agree_budget] agreed: About a hundred for the cheap seats. Two-fifty if
they're the good ones.
[open] Budget: About a hundred for the cheap seats. Two-fifty if
they're the good ones.
[pick_show] → {"section": "A", "reason": "..."}
[queue_up] ms-ams-01 section A — ticket q_5924edc2 at #14,203
[check_front] #14,203 — not yet. Pausing.
ארבעה צמתים רצים ממילה אחת. 👈 פותחים את הכרטיסייה State (מצב) ומחפשים את budget – המשפט שלכם, מילה במילה.

ללא קידומת. זהו מצב הסשן, וכדאי לקבל החלטה לגביו אם מתחילים סשן חדש והתקציב יאזל, ולכן המערכת תשאל אתכם שוב.
4. להגיע לחזית ולסיים
👈🔴 לוחצים על דילוג על ההמתנה בחלונית המקום.
👉✨ עונים על השאלה החדשה ביותר adk_request_input — שוב בתיבה שלה:
go

👉 בודקים את חלונית המקום: הזמנה אחת, במחיר שכלול בטווח המחירים שנקבע לסוג המושב הזה.

איך גרף שואל שאלה
פותחים את nightly.py וקוראים את agree_budget. אם תגדירו שלושה דברים כ-true, תצפו לאותה שגיאה אם אחד מהם חסר: השאלה תופיע לנצח.
@node(rerun_on_resume=True) # 1. run again when woken
def agree_budget(node_input, ctx):
answers = ctx.resume_inputs or {} # 3. where the answer arrives
said = answers.get(ASK_BUDGET)
if not said:
return RequestInput(
interrupt_id=ASK_BUDGET, # 2. STABLE id, not the default
message="What are you willing to spend?",
)
...
| בלעדיו, הצומת נחשב כהושלם והמערכת מדלגת עליו | |
יציב | ברירת המחדל היא מזהה ייחודי אוניברסלי (UUID) חדש לכל הפעלה, כך שאין שום דבר שאפשר לחפש את התשובה לפי | |
| לאן התשובה מגיעה. לא ערך ההחזרה של | |
התרשים
זהו הגרף של שלב 8 עם צומת נוסף בחזית. 
ההגבלות שאושרו על ידי המשתמש הן ברמת הסשן, והתוקף שלהן פג כשהשיחה הסתיימה. השארת הגבלות זמניות בין הפעלות עלולה להפוך החלטות חד-פעמיות לברירות מחדל קבועות לא מכוונות.
10. ה-Cloud Stack
מחשבים ניידים נכנסים למצב שינה וקבצים מקומיים נעלמים
במחשב נייד, סוכן תלוי בכך שהמחשב יישאר פעיל, במסדי נתונים מקומיים של SQLite ובקבצים מקומיים. אם סוגרים את מכסה המחשב הנייד או פורסים למאגר ענן שמבצע הפעלה מחדש, הקבצים המקומיים והזיכרון נעלמים.
כדי להפעיל את התכונה בצורה מהימנה בסביבת הייצור:
- Cloud Compute: הפעלת הסוכן בקונטיינר (כמו Cloud Run) שנשאר אונליין.
- מסדי נתונים עמידים בענן: החלפת קבצים מקומיים ו-SQLite בשירותי ענן (Cloud SQL לנתוני סשן, Cloud Storage לקבצים).
- תזמון ב-Cloud: שימוש בכלי תזמון מנוהלים (כמו Cloud Scheduler ו-Pub/Sub) כדי שהנציג יתעורר גם כשהמחשב הנייד מושבת.
👈🔴 בחלונית המקום, לוחצים על איפוס המקום. כל שלב מתחיל באותו עולם: 8 מופעים, כל המושבים זמינים, השעון על מהירות x1.
👈💻 מעבר לקוד הסופי:
cd ~/monstertix
. ./set_env.sh
./use-solution.sh 10 --force
קובץ | מה השתנה |
| חדש. FastAPI אחד שמשרת את דף הצ'אט ואת נקודת הקצה שהמתזמן קורא לה |
| חדש. python-slim ו-uvicorn רגילים. אין בו שום דבר שספציפי ל-ADK |
| הועתק, כך שהדף נשלח באותו מאגר כמו הסוכן |
| ללא שינוי. בייט אחר בייט, התיקייה שסיימתם איתה את שלב 9 |
הדף שכבר יצרתם
** ממשק המשתמש האינטרנטי של ADK** הוא ממשק מקומי לניפוי באגים בלבד, שמאפשר לבדוק את המצב, את זרמי האירועים ואת תרשימי תהליכי העבודה במהלך הפיתוח. הוא לא ארוז לפריסה בסביבת ייצור.
לצורך הפקה, משתמשים בחלק הקדמי של Chat שנוצר בשלב 3. הוא מטפל בהודעות למשתמשים ובהפרעות לאינטראקציה דרך נקודת הקצה /wake בלי לחשוף אמצעי בקרה פנימיים לניפוי באגים.
העברת מצב מקומי לשירותי ענן
עוצרים את השרת ובודקים מה הוא כתב:
sessions.db a SQLite file next to the code
memory/userx.md a markdown file you can open in an editor
artifacts/ seat maps written to a folder
קבצים מקומיים מתאימים לפיתוח, אבל הם לא מתאימים לייצור כי קונטיינרים של Cloud Run משתמשים באחסון זמני מקומי של מופעים.
כשמריצים ב-Cloud Run:
- התאמה אוטומטית של מופעים לאפס: כשהתנועה נפסקת, הקונטיינר מסתיים והמערכת המקומית שלו נמחקת.
- המופעים מתרחבים באופן עצמאי: לכמה מופעים בו-זמניים אין שטח דיסק משותף, וכל אחד מהם פועל עם מערכת קבצים מבודדת משלו.
אם נציג שומר כרטיס או מצב סשן בתור לקובץ SQLite מקומי (sessions.db), בקשות עוקבות שמנותבות למופע חדש או שונה של מאגר ימצאו מסד נתונים ריק. כדי לשמור על מצב עמיד במהלך שינוי גודל והפעלה מחדש, צריך להחליף את הקבצים המקומיים בשירותים חיצוניים מנוהלים.
במקום זאת, אתם צריכים מצב שקיים גם אחרי שקונטיינר מסוים מפסיק לפעול. שלושה שירותים, אחד לכל קובץ:
במחשב הנייד | בענן | מה זה |
| Cloud SQL | מסד נתונים מנוהל של Postgres. הוא קיים גם אם לא מופעל בו שום דבר |
| Cloud Storage | קטגוריה. קבצים כאובייקטים, שאפשר להגיע אליהם מכל מופע בבת אחת |
| Cloud Storage | אותו bucket |
| Cloud Scheduler | קובץ cron שמופעל על ידי Google. המסוף שלך הוא לא שעון מעורר |
יצירה של קבצים
הקצאת מסד נתונים של PostgreSQL, משתמש במסד הנתונים וקטגוריה של Cloud Storage. שימו לב: תהליך יצירת מכונת Cloud SQL נמשך כמה דקות והוא התחיל ברקע במהלך setup.sh.
👉💻 Collect it, and make the rest:
cd ~/monstertix
./setup-cloud-state.sh
→ instance RUNNABLE
→ database adk created
→ user adk created
→ bucket created
→ memory memory/userx.md → gs://your-project-agent-you/memory/
✓ durable state ready. Written to .env
הסקריפט הזה יצר ארבעה דברים, אחד לכל שורה בטבלה שלמעלה, וכתב שני URI לתוך .env:
CLOUD_SESSION_SERVICE_URI=postgresql+asyncpg://adk:...@/adk?host=/cloudsql/proj:region:instance
CLOUD_ARTIFACT_SERVICE_URI=gs://your-project-agent-you/artifacts
פריסת הסוכן באמצעות טריגרים מתוזמנים מנוהלים
אחרי שהשירותים מוכנים, נותנים לסוכן את הבית הקבוע שלו.
👈💻 שלוש או ארבע דקות. מפעילים אותו וממשיכים לקרוא:
cd ~/monstertix
. ./set_env.sh
./deploy-agent.sh
הסקריפט יוצר קובץ אימג' של קונטיינר מ-agent/, פורס אותו ב-Cloud Run ומקצה את התשתית התומכת מבוססת-האירועים.
הפריסה המלאה מורכבת מחמישה רכיבים נפרדים:
1. gcloud run deploy the container: your agent, the graph, and the page
2. a Pub/Sub topic a queue for messages
3. a service account an identity, so the message is allowed in
4. a push subscription topic ──► /trigger/wake
5. a Cloud Scheduler job the alarm clock, on a cron
Cloud Scheduler כמקור הטריגר
Cloud Scheduler מחליף את טיימרים של ביצוע מקומי (clock.py) בשירות cron מנוהל שפועל באופן עצמאי מהפעלות של מסוף מקומי, מטפל בניסיונות חוזרים אוטומטיים ותומך בתזמון לפי אזור זמן.
Pub/Sub כשכבת ניתוק ההודעות
הצבת נושא Pub/Sub בין Cloud Scheduler לבין Cloud Run מספקת שלושה יתרונות ארכיטקטוניים:
- עמידות ההודעות: המערכת שומרת בתור אירועים של טריגרים נכנסים אם שירות Cloud Run מתחיל הפעלה קרה, פורס מחדש או לא זמין באופן זמני.
- מדיניות מובנית לניסיון חוזר: המערכת מנסה באופן אוטומטי לשלוח שוב הודעות שלא נמסרו באופן זמני, בלי שצריך להגדיר לוגיקה מותאמת אישית לניסיון חוזר ברמת האפליקציה.
- יכולת הרחבה של Fan-Out: מאפשרת לכמה שירותים במורד הזרם להירשם לאותם אירועים מתוזמנים בלי להגדיר מחדש את הכלי לתזמון.

הגדרת trigger_sources=["pubsub"] רושמת נקודת קצה של Push שתואמת למטענים סטנדרטיים של Pub/Sub, ומתרגמת הודעות נכנסות ישירות לאירועים של הפעלת סוכן.
→ agent https://concert-you-xxxx.run.app
→ topic presale-you created
→ iam concert-trigger@your-project... may invoke concert-you
→ push → https://concert-you-xxxx.run.app/trigger/wake
→ schedule 0 3 * * *
לדבר איתו
👈 פותחים את כתובת ה-URL של הסוכן בדפדפן. 👈✨ מבקשים ממנו לבצע הזמנה:
Book me 3 tickets for The Midnight Signal in Tokyo. Best seats in the house,
I don't want the cheap ones.
צריך להופיע חלון שבו תתבקשו לציין את הסכום שאתם מוכנים להוציא
👉✨ עונים כמו שהייתם עונים בקול רם, ואז מאשרים:
up to 250 a seat for the good ones
ההרצה נמצאת כרגע בהמתנה.
👉🔴 לוחצים על דילוג על ההמתנה בחלונית המקום, כדי שהתור יהיה מוכן כשההודעה תגיע.
כדי להפעיל אותו לפי לוח זמנים אמיתי, צריך להפעיל את העבודה – jobs run מסרב להפעיל עבודה מושהית עם Job.state must be ENABLED:
gcloud scheduler jobs resume presale-$(gcloud config get-value account | cut -d@ -f1) \
--location=$GOOGLE_CLOUD_REGION
👉 תוך כמה שניות, הסוכן מבצע את הרכישה במסגרת מגבלות התקציב שאושרו ומחזיר אישור הזמנה באופן אוטומטי.
👈🔴 בודקים את חלונית המקום. הזמנה אחת.
כך מגדירים את ההפעלה כל 5 דקות:
gcloud scheduler jobs update pubsub presale-$(gcloud config get-value account | cut -d@ -f1) \
--location=$GOOGLE_CLOUD_REGION --schedule="*/5 * * * *"
הסרת המשאבים
סופרים שני פריטים שמוצבים: המקום והסוכן שלידו. שניהם מצטמצמים לאפס, כך שכשלא משתמשים בהם לא משלמים עליהם. למחוק אותם בכל זאת.
./destroy-agent.sh # service, topic, subscription, scheduler job
./destroy-venue.sh
שימו לב שהפקודות האלה לא משפיעות על Cloud SQL ועל הדלי, כי הם מכילים את הנתונים שלכם והחיוב עליהם נמשך. כשמסיימים:
./destroy-agent.sh --all
כל השאר הוא מקומי. אפשר למחוק את התיקייה כשמסיימים, או לשמור אותה ולהריץ אותה בפרויקט שלכם כמה זמן שרוצים.
הסוכן, הכלים שלו, הקריאות החוזרות והתקציב לא השתנו. שני מזהי URI ומשתנה סביבה אחד.
11. סקירה כללית של המערכת ומדריך לפתרון בעיות
הבדלים בסביבה: ארכיטקטורה מקומית לעומת ארכיטקטורה בענן
המחשב הנייד | בענן | מה השתנה |
| Cloud SQL | מחרוזת חיבור |
|
| URI |
|
| נתיב |
| שירות Cloud Run אחד | |
| Cloud Scheduler → Pub/Sub |
|
אתם, ליד המקלדת |
| ברירת מחדל, שניתן לשנות לכל בקשה |
ההטמעה של סוכן הליבה, כולל root_agent, הגדרות כלי, אמצעי הגנה מפני קריאות חוזרות ואכיפת תקציב, נשארת ללא שינוי. השוואה בין solutions/step10_deploy/concert/ לשלב 9 מראה התאמה מדויקת של בייט אחר בייט, ומוכיחה איך הפשטת השירות של ADK מפרידה בין הגדרת התשתית לבין הלוגיקה של הסוכן.
אם נתקעתם
בכל שלב אפשר להשתמש בקוד הפעיל המלא:
./use-solution.sh N --force # any step. resets memory + sessions + artifacts
./use-solution.sh N --force --keep # code only, leave state alone
כשמשהו לא עובד
תיאור הבעיה | תיקון |
| מזהה שגוי. |
המודל מחזיר שגיאה 404 |
|
| משהו כבר משתמש ביציאה 8000 או 8090. |
הסוכן לא יכול להגיע למקום |
|
התור לא מתקדם | לוחצים על דילוג על ההמתנה. במהירות של פי 1, התור לא יגיע לחזית באופן אוטומטי |
כרטיס שני בתור יופיע בחלונית | עניתם בתיבת הצ'אט במקום בתיבה |
התפריט הנפתח ריק או שהאפליקציה לא נטענת | |
| צפוי. |
הכול מוזר | לוחצים על איפוס המקום ואז על |
קובץ הזיכרון מלא בהזמנות כפולות |
|
|
|
פג תוקף הזמן הקצוב לתפוגה של Cloud Shell | מתחילים שוב את |