Hosting agents built with the Claude Agent SDK
Copy as MarkdownLet’s say you want an agent that can answer questions about a codebase, or write release notes from its Git history. It needs to find the relevant files, read the code, and follow references until it has enough information to complete the task.
Claude Code is a popular tool for that kind of work, and the Claude Agent SDK gives a Python or TypeScript application a programmatic interface to Claude Code, including its agent loop, built-in tools, and context management. Hosting that agent means providing somewhere for its tools to run and deciding what happens to its conversation and working files when a task finishes.
To explore and demonstrate hosting such an agent, I created an example Git Repository Agent that can answer code questions and write documentation. This example uses Agent Runtime for hosting and Claude on Vertex AI for model calls. In this simple configuration, each request starts a fresh investigation, but there are options to preserve state between turns, discussed below.
The Claude Agent SDK, the model, and the runtime
There are a few pieces involved in running this agent:
- The hosting environment runs the application and CLI, with compute and a filesystem for tool execution. Here, a FastAPI service accepts requests forwarded by Agent Runtime.
- The Claude Agent SDK connects the application to the CLI loop. In Python, it starts and manages a separate CLI subprocess, passes options and prompts to it, and parses its output into Python messages.
- The Claude Code CLI runs the agent loop: it calls the model, executes tools locally, and sends their results back for the next decision. It also manages the conversation context as the investigation grows.
- The Claude model decides what to investigate, requests tools, and produces the answer. In this example, model calls go to Claude on Vertex AI, outside the agent’s container.
Anthropic’s Hosting the Agent SDK guide describes this subprocess model. The model runs remotely, but searching the checkout, reading files, and writing documents happen in the container.
One question to the agent can require many model and tool calls. Answering a question about option parsing, for example, might mean searching for the parser, reading it, then following references into the code that configures it. All of that happens within one HTTP request, with the Claude Agent SDK providing access to the agent loop and its tools. Calling the model API directly would leave the application to implement or supply those parts.
Figure: The outer request contains the whole investigation. The Claude Agent SDK connects the Python service to Claude Code, whose inner loop can make many model and tool calls before returning the result.
State within and between requests
The benefit of using the Claude Code harness to answer a request is that it can run its own inner loop involving many model and tool calls. In this simple example, each new request starts with a fresh checkout and Claude Agent SDK session.
A potential follow-on would be to preserve session state by connecting external session storage to a Cloud Storage bucket through a storage adapter. This could let Claude Agent SDK sessions continue across requests on Agent Runtime. Modified files and generated documents need separate persistence if later requests need them.
Another approach, using a different runtime, is to retain a logical agent instance for each session, including its process state and files. An environment such as Agent Substrate can suspend and restore actors between requests, freeing their compute while they’re idle.
The repository agent in action
After deploying my example agent to Agent Runtime, I asked it how Click, a Python library for building command-line tools, parses options: “How does option parsing work? Point me at the key files.”
It searched the checkout, located the parser, and read the code that configures it. Here’s an excerpt from the answer:
This repo is **Click**, and option parsing has two layers.
**`src/click/parser.py`** — the low-level parser (a trimmed-down fork of stdlib `optparse`).
This is where the actual command-line string tokenizing happens:
- `_OptionParser` (`parser.py:224`) — the engine. Holds `_short_opt`/`_long_opt` dicts and
`_args` list, populated via `add_option` (`:265`) and `add_argument` (`:290`).
- `parse_args` (`:298`) — entry point; runs options first then positional args...
[...]
**`src/click/core.py`** — the high-level bridge that builds and drives the parser:
- `Command.make_parser` (`core.py:1256`) — creates an `_OptionParser` and calls
`param.add_to_parser` for each param.
[...]
So: `core.py` decides *what* options exist and converts types/defaults; `parser.py` does
the raw token-level parsing.
The file and line references give you places to check the explanation against the code. They don’t guarantee that the answer is correct, but they make reviewing it much easier.
The example agent supports two modes. ask answers questions about the current code using a shallow clone. document clones the full Git history and can write files, for tasks such as release notes between two tags.
Serving and deploying the example
The example wraps the Claude Agent SDK in a FastAPI service and packages both in a container.
Serving on Agent Runtime
The Agent Runtime container contract defines two HTTP routes for serving agent requests. A caller invokes the deployed agent through the public runtime API, and Agent Runtime forwards the call to the corresponding route in the container. The repository example implements these routes with FastAPI and translates requests into calls to the Claude Agent SDK’s Python interface.
| Public runtime API | Container route | Response |
|---|---|---|
:query |
/api/reasoning_engine |
JSON when the task finishes |
:streamQuery |
/api/stream_reasoning_engine |
Newline-delimited JSON events |
For clients such as the Agent Platform Python SDK and Cloud console playground, the deployment also needs matching method declarations, query and stream_query. I covered that mapping in The Agent Platform API contract.
Here’s the example’s blocking handler in server.py:
@app.post("/api/reasoning_engine")
async def run(request: Request):
req = parse_request(await request.json())
job_dir, options = await start_job(req)
try:
output, usage = "", {}
async for msg in query(prompt=req["message"], options=options):
if isinstance(msg, ResultMessage):
output = msg.result or ""
usage = usage_of(msg)
if msg.is_error:
raise HTTPException(500, f"agent error ({msg.subtype}): {output}")
# Agent Runtime's :query surface forwards only the top-level "output"
# field of the container's response, so nest everything inside it
# (:streamQuery passes NDJSON lines through verbatim).
return {"output": {"result": output, "files": collect_files(job_dir), "usage": usage}}
finally:
shutil.rmtree(job_dir, ignore_errors=True)
The Claude Agent SDK’s query() yields messages while the agent runs. This handler takes the final answer and usage from a ResultMessage and reads any documents from out/ into the response’s files map. The finally block removes the job directory after collecting the result.
The example nests its answer, files, and usage inside output because Agent Runtime’s :query response forwards that field. Putting files or usage alongside it would lose those values at the platform boundary.
The streaming handler converts Claude Agent SDK text blocks and tool calls into text and tool_use JSON events. Its final result event contains the answer, files, and usage. Cloning finishes before streaming starts, and cleanup happens when the stream closes. The events show the investigation’s progress at the message and tool-call level.
Note: This custom wrapper doesn’t automatically integrate with Agent Platform Sessions. ADK provides that integration through AdkApp. Adding continuity here would require preserving the Claude Agent SDK conversation and handling working files; saving only the final answer wouldn’t retain the tool history.
Packaging the agent
The Claude Agent SDK package bundles a version of the Claude Code CLI and uses it by default, so a Python container using it doesn’t need a separate Node.js installation. To adopt a newer bundled CLI, update the Claude Agent SDK dependency, then rebuild and redeploy the image.
The repository example packages its FastAPI service and Claude Code CLI in one container. Its image uses Python 3.12, installs Git for cloning and history queries, and runs Uvicorn as the non-root agent user.
CLAUDE_CODE_USE_VERTEX=1 selects Vertex AI for model calls. CLOUD_ML_REGION selects the model endpoint location, independently of the region hosting the container. The image defaults to global; use a location where your chosen model is enabled and has quota.
The example’s Python dependencies are:
# 0.2.x wheels bundle the Claude Code CLI as a native binary — no Node.js required
claude-agent-sdk>=0.2.174
fastapi
uvicorn[standard]
These constraints allow newer package versions at build time; pin versions if you need repeatable builds.
Deploying the example
Follow the Build and deploy steps in the example repository’s README to build the image, configure the runtime’s permissions, and deploy the agent. The requests below show how to try it once deployed.
Optional: test locally first
You can also check the service locally before deploying. The README’s Run locally section covers this setup. For the Vertex AI configuration below, you’ll need Python 3.12 and Application Default Credentials for an account allowed to invoke Claude. Run the commands from a local checkout of the example repository.
Query the deployed example
After following the README’s deployment steps, set REGION to your deployment region and replace the ENGINE placeholder below with the full resource name returned by the deployment.
To run the Click investigation shown above, send the repository URL and question to the deployed agent. This calls Agent Runtime’s :streamQuery API, which forwards the request to the FastAPI streaming route. curl -N disables output buffering so you can see events as they arrive:
ENGINE="projects/PROJECT_NUMBER/locations/REGION/reasoningEngines/ENGINE_ID"
curl --fail-with-body -sSN -X POST "https://$REGION-aiplatform.googleapis.com/v1/$ENGINE:streamQuery" \
-H "Authorization: Bearer $(gcloud auth print-access-token)" \
-H 'Content-Type: application/json' \
-d '{
"classMethod": "stream_query",
"input": {
"repo": "https://github.com/pallets/click",
"message": "How does option parsing work? Point me at the key files."
}
}'
The response streams progress followed by a final result event containing the answer and usage. Here’s the recorded stream from the earlier run, with the final answer shortened:
The reported num_turns includes the final text-only turn; max_turns caps only tool-use turns.
Runtime considerations
The request stays open while the agent works. Clone time, model calls, and tool execution all contribute to latency; clone failures or the example’s 120-second clone timeout return an error before the Claude Agent SDK starts. Longer tasks need to fit the hosting platform’s request limits, or use a job API that returns an ID and lets the caller retrieve the result later.
Each concurrent request starts its own Claude CLI subprocess and checkout. The README’s deployment example uses 2 CPUs, 4 GiB of memory, and a concurrency limit of 2. Those are sample settings to measure against your workload, especially with large repositories.
Removing the job directory doesn’t remove every CLI file: session transcripts can be stored elsewhere in the container. This example doesn’t persist or resume them for later requests.