Skip to content
Domain 1Task 1.2

Agent Construction with Claude

Learn this interactively

What You Need to Know

An agent is a model calling tools in a loop to complete a task. The harness is the code that runs the loop. In the Claude Agent SDK, a turn is one round trip: the model produces output including tool calls, the SDK executes them, and the results feed back automatically. A hook is harness code that runs at a fixed point, such as before a tool executes.

Take a holiday cottage lettings business that wants an agent to read the week's bookings and draft the Saturday cleaning rota. Its harness must cap its turns and spend, and keep it off the live bookings system.

The Bare Loop

Every agent harness wraps the same loop. Send the prompt with tool definitions. If the response requests tools, execute them, append the results and call again. The loop ends when Claude produces output with no tool calls, or when a cap trips. Against the Messages API it fits in about twenty lines:

python
import anthropic

client = anthropic.Anthropic()

TOOLS = [
    {
        "name": "list_open_incidents",
        "description": "Return the currently open incidents as a JSON array.",
        "input_schema": {"type": "object", "properties": {}},
    }
]


def run_tool(block):
    if block.name == "list_open_incidents":
        return '[{"id": "INC-201", "summary": "Payment API latency", "severity": "high"}]'
    return f"Unknown tool: {block.name}"


messages = [{"role": "user", "content": "Summarise the open incidents and file a report."}]

while True:
    response = client.messages.create(
        model="claude-sonnet-5-5",
        max_tokens=4096,
        tools=TOOLS,
        messages=messages,
    )
    if response.stop_reason != "tool_use":
        break
    messages.append({"role": "assistant", "content": response.content})
    tool_results = [
        {"type": "tool_result", "tool_use_id": block.id, "content": run_tool(block)}
        for block in response.content
        if block.type == "tool_use"
    ]
    messages.append({"role": "user", "content": tool_results})

Build the loop by hand once and the SDK's vocabulary stops being abstract.

The loop is the easy part. Production needs a harness: turn caps, spend budgets, retries, tool-result validation, state persistence and observability. These belong in code, never in the prompt. The model may not follow a prompt; code runs every time. A lettings prompt saying "stop after thirty calls" is a request; a turn cap ends the run. Write the loop yourself when the application needs full control of it or is not agent-shaped. For agentic work, the packaged harness is the default.

The Agent SDK as the Packaged Harness

The Claude Agent SDK ships the packaged harness in Python and TypeScript: tool execution, permissions, hooks, subagents, sessions with resume, and automatic compaction.

Two options bound a run. max_turns (maxTurns in TypeScript) caps the loop and counts tool-use turns only. max_budget_usd (maxBudgetUsd) stops the query when estimated spend reaches that dollar value. Both default to no limit, so a stuck lettings agent keeps spending. The docs call a budget cap a good default for production agents. Hitting a limit ends the run with a ResultMessage whose subtype names the cause:

python
import asyncio
from claude_agent_sdk import query, ClaudeAgentOptions, ResultMessage

async def run_agent():
    try:
        async for message in query(
            prompt="Find and fix the bug causing test failures in the auth module",
            options=ClaudeAgentOptions(
                allowed_tools=["Read", "Edit", "Bash", "Glob", "Grep"],
                max_turns=30,
                max_budget_usd=2.00,
            ),
        ):
            if isinstance(message, ResultMessage):
                session_id = message.session_id
                if message.subtype == "success":
                    print(f"Done: {message.result}")
                elif message.subtype == "error_max_turns":
                    print(f"Turn limit hit; resume session {session_id} with a higher cap.")
                elif message.subtype == "error_max_budget_usd":
                    cost = (
                        f"${message.total_cost_usd:.4f}"
                        if message.total_cost_usd is not None
                        else "unknown"
                    )
                    print(f"Budget limit hit after {cost}.")
    except Exception as error:
        # A single-shot query() raises after yielding an error result, so the
        # subtype branches above have already run before this handler fires.
        print(f"Session ended with an error: {error}")

asyncio.run(run_agent())

