Compila un agente de Daily Tech Digest con agentes administrados en la API de Gemini

1. Descripción general

El panorama de la IA y la tecnología avanza más rápido de lo que cualquiera puede seguir. Todos los días se lanzan nuevos modelos, documentos y productos. Un agente de resúmenes que recupera los titulares del día, escribe resúmenes concisos y genera un PDF todas las mañanas resolvería ese problema, pero antes, crear uno significaba elegir un framework, definir herramientas en Python, escribir un bucle de orquestación, empaquetar un contenedor y realizar la implementación en Cloud Run. Todo eso antes de que el agente realizara una sola solicitud web.

Los agentes administrados en la API de Gemini cambian la ecuación. Escribirás dos archivos de configuración de Markdown y una secuencia de comandos de renderizador prediseñada, realizarás una llamada a la API y se iniciará un entorno de pruebas real de Ubuntu, navegará por la Web, escribirá tus resúmenes y generará un PDF. No hay contenedores. No hay implementación. No hay código de organización.

En este codelab, compilarás exactamente ese agente: desde una función vacía hasta un resumen diario funcional, un concepto a la vez.

Qué compilarás

  • Crea y ejecuta tu primer agente administrado en una zona de pruebas de Linux real
  • Personaliza el agente con comentario editorial, fuentes web y una habilidad para archivos PDF
  • Agrega un gancho de seguridad para bloquear los comandos destructivos antes de que se ejecuten
  • Descarga el PDF que generó el agente
  • Refina el resumen en una conversación de varios turnos sin volver a recuperar la Web
  • Guarda la configuración del agente y, luego, invócalo por ID en ejecuciones futuras
  • Envía el resumen a tu bandeja de entrada a través de la API de Gmail
  • Programa el agente para que se ejecute y envíe automáticamente todos los días

Requisitos

  • Python 3.10 o versiones posteriores
  • Una clave de la API de Gemini: aistudio.google.com/api-keys (se incluye el nivel gratuito; se recomienda la facturación para ejecuciones ininterrumpidas)

2. ¿Qué son los agentes administrados en la API de Gemini?

Tres niveles de sistemas de IA

Antes de profundizar en el código, aquí se explica dónde encajan los agentes administrados en relación con las dos alternativas:

Nivel

De qué se trata

¿Quién administra la infraestructura?

LLM estándar

Tú le das una instrucción y te responde con texto. Sin manos, sin memoria y sin Internet.

N/A: No puede hacer nada por sí solo.

Agente autoalojado

Conectas ADK/LangChain/AutoGen + Docker + herramientas + memoria.

Tú: Todo (o una plataforma administrada como Agent Engine)

Agente administrado

Le das un objetivo. Google aprovisiona una zona de pruebas segura. El agente escribe código, lo ejecuta, lee errores, busca en la Web y corrige errores de forma autónoma.

Google: todo

Este codelab trata sobre la tercera fila. Proporcionas archivos de configuración y tareas. Google se encarga de todo lo demás.

Qué compilarías con el ADK y Cloud Run

Para crear un agente de resumen de noticias que navegue por la Web, ejecute Python y genere un PDF, necesitarías todo esto con ADK y 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

Esto es antes de que el agente se haya ejecutado una vez. Aún eres propietario del aislamiento de la zona de pruebas (por lo que el agente no puede dañar tu servidor), la instalación de paquetes, la administración de estados entre llamadas a herramientas y la infraestructura de transmisión para enviar eventos a un cliente.

Con qué lo reemplazan los agentes administrados

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

Requisitos de ADK y Cloud Run

Qué tareas realizan los agentes administrados por ti

Imagen de contenedor + Dockerfile + CI/CD

Zona de pruebas de Ubuntu completamente administrada (Python 3.12, Node 22, 4 CPU / 16 GB de RAM)

Implementación y ajuste de escala de Cloud Run

Se aprovisiona por interacción y vence automáticamente después de 7 días de inactividad.

Aislamiento de zona de pruebas

Aislamiento por interacción

