יצירת סוכן לסיכום חדשות יומיות בתחום הטכנולוגיה באמצעות סוכנים מנוהלים ב-Gemini API

1. סקירה כללית

תחום ה-AI והטכנולוגיה מתפתח בקצב מהיר יותר ממה שאפשר לעקוב אחריו. מדי יום מתווספים מודלים, מאמרים ומוצרים חדשים. סוכן סיכום שמחלץ את הכותרות של היום, כותב סיכומים תמציתיים ומייצר קובץ PDF בכל בוקר יכול לפתור את הבעיה הזו, אבל בעבר כדי ליצור סוכן כזה היה צריך לבחור מסגרת, להגדיר כלים ב-Python, לכתוב לולאת תזמור, לארוז קונטיינר ולבצע פריסה ב-Cloud Run. כל זה קורה לפני שהסוכן מבצע בקשה אחת לאינטרנט.

הסוכנים המנוהלים ב-Gemini API משנים את התמונה. כותבים שני קובצי הגדרות של Markdown וסקריפט מוכן מראש לעיבוד, מבצעים קריאה אחת ל-API, וארגז חול אמיתי של Ubuntu מופעל, גולש באינטרנט, כותב את הסיכומים ויוצר קובץ PDF. אין מאגרים. אין פריסה. אין קוד לתזמור.

ב-Codelab הזה תבנו בדיוק את הסוכן הזה: מפונקציה ריקה ועד תקציר יומי פעיל, שלב אחר שלב.

מה תפַתחו

  • יצירה והפעלה של סוכן מנוהל ראשון בארגז חול אמיתי של Linux
  • התאמה אישית של הסוכן עם קו מנחה, מקורות אינטרנט ומיומנות PDF
  • הוספת וו בטיחות כדי לחסום פקודות הרסניות לפני שהן מופעלות
  • הורדת קובץ ה-PDF שהסוכן יצר
  • שיפור התקציר בשיחה רב-שלבית בלי לאחזר מחדש את התוכן מהאינטרנט
  • שמירת הגדרות הסוכן והפעלתו באמצעות מזהה בהרצות עתידיות
  • שליחת הסיכום לתיבת הדואר הנכנס באמצעות Gmail API
  • תזמון הפעלת הסוכן ושליחתו באופן אוטומטי מדי יום

הדרישות

  • ‫Python 3.10 ואילך
  • מפתח Gemini API: ‏ aistudio.google.com/api-keys (כולל תוכנית בחינם; מומלץ להגדיר חיוב כדי להפעיל את המודל ללא הפרעות)

2. מה זה ניהול סוכנים ב-Gemini API?

שלוש רמות של מערכות AI

לפני שמתעמקים בקוד, כדאי להבין איפה סוכנים מנוהלים משתלבים ביחס לשתי החלופות:

רמה

מה זה

מי מנהל את התשתית?

Standard LLM

אתם מזינים הנחיה, והוא עונה בטקסט. אין ידיים, אין זיכרון, אין אינטרנט.

לא רלוונטי: הוא לא יכול לעשות שום דבר בעצמו

Self-Hosted Agent

אתם מחברים ADK/LangChain/AutoGen + Docker + כלים + זיכרון.

אתם: הכל (או פלטפורמה מנוהלת כמו Agent Engine)

סוכן מנוהל

מגדירים לו יעד. ‫Google מספקת ארגז חול מאובטח. הסוכן כותב קוד, מריץ אותו, קורא שגיאות, מחפש באינטרנט ומתקן באגים באופן אוטונומי.

‫Google: כל השירותים

ה-Codelab הזה עוסק בשורה השלישית. אתם מספקים משימה וקובצי הגדרה. Google תטפל בכל השאר.

מה אפשר לפתח באמצעות ADK + Cloud Run

כדי לבנות סוכן חדשות שמסכם את החדשות, גולש באינטרנט, מריץ Python ויוצר קובץ PDF, תצטרכו את כל אלה עם ADK + Cloud Run:

# agent.py: define tools and wire up the agent
from google.adk.agents import LlmAgent
from google.adk.tools import google_search, built_in_code_execution

agent = LlmAgent(
    name="digest-agent",
    model=MODEL,
    instruction=AGENTS_MD,          # your editorial voice and rules
    tools=[google_search, built_in_code_execution],
)
# app.py: serve the agent over HTTP
from google.adk.runners import FastApiRunner
runner = FastApiRunner(agent=agent)
app = runner.app
# pdf_tool.py: custom tool, install reportlab, render PDF
# scraper.py: custom tool, fetch each news source
# streaming.py: wire agent events to your SSE endpoint
# Dockerfile: package everything
FROM python:3.12
COPY . /app
RUN pip install google-adk reportlab requests
CMD ["uvicorn", "app:app", "--host", "0.0.0.0"]
# Deploy to Cloud Run
gcloud run deploy digest-agent \
  --image gcr.io/your-project/digest-agent \
  --set-secrets GEMINI_API_KEY=gemini-key:latest \
  --memory 2Gi

הפעולה הזו מתבצעת לפני שהסוכן מופעל בפעם הראשונה. אתם עדיין הבעלים של בידוד ארגז החול (sandbox), כך שהסוכן לא יכול לגרום נזק לשרת שלכם, להתקנת חבילות, לניהול מצב בין קריאות לכלים ולתשתית הסטרימינג כדי להעביר אירועים ללקוח.

מה מחליף את התכונה 'סוכנים מנוהלים'

from google import genai
client = genai.Client()

