Gemini API の Managed Agents を使用して Daily Tech Digest エージェントを構築する

1. 概要

AI とテクノロジーの状況は、誰もが追跡できるよりも速く変化しています。新しいモデル、論文、プロダクトが毎日リリースされます。今日の見出しを取得し、簡潔な要約を作成して、毎朝 PDF を生成するダイジェスト エージェントがあれば、この問題を解決できます。しかし、以前は、フレームワークの選択、Python でのツールの定義、オーケストレーション ループの作成、コンテナのパッケージ化、Cloud Run へのデプロイが必要でした。エージェントがウェブ リクエストを 1 つも行っていない状態です。

Gemini API のマネージド エージェントは、この状況を一変させます。2 つのマークダウン構成ファイルと事前構築済みのレンダラ スクリプトを作成し、1 つの API 呼び出しを行うと、実際の Ubuntu サンドボックスが起動し、ウェブを閲覧して、要約を書き込み、PDF を生成します。コンテナはありません。デプロイなし。オーケストレーション コードはありません。

この Codelab では、まさにそのようなエージェントを構築します。空の関数から、1 つのコンセプトを 1 つずつ実装して、動作するデイリー ダイジェストを作成します。

作業内容

  • 実際の Linux サンドボックスで初めてのマネージド エージェントを作成して実行する
  • エディター ボイス、ウェブソース、PDF スキルを使用してエージェントをカスタマイズする
  • 破壊的なコマンドが実行される前にブロックする安全フックを追加
  • エージェントが生成した PDF をダウンロードする
  • ウェブを再取得せずにマルチターンの会話で要約を絞り込む
  • エージェント構成を保存し、今後の実行で ID で呼び出す
  • Gmail API を介してダイジェストを受信トレイに送信する
  • 毎日自動的に実行して送信するようにエージェントをスケジュールする

必要なもの

  • Python 3.10 以上
  • Gemini API キー: aistudio.google.com/api-keys(無料枠が含まれます。中断のない実行には課金をおすすめします)

2. Gemini API のマネージド エージェントとは

AI システムの 3 つのレベル

コードに入る前に、2 つの代替案に対する Managed Agents の位置付けを説明します。

レベル

内容

インフラストラクチャの管理者は誰ですか?

標準 LLM

プロンプトを入力すると、テキストで返信します。手も記憶もインターネットも必要ありません。

N/A: 単独では何もできない

セルフホスト型エージェント

ADK/LangChain/AutoGen + Docker + ツール + メモリを接続します。

すべて(または Agent Engine などのマネージド プラットフォーム)

Managed Agent

目標を設定します。Google は安全なサンドボックスをプロビジョニングします。エージェントは、コードの記述、実行、エラーの読み取り、ウェブ検索、バグの修正を自律的に行います。

Google: すべて

この Codelab では、3 行目について説明します。タスクと構成ファイルを指定します。その他の処理はすべて 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

これは、エージェントが 1 回実行される前です。サンドボックスの分離(エージェントがサーバーを破損できないようにする)、パッケージのインストール、ツール呼び出し間の状態管理、イベントをクライアントに取得するためのストリーミング インフラストラクチャは、引き続きユーザーが所有します。

Managed Agents による置き換え後の値

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 の要件

Managed Agents が処理する内容

コンテナ イメージ + Dockerfile + CI/CD

フルマネージド Ubuntu サンドボックス(Python 3.12、Node 22、4 CPU / 16 GB RAM)

Cloud Run のデプロイとスケーリング

インタラクションごとにプロビジョニングされ、7 日間操作がないと自動的に期限切れになります

サンドボックスの分離

インタラクションごとに分離

カスタム PDF ツール + pip install

エージェントがサンドボックス内にパッケージをインストールする

SSE ストリーミング インフラストラクチャ

stream=True はイベントのイテラブルを返します

Python でのツール定義

組み込みツール: ウェブ閲覧、コード実行、ファイル システム

ツール呼び出し間の状態管理

エージェントの推論ループに組み込まれている

構成ファイル(AGENTS.md、SKILL.md、事前構築済みスクリプト)を作成し、1 回の 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 で開く

オプション B: ローカル設定

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() には、今すぐ入力する TODO が 1 つと、次のステップで入力する TODO が 3 つあります。上記には、2 つのヘルパーがすでに事前入力されています。

  • load_source(path): スクリプトを基準として .agents/ からファイルを読み取ります。次の演習では、このツールを使用して、エディター ボイス、PDF プレイブック、レンダラをサンドボックスにマウントします。
  • run_stream(stream): イベント ストリームを処理し、(environment_id, interaction_id) を返します。イベントループを自分で記述する必要はありません。

追加するデータ