Herramienta de PDF personalizada + pip install

El agente instala paquetes dentro de la zona de pruebas

Infraestructura de transmisión de SSE

stream=True devuelve un iterable de eventos

Definiciones de herramientas en Python

Herramientas integradas: navegación web, ejecución de código y sistema de archivos

Administración del estado entre llamadas a herramientas

Integrado en el bucle de razonamiento del agente

Escribes archivos de configuración (AGENTS.md, SKILL.md, una secuencia de comandos prediseñada) y realizas una llamada a la API. Google se encarga de todo lo demás.

Cómo funciona la zona de pruebas

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

El entorno de pruebas persiste durante 7 días de inactividad. Puedes reanudarla con environment_id para refinar el resultado, ejecutar tareas de seguimiento o bifurcarla en un agente guardado con nombre.

3. Configurar

Haz clic en el siguiente botón para abrir este codelab en Google Cloud Shell. Todas las dependencias están preinstaladas.

Abrir en Cloud Shell

Opción B: Configuración local

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

Instala uv si es necesario:

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

Configura tu clave de API

cp .env.example .env
cloudshell edit .env

Configura tu llave:

GEMINI_API_KEY=your-key-here

Instala dependencias

uv sync

4. Realiza tu primera llamada al agente

Abre el archivo de inicio

cloudshell edit run_digest.py

run_digest() tiene un TODO para completar ahora y tres más para el siguiente paso. Ya hay dos asistentes completados previamente arriba:

  • load_source(path): Lee un archivo de .agents/ en relación con la secuencia de comandos. La usarás en el próximo ejercicio para activar el comentario editorial, la guía en PDF y el renderizador en la zona de pruebas.
  • run_stream(stream): Procesa el flujo de eventos y devuelve (environment_id, interaction_id). No es necesario que escribas el bucle de eventos por tu cuenta.

Qué agregar

TODO 1: Reemplaza pass por lo siguiente (por ahora, ignora los comentarios TODO 3 y 4, ya que son para el siguiente paso):

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

Qué hace cada parte

genai.Client() lee GEMINI_API_KEY del entorno. Todo lo demás pasa por este cliente.

interactions.create() es la llamada principal. Cuatro parámetros hacen que funcione:

  • agent=BASE_AGENT: Selecciona el agente de Antigravity (antigravity-preview-05-2026), un agente administrado de uso general que, de forma predeterminada, funciona con Gemini 3.7 Flash. Puedes configurar el modelo subyacente con agent_config (opciones: gemini-3.7-flash, gemini-3.6-flash, gemini-3.5-flash, gemini-3.5-flash-lite). Incluye tres herramientas integradas habilitadas de forma predeterminada: code_execution (ejecuta Bash, Python, Node.js), google_search y url_context (recupera y lee páginas web). Las herramientas del sistema de archivos (read_file, write_file, list_files) se habilitan automáticamente cuando pasas el parámetro environment. Una llamada aprovisiona un entorno de Ubuntu completamente administrado con Python 3.12, Node.js 22, git, pip y curl preinstalados. No hay contenedor para compilar ni implementación para ejecutar.
  • input: Es la tarea de esta ejecución. El agente navega por Hacker News y razona sobre los resultados.
  • environment="remote": Aprovisiona una zona de pruebas de Cloud nueva para esta interacción.
  • stream=True: Devuelve un iterable de eventos en lugar de bloquearse. Sin él, la llamada espera entre 30 y 90 segundos y devuelve toda la salida de una vez como interaction.output_text. Con la transmisión, ves el motivo del agente y actúas a medida que sucede. La transmisión no es una función avanzada aquí: es el valor predeterminado correcto, ya que una caja negra de 90 segundos no te da ninguna señal sobre si el agente está funcionando o atascado.

environment_id es un identificador de la zona de pruebas que acaba de ejecutarse. Después de interaction.completed, el entorno de pruebas no se apaga, sino que permanece activo hasta por 7 días. El environment_id es la forma de volver a ella. Pásalo a una segunda llamada a interactions.create() y el agente se reanudará en el mismo sistema de archivos, con los mismos archivos y paquetes instalados, como si nunca se hubiera ido. El siguiente paso lo usa para descargar el PDF sin volver a ejecutar el agente, y el paso posterior lo usa para continuar la conversación.

