Membangun Agen Ringkasan Info Teknologi Harian dengan Agen Terkelola di Gemini API

1. Ringkasan

Lanskap AI dan teknologi berkembang lebih cepat daripada yang dapat dilacak oleh siapa pun. Model, makalah, dan produk baru dirilis setiap hari. Agen ringkasan yang mengambil berita utama hari ini, menulis ringkasan yang tajam, dan membuat PDF setiap pagi akan menyelesaikan masalah tersebut, tetapi untuk membuatnya, Anda harus memilih framework, menentukan alat di Python, menulis loop orkestrasi, mengemas container, dan men-deploy ke Cloud Run. Semua itu terjadi sebelum agen membuat satu permintaan web.

Agen terkelola di Gemini API mengubah persamaannya. Anda menulis dua file konfigurasi markdown dan skrip perender bawaan, melakukan satu panggilan API, dan sandbox Ubuntu yang sebenarnya akan melakukan booting, menjelajahi web, menulis ringkasan, dan membuat PDF. Tidak ada penampung. Tidak ada deployment. Tidak ada kode orkestrasi.

Dalam codelab ini, Anda akan membuat agen tersebut: dari fungsi kosong hingga ringkasan harian yang berfungsi, satu konsep dalam satu waktu.

Yang akan Anda buat

  • Membuat dan menjalankan agen terkelola pertama Anda di sandbox Linux sungguhan
  • Menyesuaikan agen dengan gaya bahasa editorial, sumber web, dan keterampilan PDF
  • Menambahkan hook keamanan untuk memblokir perintah merusak sebelum dijalankan
  • Mendownload PDF yang dibuat agen
  • Memperbaiki ringkasan dalam percakapan multi-turn tanpa mengambil ulang web
  • Simpan konfigurasi agen dan panggil berdasarkan ID pada proses mendatang
  • Mengirim ringkasan ke kotak masuk Anda melalui Gmail API
  • Jadwalkan agen untuk berjalan dan mengirim secara otomatis setiap hari

Yang Anda butuhkan

  • Python 3.10+
  • Kunci Gemini API: aistudio.google.com/api-keys (paket gratis disertakan; penagihan direkomendasikan untuk menjalankan tanpa gangguan)

2. Apa yang dimaksud dengan Agen terkelola di Gemini API?

Tiga tingkat sistem AI

Sebelum mempelajari kode, berikut adalah posisi Agen Terkelola dibandingkan dengan dua alternatif lainnya:

Level

Apa Ini

Siapa yang Mengelola Infrastruktur?

LLM Standar

Anda memberikan perintah, AI akan membalas dengan teks. Tidak ada tangan, tidak ada memori, tidak ada internet.

T/A: tidak dapat melakukan apa pun sendiri

Agen yang Dihosting Sendiri

Anda menghubungkan ADK/LangChain/AutoGen + Docker + alat + memori.

Anda: semuanya (atau platform terkelola seperti Agent Engine)

Managed Agent

Anda menetapkan sasaran. Google menyediakan sandbox yang aman. Agen menulis kode, menjalankannya, membaca error, menelusuri web, dan memperbaiki bug secara mandiri.

Google: semuanya

Codelab ini membahas baris ketiga. Anda menyediakan file tugas dan konfigurasi. Google akan menangani sisanya.

Yang akan Anda bangun dengan ADK + Cloud Run

Untuk membuat agen ringkasan berita yang menjelajahi web, menjalankan Python, dan membuat PDF, Anda memerlukan semua hal berikut dengan 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

Hal ini terjadi sebelum agen dijalankan satu kali. Anda tetap memiliki isolasi sandbox (sehingga agen tidak dapat merusak server Anda), penginstalan paket, pengelolaan status di antara panggilan alat, dan infrastruktur streaming untuk mendapatkan peristiwa ke klien.

Penggantiannya dengan Agen Terkelola

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,
            },
        ],
    },
)

Yang diperlukan ADK + Cloud Run

Yang ditangani Agen Terkelola untuk Anda

Image container + Dockerfile + CI/CD

Sandbox Ubuntu yang dikelola sepenuhnya (Python 3.12, Node 22, 4 CPU / 16 GB RAM)

Deployment + penskalaan Cloud Run

