Creare un agente di riepilogo giornaliero di notizie tecnologiche con Managed Agents nell'API Gemini

1. Panoramica

Il panorama dell'AI e della tecnologia si muove più velocemente di quanto chiunque possa tenere traccia. Ogni giorno vengono rilasciati nuovi modelli, articoli e prodotti. Un agente di riepilogo che recupera i titoli di oggi, scrive riepiloghi concisi e genera un PDF ogni mattina risolverebbe il problema, ma in passato la creazione di un agente di questo tipo richiedeva la scelta di un framework, la definizione di strumenti in Python, la scrittura di un ciclo di orchestrazione, il packaging di un container e il deployment su Cloud Run. Tutto questo prima che l'agente avesse effettuato una sola richiesta web.

Managed Agents nell'API Gemini cambia l'equazione. Scrivi due file di configurazione Markdown e uno script di rendering predefinito, effettua una chiamata API e viene avviata una sandbox Ubuntu reale, che naviga sul web, scrive i riepiloghi e genera un PDF. Nessun contenitore. Nessun deployment. Nessun codice di orchestrazione.

In questo codelab creerai esattamente questo agente: da una funzione vuota a un riepilogo giornaliero funzionante, un concetto alla volta.

Cosa creerai

  • Crea ed esegui il tuo primo agente gestito in un sandbox Linux reale
  • Personalizzare l'agente con la linea editoriale, le fonti web e una skill PDF
  • Aggiungi un meccanismo di sicurezza per bloccare i comandi distruttivi prima che vengano eseguiti
  • Scaricare il PDF generato dall'agente
  • Perfezionare il riepilogo in una conversazione multi-turno senza recuperare nuovamente il web
  • Salva la configurazione dell'agente e richiamala per ID nelle esecuzioni future
  • Inviare il riepilogo alla tua casella di posta tramite l'API Gmail
  • Pianifica l'esecuzione e l'invio automatico dell'agente ogni giorno

Che cosa ti serve

  • Python 3.10+
  • Una chiave API Gemini: aistudio.google.com/api-keys (livello senza costi incluso; fatturazione consigliata per esecuzioni senza interruzioni)

2. Che cosa sono gli agenti gestiti nell'API Gemini?

Tre livelli di sistemi AI

Prima di passare al codice, ecco dove si inseriscono gli agenti gestiti rispetto alle due alternative:

Livello

Descrizione

Chi gestisce l'infrastruttura?

LLM standard

Tu inserisci un prompt e lui risponde con un testo. Nessuna mano, nessuna memoria, nessun internet.

N/A: non può fare nulla da solo

Agente self-hosted

Colleghi ADK/LangChain/AutoGen + Docker + strumenti + memoria.

Tu: tutto (o una piattaforma gestita come Agent Engine)

Agente gestito

Assegni un obiettivo. Google esegue il provisioning di una sandbox sicura. L'agente scrive il codice, lo esegue, legge gli errori, esegue ricerche sul web e corregge i bug in modo autonomo.

Google: tutto

Questo codelab riguarda la terza riga. Fornisci un file di attività e di configurazione. Google si occupa di tutto il resto.

Cosa creeresti con ADK + Cloud Run

Per creare un agente di riepilogo delle notizie che naviga sul web, esegue Python e genera un PDF, avresti bisogno di tutto questo con 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

ovvero prima che l'agente venga eseguito per la prima volta. Mantieni l'isolamento della sandbox (in modo che l'agente non possa danneggiare il server), l'installazione dei pacchetti, la gestione dello stato tra le chiamate agli strumenti e l'infrastruttura di streaming per inviare eventi a un client.

Con cosa lo sostituisce 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,
            },
        ],
    },
)

Requisiti di ADK + Cloud Run

Cosa gestisce Managed Agents per te

Immagine container + Dockerfile + CI/CD

Sandbox Ubuntu completamente gestita (Python 3.12, Node 22, 4 CPU / 16 GB di RAM)

Deployment e scalabilità di Cloud Run

