Module 3: Building AGY Agents with the SDK¶
Duration: ~90 minutes
Goal: Build a production-ready AGY agent from scratch using the google-antigravity Python library — tools, hooks, policy, session state, multi-agent orchestration, and structured output.
Exercises: Exercise 10: Your First Agent · Exercise 11: Multi-Agent Pipeline
📖 Sources: SDK Overview · google-antigravity PyPI · Skills
Why Build an Agent Instead of Just Using the CLI?¶
The CLI is a general-purpose assistant. An agent you build with the SDK is a specialist — it has a narrow job, domain-specific tools, a carefully engineered system prompt, and it can be deployed as a service that your whole team calls.
| Antigravity CLI | AGY SDK Agent | |
|---|---|---|
| Who uses it | Individual developer | Team / API consumers |
| Customization | AGENTS.md + plugins | Full code control |
| Tools | Built-in CLI tools | Any Python function you write |
| Policy | Interactive approval prompts | Programmatic policy.* rules |
| Deployment | Local interactive session | Cloud Run service, callable via API |
| Multi-agent | Subagents in CLI session | asyncio.gather + START_SUBAGENT |
3.1 — SDK Setup 10 min¶
Prerequisites¶
- Python 3.11+
- A Gemini API key — set as
GEMINI_API_KEYor pass viaapi_key=in config
Install¶
Verify¶
from google.antigravity import Agent, LocalAgentConfig
from google.antigravity.hooks import policy
print("google-antigravity installed ✅")
API key vs Vertex AI: For quick local development, use
api_key="AIza..."inLocalAgentConfig. For production on GCP, authenticate withgcloud auth application-default login— the library picks up ADC automatically.
3.2 — Core Primitives: Agent, Config, Tool 20 min¶
The google-antigravity SDK has three building blocks: Agent, LocalAgentConfig, and tools (plain Python functions). Learn these and you can build anything.
The Tool¶
A tool is a plain Python function. No wrapper class, no decorator. The agent decides when to call it based on the docstring — that's the entire interface contract.
def get_file_contents(file_path: str) -> str:
"""Read and return the contents of a file at the given path.
Args:
file_path: Absolute or relative path to the file.
Returns:
The file contents as a string, or an error message if not found.
"""
try:
with open(file_path, "r", encoding="utf-8") as f:
return f.read()
except FileNotFoundError:
return f"Error: File not found at {file_path}"
Critical rules for tools:
- Use explicit type annotations —
str,int,bool,list[str]. Notyping.Optional.- Use a default of
Nonefor optional parameters:param: str = None- The docstring is the tool's schema — the model reads it to decide when and how to call the tool. Write it for the model, not a human.
- Keep tools narrow and focused. One job per tool.
Tools with Session State¶
To read/write session state inside a tool, declare a parameter typed as ToolContext. The SDK auto-detects it, injects it at call time, and strips it from the schema shown to the model:
from google.antigravity.tools.tool_context import ToolContext
def record_finding(
severity: str,
message: str,
ctx: ToolContext,
) -> dict:
"""Records a review finding into session state.
Args:
severity: One of 'critical', 'warning', 'info'.
message: Description of the finding.
Returns:
Confirmation dict with the finding index.
"""
findings = ctx.get_state("findings", [])
findings.append({"severity": severity, "message": message})
ctx.set_state("findings", findings)
return {"status": "recorded", "index": len(findings) - 1}
The Agent + Config¶
Agent is the single entry point. All configuration goes in LocalAgentConfig:
import asyncio
from google.antigravity import Agent, LocalAgentConfig
from google.antigravity.hooks import policy
config = LocalAgentConfig(
model="gemini-3.5-flash",
system_instructions="""You are a code reviewer specialising in Python.
When given a file path, read the file and provide a structured review covering:
- Correctness and edge cases
- Code style and readability
- Security concerns
- Suggested improvements
Always read the file first before commenting. Be specific — cite line numbers.
""",
tools=[get_file_contents, record_finding],
policies=[policy.allow_all()], # autonomous — no interactive prompts
workspaces=["/path/to/project"], # file ops scoped to this directory
)
async def main():
async with Agent(config) as agent:
response = await agent.chat("Review src/auth/login.py")
print(await response.text())
asyncio.run(main())
async with Agent(config) as agent:— always use the context manager. It starts the Go runtime bridge (bin/localharness) and tears it down cleanly on exit.
Model Selection¶
Match the model to the job. Cost-conscious policy:
| Role | Model | Rationale |
|---|---|---|
| General tasks, code review | gemini-3.5-flash | SDK default — cost-efficient, fast |
| Orchestration, routing, planning | gemini-3.1-pro-preview | Complex reasoning, multi-step decisions |
| Image generation tasks | gemini-3.1-flash-image-preview | SDK default for image generation |
| High-stakes analysis | gemini-3.1-pro-preview with ThinkingLevel.HIGH | Deep reasoning for compliance/security |
Never use
gemini-1.5-flash,gemini-1.5-pro. Deprecated.
The Skill¶
Skills are SKILL.md files loaded at runtime to inject domain knowledge. Keep your system prompt lean — load expertise from files:
from pathlib import Path
import re
def load_skill(skill_name: str) -> str:
"""Load a SKILL.md and strip YAML frontmatter."""
skill_path = Path("skills") / skill_name / "SKILL.md"
if skill_path.exists():
content = skill_path.read_text(encoding="utf-8")
return re.sub(r'^---\n.*?---\n', '', content, flags=re.DOTALL).strip()
return ""
review_guidelines = load_skill("python-review")
config = LocalAgentConfig(
model="gemini-3.5-flash",
system_instructions=f"""You are a code reviewer.
## Review Guidelines
{review_guidelines}
""",
tools=[get_file_contents],
policies=[policy.allow_all()],
)
Skills can also be loaded natively via LocalAgentConfig(skills_paths=["/path/to/skills/"]) — the SDK discovers SKILL.md files automatically.
3.3 — Policy and Safety 10 min¶
Policy is the first thing you configure — it controls what the agent is allowed to do without human approval. Every LocalAgentConfig needs a policies= list:
from google.antigravity.hooks import policy
# Fully autonomous — approve all tool calls (use for trusted, sandboxed agents)
policies=[policy.allow_all()]
# Default behaviour — ask user before running shell commands, allow everything else
policies=[policy.confirm_run_command()]
# Fine-grained rules (evaluated in order, first match wins)
async def approval_handler(tool_call) -> bool:
answer = input(f"Allow {tool_call.name}? [y/N]: ")
return answer.lower() == "y"
policies=[
policy.deny("run_command"), # never run shell commands
policy.allow("view_file"), # always allow reading
policy.ask_user("edit_file", handler=approval_handler), # ask before every write
policy.allow("*"), # allow everything else
]
# Conditional deny — block dangerous patterns
policy.deny("run_command", when=lambda args: "rm -rf" in args.get("CommandLine", ""))
# Scope file operations to a specific directory
policy.workspace_only(["/path/to/project"])
Priority order:
specific_deny>specific_ask>specific_allow>wildcard_deny>wildcard_ask>wildcard_allow
3.4 — Hooks: Observability and Control 10 min¶
Hooks let you intercept and react to every event in the agent lifecycle — for logging, auditing, guardrails, or custom approval flows:
from google.antigravity.hooks import hooks
from google.antigravity.types import ToolCall, ToolResult, HookResult
# Block dangerous tool calls BEFORE they execute
@hooks.pre_tool_call_decide
async def security_guard(tool_call: ToolCall) -> HookResult:
if tool_call.name == "run_command":
cmd = tool_call.args.get("CommandLine", "")
if any(danger in cmd for danger in ["rm -rf", "drop table", "DELETE FROM"]):
return HookResult(allow=False, message=f"Blocked dangerous command: {cmd}")
return HookResult(allow=True)
# Log all tool completions (non-blocking, read-only)
@hooks.post_tool_call
async def audit_logger(tool_result: ToolResult) -> None:
print(f"[AUDIT] tool={tool_result.name} success={tool_result.success}")
# Initialise state when a session begins
@hooks.on_session_start
async def initialise_state() -> None:
print("[AGENT] Session started — ready.")
config = LocalAgentConfig(
hooks=[security_guard, audit_logger, initialise_state],
policies=[policy.allow_all()],
model="gemini-3.5-flash",
system_instructions="You are a code reviewer.",
tools=[get_file_contents],
)
Hook types:
| Hook | Blocks execution | Modifies data | Use for |
|---|---|---|---|
@hooks.pre_tool_call_decide | Yes | No | Approve/deny tool calls |
@hooks.post_tool_call | No | No | Logging, metrics |
@hooks.pre_turn | No | No | Turn-level logging |
@hooks.post_turn | No | No | Response logging |
@hooks.on_session_start/end | No | No | Setup/teardown |
@hooks.on_tool_error | Yes | Yes | Error recovery |
3.5 — Multi-Agent Orchestration 15 min¶
google-antigravity has no SequentialAgent or ParallelAgent classes. Multi-agent is done two ways: model-driven (let the agent spawn subagents) or Python-driven (you orchestrate Agent instances directly).
Pattern A — Model-Driven Subagents¶
Enable START_SUBAGENT in capabilities. The model calls it when it decides to delegate:
from google.antigravity.types import BuiltinTools, CapabilitiesConfig
config = LocalAgentConfig(
capabilities=CapabilitiesConfig(
enable_subagents=True,
enabled_tools=BuiltinTools.all_tools(),
),
policies=[policy.allow_all()],
model="gemini-3.1-pro-preview",
system_instructions="""You are an engineering lead.
For complex tasks, spawn focused subagents to handle each part in parallel.
Synthesise their outputs into a final summary.""",
)
Pattern B — Sequential Pipeline (Python-Driven)¶
Pass the output of one agent as the input to the next:
async def sequential_review(file_path: str):
# Step 1 — read and summarise the file
async with Agent(reader_config) as reader:
r1 = await reader.chat(f"Read and summarise {file_path}")
summary = await r1.text()
# Step 2 — security audit using the summary
async with Agent(security_config) as auditor:
r2 = await auditor.chat(f"Security audit this code summary:\n\n{summary}")
report = await r2.text()
return report
Pattern C — Parallel Analysis¶
Run independent agents simultaneously with asyncio.gather:
async def parallel_analysis(file_path: str):
async with (
Agent(security_config) as security_agent,
Agent(style_config) as style_agent,
Agent(perf_config) as perf_agent,
):
results = await asyncio.gather(
security_agent.chat(f"Security review: {file_path}"),
style_agent.chat(f"Style review: {file_path}"),
perf_agent.chat(f"Performance review: {file_path}"),
)
texts = await asyncio.gather(*[r.text() for r in results])
return {
"security": texts[0],
"style": texts[1],
"perf": texts[2],
}
When to use parallel: Any time you have N independent analyses. This cuts wall-clock time by 60–80% compared to running them sequentially.
3.6 — Streaming and Structured Output 5 min¶
Streaming Responses¶
async with Agent(config) as agent:
response = await agent.chat("Write a detailed security report...")
# Stream text deltas as they arrive
async for delta in response:
print(delta, end="", flush=True)
# Stream reasoning/thinking (if thinking enabled)
async for thought in response.thoughts:
print(f"[thinking] {thought}")
Structured Output¶
Bind the agent's output to a Pydantic schema:
import asyncio
import pydantic
from google.antigravity import Agent, LocalAgentConfig
from google.antigravity.hooks import policy
class ReviewResult(pydantic.BaseModel):
issues: list[str]
severity: str # 'critical' | 'warning' | 'info'
recommendation: str
config = LocalAgentConfig(
response_schema=ReviewResult,
system_instructions="Analyse the code and return structured output via the finish tool.",
policies=[policy.allow_all()],
model="gemini-3.5-flash",
tools=[get_file_contents],
)
async def main():
async with Agent(config) as agent:
response = await agent.chat("Review src/auth/login.py")
result = await response.structured_output() # dict matching ReviewResult schema
print(result["severity"], result["issues"])
asyncio.run(main())
3.7 — Session Resume and Persistence 5 min¶
# First session — save the conversation ID
async with Agent(config) as agent:
await agent.chat("Analyse this codebase and build a mental model.")
conv_id = agent.conversation_id # persist this
# Later session — resume exactly where you left off
resume_config = LocalAgentConfig(
conversation_id=conv_id,
save_dir="/path/where/first/session/was/saved",
model="gemini-3.5-flash",
policies=[policy.allow_all()],
)
async with Agent(resume_config) as agent:
await agent.chat("Now suggest the top 3 refactoring priorities.")
3.8 — Triggers: Autonomous Background Agents 5 min¶
import asyncio
from google.antigravity import Agent, LocalAgentConfig
from google.antigravity.hooks import policy
from google.antigravity.triggers import every, on_file_change, TriggerContext
# Poll every 60 seconds
async def check_for_new_issues(ctx: TriggerContext) -> None:
await ctx.send("Scan the repo for any new TODO comments added since last run.")
# React to file changes
async def on_code_change(ctx: TriggerContext, changes) -> None:
paths = [c.path for c in changes]
await ctx.send(f"Files changed: {paths}. Run quick security check.")
config = LocalAgentConfig(
triggers=[
every(60.0, check_for_new_issues),
on_file_change("/path/to/src", on_code_change),
],
policies=[policy.allow_all()],
model="gemini-3.5-flash",
system_instructions="You are a background code monitor.",
)
async def main():
# The agent runs indefinitely, responding to triggers
async with Agent(config) as agent:
await asyncio.Event().wait() # keep alive
asyncio.run(main())
3.9 — Project Structure Conventions 5 min¶
Structure your agent project for maintainability:
my_agent/
├── main.py # entry point — asyncio.run(main())
├── config.py # LocalAgentConfig construction
├── tools/
│ ├── __init__.py
│ ├── file_reader.py # one tool per file
│ └── search_tool.py
├── hooks/
│ ├── __init__.py
│ └── security_guard.py # pre_tool_call_decide hooks
├── skills/
│ └── domain-expertise/
│ └── SKILL.md # portable skill packs
├── tests/
│ ├── test_file_reader.py
│ └── test_search_tool.py
├── requirements.txt # google-antigravity + deps
└── README.md
Deployment to Cloud Run¶
Deploy as a standard Python async application:
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .
CMD ["python", "main.py"]
gcloud run deploy my-code-reviewer \
--source . \
--project $GOOGLE_CLOUD_PROJECT \
--region $GOOGLE_CLOUD_REGION \
--allow-unauthenticated
Tip: Set
GOOGLE_CLOUD_PROJECTandGOOGLE_CLOUD_REGION(e.g.us-central1) before running.
Hands-On Exercises¶
Exercise 10: Your First AGY Agent¶
File: ex10_first_agent.md
Duration: 45 min
Build: A Code Review Agent that reads files, identifies issues, and produces a structured review report.
What you'll implement:
- Define 3 tools:
read_file,list_directory,record_finding(withToolContext) - Write a system prompt with a review rubric (loaded from a
SKILL.md) - Configure
LocalAgentConfigwithpolicy.allow_all()andCapabilitiesConfig - Add a
@hooks.pre_tool_call_decidesecurity guard - Run with streaming output and structured
ReviewResultPydantic schema
Exercise 11: Multi-Agent Pipeline¶
File: ex11_multi_agent_pipeline.md
Duration: 45 min
Build: A Write-then-Audit Pipeline — a Technical Writer agent produces a document, then a Compliance Analyst audits it for GDPR gaps.
What you'll implement:
- Build a
technical_writeragent with a GDPR SKILL.md loaded viaskills_paths - Build a
compliance_analystagent withresponse_schema=ComplianceReport - Wire them sequentially: output of writer passed as input to analyst
- Add a parallel variant using
asyncio.gatherfor simultaneous draft + legal check - Add session resume: analyst reads the writer's
conversation_idto load context - Deploy to Cloud Run as
my-pipelineusinggcloud run deploy
Summary: SDK Building Blocks¶
| Primitive | What It Does | When to Use |
|---|---|---|
Agent | Single LLM agent with tools, hooks, policy | The core — every agent starts here |
LocalAgentConfig | All config in one place (model, tools, policy, hooks) | Always |
tools=[fn] | Plain Python callable, docstring is the schema | Any external operation |
ToolContext | State read/write injected into tools | Stateful tools in pipelines |
policy.allow_all() | Approve all tool calls autonomously | Trusted, sandboxed agents |
policy.deny("run_command") | Block specific tool types | Safety guardrails |
@hooks.pre_tool_call_decide | Block/approve tool calls before execution | Security guards |
@hooks.post_tool_call | Observe completed tool calls | Audit logging |
response_schema= | Bind output to Pydantic schema | Structured data extraction |
async for delta in response: | Stream text as it arrives | Long-form generation |
asyncio.gather(...) | Run agents in parallel | Independent analyses |
every(60, handler) | Trigger agent on interval | Background monitors |
on_file_change(path, fn) | Trigger agent on filesystem events | Live code watchers |
skills_paths=[...] | Load SKILL.md files at runtime | Portable domain expertise |
conversation_id= | Resume a previous session | Multi-session workflows |
Next Step¶
→ Continue to Module 4: Multi-Agent & Advanced Patterns
→ Reference: Cheatsheet — all commands in one place