interaction_id es un identificador del turno de conversación que acaba de completarse. Pásalo como previous_interaction_id en la próxima llamada, y el agente tendrá memoria completa de lo que dijo y lo que hizo en este turno.

Verificar

uv run python run_digest.py

Deberías ver el resultado en vivo a medida que el agente trabaja:

[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

La API devuelve un environment_id real incluso con environment="remote". Se ejecutó la zona de pruebas. Lo que falta es la configuración: no hay voz, no hay habilidad, no hay generador de PDF. El agente solo imprimió historias como texto y se detuvo. En el siguiente paso, se agregarán esos elementos.

Cada línea de resultado se asigna a un evento de run_stream():

step.type

Qué es

Qué imprime run_stream()

"url_context_call"

El agente recupera una URL

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

"code_execution_call"

Agente que ejecuta código en la zona de pruebas

[tool] run_code

"google_search_call"

El agente busca en la Web.

[tool] google_search

"function_call"

Herramientas de archivos y otras

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

step.delta donde delta.type == "text"

El agente está escribiendo texto.

se transmite directamente a stdout

5. Personaliza el agente

El agente no tenía instrucciones: no tenía voz, no tenía habilidad y no tenía generador de PDF. En este paso, cargarás los archivos de configuración de .agents/ y los activarás en el entorno de pruebas.

Qué cambiar

Realiza cuatro cambios en run_digest.py:

TODO 2: Debajo de load_source(), agrega las tres constantes a nivel del módulo (se encuentran fuera de run_digest(), en la parte superior del archivo):

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

Abre cada archivo para ver lo que cargas: AGENTS.md establece el comentario editorial y las reglas del flujo de trabajo; SKILL.md es la guía paso a paso en formato PDF; generate_pdf.py es el renderizador prediseñado que ejecutará el agente.

Ahora, realiza dos cambios más dentro de run_digest():

TODO 3: Cambia environment de "remote" al diccionario de fuentes y establece input en "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: Agrega esta línea justo después de print(f"\nDone. environment_id={environment_id}"):

    save_env(ENVIRONMENT_ID=environment_id, INTERACTION_ID=interaction_id)

save_env ya está definido en run_digest.py. Escribe ambos IDs en .env para que el siguiente paso pueda descargar el PDF sin volver a ejecutar el agente.

Qué hace cada fuente

Cada fuente es un archivo activado en el sistema de archivos de la zona de pruebas al inicio, antes de que se ejecute el agente. Las rutas de acceso de target coinciden con las ubicaciones en las que el arnés de Antigravity espera encontrarlas:

.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

Ruta de acceso target

Variable

Qué hace el arnés con el cable

.agents/AGENTS.md

AGENTS_MD

Se cargan automáticamente como instrucciones persistentes: comentario editorial, flujo de trabajo y reglas de ejecución

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

SKILL_MD

Se descubre y registra automáticamente como una habilidad con nombre; el agente la invoca por su nombre.

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

GENERATE_PDF_PY

Renderizador de PDF compilado previamente: El agente escribe summaries.json y, luego, ejecuta esta secuencia de comandos.

Verificar

uv run python run_digest.py

La ejecución ahora tarda de 1 a 3 minutos. Deberías ver que el agente lee los archivos de configuración, escribe resúmenes y guarda el 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 ahora es un valor real: el entorno de pruebas se ejecutó con tus archivos de configuración y el agente creó digest.pdf. El siguiente paso agrega un gancho de seguridad antes de la descarga.

6. Agrega un gancho de seguridad

Los hooks te permiten ejecutar un script dentro del entorno de pruebas antes o después de cada llamada a la herramienta. El agente de resúmenes usa code_execution para ejecutar secuencias de comandos de Python, por lo que un gancho pre_tool_execution puede interceptar esas llamadas y bloquear los comandos destructivos del shell antes de que se ejecuten.

El tiempo de ejecución lee .agents/hooks.json desde la zona de pruebas. Antes de cada llamada a la herramienta de correlación, canaliza los detalles de la llamada a tu secuencia de comandos de puerta en stdin. La secuencia de comandos imprime {"decision": "allow"} o {"decision": "deny", "reason": "..."} en stdout. Un rechazo cancela la llamada a la herramienta, y el agente ve tu motivo y se corrige.

Qué agregar

En TODO 5: en run_digest.py, agrega estas dos constantes cerca de la parte superior, después de las llamadas load_source existentes:

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: Agrega dos entradas más a la lista sources dentro de interactions.create():

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

Cómo se activan los hooks durante la ejecución del resumen

Cada vez que el agente llama a code_execution para ejecutar una secuencia de comandos de Python o un comando de shell, el tiempo de ejecución canaliza primero los detalles de la llamada a gate.py. Si el comando contiene rm -rf, el gancho devuelve deny y el agente recibe el motivo del rechazo y vuelve a intentarlo con una alternativa segura. Todas las demás llamadas de ejecución de código pasan sin cambios.

Verificar

uv run python run_digest.py

El resultado es idéntico al anterior: la puerta de seguridad permite todos los comandos normales de generación de PDF. Para confirmar que se activa el gancho, cambia temporalmente la entrada del agente para pedirle que ejecute rm -rf /tmp/test. Verás que el agente informa que se bloqueó el comando y elige una alternativa.

7. Descargue el PDF.

El agente escribió digest.pdf en /workspace/digest.pdf dentro de la zona de pruebas. La instantánea del entorno está disponible como un archivo tar a través de la API de Gemini Files.

Instala requests si es necesario:

uv pip install requests

Qué completar

Abre download_pdf.py. Tiene dos tareas pendientes.

TODO 1: Completa la llamada a 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()

La URL dirige a la instantánea del entorno aislado. params={"alt": "media"} devuelve bytes sin procesar en lugar de metadatos. Tu GEMINI_API_KEY existente también autentica la API de Files.

TODO 2: Busca y extrae el PDF del archivo tar:

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

El prefijo de la ruta de acceso de tar varía entre las ejecuciones, por lo que se recomienda buscar por sufijo en lugar de codificar la ruta de acceso exacta. filter="data" suprime la advertencia de obsolescencia de Python 3.13 sobre la extracción de archivos tar no seguros.

Verificar

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

Abre digest.pdf en el mismo directorio. Contiene el resumen con formato que el agente generó a partir de páginas web en vivo.

8. Continúa la conversación

Ya tienes digest.pdf. Si solo querías el archivo, ya terminaste. Este paso se trata de algo diferente: pedirle al agente que cambie el resumen sin volver a recuperar la Web.

La zona de pruebas sigue activa. El agente aún tiene /workspace/digest.pdf y recuerda cada historia que resumió. Una segunda llamada a interactions.create() envía un mensaje de seguimiento al mismo entorno de pruebas. Aquí le pides que agregue una nota de "Por qué es importante" debajo de cada historia, y actualiza el PDF en su lugar, sin volver a recuperar ni resumir.

Qué completar

Abre refine_digest.py. Tiene tres tareas pendientes.

TODOs 1 y 2: Completa los dos parámetros de varias turnos dentro de interactions.create():

    environment=environment_id,
    previous_interaction_id=interaction_id,

environment=environment_id reanuda el mismo entorno de pruebas con sus archivos y paquetes. previous_interaction_id=interaction_id le proporciona al agente su historial de conversaciones. No cambia nada más desde la primera llamada.

TODO 3: Persiste el nuevo interaction_id en .env después del bucle de eventos:

save_env(INTERACTION_ID=interaction_id)

Cada llamada a interactions.create() produce un nuevo interaction_id. Volver a escribirlo significa que la próxima ejecución pasará este perfeccionamiento como previous_interaction_id, y las encadenaciones se activarán correctamente. El ID de zona de pruebas nunca cambia, por lo que no es necesario actualizar ENVIRONMENT_ID.

Los dos parámetros que hacen que funcione el chat de varios turnos

ID

Qué conserva

Analogía

environment=environment_id

Archivos, paquetes instalados, estado del sistema: todo en el sistema de archivos de Linux

Mantener el mismo escritorio de oficina entre reuniones

previous_interaction_id=interaction_id

Historial de conversaciones: Lo que dijo y lo que hizo el agente en turnos anteriores

Recordar lo que se habló en la última reunión

Puedes pasar cualquiera de los IDs de forma independiente:

  • environment_id solamente: Reutiliza archivos y paquetes, pero inicia una conversación nueva. Es útil para una tarea nueva en el mismo espacio de trabajo.
  • Solo para previous_interaction_id: Continúa el contexto de la conversación, pero en un sandbox nuevo (los archivos desaparecieron).
  • Ambos: Continuidad completa, que es lo que usa este paso.

Sin environment_id: Zona de pruebas en blanco, sin PDF. Sin previous_interaction_id: No hay contexto, el agente no puede definir mejor una sección específica.

Verificar

uv run python refine_digest.py

La transmisión debe ser rápida, ya que el agente no vuelve a recuperar nada. Después de que finalice, haz lo siguiente:

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

Abre digest_v2.pdf y compáralo con digest.pdf. Cada historia ahora debería tener una línea de "Por qué es importante" agregada.

9. Cómo conservar la configuración de un agente administrado

Hasta ahora, cada llamada pasó AGENTS.md, SKILL.md y generate_pdf.py de forma intercalada. Eso funciona, pero tu código de llamada transporta el contenido completo del archivo en cada ejecución. agents.create() incorpora la configuración en un agente con nombre guardado del lado de Google. La siguiente invocación solo pasa el ID del agente:

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

Qué completar

Abre save_agent.py. Tiene una tarea pendiente (TODO 1).

Ten en cuenta que las constantes se importan directamente desde run_digest.py (sin duplicación):

from run_digest import BASE_AGENT, AGENTS_MD, SKILL_MD, GENERATE_PDF_PY

TODO 1: Completa la llamada a 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 establece el modelo subyacente. gemini-3.7-flash es el valor predeterminado y la mejor opción para este flujo de trabajo. gemini-3.6-flash, gemini-3.5-flash y gemini-3.5-flash-lite están disponibles si deseas una ejecución más liviana o de menor costo.

base_environment (no environment) es la diferencia clave con la llamada intercalada del paso anterior: las fuentes se almacenan del lado de Google y se activan automáticamente en cada invocación futura. Ejecútalo una vez, no en cada ejecución del resumen.

Verifica y guarda el agente

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

Invoca el agente guardado

Abre invoke_agent.py. Llama al agente guardado por ID sin fuentes:

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

Compara esto con la llamada intercalada: agent=BASE_AGENT se reemplaza por "my-digest", y el bloque environment completo con tres fuentes intercaladas se reemplaza por environment="remote". La configuración ya está integrada en el sistema de Google.

Verifica: Invoca el agente guardado

uv run python invoke_agent.py

Deberías ver la misma transmisión en vivo que la ejecución intercalada, pero la llamada no contiene archivos fuente. Después de la ejecución, se actualizan ENVIRONMENT_ID y INTERACTION_ID en .env para que puedas continuar con refine_digest.py como antes.

[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. Enviar por Gmail

El agente generó el resumen y lo guardó en /workspace/digest.pdf. Hasta ahora, lo descargaste de forma local. En este paso, se entrega directamente a tu carpeta Recibidos, ya que el agente llama a la API de REST de Gmail desde la zona de pruebas.

El enfoque es obtener un token de acceso de OAuth 2.0 de forma local y pasarlo al agente en la instrucción input. El agente usa code_execution para crear un correo electrónico MIME con el PDF adjunto y enviarlo a la API de Gmail con POST. No hay herramientas personalizadas ni registro de servidores de MCP.

Requisitos previos

Habilita la API de Gmail en tu proyecto de GCP y crea un ID de cliente de OAuth 2.0:

  1. Ve a console.cloud.google.com/apis/library/gmail.googleapis.com y habilita la API de Gmail.
  2. Ve a APIs y servicios > Credenciales > Crear credenciales > ID de cliente de OAuth 2.0.
  3. Tipo de aplicación: Aplicación para computadoras. Descarga el archivo JSON y guárdalo como credentials.json en la raíz del proyecto.

Agrega el correo electrónico del destinatario a .env:

RECIPIENT_EMAIL=you@gmail.com

Instala las bibliotecas de autenticación si es necesario:

uv sync

Qué completar

Abre send_digest.py. Tiene dos tareas pendientes.

TODO 1: Carga o actualiza un token de acceso de 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())

Quita la línea raise NotImplementedError después de agregarla. La primera vez que se ejecuta, se abre un navegador para la pantalla de consentimiento de OAuth. El token se almacena en caché en .gmail_token.json para ejecuciones futuras.

TODO 2: Reemplaza input="" por las instrucciones del correo electrónico. El token ya está en el alcance como 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}"
    ),