Fornito per interazione, scade automaticamente dopo 7 giorni di inattività

Isolamento della sandbox

Isolamento per interazione

Strumento PDF personalizzato + pip install

L'agente installa i pacchetti all'interno della sandbox

Infrastruttura di streaming SSE

stream=True restituisce un iterabile di eventi

Definizioni degli strumenti in Python

Strumenti integrati: navigazione web, esecuzione del codice, file system

Gestione dello stato tra le chiamate di strumenti

Integrato nel ciclo di ragionamento dell'agente

Scrivi i file di configurazione (AGENTS.md, SKILL.md, uno script predefinito) ed esegui una chiamata API. Google si occupa di tutto il resto.

Come funziona la 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

La sandbox viene mantenuta per 7 giorni di inattività. Puoi riprenderla con environment_id per perfezionare l'output, eseguire attività di follow-up o creare una copia in un agente denominato salvato.

3. Configura

Fai clic sul pulsante di seguito per aprire questo codelab in Google Cloud Shell. Tutte le dipendenze sono preinstallate.

Apri in Cloud Shell

Opzione B: configurazione locale

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

Se necessario, installa uv:

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

Configurare la chiave API

cp .env.example .env
cloudshell edit .env

Imposta la chiave:

GEMINI_API_KEY=your-key-here

Installa le dipendenze

uv sync

4. Effettuare la prima chiamata dell'agente

Aprire il file iniziale

cloudshell edit run_digest.py

run_digest() ha un TODO da compilare ora e altri tre per il passaggio successivo. Sopra sono già precompilati due helper:

  • load_source(path): legge un file da .agents/ rispetto allo script. Lo utilizzerai nel prossimo esercizio per montare la linea editoriale, la guida pratica PDF e il renderer nella sandbox.
  • run_stream(stream): elabora il flusso di eventi e restituisce (environment_id, interaction_id). Non devi scrivere personalmente il ciclo degli eventi.

Cosa aggiungere

TODO 1: sostituisci pass con (ignora per ora i TODO 3 e 4, che verranno utilizzati nel passaggio successivo):

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

A cosa serve ogni parte

genai.Client() legge GEMINI_API_KEY dall'ambiente. Tutto il resto passa attraverso questo client.

interactions.create() è la chiamata principale. Perché funzioni, sono necessari quattro parametri:

  • agent=BASE_AGENT: seleziona l'agente Antigravity (antigravity-preview-05-2026), un agente gestito per uso generico basato su Gemini 3.7 Flash per impostazione predefinita. Puoi configurare il modello sottostante utilizzando agent_config (opzioni: gemini-3.7-flash, gemini-3.6-flash, gemini-3.5-flash, gemini-3.5-flash-lite). Sono disponibili tre strumenti integrati attivati per impostazione predefinita: code_execution (esegui Bash, Python, Node.js), google_search e url_context (recupera e leggi pagine web). Gli strumenti del file system (read_file, write_file, list_files) vengono attivati automaticamente quando passi il parametro environment. Una chiamata esegue il provisioning di un ambiente Ubuntu completamente gestito con Python 3.12, Node.js 22, git, pip e curl preinstallati. Nessun container da creare, nessun deployment da eseguire.
  • input: l'attività per questa esecuzione. L'agente sfoglia Hacker News e analizza i risultati.
  • environment="remote": esegue il provisioning di una nuova sandbox cloud per questa interazione.
  • stream=True: restituisce un iterabile di eventi anziché bloccare. Senza, la chiamata attende 30-90 secondi e restituisce tutto l'output contemporaneamente come interaction.output_text. Con lo streaming, vedi il motivo dell'agente e agisci in tempo reale. Lo streaming non è una funzionalità avanzata: è l'impostazione predefinita corretta, perché una scatola nera di 90 secondi non fornisce alcun segnale sul funzionamento o sul blocco dell'agente.

