{ "cells": [ { "cell_type": "markdown", "id": "0011418a", "metadata": {}, "source": [ "# Hosting your agent\n", "\n", "You've built a research agent in [notebook 00](./00_The_one_liner_research_agent.ipynb). It runs on your laptop. Now someone else needs to use it: a teammate, a cron job, a web app, a customer. That means it has to run *somewhere other than your terminal*, stay up, keep conversations alive across restarts, and not leak your API key.\n", "\n", "This notebook takes the exact same agent and deploys it through three tiers:\n", "\n", "| Tier | Where it runs | When to use it |\n", "|---|---|---|\n", "| **1. Docker** | Your machine / a single VM | Dev loop, internal tools, single-tenant |\n", "| **2. Modal** | Managed serverless | You want a URL and scale-to-zero without managing infra |\n", "| **3. Kubernetes** | Your own cluster | Multi-tenant, regulated environments, full control |\n", "\n", "The agent code, the container image, and the HTTP interface are **identical** across all three. Only the operational machinery around the container changes. Once the agent is containerized behind a stable interface, choosing a host is a deployment decision rather than a rewrite.\n", "\n", "> **Cost to run this notebook end-to-end:** roughly **$1–2** in Anthropic API calls plus **a few cents** in Modal compute. Every tier has a teardown step.\n", "\n", "All the deployment code lives in [`hosting/`](./hosting/) next to this notebook." ] }, { "cell_type": "markdown", "id": "553aa211", "metadata": {}, "source": [ "## Before you start: should you be using the Agent SDK?\n", "\n", "If you're building a **customer-facing chat product**, look at [Claude Managed Agents](https://platform.claude.com/docs/en/managed-agents/overview) first. You get hosting, sessions, and a UI out of the box, and you can skip most of this notebook.\n", "\n", "The Agent SDK is the right choice when you need **programmatic control**: batch and job-shaped agents, internal tools, agents embedded in your own backend, or regulated environments where you have to own the infrastructure. If that's you, read on." ] }, { "cell_type": "markdown", "id": "2e28da41", "metadata": {}, "source": [ "## The mental model\n", "\n", "Three nouns to keep straight:\n", "\n", "- A **process** is one running Python interpreter with the SDK loaded. It talks to the Anthropic API.\n", "- A **session** is one conversation: the prompt history, tool calls, and results that the SDK writes to disk so you can `resume=` it later.\n", "- A **container** is a packaged process plus its filesystem: your agent code, the SDK, Node, and a place for sessions to live.\n", "\n", "Unlike in-process SDKs (OpenAI Agents SDK, Google ADK) where an \"agent\" is an object you instantiate inside your web server, a Claude Agent SDK agent **is** a process. That makes isolation trivial (one container = one blast radius) but means hosting is a distributed-systems problem, not a `pip install` problem.\n", "\n", "Every deployment, at any tier, has to do the same four jobs:\n", "\n", "```\n", "┌────────────────────────────────────────────────────────────────────┐\n", "│ caller ──► gateway ──► [ spawn | route ] ──► agent container ──► API\n", "│ │ │\n", "│ └── auth (not the agent's job) └── /data (persist)\n", "│ │\n", "│ orchestrator ──────────────── lifecycle ────────────────────────────┘\n", "└────────────────────────────────────────────────────────────────────┘\n", "```\n", "\n", "1. **Spawn** a container when work arrives\n", "2. **Route** each request to the container that owns that session\n", "3. **Lifecycle**: kill idle containers, restart crashed ones\n", "4. **Persist** session transcripts so a restart doesn't lose the conversation\n", "\n", "Tier 1 does all four by hand. Tier 2 delegates spawn+lifecycle to Modal. Tier 3 delegates all four to Kubernetes plus a small gateway. The agent container never changes." ] }, { "cell_type": "markdown", "id": "b7d198f6", "metadata": {}, "source": [ "## The agent we're deploying\n", "\n", "We're reusing [`research_agent/agent.py`](./research_agent/agent.py) from notebook 00, the one-liner research agent with `WebSearch` and `Read`. If you haven't done notebook 00, do it first; this notebook assumes the agent already works.\n", "\n", "The only thing `hosting/` adds is a thin HTTP server and a Dockerfile. The system prompt comes straight from `research_agent.agent`; we import it rather than copy it. One deliberate difference: the hosted server narrows the tool list to `WebSearch` only. There is no upload path on a server, so the only files `Read` could reach are other sessions' transcripts and the container's own environment, and a prompt-injected web result could walk the agent into leaking them. The comment in `server.py` spells out the reasoning." ] }, { "cell_type": "code", "execution_count": 1, "id": "a05cd685", "metadata": { "execution": { "iopub.execute_input": "2026-05-22T23:46:49.354865Z", "iopub.status.busy": "2026-05-22T23:46:49.354703Z", "iopub.status.idle": "2026-05-22T23:46:49.535396Z", "shell.execute_reply": "2026-05-22T23:46:49.535009Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "model: claude-opus-4-6\n", "You are a research agent specialized in AI.\n", "\n", "When providing research findings:\n", "- Always include source URLs as citations\n", "- Format citations as markdown links: [Source Title](URL)\n", "- Group sources in a \"Sources:\" section at the end of your response\n" ] } ], "source": [ "from research_agent.agent import DEFAULT_MODEL, RESEARCH_SYSTEM_PROMPT\n", "\n", "print(f\"model: {DEFAULT_MODEL}\")\n", "print(RESEARCH_SYSTEM_PROMPT)" ] }, { "cell_type": "markdown", "id": "1082a511", "metadata": {}, "source": [ "### Setup\n", "\n", "Create `hosting/.env` with your API key. This file is gitignored." ] }, { "cell_type": "code", "execution_count": 2, "id": "155a2ba5", "metadata": { "execution": { "iopub.execute_input": "2026-05-22T23:46:49.536613Z", "iopub.status.busy": "2026-05-22T23:46:49.536524Z", "iopub.status.idle": "2026-05-22T23:46:49.566637Z", "shell.execute_reply": "2026-05-22T23:46:49.566231Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Edit hosting/.env and set ANTHROPIC_API_KEY, then re-run this cell.\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "✅ key looks set\n" ] } ], "source": [ "%%bash\n", "test -f hosting/.env || cp hosting/.env.example hosting/.env\n", "echo 'Edit hosting/.env and set ANTHROPIC_API_KEY, then re-run this cell.'\n", "grep -q '^ANTHROPIC_API_KEY=sk-ant-' hosting/.env \\\n", " && ! grep -q 'your-key-here' hosting/.env \\\n", " && echo '✅ key looks set'" ] }, { "cell_type": "markdown", "id": "e6bc3aec", "metadata": {}, "source": [ "---\n", "## Tier 1a — Ephemeral: one prompt, one container, done\n", "\n", "The simplest possible deployment: a container that runs the agent **once** on a prompt from an env var, prints the result, and exits. No server, no sessions, no state.\n", "\n", "\n", "> **Model note:** the hosting layer defaults to `claude-sonnet-4-6` so your test requests stay cheap while you work through this notebook. Set `MODEL=claude-opus-4-6` in `hosting/.env` to match notebook 00's config exactly.\n", "\n", "This is enough for a lot of real work: invoice processing, nightly report generation, batch translation, one-off analysis. If your agent's job is \"take input, produce output, stop,\" you don't need anything past this section.\n", "\n", "The [`Dockerfile`](./hosting/Dockerfile) packages the agent, the SDK, and the Claude Code CLI the SDK drives under the hood. The build context is `claude_agent_sdk/` (this directory), not `hosting/`, because the image needs `research_agent/` and `utils/` too:" ] }, { "cell_type": "code", "execution_count": 3, "id": "e4dc77be", "metadata": { "execution": { "iopub.execute_input": "2026-05-22T23:46:49.567883Z", "iopub.status.busy": "2026-05-22T23:46:49.567793Z", "iopub.status.idle": "2026-05-22T23:46:51.191906Z", "shell.execute_reply": "2026-05-22T23:46:51.191328Z" } }, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ "#0 building with \"orbstack\" instance using docker driver\n", "\n", "#1 [internal] load build definition from D" ] }, { "name": "stderr", "output_type": "stream", "text": [ "ockerfile\n", "#1 transferring dockerfile: 2.30kB done\n", "#1 DONE 0.0s\n", "\n", "#2 resolve image config for docker-i" ] }, { "name": "stderr", "output_type": "stream", "text": [ "mage://docker.io/docker/dockerfile:1\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ "#2 DONE 0.6s\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ "\n", "#3 docker-image://docker.io/docker/dockerfile:1@sha256:87999aa3d42bdc6bea60565083ee17e86d1f3339802f" ] }, { "name": "stderr", "output_type": "stream", "text": [ "543c0d03998580f9cb89\n", "#3 CACHED\n", "\n", "#4 [internal] load metadata for docker.io/library/python:3.11-slim\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ "#4 DONE 0.5s\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ "\n", "#5 [1/9] FROM docker.io/library/python:3.11-slim@sha256:a3ab0b966bc4e91546a033e22093cb840908979487a" ] }, { "name": "stderr", "output_type": "stream", "text": [ "9fc0e6e38295747e49ac0\n", "#5 DONE 0.0s\n", "\n", "#6 [internal] load build context\n", "#6 transferring context: 814.53" ] }, { "name": "stderr", "output_type": "stream", "text": [ "kB 0.0s done\n", "#6 DONE 0.0s\n", "\n", "#7 [3/9] WORKDIR /app\n", "#7 CACHED\n", "\n", "#8 [6/9] COPY research_agent/ ./research" ] }, { "name": "stderr", "output_type": "stream", "text": [ "_agent/\n", "#8 CACHED\n", "\n", "#9 [7/9] COPY utils/ ./utils/\n", "#9 CACHED\n", "\n", "#10 [2/9] RUN apt-get update && apt-get" ] }, { "name": "stderr", "output_type": "stream", "text": [ " install -y --no-install-recommends curl ca-certificates && curl -fsSL https://deb.nodesource.com/s" ] }, { "name": "stderr", "output_type": "stream", "text": [ "etup_20.x | bash - && apt-get install -y --no-install-recommends nodejs && npm install -g @anthrop" ] }, { "name": "stderr", "output_type": "stream", "text": [ "ic-ai/claude-code@2.1.140 && apt-get purge -y curl && apt-get autoremove -y && rm -rf /var/lib/ap" ] }, { "name": "stderr", "output_type": "stream", "text": [ "t/lists/*\n", "#10 CACHED\n", "\n", "#11 [4/9] COPY hosting/requirements.txt ./hosting/requirements.txt\n", "#11 CACHED\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ "\n", "#12 [5/9] RUN pip install --no-cache-dir -r hosting/requirements.txt\n", "#12 CACHED\n", "\n", "#13 [8/9] COPY hos" ] }, { "name": "stderr", "output_type": "stream", "text": [ "ting/server.py hosting/run_once.py hosting/entrypoint.sh ./hosting/\n", "#13 CACHED\n", "\n", "#14 [9/9] RUN chmod " ] }, { "name": "stderr", "output_type": "stream", "text": [ "+x hosting/entrypoint.sh && touch hosting/__init__.py\n", "#14 CACHED\n", "\n", "#15 exporting to image\n", "#15 export" ] }, { "name": "stderr", "output_type": "stream", "text": [ "ing layers done\n", "#15 writing image sha256:b77b20f557cef5e4b9ef01212f3ba3a0895ee97ff8e31f79d9e1dc0cfb7" ] }, { "name": "stderr", "output_type": "stream", "text": [ "414f5 done\n", "#15 naming to docker.io/library/research-agent done\n", "#15 DONE 0.0s\n" ] } ], "source": [ "%%bash\n", "docker build -f hosting/Dockerfile -t research-agent . | tail -n 3" ] }, { "cell_type": "code", "execution_count": 4, "id": "189ce1b0", "metadata": { "execution": { "iopub.execute_input": "2026-05-22T23:46:51.193294Z", "iopub.status.busy": "2026-05-22T23:46:51.193200Z", "iopub.status.idle": "2026-05-22T23:47:13.361273Z", "shell.execute_reply": "2026-05-22T23:47:13.359683Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "🤖 Thinking...\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "🤖 Using: ToolSearch()\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "✓ Tool completed\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "🤖 Thinking...\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "🤖 Using: WebSearch()\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "✓ Tool completed\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "🤖 Using: WebSearch()\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "✓ Tool completed\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "🤖 Thinking...\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "The **Claude Agent SDK** is a developer toolkit from Anthropic — available in Python and TypeScrip" ] }, { "name": "stdout", "output_type": "stream", "text": [ "t — that gives developers access to the same tools, agent loop, and context management that power " ] }, { "name": "stdout", "output_type": "stream", "text": [ "Claude Code, enabling them to build fully autonomous AI agents that can read files, run terminal com" ] }, { "name": "stdout", "output_type": "stream", "text": [ "mands, search the web, edit code, and interact with external APIs without requiring developers to ma" ] }, { "name": "stdout", "output_type": "stream", "text": [ "nually implement a tool execution loop. Unlike the standard Claude API (where the developer must han" ] }, { "name": "stdout", "output_type": "stream", "text": [ "dle tool-use loops themselves), the Agent SDK lets Claude manage the agentic loop autonomously, maki" ] }, { "name": "stdout", "output_type": "stream", "text": [ "ng it straightforward to build sophisticated agents such as finance assistants, personal assistants," ] }, { "name": "stdout", "output_type": "stream", "text": [ " customer support bots, and deep research agents that can operate with minimal human intervention.\n", "\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "---\n", "\n", "**Sources:**\n", "- [Agent SDK Overview – Claude Code Docs](https://code.claude.com/docs/en/agent-" ] }, { "name": "stdout", "output_type": "stream", "text": [ "sdk/overview)\n", "- [Building Agents with the Claude Agent SDK – Anthropic Engineering](https://www.an" ] }, { "name": "stdout", "output_type": "stream", "text": [ "thropic.com/engineering/building-agents-with-the-claude-agent-sdk)\n", "- [Agent SDK Overview – Anthrop" ] }, { "name": "stdout", "output_type": "stream", "text": [ "ic API Docs](https://docs.anthropic.com/en/docs/claude-code/sdk/sdk-overview)\n", "- [claude-agent-sdk-py" ] }, { "name": "stdout", "output_type": "stream", "text": [ "thon – GitHub](https://github.com/anthropics/claude-agent-sdk-python)\n", "- [claude-agent-sdk-typescri" ] }, { "name": "stdout", "output_type": "stream", "text": [ "pt – GitHub](https://github.com/anthropics/claude-agent-sdk-typescript)\n" ] } ], "source": [ "%%bash\n", "docker run --rm --env-file hosting/.env \\\n", " -e PROMPT='What is the Claude Agent SDK, in one paragraph?' \\\n", " research-agent" ] }, { "cell_type": "markdown", "id": "3718bb81", "metadata": {}, "source": [ "That's it. `entrypoint.sh` sees no `serve` argument, so [`run_once.py`](./hosting/run_once.py) calls `research_agent.agent.send_query()` with `$PROMPT` and exits 0.\n", "\n", "**When this is enough:** job-shaped tasks where each invocation is independent. Run it from a cron, a queue worker, a CI step, or anywhere else you'd run a CLI." ] }, { "cell_type": "markdown", "id": "860d51f3", "metadata": {}, "source": [ "---\n", "## Tier 1b — Hybrid: add a server so conversations can continue\n", "\n", "Ephemeral mode can't hold a conversation; every `docker run` starts a fresh session. For a chat-shaped agent you need a long-lived process that accepts follow-ups and resumes the right session each time.\n", "\n", "[`hosting/server.py`](./hosting/server.py) is a ~100-line FastAPI app that does exactly that and nothing more. The interface is the contract every tier conforms to:\n", "\n", "```\n", "GET /health → 200 {\"status\": \"ok\"}\n", "POST /sessions/{session_id}/messages → 200 text/event-stream of SDK messages\n", "```\n", "\n", "Two things worth noticing in `server.py`:\n", "\n", "- **The server has no auth.** The docstring says so loudly. Auth is the gateway's job (tier 3 shows where it goes). The server validates `session_id` format and trusts the caller.\n", "- **It keeps a small map from your `session_id` to the SDK's internal one.** The SDK generates its own session IDs; you can't choose them. The server learns the SDK's ID from the first turn's `ResultMessage` and passes it to `resume=` on follow-ups. The map is persisted next to the transcripts under `/data`.\n", "\n", "Start it with docker-compose, which also mounts `./sessions` at `/data` so transcripts survive restarts:" ] }, { "cell_type": "code", "execution_count": 5, "id": "c21ca108", "metadata": { "execution": { "iopub.execute_input": "2026-05-22T23:47:13.399869Z", "iopub.status.busy": "2026-05-22T23:47:13.399708Z", "iopub.status.idle": "2026-05-22T23:47:17.465112Z", "shell.execute_reply": "2026-05-22T23:47:17.464638Z" } }, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ " Image research-agent Building \n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "#1 [internal] load local bake definitions\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "#1 reading from stdin 579B done\n", "#1 DONE 0.0s\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "\n", "#2 [internal] load build definition from Dockerfile\n", "#2 transferring dockerfile: 2.30kB done\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "#2 DONE 0.0s\n", "\n", "#3 resolve image config for docker-image://docker.io/docker/dockerfile:1\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "#3 DONE 0.2s\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "\n", "#4 docker-image://docker.io/docker/dockerfile:1@sha256:87999aa3d42bdc6bea60565083ee17e86d1f3339802f" ] }, { "name": "stdout", "output_type": "stream", "text": [ "543c0d03998580f9cb89\n", "#4 CACHED\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "\n", "#5 [internal] load metadata for docker.io/library/python:3.11-slim\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "#5 DONE 0.2s\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "\n", "#6 [1/9] FROM docker.io/library/python:3.11-slim@sha256:a3ab0b966bc4e91546a033e22093cb840908979487a" ] }, { "name": "stdout", "output_type": "stream", "text": [ "9fc0e6e38295747e49ac0\n", "#6 DONE 0.0s\n", "\n", "#7 [internal] load build context\n", "#7 transferring context: 494B d" ] }, { "name": "stdout", "output_type": "stream", "text": [ "one\n", "#7 DONE 0.0s\n", "\n", "#8 [2/9] RUN apt-get update && apt-get install -y --no-install-recommends curl ca" ] }, { "name": "stdout", "output_type": "stream", "text": [ "-certificates && curl -fsSL https://deb.nodesource.com/setup_20.x | bash - && apt-get install -y -" ] }, { "name": "stdout", "output_type": "stream", "text": [ "-no-install-recommends nodejs && npm install -g @anthropic-ai/claude-code@2.1.140 && apt-get purge" ] }, { "name": "stdout", "output_type": "stream", "text": [ " -y curl && apt-get autoremove -y && rm -rf /var/lib/apt/lists/*\n", "#8 CACHED\n", "\n", "#9 [3/9] WORKDIR /app\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "#9 CACHED\n", "\n", "#10 [4/9] COPY hosting/requirements.txt ./hosting/requirements.txt\n", "#10 CACHED\n", "\n", "#11 [7/9] " ] }, { "name": "stdout", "output_type": "stream", "text": [ "COPY utils/ ./utils/\n", "#11 CACHED\n", "\n", "#12 [5/9] RUN pip install --no-cache-dir -r hosting/requirements.tx" ] }, { "name": "stdout", "output_type": "stream", "text": [ "t\n", "#12 CACHED\n", "\n", "#13 [6/9] COPY research_agent/ ./research_agent/\n", "#13 CACHED\n", "\n", "#14 [8/9] COPY hosting/se" ] }, { "name": "stdout", "output_type": "stream", "text": [ "rver.py hosting/run_once.py hosting/entrypoint.sh ./hosting/\n", "#14 CACHED\n", "\n", "#15 [9/9] RUN chmod +x host" ] }, { "name": "stdout", "output_type": "stream", "text": [ "ing/entrypoint.sh && touch hosting/__init__.py\n", "#15 CACHED\n", "\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "#16 exporting to image\n", "#16 exporting layers done\n", "#16 writing image sha256:feb4e050b95b69f43fa027a2b5" ] }, { "name": "stdout", "output_type": "stream", "text": [ "a8c87974fba11cd890ba7c669db4aaa459fca0 done\n", "#16 naming to docker.io/library/research-agent done\n", "#16 " ] }, { "name": "stdout", "output_type": "stream", "text": [ "DONE 0.0s\n", "\n", "#17 resolving provenance for metadata file\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "#17 DONE 0.0s\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ " Image research-agent Built \n" ] }, { "name": "stderr", "output_type": "stream", "text": [ " Network docker_default Creating \n" ] }, { "name": "stderr", "output_type": "stream", "text": [ " Network docker_default Created \n" ] }, { "name": "stderr", "output_type": "stream", "text": [ " Container docker-research-agent-1 Creating \n" ] }, { "name": "stderr", "output_type": "stream", "text": [ " Container docker-research-agent-1 Created \n" ] }, { "name": "stderr", "output_type": "stream", "text": [ " Container docker-research-agent-1 Starting \n" ] }, { "name": "stderr", "output_type": "stream", "text": [ " Container docker-research-agent-1 Started \n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "{\"status\":\"ok\"}" ] } ], "source": [ "%%bash\n", "cd hosting/docker && docker compose up --build -d\n", "sleep 3\n", "curl -s http://localhost:8000/health" ] }, { "cell_type": "markdown", "id": "7f6f47be", "metadata": {}, "source": [ "Send a prompt and stream the response (`-N` disables curl's buffering so you see events as they arrive):" ] }, { "cell_type": "code", "execution_count": 6, "id": "b2da2254", "metadata": { "execution": { "iopub.execute_input": "2026-05-22T23:47:17.466487Z", "iopub.status.busy": "2026-05-22T23:47:17.466399Z", "iopub.status.idle": "2026-05-22T23:47:39.117220Z", "shell.execute_reply": "2026-05-22T23:47:39.115649Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "event: message\n", "data: {\"subtype\": \"init\", \"data\": {\"type\": \"system\", \"subtype\": \"init\", \"cwd\": \"/app\", \"session_id\": \"77597263-a169-4236-b3d4-8ec14f90fd2b\", \"tools\": [\"Task\", \"TaskOutput\", \"Bash\", \"Glob\", \"Grep\", \"ExitPlanMode\", \"Read\", \"Edit\", \"Write\", \"N … [truncated]\n", "\n", "event: message\n", "data: {\"content\": [{\"id\": \"toolu_01LpCYD1mQgNAmiDYq55RHyC\", \"name\": \"WebSearch\", \"input\": {\"query\": \"AI agent trends 2026\"}}], \"model\": \"claude-sonnet-4-6\", \"parent_tool_use_id\": null, \"error\": null, \"usage\": {\"input_tokens\": 685, \"cache_cr … [truncated]\n", "\n", "[... 6 events omitted — thinking blocks, tool loading, and web-search result payloads ...]\n", "\n", "event: message\n", "data: {\"content\": [{\"text\": \"Great question! Based on the latest research and reports, here are the **three most interesting AI agent trends** right now in 2026:\\n\\n---\\n\\n## \\ud83e\\udd1d 1. Multi-Agent Systems & Orchestration\\nThe era of the single, all-purpose AI agent is giving way to **coordinat … [truncated]\n", "\n", "event: message\n", "data: {\"subtype\": \"success\", \"duration_ms\": 20980, \"duration_api_ms\": 20812, \"is_error\": false, \"num_turns\": 3, \"session_id\": \"77597263-a169-4236-b3d4-8ec14f90fd2b\", \"stop_reason\": \"end_turn\", \"total_cost_usd\": 0.0502906, \"usage\": {\"input_t … [truncated]\n", "\n", "event: done\n", "data: \n" ] } ], "source": [ "%%bash\n", "curl -N -s -X POST http://localhost:8000/sessions/demo-1/messages \\\n", " -H 'Content-Type: application/json' \\\n", " -d '{\"prompt\":\"What are the three most interesting AI agent trends right now?\"}'" ] }, { "cell_type": "markdown", "id": "eb36d0cd", "metadata": {}, "source": [ "Now a follow-up to the **same** `session_id`. The agent remembers the first turn because the server resumed the session:" ] }, { "cell_type": "code", "execution_count": 7, "id": "3aa95711", "metadata": { "execution": { "iopub.execute_input": "2026-05-22T23:47:39.122021Z", "iopub.status.busy": "2026-05-22T23:47:39.121553Z", "iopub.status.idle": "2026-05-22T23:48:06.659795Z", "shell.execute_reply": "2026-05-22T23:48:06.658458Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "event: message\n", "data: {\"subtype\": \"init\", \"data\": {\"type\": \"system\", \"subtype\": \"init\", \"cwd\": \"/app\", \"session_id\": \"77597263-a169-4236-b3d4-8ec14f90fd2b\", \"tools\": [\"Task\", \"TaskOutput\", \"Bash\", \"Glob\", \"Grep\", \"ExitPlanMode\", \"Read\", \"Edit\", \"Write\", \"N … [truncated]\n", "\n", "event: message\n", "data: {\"content\": [{\"id\": \"toolu_01YU5m6b66Kh6GcSN1Kbv7zq\", \"name\": \"WebSearch\", \"input\": {\"query\": \"context engineering AI agents 2026 techniques best practices\"}}], \"model\": \"claude-sonnet-4-6\", \"parent_tool_use_id\": null, \"error\": null, … [truncated]\n", "\n", "[... 5 events omitted — thinking blocks, tool loading, and web-search result payloads ...]\n", "\n", "event: message\n", "data: {\"content\": [{\"text\": \"## \\ud83e\\uddf1 Deep Dive: Context Engineering\\n\\nContext engineering has quickly become **the defining AI skill of 2026**. Here's a thorough breakdown:\\n\\n---\\n\\n### What Is It, Exactly?\\n\\nContext engineering is the discipline of **designing what information an AI mode … [truncated]\n", "\n", "event: message\n", "data: {\"subtype\": \"success\", \"duration_ms\": 26895, \"duration_api_ms\": 26791, \"is_error\": false, \"num_turns\": 3, \"session_id\": \"77597263-a169-4236-b3d4-8ec14f90fd2b\", \"stop_reason\": \"end_turn\", \"total_cost_usd\": 0.08444499999999999, \"usage\": … [truncated]\n", "\n", "event: done\n", "data: \n" ] } ], "source": [ "%%bash\n", "curl -N -s -X POST http://localhost:8000/sessions/demo-1/messages \\\n", " -H 'Content-Type: application/json' \\\n", " -d '{\"prompt\":\"Tell me more about the second one.\"}'" ] }, { "cell_type": "markdown", "id": "169d8a6d", "metadata": {}, "source": [ "Restart the container and send *another* follow-up. The volume mount kept `/data`, so the conversation survives:" ] }, { "cell_type": "code", "execution_count": 8, "id": "3b088435", "metadata": { "execution": { "iopub.execute_input": "2026-05-22T23:48:06.662901Z", "iopub.status.busy": "2026-05-22T23:48:06.662658Z", "iopub.status.idle": "2026-05-22T23:48:19.015628Z", "shell.execute_reply": "2026-05-22T23:48:19.014064Z" } }, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ " Container docker-research-agent-1 Restarting \n" ] }, { "name": "stderr", "output_type": "stream", "text": [ " Container docker-research-agent-1 Started \n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "event: message\r\n", "data: {\"subtype\": \"init\", \"data\": {\"type\": \"system\", \"subtype\": \"init\", \"cwd\": \"/app" ] }, { "name": "stdout", "output_type": "stream", "text": [ "\", \"session_id\": \"77597263-a169-4236-b3d4-8ec14f90fd2b\", \"tools\": [\"Task\", \"TaskOutput\", \"Bash\", \"Gl" ] }, { "name": "stdout", "output_type": "stream", "text": [ "ob\", \"Grep\", \"ExitPlanMode\", \"Read\", \"Edit\", \"Write\", \"NotebookEdit\", \"WebFetch\", \"TodoWrite\", \"WebS" ] }, { "name": "stdout", "output_type": "stream", "text": [ "earch\", \"TaskStop\", \"AskUserQuestion\", \"Skill\", \"EnterPlanMode\", \"EnterWorktree\", \"ExitWorktree\", \"C" ] }, { "name": "stdout", "output_type": "stream", "text": [ "ronCreate\", \"CronDelete\", \"CronList\", \"RemoteTrigger\", \"ToolSearch\"], \"mcp_servers\": [], \"model\": \"c" ] }, { "name": "stdout", "output_type": "stream", "text": [ "laude-sonnet-4-6\", \"permissionMode\": \"default\", \"slash_commands\": [\"update-config\", \"debug\", \"simpli" ] }, { "name": "stdout", "output_type": "stream", "text": [ "fy\", \"batch\", \"loop\", \"schedule\", \"claude-api\", \"compact\", \"context\", \"cost\", \"heapdump\", \"init\", \"p" ] }, { "name": "stdout", "output_type": "stream", "text": [ "r-comments\", \"release-notes\", \"review\", \"security-review\", \"insights\"], \"apiKeySource\": \"ANTHROPIC_A" ] }, { "name": "stdout", "output_type": "stream", "text": [ "PI_KEY\", \"claude_code_version\": \"2.1.81\", \"output_style\": \"default\", \"agents\": [\"general-purpose\", \"" ] }, { "name": "stdout", "output_type": "stream", "text": [ "statusline-setup\", \"Explore\", \"Plan\"], \"skills\": [\"update-config\", \"debug\", \"simplify\", \"batch\", \"lo" ] }, { "name": "stdout", "output_type": "stream", "text": [ "op\", \"schedule\", \"claude-api\"], \"plugins\": [], \"uuid\": \"753db5ec-c432-4634-abb4-048eda1e6acd\", \"fast" ] }, { "name": "stdout", "output_type": "stream", "text": [ "_mode_state\": \"off\"}, \"type\": \"SystemMessage\"}\r\n", "\r\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "event: message\r\n", "data: {\"content\": [{\"thinking\": \"The user wants a summary of our conversation so far" ] }, { "name": "stdout", "output_type": "stream", "text": [ ". No tools needed for this.\", \"signature\": \"EtYBCmcIDRgCIkBg3e+9ruTNyro9yQA8kMewU6BzRzlQR/MAvKWUy2kk" ] }, { "name": "stdout", "output_type": "stream", "text": [ "CH0rl6bddOJ1gJBmcF4L3GPf/pgLkImzUqg1JV4RfUNgKAEyEWNsYXVkZS1zb25uZXQtNC02OABCCHRoaW5raW5nEgxfs2ACXCyv" ] }, { "name": "stdout", "output_type": "stream", "text": [ "Me8OUvQaDA4MA9sG7f/fI1we2yIwQvNMoFyTGl2/Ps+arqqgLoLYtySrkCOrgNmwcZFIbzQOFWiSJIcsqA7inm8T8n90Kh0wlw1t" ] }, { "name": "stdout", "output_type": "stream", "text": [ "FbJw0suOLM6okhU+eNc0gCr59ENLuBeZhBgC\"}], \"model\": \"claude-sonnet-4-6\", \"parent_tool_use_id\": null, \"" ] }, { "name": "stdout", "output_type": "stream", "text": [ "error\": null, \"usage\": {\"input_tokens\": 7108, \"cache_creation_input_tokens\": 0, \"cache_read_input_to" ] }, { "name": "stdout", "output_type": "stream", "text": [ "kens\": 0, \"cache_creation\": {\"ephemeral_5m_input_tokens\": 0, \"ephemeral_1h_input_tokens\": 0}, \"outpu" ] }, { "name": "stdout", "output_type": "stream", "text": [ "t_tokens\": 0, \"service_tier\": \"standard\", \"inference_geo\": \"global\"}, \"type\": \"AssistantMessage\"}\r\n", "\r" ] }, { "name": "stdout", "output_type": "stream", "text": [ "\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "event: message\r\n", "data: {\"content\": [{\"text\": \"Sure! Here's a summary of our conversation so far:\\n\\n-" ] }, { "name": "stdout", "output_type": "stream", "text": [ "--\\n\\n### \\ud83d\\uddc2\\ufe0f Conversation Summary\\n\\n**1. Top 3 AI Agent Trends (May 2026)**\\nYou as" ] }, { "name": "stdout", "output_type": "stream", "text": [ "ked about the most interesting AI agent trends right now. Based on web research, the three highlight" ] }, { "name": "stdout", "output_type": "stream", "text": [ "ed were:\\n\\n- \\ud83e\\udd1d **Multi-Agent Systems & Orchestration** \\u2014 Specialized agents working" ] }, { "name": "stdout", "output_type": "stream", "text": [ " in coordinated teams, replacing single all-purpose agents. Gartner reported a 1,445% surge in multi" ] }, { "name": "stdout", "output_type": "stream", "text": [ "-agent system inquiries.\\n- \\ud83e\\uddf1 **Context Engineering** \\u2014 Designing the full informati" ] }, { "name": "stdout", "output_type": "stream", "text": [ "on architecture around an agent (memory, retrieval, data sources, token budgets) to ensure reliable," ] }, { "name": "stdout", "output_type": "stream", "text": [ " high-quality outputs at scale.\\n- \\ud83d\\udee1\\ufe0f **Governance & Deterministic Guardrails** \\u20" ] }, { "name": "stdout", "output_type": "stream", "text": [ "14 Shifting from viewing governance as a compliance burden to an enabler, combining dynamic AI with " ] }, { "name": "stdout", "output_type": "stream", "text": [ "human oversight to safely deploy agents in high-stakes scenarios.\\n\\n---\\n\\n**2. Deep Dive into Cont" ] }, { "name": "stdout", "output_type": "stream", "text": [ "ext Engineering**\\nYou asked for more detail on the second trend. Key takeaways included:\\n\\n- **Con" ] }, { "name": "stdout", "output_type": "stream", "text": [ "text Engineering \\u2260 Prompt Engineering** \\u2014 It's a broader discipline covering the entire in" ] }, { "name": "stdout", "output_type": "stream", "text": [ "formation lifecycle of an agent.\\n- **Core techniques** include RAG, memory management, context comp" ] }, { "name": "stdout", "output_type": "stream", "text": [ "ression, context offloading, state persistence, and tool output structuring.\\n- **Why it matters for" ] }, { "name": "stdout", "output_type": "stream", "text": [ " agents** \\u2014 Unlike chatbots, agents accumulate context over many steps, making careful informat" ] }, { "name": "stdout", "output_type": "stream", "text": [ "ion design critical to avoid context rot and token blowouts.\\n- **RAG alone isn't enough** \\u2014 77" ] }, { "name": "stdout", "output_type": "stream", "text": [ "% of IT leaders agree RAG is insufficient for production AI on its own.\\n- Proper context engineerin" ] }, { "name": "stdout", "output_type": "stream", "text": [ "g can improve agent task completion rates dramatically (e.g., **83% \\u2192 96%**).\\n\\n---\\n\\nWould y" ] }, { "name": "stdout", "output_type": "stream", "text": [ "ou like to explore any of these topics further?\"}], \"model\": \"claude-sonnet-4-6\", \"parent_tool_use_i" ] }, { "name": "stdout", "output_type": "stream", "text": [ "d\": null, \"error\": null, \"usage\": {\"input_tokens\": 7108, \"cache_creation_input_tokens\": 0, \"cache_re" ] }, { "name": "stdout", "output_type": "stream", "text": [ "ad_input_tokens\": 0, \"cache_creation\": {\"ephemeral_5m_input_tokens\": 0, \"ephemeral_1h_input_tokens\":" ] }, { "name": "stdout", "output_type": "stream", "text": [ " 0}, \"output_tokens\": 0, \"service_tier\": \"standard\", \"inference_geo\": \"global\"}, \"type\": \"AssistantM" ] }, { "name": "stdout", "output_type": "stream", "text": [ "essage\"}\r\n", "\r\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "event: message\r\n", "data: {\"subtype\": \"success\", \"duration_ms\": 8163, \"duration_api_ms\": 8025, \"is_error" ] }, { "name": "stdout", "output_type": "stream", "text": [ "\": false, \"num_turns\": 1, \"session_id\": \"77597263-a169-4236-b3d4-8ec14f90fd2b\", \"stop_reason\": \"end_" ] }, { "name": "stdout", "output_type": "stream", "text": [ "turn\", \"total_cost_usd\": 0.028029, \"usage\": {\"input_tokens\": 7108, \"cache_creation_input_tokens\": 0," ] }, { "name": "stdout", "output_type": "stream", "text": [ " \"cache_read_input_tokens\": 0, \"output_tokens\": 447, \"server_tool_use\": {\"web_search_requests\": 0, \"" ] }, { "name": "stdout", "output_type": "stream", "text": [ "web_fetch_requests\": 0}, \"service_tier\": \"standard\", \"cache_creation\": {\"ephemeral_1h_input_tokens\":" ] }, { "name": "stdout", "output_type": "stream", "text": [ " 0, \"ephemeral_5m_input_tokens\": 0}, \"inference_geo\": \"\", \"iterations\": [{\"input_tokens\": 7108, \"out" ] }, { "name": "stdout", "output_type": "stream", "text": [ "put_tokens\": 447, \"cache_read_input_tokens\": 0, \"cache_creation_input_tokens\": 0, \"cache_creation\": " ] }, { "name": "stdout", "output_type": "stream", "text": [ "{\"ephemeral_5m_input_tokens\": 0, \"ephemeral_1h_input_tokens\": 0}, \"type\": \"message\"}], \"speed\": \"sta" ] }, { "name": "stdout", "output_type": "stream", "text": [ "ndard\"}, \"result\": \"Sure! Here's a summary of our conversation so far:\\n\\n---\\n\\n### \\ud83d\\uddc2\\uf" ] }, { "name": "stdout", "output_type": "stream", "text": [ "e0f Conversation Summary\\n\\n**1. Top 3 AI Agent Trends (May 2026)**\\nYou asked about the most intere" ] }, { "name": "stdout", "output_type": "stream", "text": [ "sting AI agent trends right now. Based on web research, the three highlighted were:\\n\\n- \\ud83e\\udd1" ] }, { "name": "stdout", "output_type": "stream", "text": [ "d **Multi-Agent Systems & Orchestration** \\u2014 Specialized agents working in coordinated teams, re" ] }, { "name": "stdout", "output_type": "stream", "text": [ "placing single all-purpose agents. Gartner reported a 1,445% surge in multi-agent system inquiries.\\" ] }, { "name": "stdout", "output_type": "stream", "text": [ "n- \\ud83e\\uddf1 **Context Engineering** \\u2014 Designing the full information architecture around an" ] }, { "name": "stdout", "output_type": "stream", "text": [ " agent (memory, retrieval, data sources, token budgets) to ensure reliable, high-quality outputs at " ] }, { "name": "stdout", "output_type": "stream", "text": [ "scale.\\n- \\ud83d\\udee1\\ufe0f **Governance & Deterministic Guardrails** \\u2014 Shifting from viewing " ] }, { "name": "stdout", "output_type": "stream", "text": [ "governance as a compliance burden to an enabler, combining dynamic AI with human oversight to safely" ] }, { "name": "stdout", "output_type": "stream", "text": [ " deploy agents in high-stakes scenarios.\\n\\n---\\n\\n**2. Deep Dive into Context Engineering**\\nYou as" ] }, { "name": "stdout", "output_type": "stream", "text": [ "ked for more detail on the second trend. Key takeaways included:\\n\\n- **Context Engineering \\u2260 P" ] }, { "name": "stdout", "output_type": "stream", "text": [ "rompt Engineering** \\u2014 It's a broader discipline covering the entire information lifecycle of an" ] }, { "name": "stdout", "output_type": "stream", "text": [ " agent.\\n- **Core techniques** include RAG, memory management, context compression, context offloadi" ] }, { "name": "stdout", "output_type": "stream", "text": [ "ng, state persistence, and tool output structuring.\\n- **Why it matters for agents** \\u2014 Unlike c" ] }, { "name": "stdout", "output_type": "stream", "text": [ "hatbots, agents accumulate context over many steps, making careful information design critical to av" ] }, { "name": "stdout", "output_type": "stream", "text": [ "oid context rot and token blowouts.\\n- **RAG alone isn't enough** \\u2014 77% of IT leaders agree RAG" ] }, { "name": "stdout", "output_type": "stream", "text": [ " is insufficient for production AI on its own.\\n- Proper context engineering can improve agent task " ] }, { "name": "stdout", "output_type": "stream", "text": [ "completion rates dramatically (e.g., **83% \\u2192 96%**).\\n\\n---\\n\\nWould you like to explore any of" ] }, { "name": "stdout", "output_type": "stream", "text": [ " these topics further?\", \"structured_output\": null, \"type\": \"ResultMessage\"}\r\n", "\r\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "event: done\r\n", "data: \r\n", "\r\n" ] } ], "source": [ "%%bash\n", "cd hosting/docker && docker compose restart && sleep 3\n", "curl -N -s -X POST http://localhost:8000/sessions/demo-1/messages \\\n", " -H 'Content-Type: application/json' \\\n", " -d '{\"prompt\":\"Summarize what we have discussed so far.\"}'" ] }, { "cell_type": "code", "execution_count": 9, "id": "3203205b", "metadata": { "execution": { "iopub.execute_input": "2026-05-22T23:48:19.019818Z", "iopub.status.busy": "2026-05-22T23:48:19.019547Z", "iopub.status.idle": "2026-05-22T23:48:19.513949Z", "shell.execute_reply": "2026-05-22T23:48:19.513481Z" } }, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ " Container docker-research-agent-1 Stopping \n" ] }, { "name": "stderr", "output_type": "stream", "text": [ " Container docker-research-agent-1 Stopped \n", " Container docker-research-agent-1 Removing \n" ] }, { "name": "stderr", "output_type": "stream", "text": [ " Container docker-research-agent-1 Removed \n" ] }, { "name": "stderr", "output_type": "stream", "text": [ " Network docker_default Removing \n" ] }, { "name": "stderr", "output_type": "stream", "text": [ " Network docker_default Removed \n" ] } ], "source": [ "%%bash\n", "# Teardown tier 1\n", "cd hosting/docker && docker compose down" ] }, { "cell_type": "markdown", "id": "a08413d9", "metadata": {}, "source": [ "---\n", "## Tier 2 — Modal: same image, now it's a URL\n", "\n", "Tier 1 runs on your machine. Tier 2 runs the **same Dockerfile** on [Modal](https://modal.com) via `modal.Sandbox`, which gives you a public HTTPS URL, scale-to-zero, and no servers to manage.\n", "\n", "That URL is *public*: anyone who has it can spend your API budget. Tiers 1 and 3 assume an authenticating gateway in front; tier 2 has no gateway, so `modal_app.py` generates a per-deploy bearer token and passes it as `AGENT_AUTH_TOKEN`. `server.py` only enforces the token when that env var is set, so the other tiers are unaffected.\n", "\n", "[`hosting/modal/modal_app.py`](./hosting/modal/modal_app.py) is short because nothing about the agent changes:\n", "\n", "```python\n", "app = modal.App.lookup(\"research-agent-hosting\", create_if_missing=True)\n", "image = modal.Image.from_dockerfile(\"hosting/Dockerfile\", context_dir=\".\")\n", "auth_token = secrets.token_urlsafe(32)\n", "sandbox = modal.Sandbox.create(\n", " \"serve\", # appended to the image's ENTRYPOINT, like compose's `command:`\n", " app=app,\n", " image=image,\n", " secrets=[\n", " modal.Secret.from_name(\"anthropic\"),\n", " modal.Secret.from_dict({\"AGENT_AUTH_TOKEN\": auth_token}),\n", " ],\n", " volumes={\"/data\": sessions_volume},\n", " encrypted_ports=[8000],\n", ")\n", "print(sandbox.tunnels()[8000].url)\n", "```\n", "\n", "Persistence uses a `modal.Volume` mounted at `/data`, the same `CLAUDE_CONFIG_DIR` trick as tier 1. (If your workload has many sandboxes writing concurrently and you hit Volume commit-semantics issues, swap in a [`SessionStore`](https://code.claude.com/docs/en/agent-sdk/session-storage); that's also what tier 3 and production deployments use.)" ] }, { "cell_type": "markdown", "id": "99725dcf", "metadata": {}, "source": [ "One-time setup, **in your terminal** (`modal setup` opens a browser, so it can't run from a notebook cell):\n", "\n", "```bash\n", "pip install modal\n", "modal setup\n", "```\n", "\n", "Then create the secret Modal will inject as `ANTHROPIC_API_KEY`:" ] }, { "cell_type": "code", "execution_count": 10, "id": "3c97283e", "metadata": { "execution": { "iopub.execute_input": "2026-05-22T23:48:19.515436Z", "iopub.status.busy": "2026-05-22T23:48:19.515341Z", "iopub.status.idle": "2026-05-22T23:48:20.263885Z", "shell.execute_reply": "2026-05-22T23:48:20.263387Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Created a new secret \u001b[32m'anthropic'\u001b[0m with the key \u001b[32m'ANTHROPIC_API_KEY'\u001b[0m\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "\n", "Use it in your Modal app:\n", "\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "\u001b[48;2;39;40;34m \u001b[0m" ] }, { "name": "stdout", "output_type": "stream", "text": [ "\n", "\u001b[38;2;166;226;46;48;2;39;40;34m@app\u001b[0m\u001b[38;2;249;38;114;48;2;39;40;34m.\u001b[0m\u001b[38;2;248;248;242;48;" ] }, { "name": "stdout", "output_type": "stream", "text": [ "2;39;40;34mfunction\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m(\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34msecret" ] }, { "name": "stdout", "output_type": "stream", "text": [ "s\u001b[0m\u001b[38;2;249;38;114;48;2;39;40;34m=\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m[\u001b[0m\u001b[38;2;248;248;242;4" ] }, { "name": "stdout", "output_type": "stream", "text": [ "8;2;39;40;34mmodal\u001b[0m\u001b[38;2;249;38;114;48;2;39;40;34m.\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mSecret\u001b[" ] }, { "name": "stdout", "output_type": "stream", "text": [ "0m\u001b[38;2;249;38;114;48;2;39;40;34m.\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mfrom_name\u001b[0m\u001b[38;2;248;248;" ] }, { "name": "stdout", "output_type": "stream", "text": [ "242;48;2;39;40;34m(\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m\"\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34manthro" ] }, { "name": "stdout", "output_type": "stream", "text": [ "pic\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m\"\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m)\u001b[0m\u001b[38;2;248;248;24" ] }, { "name": "stdout", "output_type": "stream", "text": [ "2;48;2;39;40;34m]\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m)\u001b[0m\u001b[48;2;39;40;34m \u001b[0m\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "\u001b[38;2;102;217;239;48;2;39;40;34mdef\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;166;226;46;48;2" ] }, { "name": "stdout", "output_type": "stream", "text": [ ";39;40;34msome_function\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m(\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m)\u001b" ] }, { "name": "stdout", "output_type": "stream", "text": [ "[0m\u001b[38;2;248;248;242;48;2;39;40;34m:\u001b[0m\u001b[48;2;39;40;34m " ] }, { "name": "stdout", "output_type": "stream", "text": [ " \u001b[0m\n", "\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mos\u001b[" ] }, { "name": "stdout", "output_type": "stream", "text": [ "0m\u001b[38;2;249;38;114;48;2;39;40;34m.\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mgetenv\u001b[0m\u001b[38;2;248;248;242" ] }, { "name": "stdout", "output_type": "stream", "text": [ ";48;2;39;40;34m(\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m\"\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34mANTHROPIC" ] }, { "name": "stdout", "output_type": "stream", "text": [ "_API_KEY\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m\"\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m)\u001b[0m\u001b[48;2;39;40" ] }, { "name": "stdout", "output_type": "stream", "text": [ ";34m \u001b[0m\n", "\u001b[48;2;39;40;34m " ] }, { "name": "stdout", "output_type": "stream", "text": [ " \u001b[0m\n" ] } ], "source": [ "%%bash\n", "modal secret create anthropic ANTHROPIC_API_KEY=\"$(grep ANTHROPIC_API_KEY hosting/.env | cut -d= -f2)\"" ] }, { "cell_type": "code", "execution_count": 11, "id": "a7224e51", "metadata": { "execution": { "iopub.execute_input": "2026-05-22T23:48:20.265280Z", "iopub.status.busy": "2026-05-22T23:48:20.265193Z", "iopub.status.idle": "2026-05-22T23:48:21.821004Z", "shell.execute_reply": "2026-05-22T23:48:21.820261Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "sandbox: sb-7R7zQ7TtX0h9eKZ8qslvwo\n", "url: https://ta-01ks91e217n9fymaxjtdc9k5bh-8000-kn9n102ljd7y4" ] }, { "name": "stdout", "output_type": "stream", "text": [ "majwav00kwg0.w.modal.host\n", "token: sb-…redacted…\n", "\n", "⚠️ The URL is p" ] }, { "name": "stdout", "output_type": "stream", "text": [ "ublic. The token is the only thing gating it — don't share both.\n", "\n", "Try it:\n", " curl -N -X POST https:" ] }, { "name": "stdout", "output_type": "stream", "text": [ "//ta-01ks91e217n9fymaxjtdc9k5bh-8000-kn9n102ljd7y4majwav00kwg0.w.modal.host/sessions/demo-1/messages" ] }, { "name": "stdout", "output_type": "stream", "text": [ " \\\n", " -H 'Authorization: Bearer sb-…redacted…' \\\n", " -H 'Content-Type" ] }, { "name": "stdout", "output_type": "stream", "text": [ ": application/json' \\\n", " -d '{\"prompt\":\"What are the latest AI agent trends?\"}'\n" ] } ], "source": [ "%%bash\n", "python hosting/modal/modal_app.py | tee /tmp/modal_deploy.out\n", "MODAL_URL=$(awk '/^url:/ {print $2}' /tmp/modal_deploy.out)\n", "MODAL_TOKEN=$(awk '/^token:/ {print $2}' /tmp/modal_deploy.out)\n", "{ echo \"MODAL_URL=$MODAL_URL\"; echo \"MODAL_TOKEN=$MODAL_TOKEN\"; } > /tmp/modal_url.env" ] }, { "cell_type": "code", "execution_count": 12, "id": "d5c0458e", "metadata": { "execution": { "iopub.execute_input": "2026-05-22T23:48:21.822476Z", "iopub.status.busy": "2026-05-22T23:48:21.822363Z", "iopub.status.idle": "2026-05-22T23:48:40.367731Z", "shell.execute_reply": "2026-05-22T23:48:40.366702Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "event: message\n", "data: {\"subtype\": \"init\", \"data\": {\"type\": \"system\", \"subtype\": \"init\", \"cwd\": \"/app\", \"session_id\": \"1566ffe4-2b20-4a68-82ff-283984b64451\", \"tools\": [\"Task\", \"TaskOutput\", \"Bash\", \"Glob\", \"Grep\", \"ExitPlanMode\", \"Read\", \"Edit\", \"Write\", \"N … [truncated]\n", "\n", "event: message\n", "data: {\"content\": [{\"id\": \"toolu_01PPS8yzMBzMnRhG2Hnpk5VL\", \"name\": \"WebSearch\", \"input\": {\"query\": \"Claude Agent SDK Anthropic 2026\"}}], \"model\": \"claude-sonnet-4-6\", \"parent_tool_use_id\": null, \"error\": null, \"usage\": {\"input_tokens\": 120 … [truncated]\n", "\n", "[... 5 events omitted — thinking blocks, tool loading, and web-search result payloads ...]\n", "\n", "event: message\n", "data: {\"content\": [{\"text\": \"The **Claude Agent SDK** is Anthropic's framework that gives developers programmatic access to the same tools, agent loop, and context management that power Claude Code \\u2014 enabling the creation of AI agents that can autonomously read files, run commands, search the w … [truncated]\n", "\n", "event: message\n", "data: {\"subtype\": \"success\", \"duration_ms\": 12879, \"duration_api_ms\": 12871, \"is_error\": false, \"num_turns\": 3, \"session_id\": \"1566ffe4-2b20-4a68-82ff-283984b64451\", \"stop_reason\": \"end_turn\", \"total_cost_usd\": 0.045893199999999995, \"usage\" … [truncated]\n", "\n", "event: done\n", "data: \n" ] } ], "source": [ "%%bash\n", "source /tmp/modal_url.env\n", "curl -N -s -X POST \"$MODAL_URL/sessions/demo-1/messages\" \\\n", " -H \"Authorization: Bearer $MODAL_TOKEN\" \\\n", " -H 'Content-Type: application/json' \\\n", " -d '{\"prompt\":\"Give me a one-sentence summary of the Claude Agent SDK.\"}'" ] }, { "cell_type": "markdown", "id": "7d47f4f7", "metadata": {}, "source": [ "Same interface, same image, different host. When nothing's calling it, Modal scales the sandbox to zero and you pay nothing.\n", "\n", "Teardown so you aren't billed for idle resources:" ] }, { "cell_type": "code", "execution_count": 13, "id": "bf0a1ff4", "metadata": { "execution": { "iopub.execute_input": "2026-05-22T23:48:40.370754Z", "iopub.status.busy": "2026-05-22T23:48:40.370499Z", "iopub.status.idle": "2026-05-22T23:48:41.287667Z", "shell.execute_reply": "2026-05-22T23:48:41.286973Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "terminating sandbox sb-7R7zQ7TtX0h9eKZ8qslvwo\n", "deleted volume research-agent-sessions\n" ] } ], "source": [ "%%bash\n", "python hosting/modal/teardown.py" ] }, { "cell_type": "markdown", "id": "c2bd9f7a", "metadata": {}, "source": [ "---\n", "## Tier 3 — Kubernetes: when you need to own the infrastructure\n", "\n", "Tier 3 is for multi-tenant production, regulated environments, or anywhere you need full control over networking, isolation, and cost. The agent image and interface are still identical; what's new is the machinery *around* it:\n", "\n", "- A **gateway** in front that authenticates callers and only forwards `session_id`s they own. This is where the auth that `server.py` leaves out finally happens.\n", "- **Pod-per-session** routing (gateway → Redis → pod) so each conversation gets its own blast radius.\n", "- A **standby pool** of pre-warmed pods so new sessions don't pay cold-start latency.\n", "- **Egress lockdown** (NetworkPolicy + an allowlisting proxy) so a prompt-injected agent can reach `api.anthropic.com` and nothing else.\n", "\n", "The full manifests, gateway, and a step-by-step architecture walkthrough live in [`hosting/kubernetes/`](./hosting/kubernetes/). It runs end-to-end on a local [kind](https://kind.sigs.k8s.io/) cluster with no cloud account:\n", "\n", "```bash\n", "cd hosting/kubernetes\n", "ANTHROPIC_API_KEY=sk-ant-... ./kind-quickstart.sh\n", "```\n", "\n", "The quickstart prints bearer tokens for two demo tenants (`alice` and `bob`). The gateway scopes every session to the tenant that created it: the same `curl -N POST /sessions/{id}/messages` as tiers 1 and 2, now aimed at the gateway on `:8080` with an `Authorization: Bearer ` header, works for alice, while the same request with bob's token gets a 403. The README's *Deploying to your own cluster* section covers the registry/ingress changes for a real cluster.\n", "\n", "*The Kubernetes tier builds on internal work by Joe Shamon and Ben Lehrburger.*\n" ] }, { "cell_type": "markdown", "id": "5d250205", "metadata": {}, "source": [ "---\n", "## Making it production-ready\n", "\n", "Two production concerns you can wire up in a few lines each. The cells below show the code; the [hosting docs](https://code.claude.com/docs/en/agent-sdk/hosting) cover the full production checklist (auth, graceful shutdown, idle-timeout tuning, autoscaling, cost controls).\n", "\n", "### Observability\n", "\n", "The SDK emits OpenTelemetry spans for every turn and tool call. Point it at your collector with two env vars, with no code changes to `server.py` ([docs](https://code.claude.com/docs/en/agent-sdk/observability)):" ] }, { "cell_type": "code", "execution_count": 14, "id": "3e4991fc", "metadata": { "execution": { "iopub.execute_input": "2026-05-22T23:48:41.289573Z", "iopub.status.busy": "2026-05-22T23:48:41.289434Z", "iopub.status.idle": "2026-05-22T23:48:41.291596Z", "shell.execute_reply": "2026-05-22T23:48:41.291024Z" } }, "outputs": [], "source": [ "# In docker-compose.yml / modal_app.py / your k8s Deployment:\n", "# OTEL_EXPORTER_OTLP_ENDPOINT=http://your-collector:4317\n", "# OTEL_SERVICE_NAME=research-agent" ] }, { "cell_type": "markdown", "id": "614cdfcc", "metadata": {}, "source": [ "### Liveness\n", "\n", "`GET /health` is already in `server.py`. Point your orchestrator's liveness probe at it (compose `healthcheck:`, Modal health checks, k8s `livenessProbe`).\n", "\n", "### Persistence beyond a volume\n", "\n", "The `/data` volume mount is fine for single-host and Modal. For multi-host production, use a [`SessionStore` adapter](https://code.claude.com/docs/en/agent-sdk/session-storage) that mirrors transcripts to shared storage (S3, Postgres, Redis). Note that SessionStore is a mirror; the local disk write always happens first, and mirror failures emit `mirror_error` without interrupting the agent.\n", "\n", "### Wire format\n", "\n", "`server.py` streams raw SDK message types. That's fine for a cookbook; for a real API you'd define a stable wire format so SDK version bumps don't break clients." ] }, { "cell_type": "markdown", "id": "dafe31a1", "metadata": {}, "source": [ "---\n", "## Choosing your tier\n", "\n", "| Tier | What it gives you | Pick it when | Move up when |\n", "|---|---|---|---|\n", "| **1. Docker** | A container on a machine you control. Compose restarts it and a bind mount keeps `/data`. You operate one Docker host. | Dev loop, internal tools, single-tenant apps, cron/batch jobs. You can restart it by hand and nobody outside your network calls it. | Someone outside that machine needs a URL, or \"restart it by hand\" stops being acceptable. |\n", "| **2. Modal** | The same image behind a public HTTPS URL with scale-to-zero and remote builds. You operate nothing. | You want a callable endpoint today, traffic is bursty or zero most of the time, and a per-deploy bearer token is enough auth. | You need real multi-tenant isolation, network-level egress control, or your platform team requires workloads on their cluster. |\n", "| **3. Kubernetes** | Pod-per-session isolation, an authenticating gateway with tenant-scoped sessions, egress lockdown, and a warm standby pool. You operate the cluster, the gateway, and Redis. | Multi-tenant production, regulated environments, or you already run Kubernetes and want agents to follow the same operational model as everything else. | This is the top of this notebook's ladder; from here you tune autoscaling, multi-region routing, and durable session stores rather than migrating. |\n", "\n", "The [hosting guide](https://code.claude.com/docs/en/agent-sdk/hosting)'s deployment patterns map onto these tiers directly. **Ephemeral sessions** (pattern 1: one prompt, one process, exit) are tier 1a: no server, run the container from cron or a queue worker on any host. **Long-running** and **hybrid sessions** (patterns 2 and 3, where a process holds or rehydrates conversation state across turns) are what `server.py` implements with `resume=` plus the `/data` volume; tiers 1b, 2, and 3 all serve this shape and differ only in who keeps that process alive and how callers reach it. **Single containers** (pattern 4, many sessions multiplexed into one container) is exactly what tiers 1b and 2 do; tier 3 exists for when that stops being acceptable and each session needs its own blast radius.\n" ] }, { "cell_type": "markdown", "id": "527250d8", "metadata": {}, "source": [ "---\n", "## Appendix — Porting to other providers\n", "\n", "Same `hosting/Dockerfile`, different deploy command. Each of these exposes port 8000 and gives you a URL; mount something at `/data` for persistence.\n", "\n", "**Fly Machines**\n", "```bash\n", "fly launch --dockerfile hosting/Dockerfile --no-deploy # run from claude_agent_sdk/\n", "fly volumes create data --size 1\n", "fly deploy\n", "```\n", "\n", "**E2B**\n", "```python\n", "from e2b import Sandbox\n", "sbx = Sandbox(template=\"research-agent\") # template built from hosting/Dockerfile\n", "sbx.commands.run(\"./hosting/entrypoint.sh serve\", background=True)\n", "url = sbx.get_host(8000)\n", "```\n", "\n", "**Daytona**\n", "```python\n", "from daytona import Daytona, CreateSandboxFromImageParams\n", "sbx = Daytona().create(CreateSandboxFromImageParams(image=\"research-agent\"))\n", "sbx.process.exec(\"./hosting/entrypoint.sh serve\")\n", "```\n", "\n", "**Cloudflare Containers**\n", "```ts\n", "// wrangler.toml points at hosting/Dockerfile\n", "export class Agent extends Container { defaultPort = 8000 }\n", "```\n", "\n", "**Vercel Sandbox**\n", "```ts\n", "import { Sandbox } from \"@vercel/sandbox\";\n", "const sbx = await Sandbox.create({ image: \"research-agent\", ports: [8000] });\n", "await sbx.runCommand({ cmd: \"./hosting/entrypoint.sh\", args: [\"serve\"], detached: true });\n", "```" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.13.12" } }, "nbformat": 4, "nbformat_minor": 5 }