Disediakan per interaksi, otomatis berakhir setelah 7 hari tidak aktif

Isolasi sandbox

Terisolasi per interaksi

Alat PDF kustom + pip install

Agen menginstal paket di dalam sandbox

Infrastruktur streaming SSE

stream=True menampilkan iterable acara

Definisi alat di Python

Alat bawaan: penjelajahan web, eksekusi kode, sistem file

Pengelolaan status di antara panggilan alat

Terintegrasi ke dalam reasoning loop agen

Anda menulis file konfigurasi (AGENTS.md, SKILL.md, skrip bawaan) dan melakukan satu panggilan API. Google akan menangani sisanya.

Cara kerja sandbox

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

Sandbox akan tetap ada selama 7 hari tidak aktif. Anda dapat melanjutkannya dengan environment_id untuk menyempurnakan output, menjalankan tugas lanjutan, atau membuat fork ke agen bernama yang disimpan.

3. Siapkan

Klik tombol di bawah untuk membuka codelab ini di Google Cloud Shell. Semua dependensi telah diinstal sebelumnya.

Buka di Cloud Shell

Opsi B: Penyiapan lokal

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

Instal uv jika diperlukan:

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

Konfigurasi kunci API Anda

cp .env.example .env
cloudshell edit .env

Tetapkan kunci Anda:

GEMINI_API_KEY=your-key-here

Menginstal dependensi

uv sync

4. Melakukan Panggilan Agen Pertama Anda

Membuka file awal

cloudshell edit run_digest.py

run_digest() memiliki satu TODO yang harus diisi sekarang dan tiga lagi untuk langkah berikutnya. Dua pembantu sudah diisi otomatis di atasnya:

  • load_source(path): membaca file dari .agents/ relatif terhadap skrip. Anda akan menggunakannya dalam latihan berikutnya untuk memasang gaya editorial, buku pedoman PDF, dan perender ke dalam sandbox.
  • run_stream(stream): memproses aliran peristiwa dan menampilkan (environment_id, interaction_id). Anda tidak perlu menulis sendiri loop peristiwa.

Yang harus ditambahkan

TODO 1: ganti pass dengan (abaikan TODO 3, 4 untuk saat ini; TODO tersebut untuk langkah berikutnya):

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

Fungsi setiap bagian

genai.Client() membaca GEMINI_API_KEY dari lingkungan. Semua hal lainnya akan melalui klien ini.

interactions.create() adalah panggilan inti. Empat parameter yang membuatnya berfungsi:

  • agent=BASE_AGENT: memilih agen Antigravity (antigravity-preview-05-2026), agen terkelola serbaguna yang didukung oleh Gemini 3.7 Flash secara default. Anda dapat mengonfigurasi model pokok menggunakan agent_config (opsi: gemini-3.7-flash, gemini-3.6-flash, gemini-3.5-flash, gemini-3.5-flash-lite). Model ini dilengkapi dengan tiga alat bawaan yang diaktifkan secara default: code_execution (jalankan Bash, Python, Node.js), google_search, dan url_context (ambil dan baca halaman web). Alat sistem file (read_file, write_file, list_files) diaktifkan secara otomatis saat Anda meneruskan parameter environment. Satu panggilan menyediakan lingkungan Ubuntu yang terkelola sepenuhnya dengan Python 3.12, Node.js 22, git, pip, dan curl yang telah diinstal sebelumnya. Tidak ada container yang perlu dibangun, tidak ada deployment yang perlu dijalankan.
  • input: tugas untuk proses ini. Agen menjelajahi Hacker News dan menalar hasilnya.
  • environment="remote": menyediakan sandbox cloud baru untuk interaksi ini.
  • stream=True: menampilkan iterable peristiwa, bukan memblokir. Tanpa itu, panggilan akan menunggu 30-90 detik dan menampilkan semua output sekaligus sebagai interaction.output_text. Dengan streaming, Anda dapat melihat alasan dan tindakan agen secara real time. Streaming bukanlah fitur lanjutan di sini: ini adalah default yang tepat, karena kotak hitam 90 detik tidak memberi Anda sinyal tentang apakah agen berfungsi atau macet.