Qué hace cada parte

La interacción reanuda el mismo entorno de pruebas en el que el agente ya generó digest.pdf y summaries.json. previous_interaction_id le proporciona al agente su historial de conversaciones.

El token de acceso se pasa en la cadena input. El agente lo lee de la instrucción y lo usa en el encabezado Authorization: Bearer cuando llama a la API de Gmail. Nunca toca tu máquina local ni tu sistema de archivos.

El agente usa code_execution para escribir y ejecutar una secuencia de comandos de Python dentro de la zona de pruebas: lee summaries.json, le da formato como boletín informativo, lee digest.pdf, compila un mensaje multipart MIME, lo codifica en base64url y lo envía con POST a https://gmail.googleapis.com/gmail/v1/users/me/messages/send.

Verificar

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.

Revisa la carpeta Recibidos. El correo electrónico llega con el cuerpo con formato de boletín informativo y el archivo digest.pdf adjunto.

11. Cómo programar ejecuciones diarias

Hasta ahora, cada paso se activó de forma manual. Los activadores te permiten programar el agente con nombre para que se ejecute automáticamente en una expresión cron. El agente se activa a la hora programada, ejecuta el flujo de trabajo completo del resumen y el entorno persiste entre las ejecuciones, por lo que los paquetes instalados en la primera ejecución están disponibles en todas las ejecuciones posteriores.

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

