Hosting agents built with the Claude Agent SDK

Copy as Markdown

Let’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.

One client request passes through Agent Runtime to the repository agent's container. The FastAPI service calls the Claude Agent SDK, which starts a Claude Code CLI subprocess. Inside that outer request, the CLI runs an inner loop of remote model calls to Claude on Vertex AI and local tool execution before the service returns an answer or event stream.
Scroll horizontally to explore. Open full-size diagram

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.

Two ways to handle consecutive questions. In this demo, each request creates a fresh checkout and Claude Agent SDK session, investigates, and removes its working files. A continuing session retains or restores the conversation and checkout so a follow-up can use the earlier investigation. Either path can make many model and tool calls within one request.
Scroll horizontally to explore. Open full-size diagram

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.

Dockerfile
# Python-only image: the claude-agent-sdk wheel bundles the Claude Code CLI
# as a self-contained native binary, so no Node.js is needed.
FROM python:3.12-slim

ENV PYTHONDONTWRITEBYTECODE=1 \
    PYTHONUNBUFFERED=1 \
    PORT=8080 \
    # Send model calls to Claude on Vertex instead of the Anthropic API.
    # "global" routes to wherever capacity is available and is where
    # default quota usually lives; override per deployment if you have
    # regional quota (e.g. us-east5).
    CLAUDE_CODE_USE_VERTEX=1 \
    CLOUD_ML_REGION=global \
    # Turn off telemetry and auto-update inside the container
    DISABLE_AUTOUPDATER=1 \
    CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1

# git is used by the server to clone target repos (and by the agent's
# read-only git commands in document mode)
RUN apt-get update && apt-get install -y --no-install-recommends git ca-certificates \
 && apt-get clean && rm -rf /var/lib/apt/lists/*

# Run as a non-root user (the CLI won't run some permission modes as root)
RUN useradd -m -u 1000 agent
WORKDIR /app

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY --chown=agent:agent server.py .
# Scratch space for per-request repo clones and generated docs
RUN mkdir -p /workspace && chown agent:agent /workspace

USER agent
EXPOSE 8080

CMD ["sh", "-c", "uvicorn server:app --host 0.0.0.0 --port ${PORT}"]

Dockerfile

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]

requirements.txt

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.

Run a local preflight check

Set PROJECT_ID to your Google Cloud project, create a Python environment, and start the server:

export PROJECT_ID="YOUR_PROJECT_ID"

python3.12 -m venv .venv
. .venv/bin/activate
pip install -r requirements.txt

gcloud auth application-default login
export CLAUDE_CODE_USE_VERTEX=1
export CLOUD_ML_REGION=global
export ANTHROPIC_VERTEX_PROJECT_ID="$PROJECT_ID"
export WORKSPACE_DIR="$PWD/.workspace"

uvicorn server:app --host 127.0.0.1 --port 8080

In a second terminal, ask a question using the container’s local route:

curl --fail-with-body -sS http://localhost:8080/api/reasoning_engine \
  -H 'Content-Type: application/json' \
  -d '{
    "input": {
      "repo": "https://github.com/pallets/click",
      "message": "How does option parsing work? Point me at the key files."
    }
  }'

A successful response should include the answer in output.result. This checks cloning and model access as well as the web server. Stop Uvicorn with Ctrl-C when you’re finished with the local check.

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:

Example event stream
{"type": "tool_use", "tool": "Grep", "input": "{\"pattern\": \"argparse|optparse|getopt|OptionParser|ArgumentParser|flag\\\\.|click|commander|yargs\", \"path\": \"./repo\", \"output_mode\": \"files_with_matches\"}"}
{"type": "tool_use", "tool": "Glob", "input": "{\"pattern\": \"repo/**/*\"}"}
{"type": "text", "text": "This is the **Click** library. Let me read the parser."}
{"type": "tool_use", "tool": "Read", "input": "{\"file_path\": \"repo/src/click/parser.py\"}"}
{"type": "text", "text": "Now let me see how `core.py` builds and drives the parser."}
{"type": "tool_use", "tool": "Grep", "input": "{\"pattern\": \"_OptionParser|add_to_parser|make_parser|def parse_args|add_option|add_argument\", \"path\": \"repo/src/click/core.py\", \"output_mode\": \"content\", \"-n\": true}"}
{"type": "tool_use", "tool": "Read", "input": "{\"file_path\": \"repo/src/click/core.py\", \"offset\": 1396, \"limit\": 40}"}
{"type": "tool_use", "tool": "Read", "input": "{\"file_path\": \"repo/src/click/core.py\", \"offset\": 3516, \"limit\": 40}"}
{"type": "result", "output": "This repo is **Click**, and option parsing has two layers. ...", "is_error": false, "files": {}, "usage": {"num_turns": 7, "cost_usd": 0.37658325}}

The reported num_turns includes the final text-only turn; max_turns caps only tool-use turns.

Optional: generate release notes

To try the example’s document configuration, use the blocking :query endpoint and ask for a file:

curl --fail-with-body -sS -X POST "https://$REGION-aiplatform.googleapis.com/v1/$ENGINE:query" \
  -H "Authorization: Bearer $(gcloud auth print-access-token)" \
  -H 'Content-Type: application/json' \
  -d '{
    "classMethod": "query",
    "input": {
      "repo": "https://github.com/pallets/click",
      "mode": "document",
      "message": "Write release notes covering the changes between tags 8.1.0 and 8.1.7. Save them to out/RELEASE_NOTES.md."
    }
  }'

The example’s document mode clones the full history, so the agent can inspect both tags. If it writes the requested file, the example returns its contents in output.files, under the filename RELEASE_NOTES.md. Save the document on the client and review it against the diff; the example removes its working copy when the request finishes.

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.