environment_id adalah tuas ke sandbox yang baru saja berjalan. Setelah interaction.completed, sandbox tidak akan ditutup: sandbox akan tetap aktif hingga 7 hari. environment_id adalah cara Anda kembali ke halaman tersebut. Teruskan ke panggilan interactions.create() kedua dan agen akan dilanjutkan di sistem file yang sama, dengan file dan paket yang diinstal yang sama, seolah-olah tidak pernah keluar. Langkah berikutnya menggunakannya untuk mendownload PDF tanpa menjalankan ulang agen, dan langkah setelahnya menggunakannya untuk melanjutkan percakapan.

interaction_id adalah handle untuk giliran percakapan yang baru saja selesai. Teruskan sebagai previous_interaction_id dalam panggilan berikutnya dan agen memiliki memori penuh tentang apa yang dikatakan dan dilakukannya pada giliran ini.

Verifikasi

uv run python run_digest.py

Anda akan melihat output langsung saat agen bekerja:

[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 menampilkan environment_id yang sebenarnya meskipun dengan environment="remote". Sandbox berjalan. Yang tidak ada adalah konfigurasi: tidak ada suara, tidak ada skill, tidak ada generator PDF. Agen baru saja mencetak cerita sebagai teks dan berhenti. Langkah berikutnya akan menambahkan batasan tersebut.

Setiap baris output memetakan ke peristiwa dari run_stream():

step.type

Penjelasan

Yang dicetak run_stream()

"url_context_call"

agen mengambil URL

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

"code_execution_call"

agen yang menjalankan kode di sandbox

[tool] run_code

"google_search_call"

agen menelusuri web

[tool] google_search

"function_call"

alat file dan lainnya

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

step.delta yang delta.type == "text"

agen sedang menulis teks

di-streaming langsung ke stdout

5. Menyesuaikan Agen

Agen tidak memiliki petunjuk: tidak ada suara, tidak ada keterampilan, tidak ada generator PDF. Pada langkah ini, Anda akan memuat file konfigurasi dari .agents/ dan memasangnya ke sandbox.

Yang akan diubah

Buat empat perubahan pada run_digest.py:

TODO 2: Di bawah load_source(), tambahkan tiga konstanta tingkat modul (ini berada di luar run_digest(), di bagian atas file):

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

Buka setiap file untuk melihat apa yang Anda muat: AGENTS.md menetapkan aturan alur kerja dan gaya bahasa editorial; SKILL.md adalah panduan PDF langkah demi langkah; generate_pdf.py adalah perender bawaan yang akan dijalankan agen.

Sekarang, buat dua perubahan lagi di dalam run_digest():

TODO 3: ubah environment dari "remote" ke dict sumber, dan tetapkan input ke "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: tambahkan baris ini tepat setelah print(f"\nDone. environment_id={environment_id}"):

    save_env(ENVIRONMENT_ID=environment_id, INTERACTION_ID=interaction_id)

save_env sudah ditentukan dalam run_digest.py. Langkah ini menulis kedua ID ke .env sehingga langkah berikutnya dapat mendownload PDF tanpa menjalankan ulang agen.

Fungsi setiap sumber

Setiap sumber adalah file yang dipasang ke sistem file sandbox saat startup sebelum agen berjalan. Jalur target cocok dengan tempat harness Antigravity diharapkan menemukannya:

.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

Jalur target

Variabel

Fungsi tali pengikat

.agents/AGENTS.md

AGENTS_MD

Dimuat otomatis sebagai petunjuk persisten: gaya editorial, alur kerja, aturan eksekusi

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

SKILL_MD

Ditemukan dan didaftarkan secara otomatis sebagai skill bernama; agen memanggilnya berdasarkan nama

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

GENERATE_PDF_PY

Perender PDF bawaan; agen menulis summaries.json lalu menjalankan skrip ini

Verifikasi

uv run python run_digest.py

Sekarang, prosesnya memerlukan waktu 1-3 menit. Anda akan melihat agen membaca file konfigurasi, menulis ringkasan, dan menyimpan 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 kini menjadi nilai sebenarnya: sandbox berjalan dengan file konfigurasi Anda dan agen membuat digest.pdf. Langkah berikutnya menambahkan hook keamanan sebelum mendownload.

6. Menambahkan Hook Keamanan

Hook memungkinkan Anda menjalankan skrip di dalam sandbox sebelum atau setelah setiap panggilan alat. Agen ringkasan menggunakan code_execution untuk menjalankan skrip Python, sehingga hook pre_tool_execution dapat mencegat panggilan tersebut dan memblokir perintah shell yang merusak sebelum dijalankan.

Runtime membaca .agents/hooks.json dari sandbox. Sebelum setiap panggilan alat pencocokan, alat ini akan menyalurkan detail panggilan ke skrip gerbang Anda di stdin. Skrip mencetak {"decision": "allow"} atau {"decision": "deny", "reason": "..."} ke stdout. Penolakan akan membatalkan panggilan alat dan agen akan melihat alasan Anda serta mengoreksi diri sendiri.

Yang harus ditambahkan

TODO 5: di run_digest.py, tambahkan dua konstanta ini di dekat bagian atas, setelah panggilan load_source yang ada:

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: tambahkan dua entri lagi ke daftar sources di dalam interactions.create():

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

Cara hook dipicu dalam proses ringkasan

Setiap kali agen memanggil code_execution untuk menjalankan skrip Python atau perintah shell, runtime akan menyalurkan detail panggilan ke gate.py terlebih dahulu. Jika perintah berisi rm -rf, hook akan menampilkan deny dan agen akan menerima alasan penolakan serta mencoba lagi dengan alternatif yang aman. Semua panggilan eksekusi kode lainnya diteruskan tanpa perubahan.

Verifikasi

uv run python run_digest.py

Outputnya sama seperti sebelumnya: gerbang keamanan mengizinkan semua perintah pembuatan PDF normal. Untuk mengonfirmasi bahwa hook diaktifkan, ubah sementara input agen untuk memintanya menjalankan rm -rf /tmp/test — Anda akan melihat agen melaporkan bahwa perintah diblokir dan memilih alternatif.

7. Unduh PDF

Agen menulis digest.pdf ke /workspace/digest.pdf di dalam sandbox. Snapshot lingkungan tersedia sebagai arsip tar melalui Gemini Files API.

Instal requests jika diperlukan:

uv pip install requests

Yang harus diisi

Buka download_pdf.py. Ada dua TODO.

TODO 1: isi panggilan requests.get():

    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 ini mengarah ke snapshot sandbox. params={"alt": "media"} menampilkan byte mentah, bukan metadata. GEMINI_API_KEY yang ada juga mengautentikasi Files API.

TODO 2: temukan dan ekstrak PDF dari arsip tar:

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

Awalan jalur tar bervariasi di seluruh proses, jadi telusuri berdasarkan akhiran, bukan dengan meng-hardcode jalur persisnya. filter="data" menekan peringatan penghentian penggunaan Python 3.13 tentang ekstraksi tar yang tidak aman.

Verifikasi

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

Buka digest.pdf di direktori yang sama. Objek ini berisi ringkasan berformat yang dihasilkan agen dari halaman web aktif.

8. Melanjutkan Percakapan

Anda sudah memiliki digest.pdf. Jika hanya menginginkan file, Anda sudah selesai. Langkah ini membahas hal yang berbeda: meminta agen untuk mengubah ringkasan tanpa mengambil ulang web.

Sandbox masih aktif. Agen masih memiliki /workspace/digest.pdf dan mengingat setiap cerita yang diringkasnya. Panggilan interactions.create() kedua mengirimkan pesan lanjutan ke sandbox yang sama. Di sini, Anda memintanya untuk menambahkan catatan "Mengapa ini penting" di bawah setiap cerita, dan PDF akan diperbarui di tempat, tanpa pengambilan ulang dan tanpa meringkas ulang.

Yang harus diisi

Buka refine_digest.py. Ada tiga TODO.

TODO 1 dan 2: isi dua parameter multi-turn di dalam interactions.create():

    environment=environment_id,
    previous_interaction_id=interaction_id,

environment=environment_id melanjutkan sandbox yang sama dengan file dan paketnya. previous_interaction_id=interaction_id memberikan histori percakapan agen. Tidak ada perubahan lain dari panggilan pertama.

TODO 3: mempertahankan interaction_id baru kembali ke .env setelah loop peristiwa:

save_env(INTERACTION_ID=interaction_id)

Setiap panggilan interactions.create() menghasilkan interaction_id baru. Menulisnya kembali berarti proses berikutnya akan meneruskan penyempurnaan ini sebagai previous_interaction_id, sehingga rantai belokan dengan benar. ID sandbox tidak pernah berubah sehingga ENVIRONMENT_ID tidak perlu diperbarui.

Dua parameter yang membuat percakapan multi-giliran berfungsi

ID

Yang dipertahankan

Analogi

environment=environment_id

File, paket yang diinstal, status sistem: semuanya ada di sistem file Linux

Tetap menggunakan meja kantor yang sama di antara rapat

previous_interaction_id=interaction_id

Histori percakapan: apa yang dikatakan dan dilakukan agen pada giliran sebelumnya

Mengingat apa yang dibahas dalam rapat terakhir

Anda dapat meneruskan salah satu ID secara terpisah:

  • environment_id saja: menggunakan kembali file dan paket, tetapi memulai percakapan baru. Berguna untuk tugas baru di ruang kerja yang sama.
  • previous_interaction_id saja: melanjutkan konteks percakapan, tetapi di sandbox baru (file hilang).
  • Keduanya: kontinuitas penuh, yang digunakan oleh langkah ini.

Tanpa environment_id: sandbox kosong, tidak ada PDF. Tanpa previous_interaction_id: tidak ada konteks, agen tidak dapat menyempurnakan bagian tertentu.

Verifikasi

uv run python refine_digest.py

Streaming harus cepat; agen tidak mengambil ulang apa pun. Setelah selesai:

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

Buka digest_v2.pdf dan bandingkan dengan digest.pdf. Setiap cerita kini harus memiliki baris "Mengapa ini penting".

9. Mempertahankan Konfigurasi Agen Terkelola

Setiap panggilan sejauh ini telah meneruskan AGENTS.md, SKILL.md, dan generate_pdf.py secara inline. Cara tersebut berhasil, tetapi kode panggilan Anda membawa seluruh konten file pada setiap eksekusi. agents.create() membuat konfigurasi ke dalam agen bernama yang disimpan di sisi Google. Pemanggilan berikutnya hanya meneruskan ID agen:

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

Yang harus diisi

Buka save_agent.py. Ada satu TODO (TODO 1).

Perhatikan bahwa konstanta diimpor langsung dari run_digest.py (tidak ada duplikasi):

from run_digest import BASE_AGENT, AGENTS_MD, SKILL_MD, GENERATE_PDF_PY

TODO 1: isi panggilan agents.create():

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 menetapkan model pokok. gemini-3.7-flash adalah pilihan default dan terbaik untuk alur kerja ini; gemini-3.6-flash, gemini-3.5-flash, dan gemini-3.5-flash-lite tersedia jika Anda menginginkan proses yang lebih ringan atau berbiaya lebih rendah.

base_environment (bukan environment) adalah perbedaan utama dari panggilan inline pada langkah sebelumnya: sumber disimpan di sisi Google dan dipasang secara otomatis pada setiap pemanggilan berikutnya. Eksekusi sekali, bukan pada setiap proses ringkasan.

Verifikasi: simpan agen

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

Memanggil agen yang disimpan

Buka invoke_agent.py. Agen yang disimpan dipanggil berdasarkan ID tanpa sumber:

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

Bandingkan dengan panggilan inline: agent=BASE_AGENT digantikan oleh "my-digest", dan blok environment lengkap dengan tiga sumber inline digantikan oleh environment="remote". Konfigurasi sudah diterapkan di sisi Google.

Verifikasi: panggil agen yang disimpan

uv run python invoke_agent.py

Anda akan melihat live stream yang sama seperti saat dijalankan inline, tetapi panggilan tidak membawa file sumber. Setelah dijalankan, ENVIRONMENT_ID dan INTERACTION_ID di .env akan diperbarui sehingga Anda dapat melanjutkan refine_digest.py seperti sebelumnya.

[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. Mengirim melalui Gmail

Agen telah membuat ringkasan dan menyimpannya ke /workspace/digest.pdf. Sejauh ini Anda telah mendownloadnya secara lokal. Langkah ini mengirimkannya langsung ke kotak masuk Anda dengan membuat agen memanggil Gmail REST API dari dalam sandbox.

Pendekatan: Anda mendapatkan token akses OAuth 2.0 secara lokal dan meneruskannya ke agen di perintah input. Agen menggunakan code_execution untuk membuat email MIME dengan PDF terlampir dan mempostingnya ke Gmail API. Tidak ada alat kustom, tidak ada pendaftaran server MCP.

Prasyarat

Aktifkan Gmail API di project GCP Anda dan buat client ID OAuth 2.0:

  1. Buka console.cloud.google.com/apis/library/gmail.googleapis.com dan aktifkan Gmail API.
  2. Buka API & Layanan > Kredensial > Buat Kredensial > Client ID OAuth 2.0.
  3. Jenis aplikasi: Aplikasi desktop. Download JSON dan simpan sebagai credentials.json di root project.

Tambahkan email penerima Anda ke .env:

RECIPIENT_EMAIL=you@gmail.com

Instal library auth jika diperlukan:

uv sync

Yang harus diisi

Buka send_digest.py. Ada dua TODO.

TODO 1: memuat atau memperbarui token akses 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())

Hapus baris raise NotImplementedError setelah menambahkannya. Saat dijalankan pertama kali, browser akan terbuka untuk menampilkan layar izin OAuth. Token di-cache di .gmail_token.json untuk proses mendatang.

TODO 2: ganti input="" dengan petunjuk email. Token sudah dalam cakupan sebagai 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}"
    ),