stream = client.interactions.create(
    agent="antigravity-preview-05-2026",
    input="Generate the digest.",
    stream=True,
    environment={
        "type": "remote",
        "sources": [          # your config files, mounted at startup
            {
                "type": "inline",
                "target": ".agents/AGENTS.md",
                "content": AGENTS_MD,
            },
            {
                "type": "inline",
                "target": ".agents/skills/digest-pdf/SKILL.md",
                "content": SKILL_MD,
            },
            {
                "type": "inline",
                "target": ".agents/skills/digest-pdf/scripts/generate_pdf.py",
                "content": GENERATE_PDF_PY,
            },
        ],
    },
)

מה נדרש ל-ADK + Cloud Run

מה אפשר לעשות באמצעות סוכנים מנוהלים

קובץ אימג' של קונטיינר + Dockerfile + CI/CD

ארגז חול מנוהל באופן מלא של Ubuntu‏ (Python 3.12, ‏ Node 22, ‏ 4 יחידות CPU‏ / 16GB RAM)

פריסה והתאמה לעומס ב-Cloud Run

מוקצים לכל אינטראקציה, התוקף שלהם פג אוטומטית אחרי 7 ימים ללא פעילות

בידוד ארגז החול

מבודד לכל אינטראקציה

כלי PDF בהתאמה אישית + pip install

הסוכן מתקין חבילות בתוך ארגז החול

תשתית סטרימינג של SSE

‫stream=True מחזירה איטרציה של אירוע

הגדרות של כלים ב-Python

כלים מובנים: גלישה באינטרנט, הרצת קוד, מערכת קבצים

ניהול מצב בין הפעלות של כלים

מוטמע בלולאת החשיבה הרציונלית של הסוכן

כותבים קובצי תצורה (AGENTS.md, ‏ SKILL.md, סקריפט מוכן מראש) ושולחים קריאה אחת ל-API. Google מטפלת בכל השאר.

איך פועל ארגז החול

interactions.create() call
        │
        ▼
Google provisions Ubuntu sandbox (Python 3.12, Node 22, 4 CPU / 16 GB RAM)
        │
        ▼
Agent reasoning loop:
  plan → fetch URLs → run Python → write files → reason → repeat
        │
        ▼
Events stream back in real time: tool calls, text chunks, completion
        │
        ▼
interaction.completed → environment_id + interaction_id

ארגז החול נשמר למשך 7 ימים ללא פעילות. אפשר להמשיך את השיחה עם environment_id כדי לשפר את הפלט, להריץ משימות המשך או לשמור את הסוכן בשם.

3. הגדרה

כדי לפתוח את ה-codelab הזה ב-Google Cloud Shell, לוחצים על הלחצן שלמטה. כל יחסי התלות מותקנים מראש.

פתיחה ב-Cloud Shell

אפשרות ב': הגדרה מקומית

git clone https://github.com/Saoussen-CH/tech-digest-managed-agent.git
cd tech-digest-managed-agent

אם צריך, מתקינים את uv:

curl -LsSf https://astral.sh/uv/install.sh | sh

הגדרת מפתח API

cp .env.example .env
cloudshell edit .env

מגדירים את המפתח:

GEMINI_API_KEY=your-key-here

התקנת יחסי תלות

uv sync

4. ביצוע השיחה הראשונה לסוכן

פתיחת קובץ ההתחלה

cloudshell edit run_digest.py

‫run_digest() יש משימה אחת שצריך למלא עכשיו ועוד שלוש לשלב הבא. שני העוזרים כבר מולאו מראש מעל:

  • ‫load_source(path): קורא קובץ מ-.agents/ ביחס לסקריפט. תשתמשו בו בתרגיל הבא כדי לטעון את קו מנחה, את מדריך ה-PDF ואת מנוע הרינדור בארגז החול.
  • ‫run_stream(stream): מעבד את זרם האירועים ומחזיר (environment_id, interaction_id). אתם לא צריכים לכתוב את לולאת האירועים בעצמכם.

אילו נתונים צריך לציין

TODO 1: מחליפים את pass ב- (בינתיים מתעלמים מ-TODO 3 ו-TODO 4, הם רלוונטיים לשלב הבא):

    from google import genai
    client = genai.Client()

    stream = client.interactions.create(
        agent=BASE_AGENT,
        agent_config={"type": "antigravity", "model": "gemini-3.7-flash"},
        input="Fetch the Hacker News front page and list the top 5 stories.",
        stream=True,
        environment="remote",
    )

    environment_id, interaction_id = run_stream(stream)
    print(f"\nDone. environment_id={environment_id}")

מה עושה כל חלק

‫genai.Client() reads GEMINI_API_KEY מהסביבה. כל השאר עובר דרך הלקוח הזה.

