# 4 ways to deploy agents on Agent Platform So you've built an ADK agent—in my case the [Trading Agent](/ap/) from my post series, an agent that reads S&P 500 news, scores the sentiment, and places paper trades on Alpaca—and now you want it running on Agent Platform's managed runtime instead of on your laptop. It turns out there are actually *four* different ways to package your agent in runtime, each with some trade offs. This post explores those options and when you might want to use each one. In [Deploy an ADK agent to Agent Platform Runtime](/ap/1-agent/1.2-deploy-to-ap-runtime/) we have a deploy script that hands the ADK SDK a live agent object and lets it figure everything out. Behind the scenes it's serializing the live object graph, writing it to a storage bucket and deserializing in the manged runtime. This is enough for prototyping, but in production you likely want to deploy a container as a robust, reproducable artifact. So we have the live object graph at one end of the spectrum, a full container at the other. In between are two additional options that deploy the source code, the first that serializes a stub object, and the second a full source deploy. As you go from one end of the spectrum, you're essentially trading off convenience for reliability and robustness. For anything more than a prototype, I would recommend the container approach (option 4), or at least the full source deploy with no object serialization (option 3). For prototyping, it doesn't really matter just be OK with it potentially breaking. Let's dig into these options and the trade offs. Comment: The rest of this post was generated with input from my research into the various methods of deploying agents to Agent Platform. –William All four options deploy to the *same* place — the Agent Platform managed runtime (the API resource is a `reasoningEngine`; you may know the runtime by its old name, Vertex AI Agent Engine). This post isn't about Cloud Run or GKE — those are a different path (the repo covers them in `06_DeployToCloudRun` and `07_DeployToGKE`), and I've got a [separate post on wiring memory into those](/agents/other-services/). Here we're staying inside the managed runtime the whole way. Two common threads run through all four options. The first is **who declares the API surface** — the `class_methods` list that tells the platform which methods to route `:query` versus `:streamQuery` to. The second is **the scaffolding** each option needs — a staging bucket, a source tarball, or a full container-plus-registry-plus-IAM setup. Watch those two as we go from convenient to production-grade. ## Option 1: pass the live object (cloudpickle) The most convenient way is to hand the SDK your live, instantiated `app` object and let it do everything. ```python from trading_agent.agent import app # the live AdkApp(agent=root_agent) remote_agent = client.agent_engines.create( agent=app, config={ "staging_bucket": STAGING_BUCKET, "display_name": "Trading Agent ADK", "requirements": "trading_agent/requirements.txt", "extra_packages": ["trading_agent/agent.py"], "env_vars": { "APCA_API_KEY_ID": os.getenv("APCA_API_KEY_ID", ""), "APCA_API_SECRET_KEY": os.getenv("APCA_API_SECRET_KEY", ""), "GOOGLE_GENAI_USE_VERTEXAI": "TRUE", }, }, ) ``` Under the hood, the SDK **cloudpickles the entire live object graph** — the `AdkApp`, the `root_agent` inside it, the tool closures, and the live Alpaca clients captured at import time — into an `agent_engine.pkl` in your GCS staging bucket. Your `requirements.txt` gets installed remotely, `extra_packages` ships `agent.py`, and the `class_methods` (the API surface) are **auto-generated** by introspecting that live object. You never declare the method list. Neat! For convenience this is as easy as it gets: a few lines, no API surface to declare, and no infra beyond a staging bucket. The catch is robustness. Cloudpickling a live, stateful graph is exactly the kind of thing that's fragile. If the Python or library versions on your machine don't match the runtime, the remote un-pickle can fail. Unpicklable objects — sockets, locks, protobuf descriptors — break the dump outright, and the SDK will even warn you about protobuf objects imported at module level. And the `.pkl` is opaque: you can't read it, diff it, or reproduce it. Look at our Trading Agent's `agent.py` and you can see the trap being set — it constructs live `TradingClient`, `NewsClient`, and `StockHistoricalDataClient` HTTP clients at module import time. That live client state is exactly the sort of thing that gets brittle inside a pickle. Great for a quick prototype; risky as your production path. ## Option 2: the stub agent (ModuleAgent), still cloudpickle If Option 1's pickle is what's biting you, here's the low-effort upgrade. Instead of the live object, you pass a lightweight `ModuleAgent` that only records *where* to find the agent — the module name, the object name, and the operations to expose. ```python from vertexai import agent_engines REGISTER_OPERATIONS = { "": ["get_session", "list_sessions", "create_session", "delete_session"], "async": ["async_get_session", "async_list_sessions", "async_create_session", "async_delete_session", "async_add_session_to_memory", "async_search_memory"], "stream": ["stream_query"], "async_stream": ["async_stream_query", "streaming_agent_run_with_events"], } remote_agent = client.agent_engines.create( agent=agent_engines.ModuleAgent( module_name="trading_agent.agent", # imported on the server agent_name="app", # the AdkApp instance in agent.py register_operations=REGISTER_OPERATIONS, ), config={ "staging_bucket": STAGING_BUCKET, "requirements": "trading_agent/requirements.txt", "extra_packages": ["trading_agent/agent.py"], "env_vars": { ... }, }, ) ``` This is still cloudpickled — but now the pickled thing is just some strings and a dict, not the live graph. The real code ships as source via `extra_packages`, and at runtime the platform imports `app` from `trading_agent.agent` and calls `set_up()`. The `class_methods` are still auto-generated, this time via a local dry-run that imports the module on your machine. One detail that'll trip you up: `agent_name` must point at the `AdkApp` (`"app"`), **not** the raw `root_agent`. `ModuleAgent` uses the object as-is with no auto-wrapping, and it's the `AdkApp` — not the bare agent — that exposes `create_session`, `stream_query`, and the rest. This is a small change from Option 1, and the robustness jump is big: the brittle live-object pickle is gone, and the stub is trivially portable across versions. That said, plenty *hasn't* changed — it still uses cloudpickle (a `.pkl` is still written), it still needs a staging bucket, and the deploy still imports your agent locally to build `class_methods`. So the deploy box needs the deps and the env set, and `agent.py`'s `sys.exit` guard will fire at deploy time if your `APCA_API_KEY_ID` is unset. Dependency-version parity still matters too — but a mismatch is now a normal pip problem, not a corrupt pickle. This is the version currently in `01_TradingAgent/deploy.py`. ## Option 3: source deploy (no pickle) Options 1 and 2 both still wrote a `.pkl`. Option 3 drops pickle entirely. You omit `agent=` and instead give the SDK your source packages and an entrypoint. ```python CLASS_METHODS = [ {"api_mode": "", "name": "create_session"}, {"api_mode": "", "name": "get_session"}, {"api_mode": "", "name": "list_sessions"}, {"api_mode": "", "name": "delete_session"}, {"api_mode": "stream", "name": "stream_query"}, {"api_mode": "async_stream", "name": "async_stream_query"}, # ... plus the async_* session + memory ops ... ] remote_agent = client.agent_engines.create( config={ "source_packages": ["trading_agent"], "entrypoint_module": "trading_agent.agent", "entrypoint_object": "root_agent", # platform wraps it in AdkApp "agent_framework": "google-adk", "requirements_file": "trading_agent/requirements.txt", "class_methods": CLASS_METHODS, "env_vars": { ... }, }, ) ``` Under the hood the SDK tars and gzips your source and base64-inlines it right in the create request — no staging bucket, no pickle, nothing to provision. At runtime the platform imports `entrypoint_object` from `entrypoint_module`, and because we set `agent_framework="google-adk"`, it wraps `root_agent` in an `AdkApp` for you. Notice we point at the raw `root_agent` here, not `app` — the platform does the wrapping this time. The catch: because there's no live object to introspect, you **must** declare `class_methods` yourself. But don't let that scare you off — the minimal form is just `{"api_mode": ..., "name": ...}` pairs, the same short list the container deploy uses. It's **not** hand-written OpenAPI parameter schemas. The full parameter schemas are optional metadata that only enrich the input form in the console playground; routing only needs the name and the `api_mode`. So it's about a 13-line list you write once. And here's a nice one: this can be done with **no Python at all**. It's a plain REST call, exactly like the container deploy, because the source archive is just a base64 gzip tarball. ```shell ARCHIVE=$(tar czf - trading_agent | base64 -w0) curl -s -X POST \ -H "Authorization: Bearer $(gcloud auth print-access-token)" \ -H "Content-Type: application/json" \ "https://${REGION}-aiplatform.googleapis.com/v1/projects/${PROJECT_ID}/locations/${REGION}/reasoningEngines" \ -d @- <@gcp-sa-aiplatform-re.iam.gserviceaccount.com`) needs `artifactregistry.reader` so the platform can pull your image, and the deployer needs `iam.serviceAccountUser` on the runtime service account. The deploy itself is a REST call — all bash, no Python SDK — in `09_DeployContainerToAgentPlatform/deploy_byoc.sh`. It's a `create` POST to `reasoningEngines` carrying `spec.containerSpec.imageUri`, `spec.deploymentSpec` (env plus `resourceLimits`), `spec.agentFramework="google-adk"`, `spec.serviceAccount`, and `spec.classMethods` — the same short name-plus-`api_mode` list. ```shell curl -s -X POST \ -H "Authorization: Bearer $(gcloud auth print-access-token)" \ -H "Content-Type: application/json" \ "https://${REGION}-aiplatform.googleapis.com/v1/projects/${PROJECT_ID}/locations/${REGION}/reasoningEngines" \ -d @- <