Fungsi setiap bagian

Interaksi dilanjutkan di sandbox yang sama tempat agen telah membuat digest.pdf dan summaries.json. previous_interaction_id memberikan histori percakapan agen.

Token akses diteruskan dalam string input. Agen membacanya dari perintah dan menggunakannya di header Authorization: Bearer saat memanggil Gmail API. File tersebut tidak pernah menyentuh komputer atau sistem file lokal Anda.

Agen menggunakan code_execution untuk menulis dan menjalankan skrip Python di dalam sandbox: agen membaca summaries.json, memformatnya sebagai newsletter, membaca digest.pdf, membuat pesan multipart MIME, mengenkodenya dengan base64url, dan melakukan POST ke https://gmail.googleapis.com/gmail/v1/users/me/messages/send.

Verifikasi

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.

Periksa kotak masuk Anda. Email akan masuk dengan isi berformat newsletter dan digest.pdf terlampir.

11. Menjadwalkan Operasi Harian

Setiap langkah sejauh ini telah dipicu secara manual. Pemicu memungkinkan Anda menjadwalkan agen bernama untuk berjalan secara otomatis pada ekspresi cron. Agen akan berjalan pada waktu yang dijadwalkan, menjalankan alur kerja ringkasan lengkap, dan lingkungan akan tetap ada di antara eksekusi sehingga paket yang diinstal pada proses pertama akan tersedia pada setiap proses berikutnya.

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