Qué completar

Abre create_trigger.py. Tiene una tarea pendiente.

TODO 1: Completa la llamada a triggers.create(). El activador ejecuta el flujo de trabajo completo todos los días: genera el resumen Y lo envía a tu bandeja de entrada. Debido a que los tokens de acceso vencen en una hora, inyecta el token de actualización de .gmail_token.json como una fuente intercalada para que el agente pueda intercambiarlo por un token nuevo en cada ejecución.

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 es el tiempo de espera predeterminado. max_consecutive_failures=3 pausa el activador automáticamente después de 3 ejecuciones fallidas seguidas (el valor predeterminado de la API es 5; 3 es más conservador para un taller).

La lista sources inyecta .gmail_creds.json en la zona de pruebas en /workspace/.gmail_creds.json. El agente lo lee, intercambia el token de actualización por un token de acceso nuevo y llama a la API de Gmail. Los tokens de actualización no vencen, por lo que esto funciona en cada ejecución programada sin necesidad de actualizar el token de forma manual.

Quita la línea raise NotImplementedError después de agregar la llamada.

Verificar

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

create_trigger.py guarda el ID del activador en .env automáticamente.

Para verificar el historial de ejecución después de una ejecución, haz lo siguiente:

uv run python check_trigger.py

Para activar el disparador de inmediato sin esperar la próxima hora programada, haz lo siguiente:

uv run python fire_trigger.py

Para pausar o borrar el activador, haz lo siguiente:

uv run python pause_trigger.py

12. Limpieza

El sandbox vence automáticamente después de 7 días de inactividad. No hay servidores para detener. No hay contenedores para borrar.

Si guardaste una configuración del agente, bórrala:

uv run python delete_agent.py

13. Resumen

Compilaste un agente administrado desde cero, un concepto a la vez. A continuación, se explica lo que enseñó cada ejercicio:

Ejercicio

Concepto

API de Key

Realiza tu primera llamada

Aprovisiona un sandbox de Linux real y transmite sus eventos en vivo

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

Personaliza el agente

Se montan los archivos de configuración y se conservan los IDs en .env en la misma ejecución.

environment.sources, save_env

Agrega un gancho de seguridad

Intercepta las llamadas a herramientas antes de que se ejecuten y rechaza los comandos destructivos

hooks.json, pre_tool_execution, gate.py

Descargue el PDF.

Descarga el PDF sin volver a ejecutar el agente

API de Gemini Files :download en download_pdf.py

Continúa la conversación

Continúa la conversación sin volver a buscar en la Web

environment=environment_id, previous_interaction_id=interaction_id

Persiste la configuración del agente

Persiste la configuración del agente; invoca por ID, no se necesitan fuentes

