# Agent Platform BYOC without the wrapper: new in ADK 2.2


In [The Agent Platform API contract](/agents/ap-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.

Comment: Draft generated from my working session migrating the book samples to ADK 2.2. –William

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](/agents/4-ways-to-deploy/) (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`:

```dockerfile {hl_lines=[16]}
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}"]
```

*[Dockerfile](https://github.com/WilliamDenniss/agent-examples/blob/master/08_ContainerizedForAgentPlatform/Dockerfile)*

And that `main.py` is the wrapper: wrap the agent in `AdkApp`, then dispatch whatever `class_method` the platform forwards. The heart of it:

```python
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](https://github.com/WilliamDenniss/agent-examples/blob/b79d454c65d61a049aa57cd2b149de37dfc460be/08_ContainerizedForAgentPlatform/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`:

```dockerfile {hl_lines=[16]}
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"]
```

*[Dockerfile](https://github.com/WilliamDenniss/agent-examples/blob/master/05_Containerized/Dockerfile)*

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:

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 point several agents at one shared sessions backend.
2. **No URIs, but running on Agent Runtime** — self-wire to the engine's own resource via the injected id.
3. **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:

```json
"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](https://github.com/WilliamDenniss/agent-examples/blob/master/09_DeployContainerToAgentPlatform/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](/agents/ap-api-contract/) 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](https://github.com/WilliamDenniss/agent-examples/blob/master/09_DeployContainerToAgentPlatform/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.