The subtypes are success, error_max_turns, error_max_budget_usd and error_during_execution. As of August 2026 the docs also list error_max_structured_output_retries. Every subtype carries total_cost_usd, num_turns and session_id. The result text is present only on success, so code checks the subtype before reading it. A lettings run stopped at its turn cap has no result text; resume it from its session ID.

The budget cap is cumulative across subagents. Their requests count towards total_cost_usd, and at the cap further spawns fail and the run ends with error_max_budget_usd. Subagents can spawn their own, so spend is one of three caps on their growth. The other two are environment variables passed through the env option. As of September 2026, CLAUDE_CODE_MAX_SUBAGENT_SPAWN_DEPTH allows three layers of subagents below the main agent by default. Setting it to 1 stops subagents spawning any of their own. A subagent on the bottom layer does its delegated work itself. CLAUDE_CODE_MAX_CONCURRENT_SUBAGENTS allows 20 running at once by default.

A limit result is not a model failure

A run that ends with error_max_turns did what it was configured to do. The documented recovery is to resume the session with a raised limit (passing the captured session_id to the resume option), tighten the task brief, or split oversized work before dispatch. Prompt nudges and model swaps do not change the arithmetic of an under-scoped cap. That is why they appear as distractors.

The example above uses single message input: one query() call with a prompt string. That suits a one-shot answer or a stateless environment such as a lambda function. The docs call the other mode, streaming input, the preferred way to use the SDK. You pass an async generator of messages (through ClaudeSDKClient in Python), and the agent runs as a long-lived process. It takes image attachments, queues further messages and handles interruptions. Single message input supports none of these. The weekly rota job fits single message input.

Where the Agent Runs

The two deployment models split by who runs the harness. Self-hosted: your process runs the loop, custom or via the Agent SDK, on infrastructure you operate, with full control and full operational burden. Anthropic-hosted: Claude Managed Agents, a separate product from the SDK, where Anthropic runs the agent harness and the sandbox behind a REST API. Long-running or asynchronous work, stateful server-side sessions and minimal infrastructure point at Managed Agents. Full runtime control points at self-hosting. As of August 2026, Managed Agents is in beta (header managed-agents-2026-04-01) and its sandbox can be Anthropic-managed or self-hosted on your own infrastructure for data-residency needs. Who runs the harness and where the sandbox lives are separate questions.

Self-hosted, the SDK by default writes session transcripts to JSONL files under ~/.claude/projects/ on local disk. A container that shuts down takes its transcripts with it. A SessionStore adapter mirrors them to your own object store, key-value store or database. A session created on one host can then be resumed on another running from a matching working directory. For containers that spin down between a user's visits, the hosting guide calls the store required.

Managed Agents are versioned: each configuration change creates a new version. A session created with the plain agent ID runs the latest. Passing an object with a version pins the session to it, which lets you stage a rollout. Scheduled deployments let an agent start its own sessions on a cron schedule, set with a cron expression and a timezone. The lettings rota could run each Saturday.

Hooks Enforce, Prompts Request

Hooks run deterministically: certain actions always happen, whatever the model chooses. The canonical guardrail is a PreToolUse hook, which fires before a tool executes and can deny it.

python
async def block_prod_db(input_data, tool_use_id, context):
    command = input_data.get("tool_input", {}).get("command", "")
    if "prod-db-cli" in command:
        return {
            "hookSpecificOutput": {
                "hookEventName": "PreToolUse",
                "permissionDecision": "deny",
                "permissionDecisionReason": "Production database access is not permitted.",
            }
        }
    return {}

Registered with HookMatcher(matcher="Bash", hooks=[block_prod_db]) under the PreToolUse event, the hook blocks the call regardless of what the model was persuaded to attempt. Claude receives the denial as the tool result and works around it. A system-prompt rule is an instruction the model may not follow under adversarial input. A hook executes outside the model's discretion. The lettings agent's ban on the live bookings system belongs in such a hook.

In the exam

This is the domain's heaviest and most mechanism-precise sub-skill. The exam keys exact names: the run-limit options, the result subtypes and what each one means. Items also test whether a concern belongs in code or the prompt. "Prompts request, hooks enforce" is the exam's favourite construction here. Deeper hook events and Claude Code specifics belong to Domains 3 and 7.

