AI EngineeringZero to ProductionHome·About·What’s new·Contact
Anthropic API in Practice · Part 7

The Claude Agent SDK

Anthropic's official SDK for building agents in code — the programmatic engine behind Claude Code. It sits between the raw messages loop (you write the loop) and the Claude Code CLI (fully packaged): the SDK runs the agent loop, and you supply tools, permissions, and a system prompt through an options object. Learn the real query() pattern, sessions, and subagents — with the honest caveat that the API surface is still evolving.

⏱️ ~1.5 hours🧪 5 recipes🎯 Beginner→Tech-lead

Learning objectives

  • Say what the Claude Agent SDK is and why it sits between the raw loop and the CLI.
  • Install it: pip install claude-agent-sdk plus the Claude Code CLI and an ANTHROPIC_API_KEY.
  • Run an agent with the high-level async query(...) entry point.
  • Configure tools, a system prompt, and permissions via a ClaudeAgentOptions object.
  • Hold a multi-turn session with ClaudeSDKClient, and delegate to subagents.
  • Choose deliberately between the raw messages loop, the Agent SDK, and the Claude Code CLI.
  • Name the production concerns: permissions/sandboxing, cost caps, and observability.
📋 Before you startThis page builds on: The Claude API · Agent & tool foundations. Jumping in? Skim those first — or open the recap.
30-second recap
  • The Claude API: One call — messages.create — takes a model, a system prompt and messages, and returns content blocks; tools and streaming are options on the same call.
  • Agent & tool foundations: The model asks to call a tool, your code runs it and returns the result, and the loop repeats until the model answers — with step caps so it can't run forever.

1 · What the Claude Agent SDK is essential

Here's a moment that repeats in every agent project. Your first agent works — you hand-wrote the loop, it calls a tool, it answers. So you build a second one, and a third, and somewhere around the third you catch yourself copy-pasting: the same tool-dispatch switch, the same "stop after N turns" guard, the same try/except around a tool that might throw. You're not writing agent logic anymore; you're re-typing the same scaffolding. That itch — "I've built this exact plumbing before" — is precisely the itch the Claude Agent SDK scratches.

Think of it like the difference between wiring a house and buying an appliance. You could run your own copper for every socket, and if you're an electrician learning the trade you should — but for the fridge you just want a plug that works, built to code, maintained by someone else. The SDK is the appliance: the agent loop, packaged, standardized, and kept current by the people who build Claude itself. You still decide what the agent does; you stop re-soldering the wiring behind it.

The mistake to avoid is thinking the SDK is a different way to build agents — some rival abstraction you have to learn from zero. It isn't. It's the same perceive → act → observe loop from ch04, with the mechanical parts lifted out and given a clean surface. Everything you already understand about tool calls, turn caps, and message history is still true; the SDK just stops making you write it out by hand. Read the rest of this section as "here's the loop you know, now managed for you," not "here's a new thing to memorize."

Back in Agents from scratch you wrote the loop yourself: send a message, read the tool call Claude asked for, run the tool, feed the result back, repeat until Claude stops. That loop is the beating heart of every agent — and once you've written it a few times you notice it's always the same shape. Parsing tool-use blocks, appending results, capping the turns, gating dangerous actions, remembering the conversation: plumbing you rewrite on every project.

The Claude Agent SDK is Anthropic's official library for building agents in code — it runs that loop for you. It's the claude-agent-sdk Python package (it was called the "Claude Code SDK" earlier — same lineage), and it is the programmatic engine behind the Claude Code CLI. You hand it a prompt, a set of tools, a permission policy, and a system prompt; it drives the perceive → act → observe cycle, manages the session, enforces permissions, and can spin up subagents — all the machinery you hand-rolled in ch04, but maintained by the people who build Claude.