Yang harus diisi

Buka create_trigger.py. Ada satu TODO.

TODO 1: isi panggilan triggers.create(). Pemicu menjalankan alur kerja lengkap setiap hari: membuat ringkasan DAN mengirimkannya ke kotak masuk Anda. Karena token akses akan berakhir dalam satu jam, token akses akan menyisipkan token refresh dari .gmail_token.json sebagai sumber inline sehingga agen dapat menukarkannya dengan token baru pada setiap proses.

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 adalah waktu tunggu default. max_consecutive_failures=3 akan otomatis menjeda pemicu setelah 3 kali gagal berturut-turut (default API adalah 5; 3 lebih konservatif untuk workshop).

Daftar sources menyuntikkan .gmail_creds.json ke sandbox di /workspace/.gmail_creds.json. Agen membacanya, menukar token refresh dengan token akses baru, dan memanggil Gmail API. Token refresh tidak akan habis masa berlakunya, sehingga berfungsi pada setiap jadwal yang dijalankan tanpa perlu refresh token secara manual.

Hapus baris raise NotImplementedError setelah menambahkan panggilan.

Verifikasi

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

create_trigger.py menyimpan ID pemicu ke .env secara otomatis.

Untuk memeriksa histori eksekusi setelah menjalankan:

uv run python check_trigger.py