TODO 1: pass を次のコードに置き換えます(TODO 3、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() は環境から GEMINI_API_KEY を読み取ります。他のすべての処理はこのクライアントを介して行われます。

interactions.create() はコア呼び出しです。次の 4 つのパラメータで動作します。

  • agent=BASE_AGENT: デフォルトで Gemini 3.7 Flash を搭載した汎用マネージド エージェントである Antigravity エージェント(antigravity-preview-05-2026)を選択します。基盤となるモデルは agent_config(オプション: gemini-3.7-flash、gemini-3.6-flash、gemini-3.5-flash、gemini-3.5-flash-lite)を使用して構成できます。デフォルトで有効になっている 3 つの組み込みツール(code_execution(Bash、Python、Node.js を実行)、google_search、url_context(ウェブページを取得して読み取る))が付属しています。environment パラメータを渡すと、ファイルシステム ツール(read_file、write_file、list_files)が自動的に有効になります。1 回の呼び出しで、Python 3.12、Node.js 22、git、pip、curl がプリインストールされたフルマネージド Ubuntu 環境がプロビジョニングされます。ビルドするコンテナがなく、実行するデプロイもありません。
  • input: この実行のタスク。エージェントが Hacker News を閲覧し、結果について推論します。
  • environment="remote": このインタラクション用に新しいクラウド サンドボックスをプロビジョニングします。
  • stream=True: ブロックする代わりに、イベントのイテラブルを返します。このフラグがない場合、呼び出しは 30 ~ 90 秒待機し、すべての出力を interaction.output_text として一度に返します。ストリーミングでは、エージェントの理由と行動をリアルタイムで確認できます。ストリーミングは高度な機能ではありません。90 秒のブラック ボックスでは、エージェントが動作しているか、停止しているかについてのシグナルが得られないため、ストリーミングが適切なデフォルトです。

environment_id は、実行されたサンドボックスのハンドルです。interaction.completed 後、サンドボックスはシャットダウンされず、最長 7 日間存続します。environment_id は、その状態に戻る方法です。2 回目の 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="remote" が指定されていても実際の environment_id を返します。サンドボックスが実行されました。設定が不足しています。音声、スキル、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/...)

delta.type == "text"の場合: step.delta

エージェントがテキストを入力している

stdout に直接ストリーミングされる

5. エージェントをカスタマイズする

エージェントには指示がありませんでした。音声、スキル、PDF ジェネレータはありません。このステップでは、.agents/ から構成ファイルを読み込み、サンドボックスにマウントします。

変更内容

run_digest.py に次の 4 つの変更を加えます。