Caveats

As of September 2026, in streaming mode a message still queued when a turn hits the max_turns limit starts a new turn with the count reset. The budget keeps accumulating across the conversation.

As of September 2026, a Managed Agents self-hosted sandbox keeps tool execution, files and network egress in your environment. Tool inputs and outputs still flow to Anthropic's control plane so the model can see results. The agent's skills and memory stores are stored by Anthropic.

Exam Traps

Exam Trap

Recognising invented-but-plausible parameter names such as max_iterations, budget_limit or turn_cap as real SDK options.

The documented names are max_turns / maxTurns and max_budget_usd / maxBudgetUsd, and the terminal subtypes are error_max_turns and error_max_budget_usd. Items key the exact names; near-misses are the distractors.

Exam Trap

Reading error_max_turns as the model failing or getting lost, and fixing it with a better prompt or a bigger model.

The limit result is a budget and configuration event: the guardrail fired as configured. The keyed fixes are resuming the session with a raised limit, tightening the task brief, or splitting the work before dispatch.

Exam Trap

Treating a system-prompt prohibition as a guardrail equivalent to a hook.

Instructions steer the model but do not bind it, and injected text can override them. A PreToolUse hook returning a deny decision executes deterministically before the tool runs; that is the property the word guardrail is doing in the question.

Exam Trap

Assuming subagent spend escapes the parent's budget cap because subagents run in their own contexts.

The budget is cumulative: subagent requests count toward total_cost_usd, spawns fail once the cap is reached, and the run ends with error_max_budget_usd. Context isolation does not mean cost isolation.

Exam Trap

Describing Claude Managed Agents as the Agent SDK hosted for you.

They are separate products. The SDK is a library running the loop in your own process; Managed Agents is a hosted REST API where Anthropic runs the agent and the sandbox. Items test knowing who operates the harness in each model.

Practice Scenario

A developer-platform team ships a repository maintenance agent on the Claude Agent SDK with Bash access. Policy: it must never invoke the production database CLI, even if malicious text in a cloned repository instructs it to. Which control actually enforces the policy?

Build Exercise

Build Exercise

Trip Every Limit on Purpose

Difficulty
30 minutes

What you'll learn

  • How max_turns and max_budget_usd bound an Agent SDK run
  • How to read ResultMessage subtypes and recover by resuming a session
  • How the budget cap contains subagent spend
  • How a PreToolUse hook denies a tool call deterministically
  1. Write a small Agent SDK script (Python or TypeScript) that sets max_turns to 2 on a task that clearly needs more, and run it

    Why: Tripping the limit on purpose shows the failure signal in a controlled setting before production shows it to you.

    You should see: A ResultMessage with subtype error_max_turns, no result text, and total_cost_usd and num_turns still populated.

  2. Capture session_id from the error result, then resume the session with the resume option and a raised max_turns

    Why: Resume-with-a-raised-limit is the documented recovery, and proving the agent continues with prior context beats re-reading it.

    You should see: The run continues from where it stopped rather than starting the task over.

  3. Set max_budget_usd deliberately low on a tool-heavy task and observe the result

    Why: The budget cap is the production default worth rehearsing, and its failure signal differs from the turn cap only in subtype.

    You should see: A ResultMessage with subtype error_max_budget_usd, with total_cost_usd at or near your cap.

  4. Add a PreToolUse hook matched to Bash that denies a chosen command pattern with a reason, then prompt the agent to run that command

    Why: Watching the model receive the denial and route around it makes the prompts-request-hooks-enforce distinction concrete.

    You should see: The tool call blocked before execution, the reason surfaced back to the model, and the agent adjusting course.

  5. Write down the mapping from each run to its subtype: success, error_max_turns, error_max_budget_usd

    Why: The subtype-to-cause mapping is the keyed fact this sub-skill tests, and you have now produced every row of it yourself.

    You should see: A three-row table from your own runs, matching the documented subtype set.

Sources