Untuk mengaktifkan pemicu segera tanpa menunggu waktu terjadwal berikutnya:

uv run python fire_trigger.py

Untuk menjeda atau menghapus pemicu:

uv run python pause_trigger.py

12. Pembersihan

Sandbox akan otomatis berakhir masa berlakunya setelah 7 hari tidak aktif. Tidak ada server yang perlu dihentikan. Tidak ada penampung untuk dihapus.

Jika Anda menyimpan konfigurasi agen, hapus konfigurasi tersebut:

uv run python delete_agent.py

13. Ringkasan

Anda membuat agen terkelola dari awal, satu konsep dalam satu waktu. Berikut adalah materi yang diajarkan dalam setiap latihan:

Latihan

Konsep

Key API

Melakukan panggilan pertama Anda

Menyediakan sandbox Linux asli dan melakukan streaming langsung peristiwanya

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

Menyesuaikan agen

Memasang file konfigurasi; mempertahankan ID ke .env dalam proses yang sama

environment.sources, save_env

Tambahkan kait pengaman

Menyadap panggilan alat sebelum dieksekusi; menolak perintah yang merusak

hooks.json, pre_tool_execution, gate.py

Unduh PDF

Mendownload PDF tanpa menjalankan ulang agen

Gemini Files API :download di download_pdf.py

