1. 개요
AI 및 기술 환경은 누구도 따라갈 수 없을 정도로 빠르게 변화하고 있습니다. 매일 새로운 모델, 논문, 제품이 출시됩니다. 오늘의 헤드라인을 가져오고, 간결한 요약을 작성하고, 매일 아침 PDF를 생성하는 요약 에이전트가 있다면 이 문제를 해결할 수 있지만, 이전에는 프레임워크를 선택하고, Python에서 도구를 정의하고, 오케스트레이션 루프를 작성하고, 컨테이너를 패키징하고, Cloud Run에 배포해야 했습니다. 에이전트가 단일 웹 요청을 하기 전의 모든 작업입니다.
Gemini API의 관리형 에이전트가 이 공식을 바꿉니다. 마크다운 구성 파일 2개와 사전 빌드된 렌더러 스크립트를 작성하고 API 호출을 한 번 하면 실제 Ubuntu 샌드박스가 부팅되고, 웹을 탐색하고, 요약을 작성하고, PDF를 생성합니다. 컨테이너가 없습니다. 배포가 없습니다. 조정 코드가 없습니다.
이 Codelab에서는 빈 함수에서 작동하는 일일 요약까지 한 번에 하나의 개념을 사용하여 정확히 그 에이전트를 빌드합니다.
빌드 대상
- 실제 Linux 샌드박스에서 첫 번째 관리형 에이전트 만들기 및 실행
- 언론사 성향, 웹 소스, PDF 스킬로 에이전트 맞춤설정
- 실행되기 전에 파괴적인 명령어를 차단하는 안전 후크 추가
- 에이전트가 생성한 PDF 다운로드
- 웹을 다시 가져오지 않고 멀티턴 대화에서 요약 다듬기
- 에이전트 구성을 저장하고 향후 실행에서 ID로 호출
- Gmail API를 통해 받은편지함으로 요약 전송
- 매일 자동으로 실행되고 전송되도록 에이전트 예약
필요한 항목
- Python 3.10 이상
- Gemini API 키: aistudio.google.com/api-keys (무료 등급 포함, 중단 없는 실행을 위해 결제 권장)
2. Gemini API의 관리형 에이전트란 무엇인가요?
AI 시스템의 세 가지 수준
코드를 살펴보기 전에 관리 에이전트가 두 가지 대안과 비교하여 어떤 위치에 있는지 살펴보겠습니다.
수준 | 어떤 기능인가요? | 인프라를 관리하는 주체 |
표준 LLM | 프롬프트를 입력하면 텍스트로 대답합니다. 손도, 메모리도, 인터넷도 필요하지 않습니다. | 해당 사항 없음: 스스로 할 수 있는 작업이 없음 |
자체 호스팅 에이전트 | ADK/LangChain/AutoGen + Docker + 도구 + 메모리를 연결합니다. | 모든 것 (또는 Agent Engine과 같은 관리형 플랫폼) |
관리형 에이전트 | 목표를 설정합니다. Google은 보안 샌드박스를 프로비저닝합니다. 에이전트는 코드를 작성하고, 실행하고, 오류를 읽고, 웹을 검색하고, 버그를 자율적으로 수정합니다. | Google: 전체 |
이 Codelab에서는 세 번째 행을 다룹니다. 작업 및 구성 파일을 제공합니다. 나머지는 Google에서 처리합니다.
ADK + Cloud Run으로 빌드할 수 있는 항목
웹을 탐색하고, Python을 실행하고, PDF를 생성하는 뉴스 요약 에이전트를 빌드하려면 ADK + Cloud Run을 사용하여 다음을 모두 수행해야 합니다.
# agent.py: define tools and wire up the agent
from google.adk.agents import LlmAgent
from google.adk.tools import google_search, built_in_code_execution
agent = LlmAgent(
name="digest-agent",
model=MODEL,
instruction=AGENTS_MD, # your editorial voice and rules
tools=[google_search, built_in_code_execution],
)
# app.py: serve the agent over HTTP
from google.adk.runners import FastApiRunner
runner = FastApiRunner(agent=agent)
app = runner.app
# pdf_tool.py: custom tool, install reportlab, render PDF
# scraper.py: custom tool, fetch each news source
# streaming.py: wire agent events to your SSE endpoint
# Dockerfile: package everything
FROM python:3.12
COPY . /app
RUN pip install google-adk reportlab requests
CMD ["uvicorn", "app:app", "--host", "0.0.0.0"]
# Deploy to Cloud Run
gcloud run deploy digest-agent \
--image gcr.io/your-project/digest-agent \
--set-secrets GEMINI_API_KEY=gemini-key:latest \
--memory 2Gi
이는 에이전트가 한 번 실행되기 전입니다. 샌드박스 격리 (따라서 에이전트가 서버를 손상시킬 수 없음), 패키지 설치, 도구 호출 간 상태 관리, 클라이언트에 이벤트를 가져오는 스트리밍 인프라는 여전히 소유하고 있습니다.
관리 에이전트가 대체하는 항목
from google import genai
client = genai.Client()
stream = client.interactions.create(
agent="antigravity-preview-05-2026",
input="Generate the digest.",
stream=True,
environment={
"type": "remote",
"sources": [ # your config files, mounted at startup
{
"type": "inline",
"target": ".agents/AGENTS.md",
"content": AGENTS_MD,
},
{
"type": "inline",
"target": ".agents/skills/digest-pdf/SKILL.md",
"content": SKILL_MD,
},
{
"type": "inline",
"target": ".agents/skills/digest-pdf/scripts/generate_pdf.py",
"content": GENERATE_PDF_PY,
},
],
},
)
ADK + Cloud Run에 필요한 사항 | 관리형 에이전트가 처리하는 작업 |
컨테이너 이미지 + Dockerfile + CI/CD | 완전 관리형 Ubuntu 샌드박스 (Python 3.12, Node 22, 4 CPU / 16GB RAM) |
Cloud Run 배포 + 확장 | 상호작용당 프로비저닝, 7일 동안 활동이 없으면 자동 만료 |
샌드박스 격리 | 상호작용별로 격리됨 |
맞춤 PDF 도구 + | 에이전트가 샌드박스 내에 패키지를 설치합니다. |
SSE 스트리밍 인프라 |
|
Python의 도구 정의 | 기본 제공 도구: 웹 탐색, 코드 실행, 파일 시스템 |
도구 호출 간 상태 관리 | 에이전트 추론 루프에 내장됨 |
구성 파일 (AGENTS.md, SKILL.md, 사전 빌드된 스크립트)을 작성하고 API 호출을 한 번 실행합니다. 나머지는 Google에서 처리합니다.
샌드박스 작동 방식
interactions.create() call
│
▼
Google provisions Ubuntu sandbox (Python 3.12, Node 22, 4 CPU / 16 GB RAM)
│
▼
Agent reasoning loop:
plan → fetch URLs → run Python → write files → reason → repeat
│
▼
Events stream back in real time: tool calls, text chunks, completion
│
▼
interaction.completed → environment_id + interaction_id
샌드박스는 7일 동안 비활성 상태로 유지됩니다. environment_id로 다시 시작하여 출력을 개선하거나, 후속 작업을 실행하거나, 저장된 명명된 에이전트로 포크할 수 있습니다.
3. 설정
옵션 A: Cloud Shell (권장)
아래 버튼을 클릭하여 Google Cloud Shell에서 이 Codelab을 엽니다. 모든 종속 항목이 사전 설치되어 있습니다.
옵션 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가 하나 있고 다음 단계에 필요한 TODO가 세 개 더 있습니다. 위에는 이미 두 개의 도우미가 미리 입력되어 있습니다.
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()은 핵심 호출입니다. 다음 네 가지 매개변수가 작동합니다.
agent=BASE_AGENT: Antigravity 에이전트 (antigravity-preview-05-2026)를 선택합니다. 이는 기본적으로 Gemini 3.7 Flash로 구동되는 범용 관리 에이전트입니다.agent_config(옵션:gemini-3.7-flash,gemini-3.6-flash,gemini-3.5-flash,gemini-3.5-flash-lite)를 사용하여 기본 모델을 구성할 수 있습니다. 기본적으로 사용 설정된 세 가지 기본 제공 도구(code_execution(Bash, Python, Node.js 실행),google_search,url_context(웹페이지 가져오기 및 읽기))가 함께 제공됩니다.environment매개변수를 전달하면 파일 시스템 도구 (read_file,write_file,list_files)가 자동으로 사용 설정됩니다. 한 번의 호출로 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을 사용하면 다시 돌아갈 수 있습니다. 두 번째 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()의 이벤트에 매핑됩니다.
| 정의 |
|
| 에이전트가 URL을 가져오는 경우 |
|
| 샌드박스에서 코드를 실행하는 에이전트 |
|
| 에이전트가 웹을 검색함 |
|
| 파일 도구 및 기타 |
|
| 상담사가 텍스트를 작성하고 있음 | stdout으로 직접 스트리밍 |
5. 상담사 맞춤설정
에이전트에는 음성, 스킬, PDF 생성기 등 안내가 없었습니다. 이 단계에서는 .agents/에서 구성 파일을 로드하고 샌드박스에 마운트합니다.
변경할 내용
run_digest.py을 다음과 같이 네 가지로 변경합니다.
TODO 2: load_source() 아래에 세 개의 모듈 수준 상수를 추가합니다 (이 상수는 run_digest() 외부에 있으며 파일 상단에 있음).
AGENTS_MD = load_source(".agents/AGENTS.md")
SKILL_MD = load_source(".agents/skills/digest-pdf/SKILL.md")
GENERATE_PDF_PY = load_source(".agents/skills/digest-pdf/scripts/generate_pdf.py")
각 파일을 열어 로드하는 항목을 확인합니다. AGENTS.md는 언론사 성향 및 워크플로 규칙을 설정하고, SKILL.md는 단계별 PDF 플레이북이며, generate_pdf.py는 에이전트가 실행할 사전 빌드된 렌더러입니다.
이제 run_digest() 내에서 두 가지를 더 변경합니다.
TODO 3: environment을 "remote"에서 소스 사전으로 변경하고 input을 "Generate the digest."로 설정합니다.
environment={
"type": "remote",
"sources": [
{
"type": "inline",
"target": ".agents/AGENTS.md",
"content": AGENTS_MD,
},
{
"type": "inline",
"target": ".agents/skills/digest-pdf/SKILL.md",
"content": SKILL_MD,
},
{
"type": "inline",
"target": ".agents/skills/digest-pdf/scripts/generate_pdf.py",
"content": GENERATE_PDF_PY,
},
],
},
TODO 4: print(f"\nDone. environment_id={environment_id}") 바로 뒤에 다음 줄을 추가합니다.
save_env(ENVIRONMENT_ID=environment_id, INTERACTION_ID=interaction_id)
save_env은 이미 run_digest.py에 정의되어 있습니다. 다음 단계에서 에이전트를 다시 실행하지 않고 PDF를 다운로드할 수 있도록 두 ID를 모두 .env에 씁니다.
각 소스의 기능
각 소스는 에이전트가 실행되기 전에 시작 시 샌드박스 파일 시스템에 마운트된 파일입니다. 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
| 변수 | 하네스가 이를 어떻게 처리하는지 |
|
| 영구 안내로 자동 로드됨: 언론사 성향, 워크플로, 실행 규칙 |
|
| 자동 검색되어 이름이 지정된 스킬로 등록됩니다. 에이전트가 이름으로 호출합니다. |
|
| 사전 빌드된 PDF 렌더러. 에이전트가 |
확인
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 호출 후 상단 근처에 다음 두 상수를 추가합니다.
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 목록에 항목 두 개를 더 추가합니다.
{"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 다운로드
에이전트가 샌드박스 내에서 digest.pdf을 /workspace/digest.pdf에 썼습니다. 환경 스냅샷은 Gemini Files API를 통해 tar 보관 파일로 제공됩니다.
필요한 경우 requests을 설치합니다.
uv pip install requests
입력할 내용
download_pdf.py를 엽니다. 할 일(TODO)이 두 개 있습니다.
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")
타르 경로 접두사는 실행마다 다르므로 정확한 경로를 하드 코딩하는 대신 접미사로 검색하세요. 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가 있으며 요약한 모든 스토리를 기억합니다. 두 번째 interactions.create() 호출은 동일한 샌드박스에 후속 메시지를 전송합니다. 여기에서 각 이야기에 '중요한 이유' 메모를 추가해 달라고 요청하면 다시 가져오거나 다시 요약하지 않고 PDF가 업데이트됩니다.
입력할 내용
refine_digest.py를 엽니다. 할 일은 3개입니다.
TODO 1과 2: interactions.create() 내부에 있는 두 개의 멀티턴 매개변수를 채웁니다.
environment=environment_id,
previous_interaction_id=interaction_id,
environment=environment_id는 파일과 패키지가 포함된 동일한 샌드박스를 재개합니다. previous_interaction_id=interaction_id는 에이전트에게 대화 기록을 제공합니다. 첫 번째 호출에서 변경된 사항은 없습니다.
TODO 3: 이벤트 루프 후 새 interaction_id을 .env에 다시 유지합니다.
save_env(INTERACTION_ID=interaction_id)
interactions.create() 호출마다 새 interaction_id가 생성됩니다. 다시 작성하면 다음 실행에서 이 개선사항을 previous_interaction_id로 전달하여 턴이 올바르게 연결됩니다. 샌드박스 ID는 변경되지 않으므로 ENVIRONMENT_ID를 업데이트할 필요가 없습니다.
멀티턴을 작동시키는 두 매개변수
ID | 보존되는 항목 | 비유 |
| 파일, 설치된 패키지, 시스템 상태: Linux 파일 시스템의 모든 항목 | 회의 간에 동일한 사무실 책상 유지 |
| 대화 기록: 이전 턴에서 에이전트가 말하고 실행한 내용 | 지난 회의에서 논의한 내용 기억하기 |
다음과 같이 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 (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 측에 저장되고 향후 호출마다 자동으로 마운트됩니다. 모든 다이제스트 실행에서가 아니라 한 번만 실행합니다.
확인: 에이전트 저장
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에 게시합니다. 커스텀 도구 없음, MCP 서버 등록 없음
기본 요건
GCP 프로젝트에서 Gmail API를 사용 설정하고 OAuth 2.0 클라이언트 ID를 만듭니다.
- console.cloud.google.com/apis/library/gmail.googleapis.com으로 이동하여 Gmail API를 사용 설정합니다.
- API 및 서비스 > 사용자 인증 정보 > 사용자 인증 정보 만들기 > OAuth 2.0 클라이언트 ID로 이동합니다.
- 애플리케이션 유형: 데스크톱 앱. JSON을 다운로드하고 프로젝트 루트에
credentials.json로 저장합니다.
수신자 이메일을 .env에 추가합니다.
RECIPIENT_EMAIL=you@gmail.com
필요한 경우 인증 라이브러리를 설치합니다.
uv sync
입력할 내용
send_digest.py를 엽니다. 할 일(TODO)이 두 개 있습니다.
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에 게시합니다.
확인
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. 일일 실행 예약
지금까지의 모든 단계는 수동으로 트리거되었습니다. 트리거를 사용하면 명명된 에이전트가 크론 표현식에 따라 자동으로 실행되도록 예약할 수 있습니다. 에이전트는 예약된 시간에 실행되고 전체 다이제스트 워크플로를 실행하며, 환경은 실행 간에 유지되므로 첫 번째 실행에 설치된 패키지는 모든 후속 실행에서 사용할 수 있습니다.
Manual: python run_digest.py → runs once, now
Trigger: client.triggers.create() → runs every morning, automatically
입력할 내용
create_trigger.py를 엽니다. TODO가 하나 있습니다.
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. 요약
한 번에 하나의 개념을 사용하여 처음부터 관리 에이전트를 빌드했습니다. 각 연습에서 배운 내용은 다음과 같습니다.
연습 | 개념 | 주요 API |
첫 번째 통화하기 | 실제 Linux 샌드박스를 프로비저닝하고 이벤트를 실시간으로 스트리밍합니다. |
|
상담사 맞춤설정 | 구성 파일 마운트, 동일한 실행에서 ID를 |
|
안전 고리 추가 | 실행되기 전에 도구 호출을 가로채고 파괴적인 명령어 거부 |
|
PDF 다운로드 | 에이전트를 다시 실행하지 않고 PDF 다운로드 |
|
대화 계속하기 | 웹을 다시 가져오지 않고 대화 계속하기 |
|
에이전트 구성 유지 | 에이전트 구성을 유지합니다. ID로 호출하며 소스가 필요하지 않습니다. |
|
Gmail로 보내기 | 로컬에서 OAuth 토큰을 획득하고, | OAuth 2.0, |
매일 실행 예약 | 크론 일정에 따라 에이전트를 자동으로 실행합니다. |
|
주요 패턴
- 하나의 호출, 하나의 샌드박스:
interactions.create()에서 모든 인프라를 처리합니다 (배포할 컨테이너 없음, 로컬에 설치할 패키지 없음). - 프로그레시브 스트리밍:
stream=True는 90초 길이의 검은색 상자를 도구 호출 및 텍스트 청크의 실시간 피드로 변환합니다. - 인라인 소스: 업로드 또는 배포 단계 없이
AGENTS.md,SKILL.md, 사전 빌드된 스크립트를 샌드박스에 마운트 - 하네스 자동 검색:
.agents/에 배치된 파일이 자동으로 선택됩니다 (SDK 구성 필요 없음). - 2차원 상태:
environment_id는 파일과 패키지를 추적하고previous_interaction_id는 대화 컨텍스트를 추적하며 둘 다 독립적으로 전달할 수 있습니다. - 스냅샷 다운로드: 환경은 Gemini Files API를 통해 액세스할 수 있는 전체 파일 시스템 tar입니다.
- 명명된 에이전트:
agents.create()는 구성을 영구적으로 베이킹합니다. 향후 호출에서는 소스 없이 에이전트 ID와environment="remote"만 전달합니다. - 후크:
hooks.json+ 게이트 스크립트가 실행되기 전에 도구 호출을 가로채고deny응답은 호출을 취소하고 상담사가 자체 수정합니다. - 외부 API 호출:
input프롬프트에서 사용자 인증 정보를 전달합니다. 에이전트는code_execution를 통해 샌드박스 내에서 통합 코드를 작성하고 실행합니다. - 트리거:
client.triggers.create()을 사용하여 크론 표현식에 에이전트를 예약합니다. 환경은 실행 간에 유지됩니다.
ADK + Cloud Run과 관리형 에이전트: 차이점 한눈에 보기
기능 | ADK + Cloud Run | Gemini API의 관리형 에이전트 |
샌드박스 프로비저닝 |
|
|
도구 정의 | 에이전트에 등록된 Python 함수 | 기본 제공: 웹 탐색, 코드 실행, 파일 시스템 |
패키지 설치 | Dockerfile의 | 에이전트가 샌드박스 내에서 |
스트림 이벤트 | 맞춤 SSE 인프라 |
|
세션 계속하기 | 세션 데이터베이스 + 컨텍스트 삽입 |
|
구성 파일 | 에이전트에 하드 코딩되거나 시작 시 삽입됨 |
|
관리할 인프라 | 컨테이너, Cloud Run, IAM, 보안 비밀 | 없음 |
다음 단계
- Gemini API의 관리형 에이전트 문서를 읽어보세요.