Skip to main content

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:

  1. A GCP project with aiplatform.googleapis.com enabled and a staging GCS bucket (Agent Engine stages the deploy archive there).
  2. A runtime service account for the agent, with: roles/aiplatform.user, roles/cloudtrace.agent, roles/logging.logWriter, roles/storage.objectViewer (on the staging bucket), and roles/secretmanager.secretAccessor if 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.
  3. A deploy principal (the identity running deploy.py) with aiplatform.reasoningEngines.create, write access to the staging bucket, and iam.serviceAccounts.actAs on 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.