Lanjutkan percakapan

Melanjutkan percakapan tanpa mengambil ulang web

environment=environment_id, previous_interaction_id=interaction_id

Mempertahankan konfigurasi agen

Mempertahankan konfigurasi agen; memanggil berdasarkan ID, tidak memerlukan sumber

agents.create(), agents.list()

Mengirim melalui Gmail

Mendapatkan token OAuth secara lokal; meneruskannya ke agen, yang memanggil Gmail REST API melalui code_execution

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

Menjadwalkan lari harian

Menjalankan agen secara otomatis sesuai jadwal cron

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

Pola utama

  1. Satu panggilan, satu sandbox: interactions.create() menangani semua infrastruktur (tidak ada container untuk di-deploy, tidak ada paket untuk diinstal secara lokal)
  2. Streaming progresif: stream=True mengubah kotak hitam berdurasi 90 detik menjadi feed live dari panggilan alat dan potongan teks
  3. Sumber inline: memasang skrip AGENTS.md, SKILL.md, dan bawaan ke sandbox tanpa langkah upload atau deployment
  4. Penemuan otomatis harness: file yang ditempatkan di .agents/ akan diambil secara otomatis (tidak memerlukan konfigurasi SDK)
  5. Status dua dimensi: environment_id melacak file dan paket; previous_interaction_id melacak konteks percakapan; keduanya dapat diteruskan secara terpisah
  6. Download snapshot: lingkungan adalah tar sistem file lengkap, yang dapat diakses melalui Gemini Files API
  7. Agen bernama: agents.create() memanggang konfigurasi secara permanen; panggilan mendatang hanya meneruskan ID agen dan environment="remote", tanpa sumber
  8. Hook: hooks.json + skrip gerbang mencegat panggilan alat sebelum dieksekusi; respons deny membatalkan panggilan dan agen mengoreksi diri sendiri
  9. Panggilan API eksternal: teruskan kredensial dalam perintah input; agen menulis dan menjalankan kode integrasi di dalam sandbox melalui code_execution
  10. Pemicu: menjadwalkan agen pada ekspresi cron dengan client.triggers.create(); lingkungan tetap ada di seluruh eksekusi

ADK + Cloud Run vs. Agen Terkelola: sekilas tentang perbedaannya

Kemampuan

ADK + Cloud Run

Agen terkelola di Gemini API

Menyediakan sandbox

docker build + gcloud run deploy

interactions.create()

Menentukan alat

Fungsi Python yang terdaftar di agen

Bawaan: penjelajahan web, eksekusi kode, sistem file

Menginstal paket

pip install di Dockerfile

Agen berjalan pip install di dalam sandbox

Streaming peristiwa

Infrastruktur SSE kustom

stream=True

Melanjutkan sesi

Database sesi + injeksi konteks

environment_id + previous_interaction_id

File konfigurasi

Dikodekan secara permanen di agen atau disuntikkan saat startup

Dipasang melalui environment.sources

Infrastruktur yang akan dikelola

Container, Cloud Run, IAM, secret

Tidak ada

Langkah berikutnya