agents.create(), agents.list()

Enviar por Gmail

Obtén un token de OAuth de forma local y pásalo al agente, que llama a la API de REST de Gmail a través de code_execution.

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

Programa ejecuciones diarias

Ejecuta el agente automáticamente según una programación cron

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

Patrones de clave

  1. Una llamada, una zona de pruebas: interactions.create() controla toda la infraestructura (no hay contenedores para implementar ni paquetes para instalar de forma local).
  2. Transmisión progresiva: stream=True convierte una caja negra de 90 segundos en un feed en vivo de llamadas a herramientas y fragmentos de texto.
  3. Fuentes intercaladas: Se montan AGENTS.md, SKILL.md y secuencias de comandos prediseñadas en el entorno de pruebas sin ningún paso de carga o implementación.
  4. Detección automática de arneses: Los archivos colocados en .agents/ se detectan automáticamente (no se requiere configuración del SDK).
  5. Estado bidimensional: environment_id hace un seguimiento de los archivos y paquetes; previous_interaction_id hace un seguimiento del contexto de la conversación; cualquiera de los dos se puede pasar de forma independiente
  6. Descarga de instantáneas: El entorno es un archivo tar del sistema de archivos completo, al que se puede acceder a través de la API de Gemini Files.
  7. Agentes con nombre: agents.create() incorpora la configuración de forma permanente; las llamadas futuras solo pasan el ID del agente y environment="remote", sin fuentes.
  8. Hooks: hooks.json + un script de puerta intercepta las llamadas a herramientas antes de que se ejecuten; una respuesta de deny cancela la llamada y el agente se autocorrige
  9. Llamadas a APIs externas: Pasa una credencial en la instrucción input; el agente escribe y ejecuta el código de integración dentro de la zona de pruebas a través de code_execution
  10. Activadores: Programa un agente en una expresión cron con client.triggers.create(); el entorno persiste en las ejecuciones

ADK + Cloud Run vs. agentes administrados: la diferencia en un vistazo

Función

ADK + Cloud Run

Agentes administrados en la API de Gemini

Aprovisiona una zona de pruebas

docker build y gcloud run deploy

interactions.create()

Define herramientas

Funciones de Python registradas en el agente

Integradas: navegación web, ejecución de código, sistema de archivos

Instalar paquetes

pip install en Dockerfile

El agente ejecuta pip install dentro de la zona de pruebas

Eventos de transmisión

Infraestructura de SSE personalizada

stream=True

Cómo continuar una sesión

Base de datos de sesión + inserción de contexto

environment_id y previous_interaction_id

Archivos de configuración

Codificado de forma rígida en el agente o insertado en el inicio

Se montó con environment.sources

Infraestructura para administrar

Contenedor, Cloud Run, IAM, secretos

Ninguno

Próximos pasos