Vertex AI Agent Engine
5-minute deploy · pure Python, no Dockerfile · Google-managed runtime
Vertex AI Agent Engine hosts your agent from a live Python
object: you hand agent_engines.create() an instance of your
agent class, the SDK serializes it, and the platform builds and
runs the container for you — installing whatever is in your
requirements list. waxell-observe ships as one of those
requirements and initializes inside the managed container, so the
Agent Engine boundary is transparent to the SDK.
What you get
- Every LLM call your LangGraph agent makes → captured as a child LLM span on the parent run.
- Every tool call in the agent loop → captured as tool spans, no manual per-tool wrapping (the LangGraph instrumentor handles it).
- The full span trace of each run streams natively into your Waxell dashboard — no Cloud Trace pull, and your run history persists beyond Cloud Trace's 30-day retention window.
- Policies you author in Waxell and scope to the agent evaluate against its runs, with results in your governance dashboard.
Agent Engine's own OpenTelemetry export to Cloud Trace keeps working alongside Waxell — the two are independent.
Prerequisites
pip install "google-cloud-aiplatform[agent_engines,langchain,langgraph]"
gcloud auth application-default login
You also need:
- A GCP project with
aiplatform.googleapis.comenabled and a staging GCS bucket (Agent Engine stages the deploy archive there). - A runtime service account for the agent, with:
roles/aiplatform.user,roles/cloudtrace.agent,roles/logging.logWriter,roles/storage.objectViewer(on the staging bucket), androles/secretmanager.secretAccessorif you store the Waxell key in Secret Manager. Passing your own service account keeps the agent on a least-privilege identity instead of the platform default. - A deploy principal (the identity running
deploy.py) withaiplatform.reasoningEngines.create, write access to the staging bucket, andiam.serviceAccounts.actAson the runtime service account. Deploying executes code, so keep this principal separate from any read-only credentials.
Project layout
Three files: requirements.txt, agent.py, deploy.py.
requirements.txt — this list is shipped to Agent Engine, which
pip-installs it into the managed container (there is no
Dockerfile):
google-cloud-aiplatform[agent_engines,langchain,langgraph]>=1.128.0,<2.0.0
langchain-google-vertexai
langgraph>=0.2
langchain-core
cloudpickle
waxell-observe
# Agent Engine's managed runtime requires OpenTelemetry 1.43.x
opentelemetry-api==1.43.0
opentelemetry-sdk==1.43.0
agent.py — Agent Engine serializes your object on the deploy
machine and deserializes it in the managed container, so
__init__ holds plain config only; all runtime initialization
happens in set_up(), which runs inside the container. That makes
set_up() the place to arm waxell-observe:
import os
AGENT_NAME = "my-vertex-agent"
def _build_tool():
from langchain_core.tools import tool
@tool
def lookup_account(account_id: str) -> dict:
"""Look up an account's tier and region."""
return {
"account_id": account_id,
"tier": "enterprise",
"region": "us-central1",
}
return lookup_account
class MyVertexAgent:
"""A LangGraph react agent instrumented with waxell-observe."""
def __init__(self, model_name: str | None = None) -> None:
# Config only — this object is serialized at deploy time.
self.model_name = model_name or os.environ.get(
"MODEL_NAME", "gemini-2.5-flash"
)
self._graph = None
def set_up(self) -> None:
"""Runs inside the Agent Engine container."""
# Arm waxell-observe first. init() reads WAXELL_API_KEY /
# WAXELL_API_URL from env and auto-instruments LangGraph.
if os.environ.get("WAXELL_API_KEY"):
try:
import waxell_observe as waxell
waxell.init()
except Exception: # never let observability break the runtime
pass
from langchain_google_vertexai import ChatVertexAI
from langgraph.prebuilt import create_react_agent
model = ChatVertexAI(model=self.model_name, temperature=0.2)
graph = create_react_agent(
model,
tools=[_build_tool()],
prompt="You are a concise account assistant. One or two sentences.",
)
# LangGraph runs are attributed to graph.name — keep it equal
# to the deployed display_name.
graph.name = AGENT_NAME
self._graph = graph
def _ensure(self):
if self._graph is None:
self.set_up()
return self._graph
def query(self, input: str) -> dict:
graph = self._ensure()
result = graph.invoke({"messages": [("user", input)]})
msgs = result.get("messages", [])
text = getattr(msgs[-1], "content", str(msgs[-1])) if msgs else ""
return {"output": text}
def stream_query(self, input: str):
graph = self._ensure()
for chunk in graph.stream(
{"messages": [("user", input)]}, stream_mode="updates"
):
yield chunk
deploy.py — two things to note: register your agent module for
pickling by value so its code is embedded in the deploy
archive, and pass agent_engines.create() flat keyword
arguments (not a config= dict):
import os
import cloudpickle
import vertexai
from vertexai import agent_engines
import agent as agent_module
from agent import AGENT_NAME, MyVertexAgent
# Embed the agent module's code in the deploy archive so the
# managed container can load it.
cloudpickle.register_pickle_by_value(agent_module)
PROJECT = os.environ["PROJECT"] # your-project
REGION = os.environ.get("REGION", "us-central1")
STAGING = os.environ["STAGING"] # gs://your-staging-bucket
RUNTIME_SA = os.environ["RUNTIME_SA"] # agent-runtime@your-project.iam.gserviceaccount.com
WAXELL_API_KEY = os.environ["WAXELL_API_KEY"]
WAXELL_API_URL = os.environ.get("WAXELL_API_URL", "https://api.waxell.dev")
vertexai.init(project=PROJECT, location=REGION, staging_bucket=STAGING)
with open("requirements.txt") as fh:
requirements = [
line.strip()
for line in fh
if line.strip() and not line.startswith("#")
]
remote = agent_engines.create(
MyVertexAgent(),
display_name=AGENT_NAME, # keep equal to graph.name
requirements=requirements,
env_vars={
"WAXELL_API_URL": WAXELL_API_URL,
"WAXELL_API_KEY": WAXELL_API_KEY, # prefer a Secret Manager ref
"GOOGLE_CLOUD_AGENT_ENGINE_ENABLE_TELEMETRY": "true",
},
service_account=RUNTIME_SA,
min_instances=1, # keep warm; default 0 scales to zero
)
print("RESOURCE_NAME:", remote.resource_name)
Deploy
export WAXELL_API_KEY=<your-key> # from `wax setup` or your .env
PROJECT=your-project \
REGION=us-central1 \
STAGING=gs://your-staging-bucket \
RUNTIME_SA=agent-runtime@your-project.iam.gserviceaccount.com \
python deploy.py
Agent Engine stages the archive, builds the container, and pip-installs your requirements — first deploys take a few minutes.
Invoke
remote.query(input="Look up account acct-001 and give its tier and region.")
Then check Waxell:
wax runs list --limit 5
You'll see the run under agent_name=my-vertex-agent — parent
agent span, an LLM child span for each Gemini call, and a tool
span for each lookup_account call.
Network egress
Standard Agent Engine deployments have public egress, so
api.waxell.dev (HTTPS) is reachable out of the box. If your
project sits inside a VPC Service Controls perimeter or uses
Private Service Connect, add an egress rule allowing HTTPS to your
Waxell endpoint so runs keep streaming.
Policies
Policies you author in Waxell and scope to my-vertex-agent
evaluate as the agent runs — author them once against the agent
name, and the same policy set follows the agent across every
environment it's deployed to, with results and audit records in
your Waxell governance dashboard.