interactions.create() היא השיחה הראשית. ארבעה פרמטרים מאפשרים את הפעולה:

  • ‫agent=BASE_AGENT: בחירה בסוכן Antigravity‏ (antigravity-preview-05-2026), סוכן מנוהל למטרות כלליות שמבוסס על Gemini 3.7 Flash כברירת מחדל. אפשר להגדיר את המודל הבסיסי באמצעות agent_config (אפשרויות: gemini-3.7-flash,‏ gemini-3.6-flash,‏ gemini-3.5-flash,‏ gemini-3.5-flash-lite). הוא מגיע עם שלושה כלים מובנים שמופעלים כברירת מחדל: code_execution (הפעלת Bash,‏ Python,‏ Node.js),‏ google_search ו-url_context (אחזור וקריאה של דפי אינטרנט). הכלים של מערכת הקבצים (read_file, ‏ write_file, ‏ list_files) מופעלים אוטומטית כשמעבירים את הפרמטר environment. קריאה אחת מספקת סביבת Ubuntu מנוהלת באופן מלא עם Python 3.12,‏ Node.js 22,‏ git,‏ pip ו-curl שמותקנים מראש. אין קונטיינר לבנייה, אין פריסה להפעלה.
  • ‫input: המשימה של ההרצה הזו. הסוכן מעיין ב-Hacker News ומסיק מסקנות לגבי התוצאות.
  • ‫environment="remote": הקצאת ארגז חול חדש ב-Cloud לאינטראקציה הזו.
  • ‫stream=True: מחזירה איטרטור של אירועים במקום לחסום. בלי ההגדרה הזו, השיחה תמתין 30-90 שניות ותחזיר את כל הפלט בבת אחת כ-interaction.output_text. בשיחות סטרימינג אפשר לראות את הסיבה שהסוכן נתן ולפעול בהתאם בזמן אמת. הסטרימינג הוא לא תכונה מתקדמת כאן: הוא ברירת המחדל הנכונה, כי אם יש תיבה שחורה למשך 90 שניות, אי אפשר לדעת אם הנציג עובד או תקוע.

‫environment_id הוא שם של ארגז החול שהופעל. אחרי interaction.completed, ארגז החול לא נסגר: הוא נשאר פעיל למשך עד 7 ימים. כדי לחזור אליו, לוחצים על environment_id. השיחה מועברת לנציג שני של interactions.create() והנציג ממשיך את הטיפול באותה מערכת קבצים, עם אותם קבצים וחבילות מותקנות, כאילו השיחה מעולם לא הועברה. בשלב הבא, הוא משמש להורדת ה-PDF בלי להריץ מחדש את הסוכן, ובשלב שאחריו הוא משמש להמשך השיחה.

‫interaction_id הוא שם המשתנה של תור השיחה שהסתיים. מעבירים אותו כ-previous_interaction_id בשיחה הבאה, ולנציג יש זיכרון מלא של מה שנאמר ומה שנעשה בתור הזה.

אימות

uv run python run_digest.py

הפלט בשידור חי אמור להופיע בזמן שהסוכן פועל:

[agent started]
  [tool] run_code
Here are the top 5 stories currently on the Hacker News front page, retrieved via the official Hacker News API:

1. **Qwen 3.6 27B is the sweet spot for local development** (471 points)
2. **.self: A new top-level domain designed to support self-hosting** (116 points)
...
Done. environment_id=e3de58774073f75a6ef42924c6ce2e88

ה-API מחזיר environment_id אמיתי גם עם environment="remote". ארגז החול הופעל. מה שחסר הוא הגדרה: אין קול, אין מיומנות, אין מחולל PDF. הסוכן פשוט הדפיס את הסיפורים כטקסט והפסיק. בשלב הבא מוסיפים אותם.

כל שורה בפלט ממופה לאירוע מ-run_stream():

step.type

מה זה

מה מדפיסים ב-run_stream()

"url_context_call"

סוכן מאחזר כתובת URL