Think of it as the layer you reach for when the CLI is too packaged (you want it embedded in your program, driven by your code) but the raw loop is too much undifferentiated plumbing (you don't want to own tool dispatch and permission gating by hand).

you supply prompt tools permissions system_prompt Claude Agent SDK runs the loop for you: perceive → act → observe session · permissions · subagents answer same ch04 loop — the mechanical parts lifted out behind a clean surface The SDK is a managed loop, not a new abstraction. You hand it a prompt, tools, permissions, and a system prompt; it drives the same perceive → act → observe cycle you wrote by hand in ch04 — now maintained for you.
🗺️ How to read this diagram

Read it left to right: what you provide, what the SDK does with it, and what comes back.

  • The left column of blue boxes is everything you still own — a prompt, your tools, a permissions policy, and a system_prompt. This is declaring behavior, not coding the loop.
  • The middle box is the SDK itself: it runs the perceive → act → observe cycle, keeps the session, enforces permissions, and can spawn subagents — all the plumbing you hand-rolled in ch04.
  • The arrow out to the green box is the payoff: you get the finished answer without writing the dispatch, turn cap, or gates yourself.

In short: You supply config; the SDK runs the same loop you already know — nothing new to learn, just less to type.

Formerly "Claude Code SDK"The package was renamed from the Claude Code SDK to the Claude Agent SDK to signal it's for building any agent, not only coding ones. You'll still see the old name in older posts. The import today is claude_agent_sdk.

2 · Three ways to build with Claude essential

Most wasted effort in agent projects comes from picking the wrong altitude, not writing bad code. A team hand-rolls a raw loop for something the CLI already does in one line; another team fights to embed a terminal program into a web backend where it was never meant to live. Both wrote fine code — they just built it at the wrong level. So before a single import, the highest-leverage thing you can do is place your task on the right rung.

The cleanest way to picture the three levels is a set of Russian nesting dolls. The smallest, innermost doll is the raw messages loop — the bare API call and your own code around it. Wrap that in a managed loop with tools and permissions and you get the Agent SDK. Wrap that in a finished terminal program and you get the Claude Code CLI. Crucially, it's the same doll all the way down: the CLI is the SDK with a terminal bolted on, and the SDK is the messages loop with the plumbing filled in. You're not choosing between three different engines — you're choosing how much of the same engine comes pre-assembled.

The common mistake is to reach for the level you're most comfortable with rather than the one the job wants. Engineers who love control default to the raw loop and then drown in maintenance; people who fear code default to the CLI and then can't embed it where the product needs it. The fix is a single question, and the diagram below is built around it: who writes the loop — you, the SDK, or the CLI? Answer that honestly and the right rung usually names itself.

Before any code, get the map straight. There are three levels at which you can build on Claude, and the Agent SDK is the middle one. Read this left to right — each step hands more of the agent loop to Anthropic and asks less wiring of you:

Raw messages loop you write the loop Claude Agent SDK SDK runs the loop Claude Code CLI fully packaged
🗺️ How to read this diagram

This picture is the map for the whole lesson: three levels at which you can build on Claude. Read it left to right — each step hands more of the agent loop to Anthropic and asks less wiring of you.

  • Left — Raw messages loop: you write the loop yourself (the ch04 way). You own tool dispatch, the turn cap, permission checks, and memory. Most control, most plumbing.
  • Middle — Claude Agent SDK: the SDK runs the loop. You just declare tools, permissions, and a system prompt through an options object. This is the subject of this lesson.
  • Right — Claude Code CLI: everything is packaged into a terminal program you drive by talking to it. No code at all.
  • The arrows point from hand-written toward packaged. The key fact: the CLI is built on the SDK, which is built on the messages loop — same engine, three amounts of packaging.

In short: Ask yourself "who writes the loop?" — you (left), the SDK (middle), or the CLI (right). That single question places most tasks on the right rung.

On the left (ch04) you own everything: the loop, tool dispatch, the turn cap, permission checks, memory. Maximum control, maximum plumbing. In the middle — the Agent SDK — the SDK owns the loop and the machinery; you declare tools, permissions, and a system prompt through an options object and let it run. On the right (Claude Code) everything is packaged into a terminal program you drive by talking to it — no code at all. The SDK is what the CLI is built on: same engine, exposed as a library so you can embed it in your own application.

LevelWho writes the loop?You provideReach for it when
Raw messages loop (ch04)You doThe whole loop, tool dispatch, gates, memoryYou need total control or you're learning how agents work.
Claude Agent SDKThe SDK doesTools, permissions, system prompt via optionsYou want an agent inside your program without owning the plumbing.
Claude Code CLI (cl3)The CLI doesJust your prompts, in a terminalYou want a ready-made coding agent and don't need to embed it.

3 · Install & requirements essential

This is the section people skim, and it's the section that eats their first afternoon. The trap is a quiet one: pip install claude-agent-sdk succeeds, import claude_agent_sdk succeeds, and everything looks installed — until you actually call the agent and it dies with an error about a missing subprocess. The import working fooled you into thinking setup was done. It wasn't, and knowing why up front saves you the confused debugging session.

The reason is that the SDK isn't a self-contained library the way requests or numpy is. Picture it as a remote control, not the television. The Python package is a tidy remote with nice buttons, but the actual device doing the work is the Claude Code CLI running as a separate program — the SDK shells out to it, and it in turn talks to the Anthropic API. A remote with no TV in the room presses buttons into the void. That's exactly what a Python-only install is: buttons that connect to nothing at runtime.

So the mental model for setup is three parts, not one, and each has to be genuinely present: the Python package (the remote), the CLI on your PATH (the television), and an API key in the environment (the electricity that powers the whole thing). Miss any one and the failure shows up not at install time but the first time you press play. Keep the three-part picture in mind as you read the steps below — the point of the section is less "type these commands" and more "understand why one command was never going to be enough."

The SDK is a thin Python layer that drives the Claude Code CLI under the hood, which in turn talks to the Anthropic API. So you need three things, not one:

claude-agent-sdk Python pkg (remote) shells out Claude Code CLI on PATH (the TV) Anthropic API the model ANTHROPIC_API_KEY miss any one part → it fails at runtime, not install time Three parts, not one. The Python package is only the remote; the CLI is the device that does the work, and the API key is the power. A Python-only install imports fine but dies the first time you actually run the agent.
🗺️ How to read this diagram

Follow the arrows left to right — that is the chain a single agent call travels through.

  • The left blue box is the pip package — the tidy "remote control." It has nice buttons but does no work itself; it shells out to the next box.
  • The middle blue box is the Claude Code CLI on your PATH — the actual "television." The SDK launches it as a subprocess, and it is the piece a Python-only install is missing.
  • The amber box below is your ANTHROPIC_API_KEY in the environment — the electricity. It feeds the CLI, and it never appears in your code.
  • The green box is the Anthropic API — the model the CLI ultimately talks to.

In short: Package + CLI + key. Omit one and the import still succeeds — the failure ambushes you at the first real call.

What to install

  1. The Python package: pip install claude-agent-sdk.
  2. The Claude Code CLI on your PATH — the SDK launches it as a subprocess. Install it per the Claude Code docs (typically via npm install -g @anthropic-ai/claude-code, but check the current instructions).
  3. Credentials: an ANTHROPIC_API_KEY in your environment (or another auth method the CLI supports). The key is read from the environment — it never appears in your code.
Example code for learning — review, test, and adapt it before running against real or production systems. Commands can create, change, or delete resources. See the Terms & Disclaimer.
Shell · one-time setup ▶ needs SDK + API key
setup.sh# needs: pip install claude-agent-sdk + Claude Code CLI + ANTHROPIC_API_KEY;
# API names evolve — verify in docs (claude-agent-sdk Python reference).
# 1. the SDK (Python)
pip install claude-agent-sdk

# 2. the Claude Code CLI it drives (verify the current install command in the docs)
npm install -g @anthropic-ai/claude-code

# 3. your key, in the environment (never in code)
export ANTHROPIC_API_KEY="sk-ant-..."
The CLI is a hard dependencyThis trips people up: pip install claude-agent-sdk alone is not enough. The SDK shells out to the Claude Code CLI, so if the CLI isn't installed and on your PATH the SDK will fail at runtime even though the import succeeds. Verify both.

4 · Recipe 1 — the high-level query() essential

Remember the whole loop you wrote in ch04? The send, the parse-the-tool-call, the run-it, the feed-the-result-back, the "am I done yet" check, over and over. Now watch it collapse into a single function call. That compression is the moment the SDK's value becomes concrete instead of abstract — query() is the one line that swallows a page of your hand-written plumbing.

Here's the mental shift that trips people, and it's worth naming before the code. An ordinary function call gives you back one answer when it's done. query() doesn't work that way — it hands you a live feed, like a sports commentary rather than a final score. As the agent thinks, calls a tool, reads the result, and thinks again, each of those steps streams out to you as a separate message: here's Claude's text, here's a tool call, here's the tool's result, and finally here's the wrapped-up answer. You don't wait for a return value; you loop over the events as they arrive. That streaming shape is exactly why a whole multi-step agent can hide behind one call — the call isn't a single round-trip, it's the entire conversation playing out through an iterator.

The beginner mistake is to treat query() like client.messages.create() and expect a tidy response object you can print. You'll instead get an async iterator, and if you forget the async for (or forget to run it inside asyncio.run) nothing happens at all — no error, no output, just a coroutine you created and never awaited. So the shape of the code below isn't ceremony: async for message in query(...) inside an async def, kicked off by asyncio.run(main()), is the minimum honest way to consume a stream. Get that skeleton in your fingers and every later recipe is a variation on it.

The simplest way in is the async query() function. You give it a prompt, and it returns an async stream of messages — the SDK runs the whole agent loop internally and yields each message (Claude's text, tool calls, tool results, a final result) as it happens. This is the same loop you wrote by hand in ch04, collapsed to one call.

query() one call → a stream of messages (async for) text tool call tool result text result.result not a single return value — the whole conversation plays out as events query() is a live feed, not a final score. The SDK yields each step — text, tool call, tool result, more text — and finally a message carrying .result. You loop over them with async for; that is why a whole multi-step agent hides behind one call.
🗺️ How to read this diagram

Read it left to right as time: one query() call on the left, then each event the agent emits as it works.

  • The left purple box is the single query() call you make. It does not return one answer — it opens a stream.
  • The middle boxes are the messages that stream out in order: Claude's text, a tool call, the tool result fed back, then more text. The amber ones are the tool round-trip the SDK runs for you.
  • The green box is the final message — the one carrying .result, the finished answer you print.
  • The chain of arrows is why you write async for message in query(...): you consume events as they arrive rather than awaiting one return value.

In short: One call, many messages. Loop the stream with async for and pick the final .result off the last one.

Python · claude-agent-sdk — one-shot query() ▶ needs SDK + API key
recipe1_query.py# needs: pip install claude-agent-sdk + Claude Code CLI + ANTHROPIC_API_KEY;
# API names evolve — verify in docs (claude-agent-sdk Python reference).
import asyncio
from claude_agent_sdk import query, ClaudeAgentOptions

async def main():
    # query() is async and returns an async iterator of messages.
    # The SDK runs the agent loop for you and yields messages as they arrive.
    async for message in query(
        prompt="List the Python files in this folder and summarize what each does.",
        options=ClaudeAgentOptions(
            system_prompt="You are a concise coding assistant.",
            allowed_tools=["Read", "Glob", "Grep"],   # read-only: no edits, no shell
        ),
    ):
        # Message/block class names evolve — verify in the docs. The final
        # result typically arrives as a message carrying a `.result` string.
        if hasattr(message, "result"):
            print(message.result)

asyncio.run(main())
▶ How this works

This is the smallest possible agent with the SDK. In ch04 you wrote a whole loop to do this; here one call, query(...), runs the entire perceive → act → observe cycle and streams the messages back to you.

  1. query(...) is async and returns an async iterator, which is why we write async for message in query(...) and run it inside asyncio.run(main()). Each loop pass hands you one message the agent produced.
  2. prompt= is the task in plain English. The SDK figures out which tools to call, calls them, feeds the results back to Claude, and repeats — all inside that one call.
  3. options=ClaudeAgentOptions(...) is where you configure the agent. Here a short system_prompt and a read-only allowed_tools list (Read, Glob, Grep) so it can look but not edit or run shell.
  4. The if hasattr(message, "result"): check picks out the final message (the one carrying the finished answer) and prints it. The comment is honest: exact message class names evolve, so verify them in the docs.

What the output means: The agent lists and summarizes the Python files, and the final result string prints. (This block needs the SDK + CLI + an API key — it is not expected to run in the course sandbox.)

Try this: Compare this to ch04's hand-written loop: everything you wrote by hand — the send, the tool dispatch, the loop, the stop condition — is now hidden behind query().

It's a stream, not a return valueUnlike client.messages.create() which hands back one response, query() yields messages as the agent works — text, tool calls, tool results, and finally a result message. You loop over it. That streaming shape is why the whole multi-step loop can hide behind a single call.

5 · Recipe 2 — tools, prompt & permissions via options intermediate

An agent with the default toolbox is a stranger you've handed the keys to your house. It can read, write, edit, and run shell — useful, and also exactly how a well-meaning agent deletes a file it shouldn't have touched. The difference between a demo and something you'd let near real data is almost entirely what you allow it to do, and in the SDK that decision lives in one place: the options object.

The shift here is from writing behavior to declaring it. In ch04 you hand-built a tool dispatcher (a switch that matched a tool name to a function) and a permission gate (an if that asked before anything dangerous). With the SDK you don't write either — you fill in a ClaudeAgentOptions form and hand it over. Think of it like the settings panel on a new phone: you don't code the camera permission, you flip a switch that says "this app may use the camera." system_prompt sets the agent's persona, allowed_tools is the list of switches flipped on, and permission_mode decides how cautious the whole thing is by default. The SDK reads the form and enforces it.

Two ideas in the code below deserve a heads-up so they don't feel like magic. First, giving the agent your own code as a tool: the @tool decorator turns a plain async function into something the agent can call, and create_sdk_mcp_server(...) bundles those tools into a tiny server that runs inside your process — no separate service, no network hop, despite the "server" name. Second, the naming: custom tools show up under a namespaced mcp__<server>__<tool> label, which is the string you list in allowed_tools. The mistake to sidestep is treating allowed_tools as a nice-to-have you'll tighten later — it is the security boundary, so start it as small and as read-only as the task tolerates and widen only when something genuinely can't be done. An empty list is a perfectly valid, perfectly safe starting point.

Everything you configure about the agent goes on the options object — ClaudeAgentOptions. This is where the SDK earns its keep: instead of hand-writing a tool dispatcher and a permission gate (ch04, Lab 4.4), you declare them. Custom tools are defined with the @tool decorator and bundled into an in-process MCP server with create_sdk_mcp_server(...); the SDK handles the dispatch.

you declare — ClaudeAgentOptions system_prompt allowed_tools permission_mode @tool +create_sdk_mcp_serveryour code as a tool · in-process SDK enforcesdispatch + gate scoped agentallow-list only Declare, don't write. You fill in the options form — persona, tool allow-list, permission mode, plus your own @tool functions — and the SDK does the dispatch and gating. allowed_tools is the security boundary.
🗺️ How to read this diagram

Read left to right: the form you fill in, the SDK that reads it, and the bounded agent that results.

  • The left blue rows are the fields of ClaudeAgentOptions: system_prompt (persona), allowed_tools (the switches flipped on), and permission_mode (how cautious by default).
  • The amber row is your own code becoming a tool — @tool wraps an async function and create_sdk_mcp_server bundles it into a server that runs inside your process, no network hop.
  • The middle purple box is the SDK reading that form and doing the tool dispatch and permission gating you used to hand-write.
  • The green box is the payoff: an agent that can do exactly — and only — what allowed_tools permits. Start it small and read-only.

In short: The options object is a settings panel; you flip switches, the SDK enforces them, and allowed_tools is the switch that bounds the blast radius.

Python · claude-agent-sdk — a custom tool + options ▶ needs SDK + API key
recipe2_tools.py# needs: pip install claude-agent-sdk + Claude Code CLI + ANTHROPIC_API_KEY;
# API names evolve — verify in docs (claude-agent-sdk Python reference).
import asyncio
from claude_agent_sdk import query, ClaudeAgentOptions, tool, create_sdk_mcp_server

# @tool(name, description, input_schema) — the schema shape here is {arg_name: type}.
# Verify the exact input_schema form + return shape in the docs; names evolve.
@tool("get_weather", "Get the current weather for a city", {"city": str})
async def get_weather(args):
    city = args["city"]
    # ... your real implementation (call a weather API, read a DB, etc.) ...
    return {"content": [{"type": "text", "text": f"{city}: 22C and sunny"}]}

# Bundle your tools into an in-process MCP server the SDK can call.
weather = create_sdk_mcp_server(name="weather", version="1.0.0", tools=[get_weather])

options = ClaudeAgentOptions(
    system_prompt="You are a travel assistant. Use tools before you guess.",
    mcp_servers={"weather": weather},
    # tool names are namespaced mcp__<server>__<tool> — verify the prefix in the docs
    allowed_tools=["mcp__weather__get_weather"],
    permission_mode="default",   # default | acceptEdits | plan | dontAsk | bypassPermissions | auto
)

async def main():
    async for message in query(prompt="What should I pack for Lisbon?", options=options):
        if hasattr(message, "result"):
            print(message.result)

asyncio.run(main())
▶ How this works

This shows how you give the agent a custom tool of your own and wire up its permissions — all through the options object, instead of the hand-written tool dispatcher you built in ch04.

  1. @tool("get_weather", ...) turns a normal async Python function into a tool the agent can call. The third argument is the input schema (here {"city": str}); the function returns a content dict. Names and the exact schema shape evolve — verify in the docs.
  2. create_sdk_mcp_server(...) bundles your tool(s) into a small in-process server the SDK knows how to call. You don't run a separate process — it lives inside your program.
  3. On ClaudeAgentOptions: mcp_servers= registers that server, and allowed_tools=["mcp__weather__get_weather"] is the allow-list — note the mcp__<server>__<tool> naming so the agent may use it without a prompt.
  4. permission_mode="default" keeps the safe behavior (gate risky actions). The table under this block lists the other modes — bypassPermissions is the dangerous one.

What the output means: The agent calls your get_weather tool before answering, so its packing advice is grounded in the tool's result rather than guessed.

Try this: Change allowed_tools to an empty list and the agent can no longer call your tool — that list is exactly how you keep an agent narrowly scoped.

permission_modeWhat it does
defaultStandard behavior — prompts (or applies your rules) before risky tools.
acceptEditsAuto-accepts file edits without prompting.
planPlanning mode — the agent explores and proposes but doesn't act.
dontAskDenies anything not pre-approved instead of prompting.
bypassPermissionsSkips permission checks. Dangerous — sandbox only.
allowed_tools is your allow-listThe SDK ships with built-in tools (Read, Write, Edit, Bash, Glob, Grep, Agent, …) plus any custom tools you register. allowed_tools is the set the agent may use without a prompt — the primary lever for keeping an agent read-only or narrowly scoped. Exact built-in names evolve; verify in the docs.

6 · Recipe 3 — sessions & multi-turn intermediate

Try to build a chatbot on query() and you'll hit a wall by the second message. "What's the capital of Portugal?" → "Lisbon." "What's its population?" → and the agent has no idea what "its" refers to, because the second call started life with total amnesia. Every query() is a fresh conversation with someone who's never met you. For anything that has to remember — which is most real assistants — that's a dealbreaker, and it's the whole reason this section exists.

The fix is a shift from a vending machine to a phone call. query() is the vending machine: each transaction is complete and independent, you get your snack, it forgets you instantly. ClaudeSDKClient is the phone call: you dial once, and for the length of the call both sides remember what was already said, so "that city" resolves without you re-explaining. Under the hood the client is keeping the running message history for you — the exact bookkeeping you hand-assembled as a growing list in ch04's Lab 4.5 — but you never see or manage that list. You just keep talking on the same line.

The shape of the code follows that phone-call metaphor closely, and two habits keep it correct. You open the line with async with ClaudeSDKClient(...) so it's guaranteed to hang up cleanly when you're done; inside, each turn is a .query(...) to speak followed by draining .receive_response() to hear the full reply. The mistake beginners make is firing the next .query() before fully consuming the previous response — the equivalent of talking over the other person — which scrambles the turn order. Finish reading one reply, then send the next turn. Get that rhythm right and the session state does the remembering for free, which is the entire payoff of reaching past query().

query() is one-shot: each call is a fresh conversation. For a back-and-forth session that remembers — the multi-turn memory you built by hand in ch04's Lab 4.5 — use ClaudeSDKClient. You open it once, send turns with .query(...), and read each reply by iterating .receive_response(); the client keeps the history for you.

query() — fresh each call (amnesia) "capital?"→ Lisbon "population of it?"→ "it" = ? turn 2 forgot turn 1 ClaudeSDKClient — one live session "capital?"→ Lisbon "population of that city?"→ resolves to Lisbon history kept for you A vending machine vs a phone call. Each query() forgets you instantly, so "that city" has no referent; ClaudeSDKClient keeps the running history across turns, so the follow-up resolves to Lisbon on its own.
🗺️ How to read this diagram

Compare the two rows top and bottom — same two questions, different memory.

  • The top row is two separate query() calls. The first answers "Lisbon"; the red box is the second call, which has no idea what "it" means because it started with a blank slate.
  • The bottom row is one ClaudeSDKClient session. The arrow between the two green boxes is the retained history: turn two inherits turn one, so "that city" resolves to Lisbon without you re-stating it.
  • The payoff on the right — history kept for you — is the bookkeeping you assembled as a growing list by hand in ch04's Lab 4.5, now managed by the client.

In short: Use query() for one-off tasks; reach for ClaudeSDKClient the moment a later turn must remember an earlier one.

Python · claude-agent-sdk — a multi-turn session ▶ needs SDK + API key
recipe3_session.py# needs: pip install claude-agent-sdk + Claude Code CLI + ANTHROPIC_API_KEY;
# API names evolve — verify in docs (claude-agent-sdk Python reference).
import asyncio
from claude_agent_sdk import ClaudeSDKClient, ClaudeAgentOptions

async def main():
    options = ClaudeAgentOptions(system_prompt="You are a helpful research assistant.")

    # The client keeps the conversation alive across turns — no manual history list.
    async with ClaudeSDKClient(options=options) as client:
        await client.query("What is the capital of Portugal?")
        async for message in client.receive_response():
            # reading the text off a message: block/class names evolve — verify in docs
            print(message)

        # follow-up in the SAME session; "that city" resolves because state is kept
        await client.query("What is the population of that city?")
        async for message in client.receive_response():
            print(message)

asyncio.run(main())
▶ How this works

query() forgets everything between calls. When you need a conversation that remembers — the multi-turn memory you hand-built in ch04's Lab 4.5 — you switch to ClaudeSDKClient, which holds the session for you.

  1. async with ClaudeSDKClient(options=options) as client: opens a live session and closes it cleanly at the end of the block. One client = one ongoing conversation.
  2. await client.query("...") sends a turn; async for message in client.receive_response(): reads the reply for that turn. You do this once per turn.
  3. The second question — "the population of that city" — works because the client kept the history. You never rebuilt a messages list by hand; the session state resolves "that city" to Lisbon for you.

What the output means: Two answers print in order: the capital of Portugal, then its population — the follow-up resolving correctly proves the session remembered the first turn.

Try this: Rule of thumb: query() for one-off tasks, ClaudeSDKClient when the conversation must continue across turns (or you need interrupts).

query() vs ClaudeSDKClientUse query() for a one-off task (new session each call). Use ClaudeSDKClient when you need a continuing conversation, interrupts, or to swap the permission mode mid-run. Same engine — one is stateless-per-call, the other holds a live session.

7 · Recipe 4 — subagents advanced

Watch what happens to a single agent asked to review a whole codebase. It reads a file, then another, then twenty more, and every one of those files is now sitting in its context window — thousands of lines of code it mostly doesn't need anymore, crowding out the actual reasoning and driving up cost on every turn. By the time it forms an opinion, its "memory" is a landfill of half-relevant file dumps. The agent didn't get dumber; its workspace got buried. Subagents are the answer to that specific failure.

The intuition is delegation, exactly as a good manager does it. You don't personally read all forty files — you hand a focused brief to a specialist ("review the auth module for security issues"), they go off and do the messy reading on their own desk, and they come back with a one-paragraph verdict. All the clutter — the forty files, the dead ends, the intermediate notes — stayed on their desk and never landed on yours. In SDK terms each subagent is a separate agent instance with its own fresh context, its own system prompt, and its own restricted toolset; the parent sees only the subagent's final message. That's context isolation, and it's the headline benefit.

There are two more wins worth having in view before the code. Because subagents are independent, they can run in parallel — a review can fan out a security pass, a style pass, and a test-coverage pass at once — and because each has its own tools list, a read-only reviewer physically cannot edit a file even if it wanted to, which is a safety guarantee, not a polite request. You wire them up by declaring AgentDefinition entries on the agents option; Claude then invokes them on its own through the built-in Agent tool. The one gotcha that stops delegation cold: the parent must have Agent in its own allowed_tools, or it has no way to hand work down — a subtle omission that makes a perfectly-defined subagent simply never get called.

A single agent's context fills up fast when it reads dozens of files. Subagents fix that: each is a separate agent instance with its own fresh context, its own system prompt, and its own restricted tool set. The main agent delegates a focused subtask, the subagent does the work in isolation, and only its final message comes back — the intermediate noise never pollutes the parent. Define them programmatically with AgentDefinition on the agents option; Claude invokes them through the built-in Agent tool.

Parent agentmust allow Agent tool brief Subagent (isolated)own context · own prompttools = Read, Grep (read-only) reads 40 files — stays here verdictone paragraph only the final message returns — the clutter never reaches the parent Delegation with context isolation. The subagent reads its forty files on its own desk; the parent gets back only the one-paragraph verdict. The parent must have Agent in its own allowed_tools or it can never delegate.
🗺️ How to read this diagram

Follow the brief out to the subagent and the verdict back — that round trip is the whole idea.

  • The left purple box is the parent agent. Note the label: it must have the built-in Agent tool in its own allowed_tools, or it has no way to hand work down.
  • The middle box is the subagent, a separate instance with its own fresh context, its own system prompt, and its own read-only tool list — the amber cells are the forty files it reads.
  • The amber file cells stay inside that box: all the clutter and dead ends remain on the subagent's "desk."
  • The green box and the arrow back are the payoff: only the subagent's final one-paragraph verdict returns to the parent, keeping the parent's context clean.

In short: Hand a specialist a focused brief; they do the messy reading in isolation and return just the conclusion — cleaner context, plus parallelism and per-subagent tool limits for free.

Python · claude-agent-sdk — programmatic subagents ▶ needs SDK + API key
recipe4_subagents.py# needs: pip install claude-agent-sdk + Claude Code CLI + ANTHROPIC_API_KEY;
# API names evolve — verify in docs (claude-agent-sdk Python reference).
import asyncio
from claude_agent_sdk import query, ClaudeAgentOptions, AgentDefinition

async def main():
    async for message in query(
        prompt="Review the auth module for security issues.",
        options=ClaudeAgentOptions(
            # the parent needs the Agent tool to be allowed to delegate
            allowed_tools=["Read", "Grep", "Glob", "Agent"],
            agents={
                "code-reviewer": AgentDefinition(
                    description="Security & quality review specialist. Use for code reviews.",
                    prompt="You are a code reviewer. Flag security and correctness issues, concisely.",
                    tools=["Read", "Grep", "Glob"],   # read-only: this subagent can't edit
                    model="sonnet",                    # optional per-subagent model override
                ),
            },
        ),
    ):
        if hasattr(message, "result"):
            print(message.result)

asyncio.run(main())
▶ How this works

A subagent is a separate agent instance the main agent can hand a focused job to. It runs in its own fresh context with its own tools, and only its final answer comes back — so its file reading never clutters the main conversation.

  1. allowed_tools=[..., "Agent"] — the parent must be allowed the built-in Agent tool, because that's how it delegates to a subagent.
  2. agents={"code-reviewer": AgentDefinition(...)} defines the subagent. description tells Claude when to use it; prompt is the subagent's own system prompt (its expertise).
  3. tools=["Read", "Grep", "Glob"] restricts the subagent to read-only — it physically cannot edit files. model="sonnet" optionally picks a cheaper/faster model just for this subagent.
  4. Claude decides on its own to call the code-reviewer based on its description, runs it in isolation, and folds its summary back into the main answer.

What the output means: The review comes back as one concise summary; the dozens of file reads the subagent did to produce it stayed inside the subagent and never filled the parent's context.

Try this: This is the same delegation pattern this course's own Explore agent uses — a read-only helper that fans out, reads a lot, and returns just the conclusion.

Why subagents beat one giant promptThree wins: context isolation (the subagent's file reads don't fill the parent's window), parallelism (independent subagents run at once — a review can fan out style, security, and tests together), and tool restriction (a read-only reviewer physically can't edit). It's the same delegation pattern this very course's Explore agent uses.

8 · Recipe 5 — decide: raw loop vs SDK vs CLI (runs offline) advanced

Here's the skill that outlasts any particular API. The query() signature will drift, class names will get renamed (they already have once), whole methods will come and go — but the judgment of which level to build at is durable. A year from now you won't remember whether it was receive_response or read_reply; you'll still remember that a terminal task wants the CLI and an embedded one wants the SDK. This recipe trains the part that doesn't expire.

The reason to encode the decision as a function rather than a paragraph of advice is that a function forces you to be honest about your inputs. It's easy to hand-wave "I think the SDK feels right"; it's harder to fool yourself once you have to answer four blunt yes/no questions — am I embedding this in an app, do I need full control of the loop, is a plain terminal enough, am I here to learn the mechanics — and let the branches decide. Writing the criteria down turns a gut feeling into a repeatable rule you can point a teammate at, which is exactly what a tech lead needs when the whole team keeps asking "which one should I use?"

One friendly note about this particular lab: unlike every other code block in the lesson, it runs offline — pure standard library, no API key, no CLI, no network. That's deliberate. The decision is logic, not a call to Claude, so you can actually execute it, tweak the branch order, and watch the recommendation change. Don't over-read the output, though — the function is a thinking aid, not an oracle; real projects have nuances (compliance, existing infra, team skill) no four booleans capture. Use it to make your reasoning explicit, then apply judgment on top.

The most valuable skill here isn't SDK syntax — it's picking the right level for the job. The helper below is pure stdlib and runs with a plain python file.py (no API key, no network, no install). Answer a few yes/no questions about your situation and it recommends the raw loop, the Agent SDK, or the CLI, with a reason.

learning OR need full loop control? yes raw messages loop no terminal is fine, not embedding? yes Claude Code CLI no Claude Agent SDKthe safe default the decision is logic, not a call to Claude — so this lab runs offline The decision as a branch, not a vibe. Two honest yes/no questions route most tasks: control-or-learning → raw loop; a plain terminal is enough → CLI; otherwise the Agent SDK, which is also the safe default.
🗺️ How to read this diagram

Read it top to bottom, following the yes / no branches — it is the if / elif / else ladder of the function drawn out.

  • The first diamond-role box asks the most decisive question: are you learning, or do you need full control of the loop? A yes peels off left to the raw messages loop.
  • A no drops to the second question: is a plain terminal enough and you are not embedding? A yes routes right to the Claude Code CLI.
  • Everything else falls through to the Claude Agent SDK (green) — which the caption calls the safe default, matching the function's else.
  • The note at the bottom is why this lab, unlike the others, runs offline: it is pure decision logic, no API call.

In short: Answer two blunt questions in order and the rung names itself — raw loop, CLI, or (most often) the SDK.

Python · pick your build level (runs offline)
decide.pydef choose_build_level(*, embed_in_app, need_full_control_of_loop,
                       terminal_is_fine, learning_how_agents_work):
    """Recommend raw messages loop vs Claude Agent SDK vs Claude Code CLI.
    All inputs are booleans describing your situation."""
    if learning_how_agents_work or need_full_control_of_loop:
        pick = "raw messages loop"
        why = "you want to own the loop end-to-end (control, or learning the mechanics)."
    elif terminal_is_fine and not embed_in_app:
        pick = "Claude Code CLI"
        why = "a packaged terminal agent is enough; no need to embed it in code."
    elif embed_in_app:
        pick = "Claude Agent SDK"
        why = "you want the agent loop inside your program without hand-writing the plumbing."
    else:
        pick = "Claude Agent SDK"
        why = "the safe default: managed loop, your tools and permissions."
    return {"pick": pick, "why": why}

scenarios = {
    "Embed an agent in our web backend": dict(
        embed_in_app=True, need_full_control_of_loop=False,
        terminal_is_fine=False, learning_how_agents_work=False),
    "Quick coding help in my terminal": dict(
        embed_in_app=False, need_full_control_of_loop=False,
        terminal_is_fine=True, learning_how_agents_work=False),
    "Teach myself how agent loops work": dict(
        embed_in_app=False, need_full_control_of_loop=False,
        terminal_is_fine=False, learning_how_agents_work=True),
}

for name, situation in scenarios.items():
    r = choose_build_level(**situation)
    print(f"{name}\n  -> {r['pick']} ({r['why']})")
Embed an agent in our web backend
  -> Claude Agent SDK (you want the agent loop inside your program without hand-writing the plumbing.)
Quick coding help in my terminal
  -> Claude Code CLI (a packaged terminal agent is enough; no need to embed it in code.)
Teach myself how agent loops work
  -> raw messages loop (you want to own the loop end-to-end (control, or learning the mechanics).)
▶ How this works

This one runs offline — pure stdlib, no API key, no install. It turns the "which level?" decision into a function so you can reason about it deliberately instead of by gut.

  1. choose_build_level(...) takes four yes/no facts about your situation (keyword-only, so each call reads clearly) and returns a pick plus the why.
  2. The if / elif / else ladder encodes the priorities: wanting control or learning sends you to the raw loop first; a terminal being enough sends you to the CLI; otherwise embedding in an app lands on the Agent SDK, which is also the safe default.
  3. The scenarios dict runs three realistic situations through the function and prints each recommendation with its reason — so you can see the rule applied, not just stated.

What the output means: Three lines: "embed in our backend" → Agent SDK; "quick terminal help" → CLI; "teach myself" → raw loop. Exactly the mapping the lesson argues for.

Try this: Add your own scenario to the scenarios dict with your real answers and run it — the recommendation for your actual project falls out.

The one-line testIn a terminal, for me? → CLI. In my program, but I don't want to own the loop? → Agent SDK. I need total control or I'm learning? → raw loop. Most production "agent inside our app" work lands squarely on the SDK.

9 · Production concerns professional

The gap between "it worked in my terminal" and "we run this in production" is where the scary stories live. An agent that reads files, runs shell commands, and spends real money on every turn is, from an operations standpoint, an autonomous process with a credit card and a keyboard. In a demo that's thrilling. Pointed at a real repository, a real database, and a real billing account, the same autonomy is a liability you have to actively contain. This section is the containment.

The mental model to carry in is defense in depth, the same layered idea a bank uses: a locked door, then a vault, then a camera, then an alarm — not one perfect wall but several imperfect ones, so a failure in any single layer isn't catastrophic. The three concerns below map onto three layers. Permissions decide what the agent is even allowed to touch (the locked door). Cost caps bound how much damage a runaway loop can do to your bill before someone notices (the vault limit). Observability is the camera — it doesn't prevent anything, but without it you can't debug, price, or explain what the agent did after the fact.

The costly mistake is treating these as things you'll "add later," because later usually means after the incident. The good news is the SDK gives you the levers cheaply and up front: a tight allowed_tools and a sane permission_mode cost you nothing but a moment's thought; a spend cap is one option; and the message stream you're already looping over is your audit log if you just persist it. Wire the guardrails in on day one, when they're a few lines, rather than on the day you're explaining to someone why an agent ran Bash against production. The checklist below is the day-one version.

Letting an SDK drive a loop that can read files, run shell commands, and spend money is powerful and risky in equal measure. Three concerns turn a demo into something you'd run for real:

Permissions & sandboxingallowed_tools · permission_mode — the locked door Cost capsspend limit · bound subagent depth — the vault limit Observabilitypersist the stream — the camera the agent Defense in depth, not one perfect wall. Permissions decide what the agent may touch, cost caps bound the damage of a runaway loop, and observability lets you debug and price it after the fact — three imperfect layers so no single failure is catastrophic.
🗺️ How to read this diagram

Read it from the outside in — each band is a layer of protection wrapped around the agent at the center.

  • The outer band — Permissions & sandboxing is the locked door: allowed_tools and permission_mode decide what the agent is even allowed to touch.
  • The middle amber band — Cost caps is the vault limit: a spend cap and bounded subagent depth cap how much a runaway loop can cost before anyone notices.
  • The inner green band — Observability is the camera: persisting the message stream doesn't prevent anything, but without it you can't debug, price, or explain what the agent did.
  • The center is the agent itself — powerful and autonomous, which is exactly why it needs all three layers rather than trusting any one.

In short: Wrap the agent in three cheap layers on day one — allow-list, spend cap, and a persisted trace — not after the incident.

Production checklist

  1. Permissions & sandboxing. Keep allowed_tools as small as the task allows, prefer read-only tools, and use permission_mode deliberately — never bypassPermissions outside a sandbox. For fine-grained control, pass a can_use_tool callback that inspects each call and returns allow/deny (verify the exact result types in the docs). Run untrusted work in a container or throwaway workspace.
  2. Cost. An agent loop makes many API calls, and subagents multiply that into a tree. Cap it: set a spend limit (max_budget_usd) and bound subagent depth/concurrency (via the SDK's env options). Prefer a cheap model for routine subagent work.
  3. Observability. The message stream is your trace — log every tool call, tool result, and the final result/usage. You cannot debug or price an agent you can't see. Persist the stream (or key events) so a failed run is diagnosable after the fact.
bypassPermissions is a loaded gunpermission_mode="bypassPermissions" lets the agent run any allowed tool with no gate — including Bash and file writes. It's fine in a disposable sandbox and genuinely dangerous anywhere near real data or credentials. Default to default, narrow allowed_tools, and reach for a can_use_tool callback when you need real policy.

10 · Tech-lead — where the SDK fits tech-lead

An individual engineer picks the right tool for one task; a tech lead picks the right default for a hundred tasks they'll never personally see. That's a different and harder problem. Left to their own instincts, ten engineers will make ten different calls — some hand-rolling loops, some fighting to embed the CLI — and six months later you have a codebase with three ways to talk to Claude and nobody who understands all of them. The lead's job here isn't to make the clever choice once; it's to set a standard that keeps the team out of the ditches on either side.

The useful frame is a highway with guardrails, not a maze. The Agent SDK is the main lane, and it should be the boring default for the most common request you'll get — "put an agent inside our product" — because it hands you the managed loop, sessions, permissions, and subagents without a bespoke framework someone will have to maintain after they've left the company. The guardrails are the two exits: reach down to the raw loop only when you need control the SDK genuinely doesn't expose (and accept you now own that plumbing forever), and reach up to the CLI for developer-facing terminal work where embedding buys nothing. Naming those exits explicitly is what stops an engineer from taking one out of mere preference.

The trap a lead must actively manage is this surface's youth. The SDK has already been renamed once and its class and option names keep moving — which means a snippet that worked last quarter (including the ones in this very lesson) can quietly rot. Treat that as a standing policy, not a footnote: pin the claude-agent-sdk version, read the changelog before any upgrade, and make the official Python reference — never memory, never a blog post, never this page — the source of truth for names. A team that pins and verifies ships calmly through the churn; a team that trusts snippets debugs the same rename three times. The rest of this section is that discipline spelled out.

A lead's job is to place each build on the right rung and keep the team off the wrong ones. The Agent SDK is the right default for "an agent embedded in our product": it gives you the managed loop, session handling, permissions, and subagents without a bespoke framework you'd have to maintain — and because it's the same engine as the CLI, behavior you validate in the terminal transfers to production.

Steer by these lines. Reach down to the raw loop only when you need control the SDK doesn't expose, or you're deliberately teaching the mechanics — accept that you now own the plumbing forever. Reach up to the CLI for developer-facing terminal work where embedding buys nothing. Sit on the SDK for everything in between. And treat the honest caveat as a standing rule: this surface is young and moving — pin your claude-agent-sdk version, read the changelog before upgrading, and verify class and option names against the current docs rather than trusting a snippet (including these).

Pin the version; verify the namesThe Agent SDK's API has already been renamed once (Claude Code SDK → Claude Agent SDK) and its classes and options keep evolving. Every SDK block in this lesson is labeled and carries a "verify in docs" comment for exactly this reason. In production: pin the version, and let the official Python reference — not memory — be the source of truth for names.

Exercise AP7.1 — Three levels, one task

Context: The same task can be built three ways with wildly different effort and control. Reasoning through one concrete job in all three shows you where each build level actually pays off.

Your task: Take one concrete task (e.g. summarize every README in a repo) and describe three ways to do it: (a) the raw messages loop, listing the plumbing you'd hand-write; (b) the Agent SDK with query() and a read-only allowed_tools; (c) the Claude Code CLI. Then use the decision helper to confirm what you'd ship and justify it.

Requirements:

  • For the raw loop, name the plumbing you own: the tool-use loop, tool dispatch, message accumulation, error handling
  • For the SDK, show query() plus a read-only allowed_tools and note what the SDK runs for you
  • For the CLI, describe the zero-code path and its trade-off in control
  • Feed your task's traits into the build-level decision helper and report its answer
  • Justify the shipped choice in one or two sentences tied to this task

💡 Hint: Frame the comparison as control-vs-convenience: the raw loop maximizes control, the CLI maximizes convenience, and the SDK is the middle you usually want.

Exercise AP7.2 — Lock it down

Context: A code-reading assistant that can write files or run shell is a liability. The SDK gives you several independent guardrails — allowlists, permission modes, and a tool-use callback — and real safety comes from combining them.

Your task: Sketch a ClaudeAgentOptions for an agent that may read and search a codebase but must never write files or run shell commands. Decide the allowed_tools, the permission_mode, where a can_use_tool callback adds value, how you'd cap cost, and what you'd log.

Requirements:

  • List an allowed_tools set that is read/search only — no write or shell tools
  • Choose a permission_mode and say why it fits a read-only agent
  • Describe what a can_use_tool callback would deny at runtime as a second line of defense
  • State a concrete cost cap (e.g. a token or turn budget) and how you'd enforce it
  • Name what you'd log for observability (tool calls, denials, token usage)

💡 Hint: Treat the allowlist, the permission mode, and the callback as layers: the allowlist removes the tool, the mode gates it, and the callback is your final programmatic veto.

🪜 Practice ladder beginner → industry

Six graded exercises, easy to real-world. Try each before opening its solution.

Exercise 1 · One-shot query() callBeginner

Context: The Claude Agent SDK's query() is the fastest way off the ground: one async call runs the whole agent loop for you and streams messages back. It is how most SDK apps start before they need session state.

Your task: Write a single one-shot query() call that asks Claude a question and prints the text of every message it yields.

Requirements:

  • Import query from claude_agent_sdk
  • query() is async — drive it with async for inside a coroutine run by asyncio.run
  • Iterate each message's content blocks and print only the text blocks
  • Guard for messages that carry no content instead of assuming a shape
  • Note in a comment that this needs an API key to actually run

💡 Hint: Messages stream in as an async iterator; loop their content and keep just the blocks whose type is text.

Show solution

query() is async and returns an async iterator of messages. Await it inside asyncio.run.

import asyncio
from claude_agent_sdk import query

async def main():
    async for message in query(prompt="List three uses for a hash map."):
        # messages stream in; print any text blocks
        for block in getattr(message, "content", []) or []:
            if getattr(block, "type", None) == "text":
                print(block.text)

asyncio.run(main())
Exercise 2 · Set a system prompt and restrict toolsIntermediate

Context: Handing an agent your whole toolbox is how accidents happen. ClaudeAgentOptions is where you pin the agent's persona and fence it into a safe, read-only set of tools before it ever runs.

Your task: Call query() with a ClaudeAgentOptions that sets a system_prompt and limits allowed_tools to read-only file access.

Requirements:

  • Build a ClaudeAgentOptions and pass it as the options= argument to query()
  • system_prompt gives the agent a clear, cautious role
  • allowed_tools lists only read-oriented tools (e.g. Read, Grep) — nothing that writes
  • Set an explicit permission_mode rather than relying on a default
  • Print the model's text output as before

💡 Hint: The persona, the tool allowlist, and the permission mode all live on the same ClaudeAgentOptions object — construct it once and pass it in.

Show solution

ClaudeAgentOptions carries the system prompt, the tool allowlist, and the permission mode.

import asyncio
from claude_agent_sdk import query, ClaudeAgentOptions

opts = ClaudeAgentOptions(
    system_prompt="You are a careful code reviewer. Never edit files.",
    allowed_tools=["Read", "Grep"],       # read-only
    permission_mode="default",
)

async def main():
    async for m in query(prompt="Summarize what src/ contains.", options=opts):
        for b in getattr(m, "content", []) or []:
            if getattr(b, "type", None) == "text":
                print(b.text)

asyncio.run(main())
Exercise 3 · Register a custom in-process toolAdvanced

Context: Agents get useful when they can call your code. The SDK lets you define a tool in-process with the @tool decorator and serve it over an in-memory MCP server — no separate process, no network.

Your task: Define a @tool named add that sums two numbers, wrap it in an in-process server with create_sdk_mcp_server, and expose it through ClaudeAgentOptions.

Requirements:

  • The @tool decorator takes a name, a description, and an input schema
  • The tool returns a content-block dict (a content list of typed blocks), not a bare value
  • Register it with create_sdk_mcp_server and list it under mcp_servers in the options
  • Reference the tool in allowed_tools using the mcp__<server>__<tool> naming
  • Show the tool's contract offline by calling it directly and printing the returned block

💡 Hint: The tool body is just an async function; you can invoke it yourself with a plain args dict to verify the content-block shape without any API key.

Show solution

The @tool decorator takes name, description, and an input schema; tools return a content-block dict. Custom tools are referenced as mcp__<server>__<tool>.

from claude_agent_sdk import tool, create_sdk_mcp_server, ClaudeAgentOptions

@tool("add", "Add two numbers", {"a": float, "b": float})
async def add(args):
    total = args["a"] + args["b"]
    return {"content": [{"type": "text", "text": str(total)}]}

server = create_sdk_mcp_server(name="calc", version="1.0.0", tools=[add])

opts = ClaudeAgentOptions(
    mcp_servers={"calc": server},
    allowed_tools=["mcp__calc__add"],
)
# offline check of the tool contract:
import asyncio
print(asyncio.run(add({"a": 2, "b": 3})))   # {'content': [{'type':'text','text':'5.0'}]}
Exercise 4 · Multi-turn session with ClaudeSDKClientExpert

Context: query() forgets everything between calls, which breaks any real conversation. ClaudeSDKClient holds a live session so turn two can build on turn one.

Your task: Hold a two-turn conversation where the second turn remembers a fact from the first, using ClaudeSDKClient as an async context manager.

Requirements:

  • Enter the client with async with ClaudeSDKClient(...) so the session spans both turns
  • Send each turn with .query() and drain it with .receive_response()
  • Turn one states a fact; turn two asks about it in the same session
  • Fully consume the first response before sending the second turn
  • Print the second answer to show the fact was retained

💡 Hint: The point is that both .query() calls happen inside one async with block — the same client instance is the memory.

Show solution

query() forgets between calls; ClaudeSDKClient keeps session memory. Use async with, then .query() and .receive_response().

import asyncio
from claude_agent_sdk import ClaudeSDKClient, ClaudeAgentOptions

async def main():
    async with ClaudeSDKClient(options=ClaudeAgentOptions()) as client:
        await client.query("My name is Ada. Remember it.")
        async for _ in client.receive_response():
            pass
        await client.query("What is my name?")   # same session -> remembers
        async for m in client.receive_response():
            for b in getattr(m, "content", []) or []:
                if getattr(b, "type", None) == "text":
                    print(b.text)

asyncio.run(main())
Exercise 5 · A specialized subagentProfessional

Context: Big agents stay sane by delegating: a focused subagent gets its own prompt, its own restricted tools, and often a cheaper model, keeping the main agent's context clean.

Your task: Define an AgentDefinition for a read-only reviewer subagent and register it so the main agent can delegate code review to it.

Requirements:

  • Create an AgentDefinition with its own description and prompt
  • Restrict its tools to read-only access (e.g. Read, Grep)
  • Override its model to a cheaper tier (e.g. sonnet)
  • Register it under the agents dict of ClaudeAgentOptions keyed by name
  • Have the main system_prompt instruct delegation to that subagent
  • Read the registered subagent's model back out to prove it took

💡 Hint: A subagent is an isolated instance — everything that scopes it (prompt, tools, model) lives on its AgentDefinition, not on the parent.

Show solution

Subagents are isolated instances with their own context, tools, and model override. Register them in the agents dict.

from claude_agent_sdk import ClaudeAgentOptions, AgentDefinition

reviewer = AgentDefinition(
    description="Reviews diffs for bugs; read-only.",
    prompt="You are a strict reviewer. Report issues; never edit.",
    tools=["Read", "Grep"],
    model="sonnet",                 # per-agent model override
)

opts = ClaudeAgentOptions(
    agents={"reviewer": reviewer},
    system_prompt="Delegate code review to the reviewer subagent.",
)
print(opts.agents["reviewer"].model)   # sonnet
Exercise 6 · Choose raw loop vs SDK vs CLI for a real projectIndustry scenario

Context: Platform teams have to pick one way for every internal app to call Claude. The real trade is control vs convenience: a raw messages loop, the Agent SDK, or the zero-code Claude Code CLI.

Your task: Encode the decision as choose_build_level(need_custom_tools, want_managed_loop, want_zero_code, need_fine_control) returning 'raw-loop', 'agent-sdk', or 'claude-code-cli', then justify one recommendation.

Requirements:

  • Zero-code with no need for fine control routes to the claude-code-cli
  • A demand for fine control without a managed loop routes to the raw-loop
  • Custom tools plus a managed loop routes to the agent-sdk
  • The function returns exactly one of the three literal strings
  • Runs fully offline — it is pure decision logic, no API call
  • Walk through one concrete team's inputs and justify the recommendation in a comment

💡 Hint: Order the branches so the most decisive signals (zero-code, fine-control) are tested first and the SDK falls out as the balanced default.

Show solution

The three build levels trade control for convenience. This mirrors the lesson's decision helper.

def choose_build_level(need_custom_tools, want_managed_loop,
                       want_zero_code, need_fine_control):
    if want_zero_code and not need_fine_control:
        return "claude-code-cli"         # fully packaged, no code
    if need_fine_control and not want_managed_loop:
        return "raw-loop"                # you own the perceive-act-observe loop
    return "agent-sdk"                   # SDK runs the loop, you supply tools

# A team wanting custom tools + a managed loop, some control:
rec = choose_build_level(need_custom_tools=True, want_managed_loop=True,
                         want_zero_code=False, need_fine_control=False)
print(rec)   # agent-sdk
# Justification: custom tools + managed loop = SDK; CLI is too opaque for
# an embeddable library, a raw loop is needless maintenance here.

✓ Checkpoint — you can move on when you can…

  • Explain what the Claude Agent SDK is and why it sits between the raw loop and the CLI.
  • Install it: pip install claude-agent-sdk + the Claude Code CLI + an API key.
  • Run an agent with the async query() entry point and read its message stream.
  • Configure tools, a system prompt, and permission_mode via ClaudeAgentOptions.
  • Hold a multi-turn session with ClaudeSDKClient and delegate to a subagent.
  • Choose the right level (raw loop / SDK / CLI) and name the permissions, cost, and observability concerns.

Knowledge check check yourself

✓ Knowledge check

Where does the Claude Agent SDK sit relative to the raw messages loop and the Claude Code CLI, and when should you reach for the SDK?

Show answer
The SDK runs the agent loop for you while you supply tools, permissions, and a system prompt via ClaudeAgentOptions; the raw loop means you write everything, and the CLI is a fully packaged terminal program built on the SDK. Reach for the SDK when embedding an agent in your own product and you want a managed loop plus permissions without owning the plumbing.
✓ Knowledge check

Why is allowed_tools the primary access-control lever in the SDK, and what makes permission_mode="bypassPermissions" dangerous?

Show answer
allowed_tools is an explicit allow-list of exactly which tools the agent may call, so keeping it minimal and read-only bounds the blast radius. bypassPermissions skips all permission checks, so it removes every gate on risky actions and should only be used inside a sandbox.
© 2026 studybydoing.in · AI Engineering: Zero to Production · All rights reserved. · About · Privacy Policy · Terms · Contact
Educational content, provided as-is and without warranty. Code samples are examples — review, test, and adapt them before using in production. See the Terms of Use & Disclaimer. Use at your own risk.
© studybydoing.in