TODO 2: load_source() の下に、3 つのモジュール レベルの定数を追加します(これらは 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 はエージェントが実行する事前構築済みのレンダラです。

次に、run_digest() 内でさらに 2 つの変更を行います。

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 で定義済みです。両方の ID を .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 パス

変数

ハーネスがどのように処理するか

.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

実行時間は 1 ~ 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. 安全フックを追加する

フックを使用すると、各ツール呼び出しの前または後にサンドボックス内でスクリプトを実行できます。ダイジェスト エージェントは code_execution を使用して Python スクリプトを実行するため、pre_tool_execution フックはこれらの呼び出しをインターセプトし、破壊的なシェル コマンドが実行される前にブロックできます。

ランタイムはサンドボックスから .agents/hooks.json を読み取ります。各照合ツールは、呼び出しの前に、呼び出しの詳細を stdin のゲート スクリプトにパイプします。スクリプトは {"decision": "allow"} または {"decision": "deny", "reason": "..."} を stdout に出力します。拒否すると、ツールの呼び出しがキャンセルされ、エージェントは理由を確認して自己修正します。

追加するデータ

TODO 5: run_digest.py で、既存の load_source 呼び出しの後に、次の 2 つの定数を先頭付近に追加します。

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: interactions.create() 内の sources リストに 2 つのエントリを追加します。

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

ダイジェスト実行でフックが発火する仕組み

エージェントが code_execution を呼び出して Python スクリプトまたはシェル コマンドを実行するたびに、ランタイムは最初に呼び出しの詳細を gate.py にパイプします。コマンドに rm -rf が含まれている場合、フックは deny を返し、エージェントは拒否理由を受け取って、安全な代替手段で再試行します。他のすべてのコード実行呼び出しは変更されずに渡されます。

確認

uv run python run_digest.py

出力は以前と同じです。安全ゲートは、すべての通常の PDF 生成コマンドを許可します。フックが起動することを確認するには、エージェントの入力を一時的に変更して rm -rf /tmp/test の実行を要求します。エージェントがコマンドがブロックされたことを報告し、代替手段を選択します。

7. PDF をダウンロードする

エージェントはサンドボックス内の /workspace/digest.pdf に digest.pdf を書き込みました。環境スナップショットは、Gemini Files API を介して tar アーカイブとして使用できます。

必要に応じて requests をインストールします。

uv pip install requests

記入する内容

download_pdf.py を開きます。TODO が 2 つあります。

TODO 1: 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 はサンドボックス スナップショットのアドレスです。params={"alt": "media"} は、メタデータではなく未加工のバイトを返します。既存の GEMINI_API_KEY は Files API も認証します。

TODO 2: tar アーカイブから PDF を見つけて抽出します。

            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" は、安全でない tar 抽出に関する Python 3.13 の非推奨警告を抑制します。

確認

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

同じディレクトリで digest.pdf を開きます。これには、エージェントがライブ ウェブページから生成したフォーマット済みのダイジェストが含まれます。

8. 会話を続ける

digest.pdf はすでに所有しています。ファイルだけが必要な場合は、これで完了です。このステップは、ウェブを再取得せずにダイジェストを変更するようエージェントに依頼するという、別の内容です。

サンドボックスはまだ有効です。エージェントは /workspace/digest.pdf を保持し、要約したすべてのストーリーを記憶しています。2 回目の interactions.create() 呼び出しでは、同じサンドボックスにフォローアップ メッセージが送信されます。ここでは、各記事の下に「重要性」のメモを追加するよう指示しています。PDF は再取得や再要約なしで、その場で更新されます。

記入する内容

refine_digest.py を開きます。TODO が 3 つあります。

TODO 1 と 2: interactions.create() 内の 2 つのマルチターン パラメータを入力します。

    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 として渡され、連鎖が正しく行われます。サンドボックス ID は変更されないため、ENVIRONMENT_ID を更新する必要はありません。

マルチターンを機能させる 2 つのパラメータ

ID

保持されるもの

説明のための例え

environment=environment_id

ファイル、インストールされたパッケージ、システム状態: Linux ファイル システム上のすべて

会議の合間に同じオフィスデスクを使用する

previous_interaction_id=interaction_id

会話履歴: 以前のターンでエージェントが言ったことと行ったこと

前回の会議で話し合った内容を思い出す

どちらの 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 側で保存された名前付きエージェントに構成を組み込みます。次の呼び出しでは、エージェント ID のみが渡されます。

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

記入する内容

save_agent.py を開きます。TODO が 1 つ(TODO 1)あります。

定数は run_digest.py から直接インポートされます(重複はありません)。

from run_digest import BASE_AGENT, AGENTS_MD, SKILL_MD, GENERATE_PDF_PY

TODO 1: 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 は、基盤となるモデルを設定します。gemini-3.7-flash はデフォルトであり、このワークフローに最適な選択肢です。より軽量で低コストの実行が必要な場合は、gemini-3.6-flash、gemini-3.5-flash、gemini-3.5-flash-lite を使用できます。

base_environment(environment ではない)は、前のステップのインライン呼び出しとの大きな違いです。ソースは Google 側に保存され、以降の呼び出しごとに自動的にマウントされます。ダイジェストの実行ごとに実行するのではなく、1 回だけ実行します。

確認: エージェントを保存する

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

保存したエージェントを呼び出す

invoke_agent.py を開きます。ソースなしで ID によって保存されたエージェントを呼び出します。

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

インライン呼び出しと比較すると、agent=BASE_AGENT は "my-digest" に置き換えられ、3 つのインライン ソースを含む environment ブロック全体が environment="remote" に置き換えられています。構成は Google 側ですでに組み込まれています。

確認: 保存したエージェントを呼び出す

uv run python invoke_agent.py

インライン実行と同じライブ ストリームが表示されますが、呼び出しにはソースファイルは含まれません。実行後、.env の ENVIRONMENT_ID と INTERACTION_ID が更新されるため、これまでどおり 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 REST API を呼び出すことで、メールが受信トレイに直接配信されます。

このアプローチでは、OAuth 2.0 アクセス トークンをローカルで取得し、input プロンプトでエージェントに渡します。エージェントは code_execution を使用して PDF を添付した MIME メールを作成し、Gmail API に POST します。カスタムツールなし、MCP サーバー登録なし。

前提条件

GCP プロジェクトで Gmail API を有効にし、OAuth 2.0 クライアント ID を作成します。

  1. console.cloud.google.com/apis/library/gmail.googleapis.com に移動し、Gmail API を有効にします。
  2. [API とサービス] > [認証情報] > [認証情報を作成] > [OAuth 2.0 クライアント ID] に移動します。
  3. アプリケーションの種類: [デスクトップ アプリ]。JSON をダウンロードして、プロジェクトのルートに credentials.json として保存します。

受信者のメールアドレスを .env に追加します。

RECIPIENT_EMAIL=you@gmail.com

必要に応じて認証ライブラリをインストールします。

uv sync

記入する内容

send_digest.py を開きます。TODO が 2 つあります。

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 文字列で渡されます。エージェントはプロンプトから読み取り、Gmail API を呼び出すときに Authorization: Bearer ヘッダーで使用します。ローカルマシンやファイル システムにアクセスすることはありません。

エージェントは code_execution を使用して、サンドボックス内で Python スクリプトを記述して実行します。summaries.json を読み取ってニュースレターとしてフォーマットし、digest.pdf を読み取って MIME マルチパート メッセージを構築し、base64url でエンコードして https://gmail.googleapis.com/gmail/v1/users/me/messages/send に POST します。

確認

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 つあります。

TODO 1: triggers.create() 呼び出しを記入します。トリガーは毎日ワークフロー全体を実行し、ダイジェストを生成して受信トレイに送信します。アクセス トークンの有効期限は 1 時間であるため、.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 リストは、/workspace/.gmail_creds.json で .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 は、トリガー ID を .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. まとめ

マネージド エージェントをゼロから、一度に 1 つのコンセプトで構築しました。各演習で学んだ内容は次のとおりです。

演習

コンセプト

Key API

最初の呼び出しを実行する

実際の Linux サンドボックスをプロビジョニングし、そのイベントをライブ ストリーミングする

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

エージェントをカスタマイズする

構成ファイルをマウントし、同じ実行で ID を .env に保持

environment.sources、save_env

安全フックを追加する

ツール呼び出しを実行前にインターセプトし、破壊的なコマンドを拒否する

hooks.json、pre_tool_execution、gate.py

PDF 資料をダウンロードする

エージェントを再実行せずに PDF をダウンロードする

download_pdf.py の Gemini Files API :download

会話を継続

ウェブを再取得せずに会話を続ける

environment=environment_id、previous_interaction_id=interaction_id

エージェント構成を永続化

エージェント構成を永続化する。ID で呼び出す。ソースは不要

agents.create()、agents.list()

Gmail で送信

OAuth トークンをローカルで取得し、エージェントに渡します。エージェントは code_execution 経由で Gmail REST API を呼び出します。

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

毎日の実行をスケジュールする

Cron スケジュールでエージェントを自動的に実行する

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

キーパターン

  1. 1 回の呼び出し、1 つのサンドボックス: interactions.create() がすべてのインフラストラクチャを処理します(デプロイするコンテナやローカルにインストールするパッケージはありません)。
  2. プログレッシブ ストリーミング: stream=True は、90 秒のブラック ボックスをツール呼び出しとテキスト チャンクのライブフィードに変換します。
  3. インライン ソース: アップロードやデプロイの手順なしで、AGENTS.md、SKILL.md、事前構築済みスクリプトをサンドボックスにマウントします。
  4. Harness の自動検出: .agents/ に配置されたファイルが自動的に取得されます(SDK 構成は不要)。
  5. 2 次元状態: environment_id はファイルとパッケージを追跡し、previous_interaction_id は会話コンテキストを追跡します。どちらも個別に渡すことができます。
  6. スナップショットのダウンロード: 環境は Gemini Files API を介してアクセス可能な完全なファイル システム tar です。
  7. 名前付きエージェント: agents.create() は構成を永続的にベイクします。以降の呼び出しでは、ソースなしでエージェント ID と environment="remote" のみが渡されます。
  8. フック: hooks.json + ゲート スクリプトがツール呼び出しを実行前にインターセプトします。deny レスポンスは呼び出しをキャンセルし、エージェントが自己修正します
  9. 外部 API 呼び出し: input プロンプトで認証情報を渡します。エージェントは code_execution を介してサンドボックス内で統合コードを記述して実行します。
  10. トリガー: client.triggers.create() を使用して cron 式でエージェントをスケジュールします。環境は実行間で保持されます。

ADK + Cloud Run とマネージド エージェント: 違いの概要

能力

ADK + Cloud Run

Gemini API のマネージド エージェント

サンドボックスをプロビジョニングする

docker build + gcloud run deploy

interactions.create()

ツールを定義する

エージェントに登録された Python 関数

組み込み: ウェブ閲覧、コード実行、ファイル システム

パッケージをインストールする

Dockerfile の pip install

エージェントがサンドボックス内で pip install を実行する

イベントをストリーミングする

カスタム SSE インフラストラクチャ

stream=True

セッションを続行する

セッション データベース + コンテキストの挿入

environment_id + previous_interaction_id

構成ファイル

エージェントにハードコードされているか、起動時に挿入される

environment.sources 経由でマウント

管理するインフラストラクチャ

コンテナ、Cloud Run、IAM、シークレット

なし

次のステップ