Agent Platform BYOC without the wrapper: new in ADK 2.2

Copy as Markdown

In The Agent Platform API contract I described 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 to an AdkApp. It worked, but the wrapper contained 117 lines of code unrelated to the agent itself.

I wrote this post after updating the site’s agent examples to ADK 2.2. –William

As of ADK 2.2.0, you can delete that wrapper. A new flag, --gemini_enterprise_app_name, lets adk api_server serve those routes directly. 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 uses the Agent Engine wire protocol, which is also the protocol Agent Runtime uses to forward requests to a BYOC container. Set the flag, and the same container image described in 4 ways to deploy agents on Agent Platform (option 4) serves the entire contract without custom request-handling code.

Before, you implement the Agent Runtime API contract in a custom wrapper and maintain the agent code. With ADK 2.2, ADK implements the contract and the dashed responsibility box shrinks to just your agent code.
Scroll horizontally to explore. Open full-size diagram

adk deploy agent_engine in ADK 2.x uses the same approach: it generates a Dockerfile running adk api_server with --gemini_enterprise_app_name set. Here we’re configuring it in our own image. 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 previous wrapper

Here is the previous implementation. 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 implements 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}"]

Dockerfile

That main.py is the wrapper. It wraps the agent in AdkApp, then dispatches the class_method that the platform forwards. The dispatch code looks like this:

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",
    )

The complete implementation also JSON-encodes each streamed chunk, handles both sync and async generators, and parses the request body defensively. The full 117 lines are in main.py.

The updated Dockerfile

The updated version uses the site’s general-purpose ADK container, which 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: use explicit SESSION_SERVICE_URI / MEMORY_SERVICE_URI
# when set. Otherwise, when Agent Runtime injects GOOGLE_CLOUD_AGENT_ENGINE_ID (a
# BYOC deploy, see ../09_DeployContainerToAgentPlatform), use the engine's own
# resource for both services. With neither set, use 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"]

Dockerfile

The image no longer includes main.py. 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. It uses the same class_method dispatch as our wrapper, with the thirteen-method allowlist we declare as classMethods at deploy time. It also adds middleware that reads the platform’s trace header, which the wrapper didn’t handle.

One rule to know: the flag’s value must be the name of an agent folder in the image (trading_agent here). It isn’t a display name. It identifies the agent folder that the server exposes as the engine, and the server exits during startup if no folder matches.

The environment variables

Here’s the CMD again with line wrapping enabled:

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"]

There are two parts to the environment-variable handling:

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. The contract routes are enabled 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 runs adk api_server without the additional flag, exactly as it did before this change. Hardcoding --gemini_enterprise_app_name would make the server require Google Cloud credentials at startup, and the local docker run workflow would fail. Making the flag conditional preserves the local workflow.

Session and memory defaults. The first two statements — : ${SESSION_SERVICE_URI:=...} — use the shell’s no-op command (:) to assign a default when the variable is unset. The default is also conditional: agentengine://$GOOGLE_CLOUD_AGENT_ENGINE_ID, only if that variable exists.

Agent Runtime injects GOOGLE_CLOUD_AGENT_ENGINE_ID into every BYOC container, and it’s the engine’s own resource id. On the platform, with no explicit URIs configured, the container configures sessions and Memory Bank to use its own reasoningEngine resource, matching the old wrapper’s AdkApp.set_up() behavior. ADK accepts the resource id without a full projects/.../locations/... path because it obtains the project and location from GOOGLE_CLOUD_PROJECT and GOOGLE_CLOUD_LOCATION, which the platform also injects.

The precedence order is:

  1. Explicit SESSION_SERVICE_URI / MEMORY_SERVICE_URI (from the deploy env, or your docker-env file) — used as-is. This is how you’d configure several agents to use one shared sessions backend.
  2. No URIs, but running on Agent Runtime — automatically use the engine’s own resource through the injected id.
  3. Neither (a plain local docker run) — use non-persistent local storage, which is suitable for a short local test.

The engine’s id isn’t available when you set SESSION_SERVICE_URI at deploy time: the create call that carries the env vars also creates the engine. The CMD can set the URI once Agent Runtime provides the id at startup. Setting the value to the literal string agentengine://$GOOGLE_CLOUD_AGENT_ENGINE_ID doesn’t work, because the shell doesn’t re-expand variables found inside another variable’s value.

Deploying it

The deploy script requires two changes. Build and push the general-purpose ADK image described above instead of the wrapper image, then 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 handles the requests: 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 with a managed deployment. The contract post covers what each method is for.

One issue I observed: calling the sync stream_query method logs a 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 occurs after the last event has been delivered, so the stream itself is complete and correct, but the traceback still appears in the logs. Call async_stream_query instead to avoid this behavior.

trade.sh runs against the new deployment unchanged, sessions are stored in the engine’s own resource, and the custom wrapper file is no longer required.