environment_id è un handle della sandbox appena eseguita. Dopo interaction.completed, la sandbox non viene chiusa, ma rimane attiva per un massimo di 7 giorni. Il environment_id ti consente di tornare alla pagina. Trasferiscilo a una seconda interactions.create() chiamata e l'agente riprende lo stesso file system, con gli stessi file e pacchetti installati, come se non fosse mai uscito. Il passaggio successivo lo utilizza per scaricare il PDF senza eseguire nuovamente l'agente, mentre il passaggio successivo lo utilizza per continuare la conversazione.

interaction_id è un handle del turno di conversazione appena completato. Passalo come previous_interaction_id nella chiamata successiva e l'agente avrà piena memoria di ciò che ha detto e fatto in questo turno.

Verifica

uv run python run_digest.py

Dovresti vedere l'output in tempo reale mentre l'agente lavora:

[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

L'API restituisce un environment_id reale anche con environment="remote". La sandbox è stata eseguita. Ciò che manca è la configurazione: nessuna voce, nessuna skill, nessun generatore di PDF. L'agente ha stampato le storie come testo e si è fermato. Il passaggio successivo aggiunge questi dati.

Ogni riga di output corrisponde a un evento di run_stream():

step.type

Descrizione

Cosa stampa run_stream()

"url_context_call"

agente che recupera un URL

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

"code_execution_call"

agente che esegue il codice nella sandbox

[tool] run_code

"google_search_call"

agente che cerca sul web

[tool] google_search

"function_call"

strumenti per i file e altri

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

step.delta dove delta.type == "text"

L'agente sta scrivendo un testo

trasmesso direttamente a stdout

5. Personalizzare l'agente

L'agente non aveva istruzioni: nessuna voce, nessuna skill, nessun generatore di PDF. In questo passaggio carichi i file di configurazione da .agents/ e li monti nella sandbox.

Cosa modificare

Apporta quattro modifiche a run_digest.py:

TODO 2: sotto load_source(), aggiungi le tre costanti a livello di modulo (si trovano al di fuori di run_digest(), nella parte superiore del 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")

Apri ogni file per vedere cosa stai caricando: AGENTS.md imposta le regole del workflow e la linea editoriale; SKILL.md è la guida pratica PDF passo passo; generate_pdf.py è il renderer predefinito che verrà eseguito dall'agente.

Ora apporta altre due modifiche all'interno di run_digest():

TODO 3: modifica environment da "remote" al dizionario delle origini e imposta input su "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: aggiungi questa riga subito dopo print(f"\nDone. environment_id={environment_id}"):

    save_env(ENVIRONMENT_ID=environment_id, INTERACTION_ID=interaction_id)

save_env è già definito in run_digest.py. Scrive entrambi gli ID in .env in modo che il passaggio successivo possa scaricare il PDF senza eseguire nuovamente l'agente.

A cosa serve ogni origine

Ogni origine è un file montato nel file system della sandbox all'avvio prima dell'esecuzione dell'agente. I percorsi target corrispondono a quelli in cui la pettorina Antigravity prevede di trovarli:

.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

Percorso target

Variabile

Cosa fa la pettorina

.agents/AGENTS.md

AGENTS_MD

Caricamento automatico come istruzioni persistenti: linea editoriale, workflow, regole di esecuzione

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

SKILL_MD

Rilevata e registrata automaticamente come competenza denominata; l'agente la richiama per nome

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

GENERATE_PDF_PY

Renderer PDF predefinito; l'agente scrive summaries.json e poi esegue questo script

Verifica

uv run python run_digest.py

L'esecuzione ora richiede 1-3 minuti. Dovresti vedere l'agente leggere i file di configurazione, scrivere i riepiloghi e salvare il 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 ora è un valore reale: la sandbox è stata eseguita con i file di configurazione e l'agente ha creato digest.pdf. Il passaggio successivo aggiunge un controllo di sicurezza prima del download.

6. Aggiungere un aggancio di sicurezza

Gli hook ti consentono di eseguire uno script all'interno della sandbox prima o dopo ogni chiamata allo strumento. L'agente di riepilogo utilizza code_execution per eseguire script Python, quindi un hook pre_tool_execution può intercettare queste chiamate e bloccare i comandi shell distruttivi prima che vengano eseguiti.

Il runtime legge .agents/hooks.json dalla sandbox. Prima di ogni chiamata dello strumento di corrispondenza, i dettagli della chiamata vengono inviati allo script di gate su stdin. Lo script stampa {"decision": "allow"} o {"decision": "deny", "reason": "..."} in stdout. Un rifiuto annulla la chiamata dello strumento e l'agente vede il tuo motivo e si corregge.

Cosa aggiungere

TODO 5: in run_digest.py, aggiungi queste due costanti nella parte superiore, dopo le chiamate load_source esistenti:

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: aggiungi altre due voci all'elenco sources all'interno di interactions.create():

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

Come vengono attivati gli hook durante l'esecuzione del digest

Ogni volta che l'agente chiama code_execution per eseguire uno script Python o un comando shell, il runtime invia i dettagli della chiamata a gate.py. Se il comando contiene rm -rf, l'hook restituisce deny e l'agente riceve il motivo del rifiuto e riprova con un'alternativa sicura. Tutte le altre chiamate di esecuzione del codice vengono trasmesse invariate.

Verifica

uv run python run_digest.py

L'output è identico a prima: il meccanismo di sicurezza consente tutti i normali comandi di generazione di PDF. Per verificare che l'hook venga attivato, modifica temporaneamente l'input dell'agente per chiedergli di eseguire rm -rf /tmp/test. L'agente segnalerà che il comando è stato bloccato e sceglierà un'alternativa.

7. Scarica il PDF

L'agente ha scritto digest.pdf a /workspace/digest.pdf all'interno della sandbox. Lo snapshot dell'ambiente è disponibile come archivio tar tramite l'API Gemini Files.

Se necessario, installa requests:

uv pip install requests

Cosa compilare

Apri download_pdf.py. Ha due cose da fare.

TODO 1: compila la chiamata 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()

L'URL indirizza lo snapshot della sandbox. params={"alt": "media"} restituisce byte non elaborati anziché metadati. Il tuo GEMINI_API_KEY esistente autentica anche l'API Files.

TODO 2: trova ed estrai il PDF dall'archivio tar:

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

Il prefisso del percorso tar varia a seconda delle esecuzioni, quindi esegui la ricerca per suffisso anziché codificare il percorso esatto. filter="data" elimina l'avviso di ritiro di Python 3.13 relativo all'estrazione non sicura di tar.

Verifica

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

Apri digest.pdf nella stessa directory. Contiene il riepilogo formattato generato dall'agente dalle pagine web pubblicate.

8. Continua la conversazione

Hai già digest.pdf. Se volevi solo il file, hai finito. Questo passaggio riguarda un'altra operazione: chiedere all'agente di modificare il riepilogo senza recuperare nuovamente il web.

La sandbox è ancora attiva. L'agente ha ancora /workspace/digest.pdf e ricorda ogni storia che ha riassunto. Una seconda chiamata interactions.create() invia un messaggio di follow-up nella stessa sandbox. Qui chiedi di aggiungere una nota "Perché è importante" sotto ogni storia e il PDF viene aggiornato sul posto, senza recupero e riepilogo.

Cosa compilare

Apri refine_digest.py. Ha tre cose da fare.

TODO 1 e 2: compila i due parametri multi-turn all'interno di interactions.create():

    environment=environment_id,
    previous_interaction_id=interaction_id,

environment=environment_id ripristina la stessa sandbox con i relativi file e pacchetti. previous_interaction_id=interaction_id fornisce all'agente la cronologia delle conversazioni. Non cambierà nient'altro rispetto alla prima chiamata.

TODO 3: mantieni il nuovo interaction_id in .env dopo il ciclo di eventi:

save_env(INTERACTION_ID=interaction_id)

Ogni chiamata interactions.create() produce un nuovo interaction_id. La riscrittura significa che l'esecuzione successiva passa questo perfezionamento come previous_interaction_id, concatenando correttamente le svolte. L'ID sandbox non cambia mai, quindi non è necessario aggiornare ENVIRONMENT_ID.

I due parametri che consentono il funzionamento della conversazione multi-turno

ID

Che cosa conserva

Analogia

environment=environment_id

File, pacchetti installati, stato del sistema: tutto ciò che si trova nel file system Linux

Mantenere la stessa scrivania tra una riunione e l'altra

previous_interaction_id=interaction_id

Cronologia della conversazione: cosa ha detto e fatto l'agente nei turni precedenti

Ricordare cosa è stato discusso nell'ultima riunione

Puoi superare la verifica di uno dei due ID in modo indipendente:

  • environment_id: riutilizza file e pacchetti, ma avvia una nuova conversazione. Utile per una nuova attività nello stesso spazio di lavoro.
  • previous_interaction_id: continua il contesto della conversazione, ma in una nuova sandbox (i file non sono più presenti).
  • Entrambi: continuità completa, che è ciò che utilizza questo passaggio.

Senza environment_id: sandbox vuota, nessun PDF. Senza previous_interaction_id: nessun contesto, l'agente non può perfezionare una sezione specifica.

Verifica

uv run python refine_digest.py

Lo streaming dovrebbe essere veloce, l'agente non recupera nulla. Al termine:

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

Apri digest_v2.pdf e confrontalo con digest.pdf. Ogni storia dovrebbe ora avere una riga "Perché è importante".

9. Persistenza di una configurazione dell'agente gestita

Tutte le chiamate finora hanno superato AGENTS.md, SKILL.md e generate_pdf.py inline. Funziona, ma il codice di chiamata trasporta l'intero contenuto del file a ogni esecuzione. agents.create() incorpora la configurazione in un agente denominato salvato sul lato di Google. La chiamata successiva passa solo l'ID agente:

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

Cosa compilare

Apri save_agent.py. Contiene un TODO (TODO 1).

Tieni presente che le costanti vengono importate direttamente da run_digest.py (senza duplicazione):

from run_digest import BASE_AGENT, AGENTS_MD, SKILL_MD, GENERATE_PDF_PY

TODO 1: compila la chiamata 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 imposta il modello sottostante. gemini-3.7-flash è l'opzione predefinita e la scelta migliore per questo flusso di lavoro; gemini-3.6-flash, gemini-3.5-flash e gemini-3.5-flash-lite sono disponibili se vuoi un'esecuzione più leggera o a costi inferiori.

base_environment (non environment) è la differenza principale rispetto alla chiamata inline nel passaggio precedente: le origini vengono archiviate lato Google e montate automaticamente a ogni invocazione futura. Esegui una volta, non a ogni esecuzione del riepilogo.

Verifica: salva l'agente

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

Richiamare l'agente salvato

Apri invoke_agent.py. Chiama l'agente salvato per ID senza origini:

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

Confronta questo risultato con la chiamata inline: agent=BASE_AGENT viene sostituito da "my-digest" e l'intero blocco environment con tre fonti inline viene sostituito da environment="remote". La configurazione è già integrata nel sistema di Google.

Verifica: richiama l'agente salvato

uv run python invoke_agent.py

Dovresti vedere lo stesso live streaming dell'esecuzione in linea, ma la chiamata non contiene file di origine. Dopo la corsa, ENVIRONMENT_ID e INTERACTION_ID in .env vengono aggiornati in modo da poter continuare con refine_digest.py come prima.

[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. Invia tramite Gmail

L'agente ha generato il riepilogo e lo ha salvato in /workspace/digest.pdf. Finora l'hai scaricato localmente. Questo passaggio lo invia direttamente alla tua posta in arrivo facendo in modo che l'agente chiami l'API REST di Gmail dall'interno della sandbox.

L'approccio: ottieni un token di accesso OAuth 2.0 localmente e lo passi all'agente nel prompt input. L'agente utilizza code_execution per creare un'email MIME con il PDF allegato e inviarla all'API Gmail. Nessuno strumento personalizzato, nessuna registrazione del server MCP.

Prerequisiti

Abilita l'API Gmail nel tuo progetto GCP e crea un ID client OAuth 2.0:

  1. Vai alla pagina console.cloud.google.com/apis/library/gmail.googleapis.com e attiva l'API Gmail.
  2. Vai ad API e servizi > Credenziali > Crea credenziali > ID client OAuth 2.0.
  3. Tipo di applicazione: App desktop. Scarica il file JSON e salvalo come credentials.json nella radice del progetto.

Aggiungi l'email del destinatario a .env:

RECIPIENT_EMAIL=you@gmail.com

Se necessario, installa le librerie di autenticazione:

uv sync

Cosa compilare

Apri send_digest.py. Ha due cose da fare.

TODO 1: carica o aggiorna un token di accesso 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())

Rimuovi la riga raise NotImplementedError dopo averla aggiunta. Al primo avvio, si apre un browser per la schermata per il consenso OAuth. Il token viene memorizzato nella cache in .gmail_token.json per le esecuzioni future.

TODO 2: sostituisci input="" con le istruzioni via email. Il token è già nell'ambito come 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}"
    ),

A cosa serve ogni parte

L'interazione riprende la stessa sandbox in cui l'agente ha già generato digest.pdf e summaries.json. previous_interaction_id fornisce all'agente la cronologia delle conversazioni.

Il token di accesso viene passato nella stringa input. L'agente lo legge dal prompt e lo utilizza nell'intestazione Authorization: Bearer quando chiama l'API Gmail. Non tocca mai la macchina locale o il file system.

L'agente utilizza code_execution per scrivere ed eseguire uno script Python all'interno della sandbox: legge summaries.json, lo formatta come newsletter, legge digest.pdf, crea un messaggio MIME multipart, lo codifica in base64url e lo invia tramite POST a https://gmail.googleapis.com/gmail/v1/users/me/messages/send.

Verifica

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.

Controlla la posta in arrivo. L'email arriva con il corpo formattato come newsletter e digest.pdf allegato.

11. Pianificare le corse giornaliere

Finora ogni passaggio è stato attivato manualmente. I trigger consentono di pianificare l'esecuzione automatica dell'agente denominato in base a un'espressione cron. L'agente viene eseguito all'ora pianificata, esegue il flusso di lavoro di riepilogo completo e l'ambiente persiste tra le esecuzioni, quindi i pacchetti installati alla prima esecuzione sono disponibili in ogni esecuzione successiva.

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

Cosa compilare

Apri create_trigger.py. Ha un TODO.

TODO 1: fill in the triggers.create() call. Il trigger esegue l'intero flusso di lavoro ogni giorno: genera il riepilogo E lo invia alla tua casella di posta. Poiché i token di accesso scadono dopo un'ora, viene inserito il token di aggiornamento da .gmail_token.json come origine in linea, in modo che l'agente possa scambiarlo con un token nuovo a ogni esecuzione.

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 è il timeout predefinito. max_consecutive_failures=3 mette in pausa automaticamente il trigger dopo 3 esecuzioni non riuscite di fila (il valore predefinito dell'API è 5; 3 è più conservativo per un workshop).

L'elenco sources inserisce .gmail_creds.json nella sandbox in /workspace/.gmail_creds.json. L'agente lo legge, scambia il token di aggiornamento con un nuovo token di accesso e chiama l'API Gmail. I token di aggiornamento non scadono, quindi questa operazione funziona a ogni esecuzione pianificata senza alcun aggiornamento manuale del token.

Rimuovi la riga raise NotImplementedError dopo aver aggiunto la chiamata.

Verifica

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

create_trigger.py salva automaticamente l'ID trigger in .env.

Per controllare la cronologia di esecuzione dopo una sessione:

uv run python check_trigger.py

Per attivare il trigger immediatamente senza attendere il successivo orario pianificato:

uv run python fire_trigger.py

Per mettere in pausa o eliminare il trigger:

uv run python pause_trigger.py

12. Elimina

La sandbox scade automaticamente dopo 7 giorni di inattività. Nessun server da arrestare. Nessun contenitore da eliminare.

Se hai salvato una configurazione dell'agente, eliminala:

uv run python delete_agent.py

13. Riepilogo

Hai creato un agente gestito da zero, un concetto alla volta. Ecco cosa insegnava ogni esercizio:

Esercizio

Concetto

API Key

Effettuare la prima chiamata

Esegui il provisioning di una sandbox Linux reale e trasmetti in streaming i relativi eventi in diretta

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

Personalizzare l'agente

Monta i file di configurazione; mantieni gli ID in .env nella stessa esecuzione

environment.sources, save_env

Aggiungere un gancio di sicurezza

Intercettare le chiamate agli strumenti prima che vengano eseguite; negare i comandi distruttivi

hooks.json, pre_tool_execution e gate.py

Scarica il PDF

Scaricare il PDF senza eseguire di nuovo l'agente

API Gemini Files :download in download_pdf.py

Continua la conversazione

Continua la conversazione senza recuperare di nuovo il web

environment=environment_id, previous_interaction_id=interaction_id

Mantieni configurazione agente

Persistenza della configurazione dell'agente; richiamo per ID, nessuna origine necessaria

agents.create(), agents.list()

Invia tramite Gmail

Ottieni un token OAuth localmente e passalo all'agente, che chiama l'API REST Gmail tramite code_execution

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

Pianificare le esecuzioni giornaliere

Esegui l'agente automaticamente in base a una pianificazione cron

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

Pattern chiave

  1. Una chiamata, una sandbox: interactions.create() gestisce tutta l'infrastruttura (nessun container da eseguire il deployment, nessun pacchetto da installare localmente)
  2. Streaming progressivo: stream=True trasforma una scatola nera di 90 secondi in un feed pubblicato di chiamate di strumenti e blocchi di testo
  3. Origini inline: monta AGENTS.md, SKILL.md e script predefiniti nella sandbox senza alcun passaggio di caricamento o deployment
  4. Rilevamento automatico dell'imbracatura: i file inseriti in .agents/ vengono rilevati automaticamente (non è richiesta alcuna configurazione dell'SDK)
  5. Stato bidimensionale: environment_id tiene traccia di file e pacchetti; previous_interaction_id tiene traccia del contesto della conversazione; entrambi possono essere passati in modo indipendente
  6. Download dello snapshot: l'ambiente è un tar completo del file system, accessibile tramite l'API Gemini Files
  7. Agenti denominati: agents.create() incorpora la configurazione in modo permanente; le chiamate future trasmettono solo l'ID agente e environment="remote", senza origini
  8. Hook: hooks.json + uno strumento di intercettazione degli script di gate intercetta le chiamate prima dell'esecuzione; una risposta deny annulla la chiamata e l'agente si autocorregge
  9. Chiamate API esterne: passa una credenziale nel prompt input; l'agente scrive ed esegue il codice di integrazione all'interno della sandbox tramite code_execution
  10. Trigger: pianifica un agente in base a un'espressione cron con client.triggers.create(); l'ambiente persiste tra le esecuzioni

ADK + Cloud Run e agenti gestiti: la differenza in sintesi

Capacità

ADK + Cloud Run

Managed Agents nell'API Gemini

Esegui il provisioning di una sandbox

docker build + gcloud run deploy

interactions.create()

Definisci gli strumenti

Funzioni Python registrate con l'agente

Funzionalità integrate: navigazione web, esecuzione di codice, file system

Installa pacchetti

pip install in Dockerfile

L'agente esegue pip install all'interno della sandbox

Eventi di streaming

Infrastruttura SSE personalizzata

stream=True

Continuare una sessione

Database della sessione + inserimento del contesto

environment_id + previous_interaction_id

File di configurazione

Codificati nell'agente o inseriti all'avvio

Montato tramite environment.sources

Infrastruttura da gestire

Container, Cloud Run, IAM, secret

Nessuno

Passaggi successivi