[tool] url_context (https://...)

"code_execution_call"

סוכן שמריץ קוד בארגז החול

[tool] run_code

"google_search_call"

סוכן שמבצע חיפוש באינטרנט

[tool] google_search

"function_call"

כלים לקבצים ועוד

[tool] read_file (/workspace/...)

‫step.delta כשdelta.type == "text"

הנציג כותב טקסט

השידור מוזרם ישירות ל-stdout

5. התאמה אישית של הנציג

לא היו לנציג הוראות: לא קול, לא מיומנות ולא מחולל PDF. בשלב הזה טוענים את קובצי ההגדרות מ-.agents/ ומעלים אותם לארגז החול.

מה צריך לשנות

תבצע ארבעה שינויים ב-run_digest.py:

‫TODO 2: מתחת ל-load_source(), מוסיפים את שלושת הקבועים ברמת המודול (הם ממוקמים מחוץ ל-run_digest(), בחלק העליון של הקובץ):

AGENTS_MD       = load_source(".agents/AGENTS.md")
SKILL_MD        = load_source(".agents/skills/digest-pdf/SKILL.md")
GENERATE_PDF_PY = load_source(".agents/skills/digest-pdf/scripts/generate_pdf.py")

פותחים כל קובץ כדי לראות מה נטען: AGENTS.md מגדיר את הקו המנחה ואת כללי תהליך העבודה, SKILL.md הוא מדריך PDF מפורט ו-generate_pdf.py הוא רכיב ה-renderer המובנה שהסוכן יפעיל.

עכשיו מבצעים עוד שני שינויים בתוך run_digest():

‫TODO 3: משנים את environment מ-"remote" למילון המקורות, ומגדירים את input ל-"Generate the digest.":

        environment={
            "type": "remote",
            "sources": [
                {
                    "type": "inline",
                    "target": ".agents/AGENTS.md",
                    "content": AGENTS_MD,
                },
                {
                    "type": "inline",
                    "target": ".agents/skills/digest-pdf/SKILL.md",
                    "content": SKILL_MD,
                },
                {
                    "type": "inline",
                    "target": ".agents/skills/digest-pdf/scripts/generate_pdf.py",
                    "content": GENERATE_PDF_PY,
                },
            ],
        },

‫TODO 4: מוסיפים את השורה הזו מיד אחרי print(f"\nDone. environment_id={environment_id}"):

    save_env(ENVIRONMENT_ID=environment_id, INTERACTION_ID=interaction_id)

הערך save_env כבר מוגדר ב-run_digest.py. הוא כותב את שני המזהים ל-.env כדי שבשלב הבא אפשר יהיה להוריד את ה-PDF בלי להפעיל מחדש את הסוכן.

מה כל מקור עושה

כל מקור הוא קובץ שנטען למערכת הקבצים של ארגז החול בהפעלה לפני שהסוכן פועל. הנתיבים target תואמים למקומות שבהם רתמת Antigravity מצפה למצוא אותם:

.agents/
├── AGENTS.md                              ← auto-loaded as global instructions
└── skills/
    └── digest-pdf/
        ├── SKILL.md                       ← auto-discovered and registered as a skill
        └── scripts/
            └── generate_pdf.py            ← pre-built renderer the agent can run

target path

משתנה

מה קורה עם הנתונים ברתמה

.agents/AGENTS.md

AGENTS_MD

נטען אוטומטית כהוראות קבועות: קו מנחה, תהליך עבודה, כללי ביצוע

.agents/skills/digest-pdf/SKILL.md

SKILL_MD

התגלה ונרשם אוטומטית כמיומנות עם שם; הסוכן מפעיל אותו לפי שם

.agents/skills/digest-pdf/scripts/generate_pdf.py

GENERATE_PDF_PY

כלי מוכן מראש לרינדור PDF; הסוכן כותב summaries.json ואז מריץ את הסקריפט הזה

אימות

uv run python run_digest.py

ההרצה נמשכת עכשיו דקה עד 3 דקות. אפשר לראות את הסוכן קורא קובצי הגדרות, כותב סיכומים ושומר את קובץ ה-PDF:

[agent started]
  [tool] read_file (/.agents/skills/digest-pdf/SKILL.md)
  [tool] list_files (/.agents/skills/digest-pdf/scripts)
  [tool] read_file (/.agents/skills/digest-pdf/scripts/generate_pdf.py)
  [tool] run_code
  [tool] write_file (/workspace/summaries.json)
  [tool] run_code
  [tool] delete_file (/tmp/test_scrape.py)
I have successfully generated today's tech news digest and saved the formatted document to /workspace/digest.pdf.
Done. environment_id=4129ffd75574e308748e9425d7ec828f

‫environment_id הוא עכשיו ערך אמיתי: ארגז החול פעל עם קובצי ההגדרות שלכם והסוכן יצר את digest.pdf. בשלב הבא מוסיפים אמצעי בטיחות לפני ההורדה.

6. הוספת Safety Hook

ה-Hooks מאפשרים להריץ סקריפט בתוך ארגז החול לפני או אחרי כל קריאה לכלי. הסוכן של התקציר משתמש ב-code_execution כדי להריץ סקריפטים של Python, ולכן ווֹק (hook) של pre_tool_execution יכול ליירט את הקריאות האלה ולחסום פקודות הרסניות של מעטפת לפני שהן מופעלות.

סביבת זמן הריצה קוראת את .agents/hooks.json מארגז החול. לפני כל קריאה לכלי ההתאמה, הוא מעביר את פרטי השיחה לסקריפט השער ב-stdin. הסקריפט מדפיס {"decision": "allow"} או {"decision": "deny", "reason": "..."} ל-stdout. אם תסרבו, השיחה עם הכלי תבוטל והסוכן יראה את הסיבה שלכם ויבצע תיקון עצמי.

אילו נתונים צריך לציין

‫TODO 5: ב-run_digest.py, מוסיפים את שתי הקבועים האלה קרוב לחלק העליון, אחרי הקריאות הקיימות ל-load_source:

import json

HOOKS_JSON = json.dumps({
    "safety-gate": {
        "pre_tool_execution": [
            {
                "matcher": "code_execution",
                "hooks": [
                    {
                        "type": "command",
                        "command": "python3 /.agents/hooks-scripts/gate.py",
                        "timeout": 10,
                    }
                ],
            }
        ]
    }
}, indent=2)

GATE_PY = """\
#!/usr/bin/env python3
import sys, json
data = json.load(sys.stdin)
cmd = str(data.get("tool_call", {}).get("args", {}))
if "rm -rf" in cmd:
    print(json.dumps({"decision": "deny", "reason": "Destructive command blocked by safety gate."}))
else:
    print(json.dumps({"decision": "allow"}))
"""

‫TODO 6: מוסיפים עוד שני ערכים לsources list בתוך interactions.create():

{"type": "inline", "target": ".agents/hooks.json",            "content": HOOKS_JSON},
{"type": "inline", "target": ".agents/hooks-scripts/gate.py", "content": GATE_PY},

איך מופעלים ה-hooks במהלך הרצת התקציר

בכל פעם שהסוכן מתקשר אל code_execution כדי להפעיל סקריפט Python או פקודת מעטפת, סביבת זמן הריצה מעבירה את פרטי השיחה אל gate.py קודם. אם הפקודה מכילה את rm -rf, ה-hook מחזיר את deny והסוכן מקבל את הסיבה לדחייה ומנסה שוב עם חלופה בטוחה. כל שאר הקריאות להפעלת קוד עוברות ללא שינוי.

אימות

uv run python run_digest.py

הפלט זהה למה שהיה קודם: שער הבטיחות מאפשר את כל הפקודות הרגילות ליצירת PDF. כדי לוודא שה-hook מופעל, משנים באופן זמני את הקלט של הסוכן ומבקשים ממנו להריץ את rm -rf /tmp/test. הסוכן ידווח שהפקודה נחסמה ויבחר חלופה.

7. הורד את ה-PDF

הנציג כתב digest.pdf אל /workspace/digest.pdf בארגז החול. תמונת המצב של הסביבה זמינה כארכיון tar דרך Gemini Files API.

אם צריך, מתקינים את requests:

uv pip install requests

מה צריך למלא

פתיחת download_pdf.py. יש בו שתי משימות לביצוע.

‫TODO 1: למלא את requests.get() call:

    r = requests.get(
        f"https://generativelanguage.googleapis.com/v1beta/files/environment-{environment_id}:download",
        params={"alt": "media"},
        headers={"x-goog-api-key": api_key},
        allow_redirects=True,
    )
    r.raise_for_status()

כתובת ה-URL מפנה לתמונת המצב של ארגז החול. ‫params={"alt": "media"} מחזירה בייטים גולמיים במקום מטא-נתונים. המינוי הקיים שלכם ל-GEMINI_API_KEY מאמת גם את Files API.

‫TODO 2: חיפוש וחילוץ של קובץ ה-PDF מארכיון ה-TAR:

            member = next(m for m in tar.getmembers() if m.name.endswith("workspace/digest.pdf"))
            tar.extract(member, path=tmp, filter="data")

התחילית של הנתיב ב-tar משתנה בין הרצות, לכן כדאי לחפש לפי הסיומת במקום להוסיף את הנתיב המדויק בתוך הקוד. ‫filter="data" משתיק את אזהרת ההוצאה משימוש של Python 3.13 לגבי חילוץ לא בטוח של tar.

אימות

uv run python download_pdf.py
Saved digest.pdf (48,231 bytes)

פותחים את digest.pdf באותה ספרייה. הוא מכיל את התקציר המעוצב שהסוכן יצר מדפי אינטרנט פעילים.

8. המשך השיחה

כבר יש לך digest.pdf. אם רק רצית את הקובץ, סיימת. בשלב הזה לא מבקשים מהסוכן לשנות את התקציר בלי לאחזר מחדש את האינטרנט.

ארגז החול עדיין פעיל. הסוכן עדיין מחזיק ב-/workspace/digest.pdf וזוכר כל סיפור שהוא סיכם. קריאה שנייה ל-interactions.create() שולחת הודעת המשך לאותו ארגז חול. כאן מבקשים ממנו להוסיף הערה עם הכותרת 'למה זה חשוב' מתחת לכל סיפור, והוא מעדכן את קובץ ה-PDF במקום, בלי לאחזר מחדש ובלי לסכם מחדש.

מה צריך למלא

פתיחת refine_digest.py. יש בו שלוש משימות לביצוע.

TODOs 1 and 2: ממלאים את שני הפרמטרים הרב-שלביים בתוך interactions.create():

    environment=environment_id,
    previous_interaction_id=interaction_id,

‫environment=environment_id מפעיל מחדש את אותו ארגז חול עם הקבצים והחבילות שלו. ‫previous_interaction_id=interaction_id מעביר לסוכן את היסטוריית השיחות. שום דבר אחר לא משתנה מהשיחה הראשונה.

TODO 3: צריך לשמור את הערך החדש של interaction_id בחזרה ב-.env אחרי לולאת האירועים:

save_env(INTERACTION_ID=interaction_id)

כל שיחה ל-interactions.create() יוצרת interaction_id חדש. הכתיבה חזרה של השאילתה המשופרת פירושה שההרצה הבאה תעבור את השיפור הזה כ-previous_interaction_id, ותשרשר את התורות בצורה נכונה. המזהה של ארגז החול לא משתנה אף פעם, ולכן אין צורך לעדכן את ENVIRONMENT_ID.

שני הפרמטרים שמאפשרים עבודה רב-שלבית

מזהה

מה נשמר

אנלוגיה

environment=environment_id

קבצים, חבילות מותקנות, מצב המערכת: כל מה שנמצא במערכת הקבצים של Linux

שמירה על אותו שולחן במשרד בין פגישות

previous_interaction_id=interaction_id

היסטוריית השיחה: מה הסוכן אמר ועשה בתורות הקודמים

לזכור על מה דיברו בפגישה האחרונה

אפשר להעביר כל מזהה בנפרד:

  • ‫environment_id בלבד: אפשר לעשות שימוש חוזר בקבצים ובחבילות, אבל השיחה מתחילה מחדש. זה שימושי למשימה חדשה באותו מרחב עבודה.
  • previous_interaction_id בלבד: המשך ההקשר של השיחה, אבל בארגז חול חדש (הקבצים נעלמו).
  • שניהם: המשכיות מלאה, שזה מה שמשמש בשלב הזה.

ללא environment_id: ארגז חול ריק, ללא PDF. ללא previous_interaction_id: אין הקשר, הסוכן לא יכול לשפר קטע ספציפי.

אימות

uv run python refine_digest.py

השידור צריך להיות מהיר, כי הסוכן לא מאחזר מחדש שום דבר. אחרי שהתהליך יסתיים:

Refinement done.
Saved digest_v2.pdf (52,418 bytes)

פותחים את digest_v2.pdf ומשווים אותו ל-digest.pdf. לכל סיפור נוספה עכשיו השורה 'למה זה חשוב'.

9. שמירה של הגדרת סוכן מנוהלת

כל השיחות עד עכשיו עברו את AGENTS.md, SKILL.md ו-generate_pdf.py בתוך השורה. זה עובד, אבל קוד ההתקשרות שלכם מעביר את התוכן המלא של הקובץ בכל הפעלה. ‫agents.create() משלב את ההגדרה בסוכן בעל שם שנשמר בצד של Google. בהפעלה הבאה, רק מזהה הסוכן מועבר:

Inline calls:   send sources on every call
Named agent:    bake once → invoke by ID, no sources

מה צריך למלא

פתיחת save_agent.py. יש בו משימה אחת לביצוע (TODO 1).

שימו לב שהקבועים מיובאים ישירות מ-run_digest.py (ללא כפילויות):

from run_digest import BASE_AGENT, AGENTS_MD, SKILL_MD, GENERATE_PDF_PY

‫TODO 1: למלא את agents.create() call:

agent = client.agents.create(
    id="my-digest",
    base_agent=BASE_AGENT,
    agent_config={
        "type": "antigravity",
        "model": "gemini-3.7-flash",
    },
    description="Daily tech digest with editorial voice and PDF generation.",
    base_environment={
        "type": "remote",
        "sources": [
            {
                "type": "inline",
                "target": ".agents/AGENTS.md",
                "content": AGENTS_MD,
            },
            {
                "type": "inline",
                "target": ".agents/skills/digest-pdf/SKILL.md",
                "content": SKILL_MD,
            },
            {
                "type": "inline",
                "target": ".agents/skills/digest-pdf/scripts/generate_pdf.py",
                "content": GENERATE_PDF_PY,
            },
        ],
    },
)

‫agent_config מגדיר את המודל הבסיסי. ‫gemini-3.7-flash היא ברירת המחדל והבחירה הכי טובה לתהליך העבודה הזה. האפשרויות gemini-3.6-flash,‏ gemini-3.5-flash ו-gemini-3.5-flash-lite זמינות אם רוצים להפעיל את התהליך בצורה קלה יותר או בעלות נמוכה יותר.

base_environment (לא environment) הוא ההבדל העיקרי מהקריאה המובנית בשלב הקודם: המקורות מאוחסנים בצד של Google ומצורפים אוטומטית לכל הפעלה עתידית. הפעלה חד-פעמית, לא בכל הפעלה של התקציר.

אימות: שמירת הסוכן

uv run python save_agent.py
Saved: my-digest
my-digest: Daily tech digest with editorial voice and PDF generation.

הפעלת הסוכן השמור

פתיחת invoke_agent.py. הוא קורא לסוכן השמור לפי מזהה ללא מקורות:

stream = client.interactions.create(
    agent="my-digest",
    input="Generate the digest.",
    stream=True,
    environment="remote",
)

השוואה לשיחה בתוך השורה: agent=BASE_AGENT מוחלף ב-"my-digest", ובלוק environment המלא עם שלושה מקורות בתוך השורה מוחלף ב-environment="remote". ההגדרה כבר מוטמעת בצד של Google.

אימות: הפעלת הסוכן שנשמר

uv run python invoke_agent.py

השידור החי שיוצג יהיה זהה לשידור החי שמוצג בתוך הדף, אבל הקריאה לא תכלול קבצי מקור. אחרי ההרצה, הערכים של ENVIRONMENT_ID ושל INTERACTION_ID ב-.env מתעדכנים, כך שאפשר להמשיך להשתמש ב-refine_digest.py כמו קודם.

[agent started]
  [tool] read_file
  [tool] write_file
  [tool] run_code
I have successfully created today's tech news digest.
Done. environment_id=9a1c3e02-...

10. שליחה באמצעות Gmail

הסוכן יצר את התקציר ושמר אותו ב-/workspace/digest.pdf. עד עכשיו הורדת אותו באופן מקומי. בשלב הזה, הסוכן מתקשר אל Gmail API בארכיטקטורת REST מתוך ארגז החול, וההודעה מועברת ישירות לתיבת הדואר הנכנס.

הגישה: מקבלים אסימון גישה מסוג OAuth 2.0 באופן מקומי ומעבירים אותו לסוכן בהנחיה input. הסוכן משתמש ב-code_execution כדי ליצור אימייל MIME עם קובץ ה-PDF המצורף, ושולח אותו באמצעות POST ל-Gmail API. אין כלים בהתאמה אישית, אין רישום של שרת MCP.

דרישות מוקדמות

מפעילים את Gmail API בפרויקט GCP ויוצרים מזהה לקוח OAuth 2.0:

  1. נכנסים לכתובת console.cloud.google.com/apis/library/gmail.googleapis.com ומפעילים את Gmail API.
  2. עוברים אל APIs & Services > Credentials > Create Credentials > OAuth 2.0 Client ID.
  3. סוג האפליקציה: אפליקציה למחשב. מורידים את קובץ ה-JSON ושומרים אותו בשם credentials.json בתיקיית הבסיס של הפרויקט.

מוסיפים את כתובת האימייל של הנמען לתיבה .env:

RECIPIENT_EMAIL=you@gmail.com

אם צריך, מתקינים את ספריות האימות:

uv sync

מה צריך למלא

פתיחת send_digest.py. יש בו שני פריטים ברשימת המשימות.

‫TODO 1: טעינה או רענון של אסימון גישה מסוג OAuth 2.0:

creds = None
if TOKEN_FILE.exists():
    creds = Credentials.from_authorized_user_file(TOKEN_FILE, SCOPES)
if not creds or not creds.valid:
    if creds and creds.expired and creds.refresh_token:
        creds.refresh(Request())
        TOKEN_FILE.write_text(creds.to_json())
    else:
        flow = InstalledAppFlow.from_client_secrets_file("credentials.json", SCOPES)
        creds = flow.run_local_server(port=8080, open_browser=False)
        TOKEN_FILE.write_text(creds.to_json())

אחרי שמוסיפים את השורה raise NotImplementedError, מסירים אותה. בפעם הראשונה שמריצים את התוסף, נפתח דפדפן עם מסך ההסכמה ל-OAuth. האסימון נשמר במטמון ב-.gmail_token.json להרצות עתידיות.

TODO 2: מחליפים את input="" בהוראות האימייל. הטוקן כבר נמצא בהיקף כ-creds.token:

    input=(
        "Use the Gmail REST API to send an email:\n"
        f"  To: {recipient}\n"
        "  Subject: Tech Digest - <today's date in YYYY-MM-DD format>\n"
        "  Attachment: /workspace/digest.pdf attached as digest.pdf\n\n"
        "For the body, read /workspace/summaries.json and format it as a "
        "human-readable newsletter, NOT raw JSON. Use this structure:\n"
        "  Tech Digest - <date>\n\n"
        "  === <source name> ===\n"
        "  1. <title>\n"
        "     <summary>\n\n"
        "Steps:\n"
        "1. Parse /workspace/summaries.json and build the formatted body text above.\n"
        "2. Read /workspace/digest.pdf as bytes.\n"
        "3. Build a MIME multipart message using Python's email library.\n"
        "4. Base64url-encode the raw message.\n"
        "5. POST to https://gmail.googleapis.com/gmail/v1/users/me/messages/send "
        "with Authorization header using this token: "
        f"{creds.token}"
    ),

מה עושה כל חלק

האינטראקציה ממשיכה באותו ארגז חול שבו הסוכן כבר יצר את digest.pdf ואת summaries.json. ‫previous_interaction_id מעביר לסוכן את היסטוריית השיחות.

אסימון הגישה מועבר במחרוזת input. הסוכן קורא את ההנחיה ומשתמש בה בכותרת Authorization: Bearer כשמתבצעת קריאה ל-Gmail API. הוא אף פעם לא נוגע במחשב המקומי או במערכת הקבצים.

הסוכן משתמש ב-code_execution כדי לכתוב ולהריץ סקריפט Python בתוך ארגז החול: הוא קורא את summaries.json, מעצב אותו כניוזלטר, קורא את digest.pdf, יוצר הודעת MIME מרובת חלקים, מקודד אותה ב-base64url ושולח אותה ב-POST אל https://gmail.googleapis.com/gmail/v1/users/me/messages/send.

אימות

uv run python send_digest.py
Sending digest...
[agent started]
  [tool] read_file (/workspace/summaries.json)
  [tool] run_code
  [tool] run_code
Email sent successfully.
Email sent. Check your inbox.

בודקים את תיבת הדואר הנכנס. האימייל מגיע עם גוף ההודעה בפורמט של ניוזלטר ועם הקובץ digest.pdf כמצורף.

11. תזמון הרצות יומיות

עד עכשיו, כל שלב הופעל באופן ידני. טריגרים מאפשרים לתזמן את הסוכן שנקרא כך שיפעל אוטומטית על ביטוי cron. הסוכן מופעל בשעה המתוזמנת, מריץ את תהליך העבודה המלא של התקציר, והסביבה נשמרת בין ההרצות, כך שהחבילות שהותקנו בהרצה הראשונה זמינות בכל הרצה שלאחר מכן.

Manual:     python run_digest.py     → runs once, now
Trigger:    client.triggers.create() → runs every morning, automatically

מה צריך למלא

פתיחת create_trigger.py. יש בו משימה אחת.

‫TODO 1: למלא את triggers.create() השיחה. הטריגר מפעיל את כל תהליך העבודה בכל יום: יצירת התקציר ושליחתו לתיבת הדואר הנכנס. אסימוני הגישה פוקעים אחרי שעה, ולכן הוא מוסיף את אסימון הרענון מ-.gmail_token.json כמקור מוטבע, כדי שהסוכן יוכל להחליף אותו באסימון חדש בכל הפעלה.

trigger = client.triggers.create(
    schedule="0 9 * * *",
    time_zone="UTC",
    display_name="daily-tech-digest",
    max_consecutive_failures=3,
    execution_timeout_seconds=600,
    interaction={
        "agent": "my-digest",
        "input": (
            f"Generate the daily tech digest following AGENTS.md instructions. "
            f"Then send an email to {recipient}:\n"
            "- Subject: Tech Digest - <today's date in YYYY-MM-DD format>\n"
            "- Body: the content of /workspace/summaries.json formatted as a readable "
            "newsletter (NOT raw JSON).\n"
            "- Attachment: /workspace/digest.pdf\n\n"
            "For Gmail auth: read /workspace/.gmail_creds.json, POST to "
            "https://oauth2.googleapis.com/token with grant_type=refresh_token "
            "and the client_id, client_secret, refresh_token from the file to get an "
            "access_token. Then POST to "
            "https://gmail.googleapis.com/gmail/v1/users/me/messages/send "
            "with Authorization: Bearer <access_token>."
        ),
        "environment": {
            "type": "remote",
            "sources": [
                {
                    "type": "inline",
                    "target": "/workspace/.gmail_creds.json",
                    "content": gmail_creds,
                }
            ],
        },
    },
)

execution_timeout_seconds=600 הוא הזמן הקצוב לתפוגה שמוגדר כברירת מחדל. max_consecutive_failures=3 משבית את הטריגר באופן אוטומטי אחרי 3 הפעלות כושלות ברצף (ברירת המחדל של ה-API היא 5, אבל 3 היא הגדרה שמרנית יותר לסדנה).

הרשימה sources מוסיפה את .gmail_creds.json לארגז החול בכתובת /workspace/.gmail_creds.json. הסוכן קורא את אסימון הרענון, מחליף אותו באסימון גישה חדש ושולח קריאה ל-Gmail API. תוקף של טוקנים לרענון לא פג, כך שהפעולה הזו מתבצעת בכל הפעלה מתוזמנת בלי צורך ברענון ידני של הטוקן.

אחרי שמוסיפים את השיחה, מסירים את השורה raise NotImplementedError.

אימות

uv run python create_trigger.py
Trigger created: trig_abc123
Next run:        2026-07-23T09:00:00Z

‫create_trigger.py שומר את מזהה הטריגר ב-.env באופן אוטומטי.

כדי לבדוק את היסטוריית הביצוע אחרי הרצה:

uv run python check_trigger.py

כדי להפעיל את הטריגר באופן מיידי בלי לחכות למועד המתוזמן הבא:

uv run python fire_trigger.py

כדי להשהות או למחוק את הטריגר:

uv run python pause_trigger.py

12. מחיקה

התוקף של ארגז החול יפוג אוטומטית אחרי 7 ימים של חוסר פעילות. אין שרתים להפסיק. אין מאגרים למחיקה.

אם שמרתם הגדרות של סוכן, מחקו אותן:

uv run python delete_agent.py

13. סיכום

בניתם סוכן מנוהל מאפס, מושג אחד בכל פעם. הנה מה שלומדים בכל תרגיל:

פעילות גופנית

קונספט

Key API

ביצוע השיחה הראשונה

הקצאת ארגז חול אמיתי של Linux ושידור חי של האירועים שלו

interactions.create(agent, input, environment, stream=True), event.event_type

התאמה אישית של הסוכן

טוענים קובצי הגדרה; שומרים מזהים ב-.env באותה הרצה

environment.sources, save_env

הוספת וו בטיחות

ליירט קריאות לכלים לפני שהן מופעלות ולדחות פקודות הרסניות

hooks.json,‏ pre_tool_execution,‏ gate.py

הורד את ה-PDF

הורדת קובץ ה-PDF בלי להפעיל מחדש את הסוכן

‫Gemini Files API‏ :download ב-download_pdf.py

המשך השיחה

המשך השיחה בלי לאחזר מחדש את האינטרנט

environment=environment_id, previous_interaction_id=interaction_id

שמירת הגדרות הסוכן

שמירת הגדרות הסוכן; הפעלה לפי מזהה, ללא צורך במקורות

agents.create(), agents.list()

שליחה באמצעות Gmail

מקבלים טוקן OAuth באופן מקומי ומעבירים אותו לסוכן, שקורא ל-Gmail API בארכיטקטורת REST באמצעות code_execution

‫OAuth 2.0, ‏ client.interactions.create(input=...)

תזמון הרצות יומיות

הפעלת הסוכן באופן אוטומטי לפי לוח זמנים של cron

client.triggers.create(schedule, time_zone, interaction)

דפוסים מרכזיים

  1. קריאה אחת, ארגז חול אחד: interactions.create() מטפל בכל התשתית (אין קונטיינרים לפריסה, אין חבילות להתקנה מקומית)
  2. סטרימינג מתקדם: stream=True הופך תיבה שחורה של 90 שניות לפיד פעיל של קריאות לכלים וקטעי טקסט
  3. מקורות בתוך השורה: טעינה של AGENTS.md,‏ SKILL.md וסקריפטים מוכנים מראש אל ארגז החול בלי צורך בהעלאה או בפריסה
  4. שימוש בזיהוי אוטומטי: קבצים שמוצבים בתיקייה .agents/ נאספים באופן אוטומטי (אין צורך בהגדרת SDK)
  5. מצב דו-ממדי: environment_id עוקב אחרי קבצים וחבילות; previous_interaction_id עוקב אחרי הקשר השיחה; אפשר להעביר כל אחד מהם בנפרד
  6. הורדת תמונת מצב: הסביבה היא קובץ tar מלא של מערכת הקבצים, שאפשר לגשת אליו דרך Gemini Files API
  7. סוכנים עם שם: agents.create() מגדיר את ההגדרה באופן קבוע; בשיחות עתידיות מועברים רק מזהה הסוכן ו-environment="remote", ללא מקורות
  8. Hooks: hooks.json + a gate script intercept tool calls before they execute; a deny response cancels the call and the agent self-corrects
  9. קריאות ל-API חיצוני: העברת פרטי כניסה בהנחיה input; הסוכן כותב ומריץ את קוד השילוב בתוך ארגז החול באמצעות code_execution
  10. טריגרים: תזמון של סוכן באמצעות ביטוי cron עם client.triggers.create(); הסביבה נשמרת בין ההרצות

‫ADK + Cloud Run לעומת סוכנים מנוהלים: סקירה קצרה של ההבדלים

יכולת

ADK + Cloud Run

ניהול סוכנים ב-Gemini API

הקצאת ארגז חול

docker build + gcloud run deploy

interactions.create()

הגדרת כלים

פונקציות Python שרשומות בסוכן

מובנה: גלישה באינטרנט, הרצת קוד, מערכת קבצים

התקנת חבילות

‫pip install ב-Dockerfile

הסוכן פועל pip install בתוך ארגז חול

אירועים בשידור חי

תשתית SSE בהתאמה אישית

stream=True

המשך הסשן

מסד נתונים של סשנים + החדרת הקשר

environment_id + previous_interaction_id

קובצי הגדרה

מוטמעים בנציג או מוזרקים בזמן ההפעלה

התקנה באמצעות environment.sources

התשתית לניהול

קונטיינר, Cloud Run, ‏ IAM, סודות

ללא

השלבים הבאים