---
title: "The Harness: Models, the Agent SDK, and Stop-Reason Loops"
url: "https://vanducng.dev/2026/08/15/the-harness-models-agent-sdk-and-stop-reason-loops/"
date: "2026-08-15"
series: "agentic-coding-with-claude-code"
categories: ["AI"]
tags: ["agentic-coding","claude-code","agent-sdk"]
---

# The Harness: Models, the Agent SDK, and Stop-Reason Loops

[Part 2](https://vanducng.dev/2026/08/15/context-engineering-for-claude-code/) ended with a debt: when I own the runtime, I have to implement trim, a facts block, tiered compress, and placement myself. Claude Code already does a version of that. A product I ship does not get it for free.

This is Part 3 of 6 in my **Agentic Coding with Claude Code** series. The working model I am keeping is simple: the model reasons, the harness decides what happens next.

Claude Code is one harness. The [Agent SDK](https://code.claude.com/docs/en/agent-sdk/overview) is that same loop as a library. The Messages API is the loop with the cover off: every response has a `stop_reason`, and your code chooses whether to run a tool, ask the user, route, escalate, or halt.

Later posts will cover MCP, evals, and guardrails. This one stays on the runtime around the model.

## The Harness Is the Runtime Around the Model

[Part 1](https://vanducng.dev/2026/08/09/core-concepts-of-agentic-coding-with-claude-code/) treated Claude Code as a bounded loop: perceive, reason, act, then feed the evidence back in. That loop does not live inside the weights. It lives in the process that calls the model.

Official docs say the same thing in product language. [How Claude Code works](https://code.claude.com/docs/en/how-claude-code-works) calls Claude Code the **agentic harness** around Claude: tools, context management, and an execution environment that turn a language model into a coding agent. The [Agent SDK](https://code.claude.com/docs/en/agent-sdk/overview) is that harness as a library. The [Client SDK](https://platform.claude.com/docs/en/cli-sdks-libraries/overview) calls the Messages API: you implement the tool loop yourself.

My working split:

| Piece | Job | Who owns it |
| --- | --- | --- |
| **Model** | Read the window, pick the next action or the next sentence | Anthropic |
| **Loop** | Call the model again until the task is done or a limit fires | Harness |
| **Tools** | The agent's public API: what it can observe or change | Harness |
| **Permissions** | Which tool calls run, which ask, which never exist | Harness |
| **Context assembly** | What occupies the window on the next turn | Harness |
| **Stop conditions** | When to run a tool, ask a human, route, escalate, or halt | Harness |

```mermaid
flowchart TB
    Goal[Goal] --> Assemble[Assemble context]
    Assemble --> Model[Model]
    Model --> Stop["stop_reason"]
    Stop -->|tool_use| Tool[Run or deny tool]
    Tool --> Trim[Trim the result]
    Trim --> Assemble
    Stop -->|end_turn| Decide[Ask / route / halt]
    Stop -->|refusal / max_tokens| Halt[Retry or fail closed]

    classDef blueClass fill:#4A90E2,stroke:#333,stroke-width:2px,color:#fff
    classDef orangeClass fill:#F39C12,stroke:#333,stroke-width:2px,color:#fff
    classDef greenClass fill:#27AE60,stroke:#333,stroke-width:2px,color:#fff
    classDef purpleClass fill:#9B59B6,stroke:#333,stroke-width:2px,color:#fff
    classDef tealClass fill:#16A085,stroke:#333,stroke-width:2px,color:#fff
    classDef redClass fill:#E74C3C,stroke:#333,stroke-width:2px,color:#fff

    class Goal blueClass
    class Assemble,Trim orangeClass
    class Model greenClass
    class Stop purpleClass
    class Tool tealClass
    class Decide,Halt redClass
```

The model does not invoke your function. It returns a `tool_use` block and stops. The harness runs the function, or refuses to, then writes a `tool_result` back into the window. If I skip that step, the user sees an agent that dies the first time it tries to act.

Claude Code hides that dispatch. The Agent SDK hides it too, and yields a stream of messages while it works. When I want the dispatch itself to encode product policy (clarify vs route vs escalate), I read `stop_reason` on the Messages API and write the branch.

## Model Choice Is an Engineering Decision

Course 1 used the Claude 4.5 family: Haiku, Sonnet, Opus. The IDs move. The decision does not. Match the model to the step, not one model to the whole system.

Current aliases I fetched from the [models overview](https://platform.claude.com/docs/en/about-claude/models/overview) (August 2026):

| Tier | Alias | Use it for | Do not use it for |
| --- | --- | --- | --- |
| **Haiku** | `claude-haiku-4-5` | Classify, extract, cheap tool hops, high-volume substeps | Ambiguous policy, first-time architecture |
| **Sonnet** | `claude-sonnet-5` | The default agent loop: read, edit, test, route | Overnight research you will not review |
| **Opus** | `claude-opus-5` | Hard judgment: escalate, conflicting evidence, a plan that will spawn other work | Every lookup in a tight loop |

Pricing and context windows change. The current table on that page is Haiku at $1 / $5 per million tokens (200k context), Sonnet at $2 / $10 (1M), Opus at $5 / $25 (1M). I treat those as inputs to the step budget, not as a reason to pick one model and forget it.

The Agent SDK makes the choice explicit. If you omit `model`, it uses Claude Code's default for the authentication method and subscription. Pin it when the step has a known cost shape:

```python
from claude_agent_sdk import ClaudeAgentOptions

options = ClaudeAgentOptions(
    model="claude-sonnet-5",
    max_turns=20,
)
```

A pattern I will reuse: Haiku classifies the incoming message, Sonnet runs the tool loop, Opus only sees the cases the classifier marked `escalate`. That is the same "smallest authority that still works" rule from [Part 1](https://vanducng.dev/2026/08/09/core-concepts-of-agentic-coding-with-claude-code/), applied to intelligence instead of file permissions.

`effort` on the Agent SDK is a second knob, not a substitute for model choice. Official docs: `"low"` for file lookups, `"high"` for refactors. Set it per session or per subagent. I leave it unset until I have a cost problem.

## Agentic Design, One Pointer

I am not re-teaching the loop. [Part 1](https://vanducng.dev/2026/08/09/core-concepts-of-agentic-coding-with-claude-code/) already has the components (goal, tools, memory, reasoning engine, human) and the patterns (single-agent, multi-agent, hierarchical).

The harness question is narrower: which of those components do I implement in code, and which do I leave to Claude Code? For this post the default is still one agent. Add a second model or a second loop only when a step is cheaper or safer in isolation (classify on Haiku, research in a subagent). Coordination cost is a later post.

## The Agent SDK: Goal, Tools, Permissions, Structured Outputs

The [Agent SDK](https://code.claude.com/docs/en/agent-sdk/overview) is how I embed Claude Code's loop in a process I own. Python and TypeScript only. Other languages drive the CLI with `-p` and `--output-format json`.

What I configure on the SDK call is the product. Python field names below; TypeScript uses the same knobs in camelCase.

| Knob | Official field | What I am actually deciding |
| --- | --- | --- |
| **Goal** | `prompt` on `query()`, plus any loaded `CLAUDE.md` | What evidence means done |
| **Tools** | `allowed_tools` on [`ClaudeAgentOptions`](https://code.claude.com/docs/en/agent-sdk/python#claudeagentoptions), plus any custom tools I register | The agent's public API |
| **Permissions** | `permission_mode`, `disallowed_tools`, `can_use_tool` | What may run without a human |
| **Structured output** | `output_format` | The shape my code will consume |
| **Budget** | `max_turns`, `max_budget_usd` | When the loop must stop even if the model wants another hop |

A single-shot `query()` streams messages and ends on a `ResultMessage`. Check `subtype` before you read `result`:

| `ResultMessage.subtype` | Meaning |
| --- | --- |
| `success` | The loop finished; `result` is the final text |
| `error_max_turns` | Hit `max_turns` (tool-use turns only) |
| `error_max_budget_usd` | Hit `max_budget_usd` |
| `error_during_execution` | The loop broke (API failure, cancel) |
| `error_max_structured_output_retries` | Schema validation never succeeded |

`query()` then raises after an error result. Wrap the loop. The session id on that message is how you resume. Crash recovery and dual-write session stores are a later layer; this post stops at the per-turn loop.

### Tools Are the Agent's Public API

[Part 2](https://vanducng.dev/2026/08/15/context-engineering-for-claude-code/) said a vague tool contract wastes the loop. In a harness I own, that contract is the product surface.

Official [tool definitions](https://platform.claude.com/docs/en/agents-and-tools/tool-use/define-tools) need a `name`, a `description`, and an `input_schema`. The description is the prompt. Anthropic's current guidance: say what the tool does, when to use it, when not to, what each parameter means, and what an empty or error result means. Return only the fields the next reason step will read.

Badly shaped tools produce badly shaped agents. Two tools that both "look up a customer" will fight. A tool that dumps a raw order JSON will tax every later turn. A tool named `search` with no "when not to use" will get called instead of the precise one.

The Agent SDK ships the same built-ins as Claude Code (`Read`, `Edit`, `Bash`, `Glob`, `Grep`, and the rest). Custom tools are functions I define and register. I will not walk the registration path here (it is an in-process server; MCP is the next post). The contract I keep is the same on either API:

```text
Name: the verb the model should think
When to use / when not to use
Inputs: required vs optional, units, invariants
Output: the fields the next turn will read
Failure: not_found, not_allowed, or error; never a guess
```

### Permissions Are Not Prompt Text

`CLAUDE.md` is guidance. If a write must not happen, that is a permission rule.

The SDK evaluates [permissions in a fixed order](https://code.claude.com/docs/en/agent-sdk/permissions#how-permissions-are-evaluated): hooks, deny rules, ask rules, the active mode, allow rules, then `can_use_tool`. A bare `disallowed_tools=["Bash"]` removes the tool from the model's context. A scoped deny like `Bash(rm *)` keeps Bash and blocks that pattern in every mode, including `bypassPermissions`.

Modes I will actually use:

| Mode | Behavior | When I use it |
| --- | --- | --- |
| `default` | Unmatched tools hit `can_use_tool` | Interactive product with an approval UI |
| `dontAsk` | Tools not pre-approved by allow rules are denied; no prompt | Headless agent with a fixed surface |
| `acceptEdits` | File edits and common filesystem commands auto-approve; other Bash still gated | Isolated repo, I trust the diffs |
| `plan` | Explore and propose; writes are never auto-approved | I want a plan, not a patch |
| `bypassPermissions` | Almost everything runs | CI or a container I can throw away |

`allowed_tools` does not constrain `bypassPermissions`. Listing `Read` and then setting bypass still approves `Bash`. For a locked-down agent, pair an allow list with `dontAsk`.

### Structured Outputs Close the Loop

Free-form text is a chat. A harness needs a value it can branch on.

On the Agent SDK, pass [`output_format`](https://code.claude.com/docs/en/agent-sdk/structured-outputs) as `{"type": "json_schema", "schema": ...}`. The agent may still use tools. When it finishes, `ResultMessage.structured_output` is validated JSON. If validation fails past the retry limit, you get `error_max_structured_output_retries`, not a best-effort blob.

On the Messages API, the equivalent is [`output_config.format`](https://platform.claude.com/docs/en/build-with-claude/structured-outputs) with `type: "json_schema"`. Same idea: the last text block is parseable JSON matching the schema.

I use structured output for decisions the harness must act on (clarify / route / escalate). I do not use it for the essay the user reads.

## `stop_reason` Drives the Loop

Every successful Messages API response includes [`stop_reason`](https://platform.claude.com/docs/en/build-with-claude/handling-stop-reasons). That field is why the model stopped generating. It is not an HTTP error. Your harness reads it and chooses the next verb.

| `stop_reason` | Official meaning | What my harness does |
| --- | --- | --- |
| `end_turn` | Finished naturally | Use the text or the structured decision: ask, route, or halt |
| `tool_use` | One or more `tool_use` blocks | Run permitted tools, append `tool_result`, call again |
| `max_tokens` | Hit `max_tokens` | Raise the limit or treat as truncated; do not pretend it is complete |
| `stop_sequence` | Hit a custom `stop_sequences` value | Read `stop_sequence`, then continue or halt |
| `pause_turn` | A server-tool loop hit its iteration limit | Send the assistant content back to resume |
| `refusal` | Declined the request | Read `stop_details`, fail closed, do not invent a workaround |
| `model_context_window_exceeded` | The response filled the window | Treat as truncated; compact or halt |

The `tool_use` branch is the whole agent, in four steps the docs repeat:

1. Append the assistant turn **verbatim**, including every `tool_use` block.
2. Run each tool (or deny it).
3. Append a user turn of `tool_result` blocks, each with the matching `tool_use_id`.
4. Call the API again with the same tools.

Skip step 1 and the next request is malformed. Skip step 3 and the model has no evidence. Cap the loop (`max_turns` on the Agent SDK, a counter on the Messages API) or an open-ended prompt will spend until you notice.

```python
# Sketch of a Messages API loop. Official fields only.
# Not a product, and not a course file.
# run_or_deny, decide_from_text, halt, TOOLS, and MAX_TURNS are placeholders you own, not SDK symbols.

from anthropic import Anthropic

def handle_message(user_text):
    client = Anthropic()
    messages = [{"role": "user", "content": user_text}]

    for _ in range(MAX_TURNS):
        response = client.messages.create(
            model="claude-sonnet-5",
            max_tokens=1024,
            tools=TOOLS,
            messages=messages,
        )

        if response.stop_reason == "tool_use":
            messages.append({"role": "assistant", "content": response.content})
            results = []
            for block in response.content:
                if block.type == "tool_use":
                    results.append(
                        {
                            "type": "tool_result",
                            "tool_use_id": block.id,
                            "content": run_or_deny(block.name, block.input),
                        }
                    )
            messages.append({"role": "user", "content": results})
            continue

        if response.stop_reason == "end_turn":
            return decide_from_text(response)  # ask / route / escalate / halt

        if response.stop_reason == "refusal":
            return halt("model_refused")

        return halt(response.stop_reason)

    return halt("max_turns")
```

The Agent SDK runs that `tool_use` branch for you. You still handle the *result*: success, turn cap, budget cap, refusal (`ResultMessage.stop_reason == "refusal"`). The Messages API is what I reach for when the branch itself is the product: after `end_turn`, I may ask a clarifying question instead of closing the ticket.

### Prompt Chain vs Stop-Reason Loop

A prompt chain is a fixed pipeline. Step 2 always runs after step 1. Dynamic decomposition is the opposite: the model picks the next tool from evidence, and the harness only enforces permissions and stop policy.

```mermaid
flowchart TB
    subgraph Loop["Stop-reason loop"]
        direction LR
        L1[Observe] --> L2[Reason] --> L3{stop_reason} --> L4[Act or ask]
        L4 --> L1
    end

    subgraph Chain["Prompt chain"]
        direction LR
        C1[Parse] --> C2[Validate] --> C3[Classify] --> C4[Reply]
    end

    classDef orangeClass fill:#F39C12,stroke:#333,stroke-width:2px,color:#fff
    classDef greenClass fill:#27AE60,stroke:#333,stroke-width:2px,color:#fff
    classDef blueClass fill:#4A90E2,stroke:#333,stroke-width:2px,color:#fff
    classDef purpleClass fill:#9B59B6,stroke:#333,stroke-width:2px,color:#fff

    class C1,C2,C3,C4 orangeClass
    class L1 blueClass
    class L2 greenClass
    class L3 purpleClass
    class L4 greenClass
```

| | Prompt chain | `stop_reason` loop |
| --- | --- | --- |
| **Control flow** | You named the steps in advance | The model names the next tool; you name the stop policy |
| **Missing input** | A later step inherits a bad parse | The harness can ask, then resume |
| **New evidence** | The chain cannot grow a step you did not write | A new `tool_use` is just another turn |
| **Cost** | Bounded and predictable | Needs `max_turns` / `max_budget_usd` |
| **Use it when** | The path is truly linear | The next action depends on what the last tool returned |

I still write chains for work that is actually a pipeline (normalize, then validate, then persist). I write a loop when the agent must discover the path. Course 1 spends time on that contrast. The takeaway I kept: do not encode a tree of "if the user said X, call Y" in prompts if `stop_reason` plus tools already is that tree.

## Claims Intake: Clarify, Route, Escalate

Course 1 trains this on a claims-style copilot. I am not reprinting their files. Invented example: **warranty-desk**, a small intake bot for a headphone company. A customer pastes a messy email. The harness must either ask a question, open a claim in the right queue, or hand the thread to a human.

```text
Goal: turn one inbound message into a claim decision.
Definition of done: a structured decision, plus a claim id if we opened one.
Clarify when: no order id, no defect photos, or the defect text is ambiguous.
Route when: we can look up the order and the warranty window is clear.
  repair | replace | deny
Escalate when: battery swell, legal language, a repeat claimant, or retail over $400.
Stop and ask before: issuing a refund, writing to the customer, or closing a claim.
```

The tools are the public API. Invented, and intentionally small:

| Tool | Returns | When not to use |
| --- | --- | --- |
| `lookup_order` | `order_id`, `sku`, `purchased_on`, `warranty_until`, `status` | You only have an email; use `lookup_customer` |
| `lookup_customer` | `customer_id`, `open_claim_ids` | You already have the order |
| `create_claim` | `claim_id`, `queue` | Any escalate condition is true |
| `escalate_to_human` | `ticket_id` | The case is a normal repair / replace / deny |

Each result is a handful of fields. `lookup_order` does not return the raw commerce payload. That is [Part 2](https://vanducng.dev/2026/08/15/context-engineering-for-claude-code/)'s trim rule, implemented at the tool boundary.

The structured decision the loop must produce:

```python
DECISION_SCHEMA = {
    "type": "object",
    "properties": {
        "action": {
            "type": "string",
            "enum": ["clarify", "route", "escalate"],
        },
        "queue": {
            "type": "string",
            "enum": ["repair", "replace", "deny", "human", "none"],
        },
        "question": {"type": "string"},
        "claim_id": {"type": "string"},
        "reason": {"type": "string"},
    },
    "required": ["action", "queue", "reason"],
    "additionalProperties": False,
}
```

On the Agent SDK I would pass that schema as `output_format` and let `query()` run the `tool_use` turns. On the Messages API I run the sketch above, then branch on `action`:

```mermaid
flowchart TB
    In[Inbound email] --> Loop[stop_reason loop]
    Loop -->|tool_use| Tools[lookup / create / escalate]
    Tools --> Loop
    Loop -->|end_turn + clarify| Ask[Ask the customer one question]
    Loop -->|end_turn + route| Ticket[Open claim in repair / replace / deny]
    Loop -->|end_turn + escalate| Human[Hand off with the facts block]
    Loop -->|refusal / max_turns| Closed[Fail closed]

    classDef blueClass fill:#4A90E2,stroke:#333,stroke-width:2px,color:#fff
    classDef orangeClass fill:#F39C12,stroke:#333,stroke-width:2px,color:#fff
    classDef greenClass fill:#27AE60,stroke:#333,stroke-width:2px,color:#fff
    classDef purpleClass fill:#9B59B6,stroke:#333,stroke-width:2px,color:#fff
    classDef tealClass fill:#16A085,stroke:#333,stroke-width:2px,color:#fff
    classDef redClass fill:#E74C3C,stroke:#333,stroke-width:2px,color:#fff

    class In blueClass
    class Loop orangeClass
    class Tools purpleClass
    class Ask tealClass
    class Ticket greenClass
    class Human,Closed redClass
```

A chain would always parse, then always validate, then always classify. The loop can look up the order first, notice the warranty expired last month, and route to `deny` without inventing a "validate photos" step I forgot to write. It can also stop after one turn and ask for the order id. That ask is not a tool. It is `end_turn` plus `action: "clarify"`, and the harness owns the pause.

Haiku can draft the first `action` guess. Sonnet should own the tool loop. Opus only sees `escalate` drafts I do not want a cheaper model to close.

## When You Own the Runtime, You Own Part 2

Claude Code already caps bash output, runs `/compact`, re-injects root `CLAUDE.md` after compact, and can fork a noisy skill. The Agent SDK inherits that loop, including [automatic compaction](https://code.claude.com/docs/en/agent-sdk/agent-loop#automatic-compaction) and a `compact_boundary` message. Persistent rules still belong in `CLAUDE.md` loaded via `setting_sources`, because compaction replaces early turns with a summary.

If I am on the Messages API, none of that exists until I write it. The four rules from [Part 2](https://vanducng.dev/2026/08/15/context-engineering-for-claude-code/) become code:

| Rule | In Claude Code | In a runtime I own |
| --- | --- | --- |
| **Trim at the tool** | Bash cap, structured tool results | The handler returns six fields, not the vendor dump |
| **Persistent facts block** | Root `CLAUDE.md`, auto memory | A short list I rewrite every turn: order id, claim id, confirmed defect, what the user already answered |
| **Tiered compress** | `/compact`, auto compact | Squeeze resolved clarify turns hard; keep the active lookup; never drop the facts block; budget in tokens |
| **Place facts at the edge** | Root `CLAUDE.md` is re-injected after compact | Prepend or append the facts block on every `messages.create`; do not bury it between tool traces |

A facts block for warranty-desk looks like this, not like a transcript:

```text
order_id: AO-18422
sku: Auraline-H2
warranty_until: 2026-03-01
user_confirmed: no receipt, photos attached
open_claims: none
decision_so_far: none
```

I rebuild that block from durable state after every tool result. I do not ask the model to remember it from turn 3. After a compact, the middle of the window is a summary. The facts still sit at the head or tail.

The squeeze rule does not change:

| Layer | Squeeze | Keep |
| --- | --- | --- |
| Resolved turns | Hard | Outcome and the evidence that proved it |
| Active turns | Light | Enough to continue the current step |
| Facts block | Never | Ids, constraints, confirmed answers |

Custom tool handlers are the first place this fails. If `lookup_order` returns 40 KB of line items, the next `end_turn` is already working from noise. Trim is a harness job, not a prompt.

## Putting the Concepts into Practice

Pick one workflow you would not trust to a single prompt (intake, triage, a support copilot) and write this brief before you open an SDK:

```text
Goal and definition of done:
Model per step (Haiku / Sonnet / Opus):
Tools (name, when not to use, return fields):
Permission mode and the deny list:
Structured decision the harness will branch on:
stop_reason policy (tool / ask / route / escalate / halt):
Facts that must survive compact, and where they sit:
What a prompt chain would force you to pre-write:
```

Then inspect a real run:

1. Did the first `stop_reason` match what you expected (`tool_use` vs `end_turn`)?
2. Did any tool return a dump you then asked the model to ignore?
3. After three turns, can you point to the facts block at the edge of the window?
4. If you force `dontAsk` and omit a write tool, does the agent stop instead of inventing a workaround?
5. If you swap the main loop to Haiku, which decisions get worse?

If you cannot answer those, you have a demo, not a harness.

## Key Terms

- **Harness**: The runtime around the model: loop, tools, permissions, context assembly, and stop policy
- **Claude Code**: One harness, built for interactive coding
- **Agent SDK**: The same Claude Code loop as a Python or TypeScript library (`query()`, `ClaudeAgentOptions`)
- **Client SDK / Messages API**: Language libraries that call the Messages API; you implement the tool loop yourself
- **`stop_reason`**: Why the model stopped generating; the harness reads it and chooses the next verb
- **`tool_use` / `tool_result`**: The model requests a call; the harness runs or denies it and writes the evidence back
- **Permission mode**: How unmatched tool calls are approved, denied, or prompted (`default`, `dontAsk`, `acceptEdits`, `plan`, `bypassPermissions`)
- **Structured output**: A JSON Schema the final answer must match, so your code can branch
- **Prompt chain**: A fixed sequence of model calls you named in advance
- **Dynamic decomposition**: The model picks the next tool from evidence; you enforce permissions and stop policy
- **Clarify / route / escalate**: An intake policy: ask, act in a known queue, or hand off
- **Facts block**: Short durable state re-attached at the head or tail so it survives prune and compact

## Final Thoughts

The model is the reasoning engine from [Part 1](https://vanducng.dev/2026/08/09/core-concepts-of-agentic-coding-with-claude-code/). The harness is everything that makes that engine a product: which model runs which step, which tools exist, which calls are allowed, what the window contains, and what `stop_reason` means in your domain. Claude Code is a good harness I did not have to write. The Agent SDK is that harness in my process. The Messages API is the same loop with the cover off.

[Part 2](https://vanducng.dev/2026/08/15/context-engineering-for-claude-code/)'s context strategy does not disappear when I leave the terminal. If I own the runtime, I own trim, the facts block, the compact budget, and placement. Tools stay the public API. A badly shaped tool still produces a badly shaped agent.

The next post moves from tools I register in-process to tools that live outside the process: MCP servers, how they enter the window, and how to keep that surface small.

## References

- AI Engineering with Claude
- [Agent SDK overview](https://code.claude.com/docs/en/agent-sdk/overview)
- [Agent loop](https://code.claude.com/docs/en/agent-sdk/agent-loop)
- [Permissions](https://code.claude.com/docs/en/agent-sdk/permissions)
- [Structured outputs](https://code.claude.com/docs/en/agent-sdk/structured-outputs)
- [How Claude Code works](https://code.claude.com/docs/en/how-claude-code-works)
- [Stop reasons](https://platform.claude.com/docs/en/build-with-claude/handling-stop-reasons)
- [Models](https://platform.claude.com/docs/en/about-claude/models/overview)
- [Tool use](https://platform.claude.com/docs/en/agents-and-tools/tool-use/overview)

---

*This is Part 3 of 6 in the Agentic Coding with Claude Code series.*
