Agent Platform BYOC without the wrapper: new in ADK 2.2
Copy as MarkdownIn The Agent Platform API contract I walked through the two routes a bring-your-own-container agent has to serve — /api/reasoning_engine and /api/stream_reasoning_engine — and the FastAPI wrapper I wrote to dispatch them onto an AdkApp. It worked, but it was 117 lines of plumbing that had nothing to do with the agent itself.
Good news: as of ADK 2.2.0 you can delete that wrapper. A new flag, --gemini_enterprise_app_name, teaches the plain adk api_server to serve those exact routes itself. The name is a little misleading for our purposes — the flag exists so you can register a self-hosted ADK server as an agent in Gemini Enterprise — but Gemini Enterprise speaks the Agent Engine wire protocol, and that’s precisely the protocol Agent Runtime forwards to a BYOC container. Set the flag, and the same container image from 4 ways to deploy agents on Agent Platform (option 4) serves the whole contract with no custom code.
And this isn’t a side door: adk deploy agent_engine in ADK 2.x generates exactly this arrangement under the hood — a Dockerfile running adk api_server with --gemini_enterprise_app_name set. We’re just doing the same thing with our own image, on our own terms. You’ll need google-adk>=2.2.0 in requirements.txt (the flag doesn’t exist in 1.x or 2.0/2.1).
The wrapper we get to delete
Here’s the “before” picture. The container served a hand-written FastAPI app instead of adk api_server:
FROM python:3.12-slim
WORKDIR /app
COPY trading_agent/requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
RUN mkdir -p trading_agent
COPY trading_agent/agent.py trading_agent/
COPY main.py .
# Agent Runtime injects $PORT. Serve the BYOC FastAPI wrapper (not adk api_server)
# so the container speaks the :query / :streamQuery contract. Sessions + Memory
# come from SESSION_SERVICE_URI / MEMORY_SERVICE_URI in the deployment env.
CMD ["sh", "-c", "uvicorn main:app --host 0.0.0.0 --port ${PORT:-8080}"]
And that main.py is the wrapper: wrap the agent in AdkApp, then dispatch whatever class_method the platform forwards. The heart of it:
adk_app = agent_engines.AdkApp(agent=root_agent)
@app.post("/api/reasoning_engine")
async def query(http_request: Request) -> responses.JSONResponse:
request = await _parse_request(http_request)
method = getattr(adk_app, request.class_method)
output = await _invoke_callable_or_raise(method, request.input or {})
return responses.JSONResponse(
content=encoders.jsonable_encoder({"output": output})
)
@app.post("/api/stream_reasoning_engine")
async def stream_query(http_request: Request) -> responses.StreamingResponse:
request = await _parse_request(http_request)
method = getattr(adk_app, request.class_method)
output = await _invoke_callable_or_raise(method, request.input or {})
return responses.StreamingResponse(
content=json_generator(output),
media_type="application/json",
)
Plus the parts you don’t see here: JSON-encoding each streamed chunk, handling both sync and async generators, and parsing the request body defensively. The full 117 lines are in main.py if you want to appreciate exactly what we’re about to not maintain.
The Dockerfile, after
The “after” is the general-purpose ADK container from chapter 5 of the book — the one that already runs locally, on Cloud Run, and on GKE — with two additions to the CMD:
FROM python:3.12-slim
WORKDIR /app
COPY trading_agent/requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
RUN mkdir -p trading_agent
COPY trading_agent/agent.py trading_agent/
# Sessions + Memory precedence: explicit SESSION_SERVICE_URI / MEMORY_SERVICE_URI
# win; otherwise, when Agent Runtime injects GOOGLE_CLOUD_AGENT_ENGINE_ID (a BYOC
# deploy, see ../09_DeployContainerToAgentPlatform), self-wire both to the
# engine's own resource; with neither set, fall back to local storage.
CMD ["sh", "-c", ": ${SESSION_SERVICE_URI:=${GOOGLE_CLOUD_AGENT_ENGINE_ID:+agentengine://$GOOGLE_CLOUD_AGENT_ENGINE_ID}}; : ${MEMORY_SERVICE_URI:=${GOOGLE_CLOUD_AGENT_ENGINE_ID:+agentengine://$GOOGLE_CLOUD_AGENT_ENGINE_ID}}; adk api_server --host 0.0.0.0 --port ${PORT:-8080} ${SESSION_SERVICE_URI:+--session_service_uri $SESSION_SERVICE_URI} ${MEMORY_SERVICE_URI:+--memory_service_uri $MEMORY_SERVICE_URI} ${GEMINI_ENTERPRISE_APP_NAME:+--gemini_enterprise_app_name $GEMINI_ENTERPRISE_APP_NAME} --no-reload /app"]
No main.py in the image at all. When --gemini_enterprise_app_name is set, adk api_server registers POST /api/reasoning_engine and POST /api/stream_reasoning_engine alongside its normal routes, dispatching to an AdkApp internally — the same getattr-on-class_method dance our wrapper did, with the same thirteen-method allowlist we declare as classMethods at deploy time. It also adds a middleware that picks up the platform’s trace header, which the wrapper never bothered with.
One rule to know: the flag’s value must be the name of an agent folder in the image (trading_agent here). It’s not a display name — it tells the server which agent plays the role of the engine, and it refuses to boot if there’s no match.
The env variable dance
That CMD is dense, so here it is again, wrapped so you can actually read it:
CMD ["sh", "-c", ": ${SESSION_SERVICE_URI:=${GOOGLE_CLOUD_AGENT_ENGINE_ID:+agentengine://$GOOGLE_CLOUD_AGENT_ENGINE_ID}}; : ${MEMORY_SERVICE_URI:=${GOOGLE_CLOUD_AGENT_ENGINE_ID:+agentengine://$GOOGLE_CLOUD_AGENT_ENGINE_ID}}; adk api_server --host 0.0.0.0 --port ${PORT:-8080} ${SESSION_SERVICE_URI:+--session_service_uri $SESSION_SERVICE_URI} ${MEMORY_SERVICE_URI:+--memory_service_uri $MEMORY_SERVICE_URI} ${GEMINI_ENTERPRISE_APP_NAME:+--gemini_enterprise_app_name $GEMINI_ENTERPRISE_APP_NAME} --no-reload /app"]
Let’s unpack it — there are two tricks layered in there, and they’re what let one image serve every platform.
Trick one: opt-in flags. ${VAR:+--flag $VAR} is shell parameter expansion that produces the flag only when the variable is set, and nothing at all otherwise. So the contract routes aren’t hardcoded on — they light up only when the deployment provides GEMINI_ENTERPRISE_APP_NAME=trading_agent in its env. Run the same image locally with just an API key and it’s a bare adk api_server, exactly as it was before this change. That matters more than it looks: hardcoding --gemini_enterprise_app_name would make the server demand Google Cloud credentials at boot, and the local docker run workflow would crash on startup. With the env-gating, nobody who isn’t deploying to Agent Platform ever notices the feature exists.
Trick two: self-wiring sessions and memory. The two statements at the front — : ${SESSION_SERVICE_URI:=...} — use the shell’s no-op command (:) purely for its side effect: assign this variable a default if it’s unset. And the default is itself conditional: agentengine://$GOOGLE_CLOUD_AGENT_ENGINE_ID, only if that variable exists.
GOOGLE_CLOUD_AGENT_ENGINE_ID is the key. Agent Runtime injects it into every BYOC container, and it’s the engine’s own resource id. So on the platform, with no explicit URIs configured, the container points sessions and Memory Bank at its own reasoningEngine resource — the agent is fully self-contained, state and all, just like the old wrapper’s AdkApp.set_up() behavior. ADK happily accepts the bare id (no full projects/.../locations/... path) because it fills in the project and location from GOOGLE_CLOUD_PROJECT and GOOGLE_CLOUD_LOCATION — which the platform also injects.
Putting it together, the precedence is:
- Explicit
SESSION_SERVICE_URI/MEMORY_SERVICE_URI(from the deploy env, or yourdocker-envfile) — used as-is. This is how you’d point several agents at one shared sessions backend. - No URIs, but running on Agent Runtime — self-wire to the engine’s own resource via the injected id.
- Neither (a plain local
docker run) — non-persistent local storage, fine for a quick spin.
Why do this at boot instead of just setting SESSION_SERVICE_URI at deploy time? Because you can’t: the engine’s id is minted by the very create call that carries the env vars, so there’s nothing to reference when you write them. (And no, setting the value to the literal string agentengine://$GOOGLE_CLOUD_AGENT_ENGINE_ID doesn’t work either — the shell doesn’t re-expand variables found inside another variable’s value.) The runtime injecting the id is the escape hatch, and the CMD is the one place that can use it.
Deploying it
The deploy script barely changes. Build and push the chapter 5 image instead of the wrapper image, and add one line to the deployment env in the create request:
"env": [
{"name": "GOOGLE_GENAI_USE_VERTEXAI", "value": "TRUE"},
{"name": "GEMINI_ENTERPRISE_APP_NAME", "value": "trading_agent"},
...
]
Everything else — the service account, the Artifact Registry IAM, and notably the classMethods list — stays exactly as it was in deploy_byoc.sh. You still declare classMethods even though ADK now owns the serving side: the platform can’t introspect a container, the SDK builds its client methods from that list, and it’s the same thirteen entries adk deploy agent_engine declares for you on the managed path. The contract post covers what each method is for.
One wrinkle I hit running this for real: calling the sync stream_query method logs a scary RuntimeError: coroutine raised StopIteration traceback at the end of every stream. It’s a bug in ADK’s sync-generator bridging (present through 2.4.0 at the time of writing) — the error fires after the last event has been delivered, so the stream itself is complete and correct, just noisy in the logs. Call async_stream_query instead and you avoid the buggy code path entirely.
And that’s it — trade.sh runs against the new deployment unchanged, sessions land in the engine’s own resource, and there’s one less file to maintain. The wrapper had a good run.
Draft generated from my working session migrating the book samples to ADK 2.2. –William