Inside the Agent SDK loop: how Claude Code runs your prompts
How Claude Code and the Agent SDK execute prompts through a five-step tool loop, expose typed lifecycle messages, and bound work with permissions, turns, and budgets.
The Agent SDK does not turn query() into one model call. It embeds Claude Code’s evaluate-act-observe loop: Claude requests tools, the SDK executes them, results return as user messages, and the cycle stops when Claude produces a response without tool calls.
That loop explains tool selection, parallel execution, context compaction, and termination. Its typed message stream exposes progress while permissions, turn limits, budgets, and hooks constrain execution.
The loop cycle
Every Agent SDK session follows the same five-step cycle. The SDK sits between the user and Claude, forwarding prompts, executing tools, and collecting results. Claude never talks to tools directly — the SDK mediates every interaction.
The loop has a branching point. When Claude responds with tool calls (row 3), the SDK executes them and feeds results back (rows 4–6). The cycle repeats — Claude may call more tools or produce a final answer. When Claude responds with text only and no tool calls (row 7), the loop ends and the ResultMessage fires (row 8).
A quick question like “what files are here?” might take one turn: Claude calls Glob and responds with the listing. A complex task like “refactor the auth module and update the tests” can chain dozens of tool calls across many turns — reading files, editing code, running tests — with Claude adjusting its approach based on each result.
Inside a turn
A turn is one round trip inside the loop: Claude produces output, the SDK executes any tools, and the results feed back. This happens without yielding control back to the calling code. Turns repeat until Claude produces output with no tool calls.
The SDK yields five core message categories as the loop runs. Each category marks a specific lifecycle stage.
SystemMessage marks session lifecycle events such as "init" and "compact_boundary". In TypeScript, every system subtype except "init" is its own type in the SDKMessage union. AssistantMessage carries each Claude response, including text and tool calls. UserMessage carries tool results and streamed user input. StreamEvent is optional and carries raw API deltas when partial messages are enabled. ResultMessage reports final text, usage, cost, session ID, and termination subtype; a few trailing system events can follow it, so consumers should iterate the stream to completion.
Consider the prompt “Fix the failing tests in auth.ts” across four turns:
- Claude calls
Bashto runnpm test— the SDK yields an AssistantMessage, executes the command, then yields a UserMessage with the output (three failures). - Claude calls
Readonauth.tsandauth.test.ts— the SDK returns the file contents. - Claude calls
Editto fix the code, thenBashto re-run tests — all three pass. - Claude produces a text-only response: “Fixed the auth bug, all three tests pass now.” The SDK yields the final AssistantMessage, then a ResultMessage.
That was four turns: three with tool calls, one final text-only response.
Tool execution
Tools turn Claude from a text generator into an agent that reads files, runs commands, searches code, and spawns subagents. The SDK includes the same tools that power Claude Code: Read, Edit, and Write for files; Glob and Grep for search; Bash for execution; WebSearch and WebFetch for web access; ToolSearch for on-demand discovery; and Agent, Skill, AskUserQuestion, TaskCreate, and TaskUpdate for orchestration. Models without task tracking expose TaskCreate and TaskUpdate only when the application opts in.
Tools can be extended with MCP servers, custom tool handlers, and project-level skills loaded via settingSources.
Permissions
Three options work together to control what runs. allowed_tools / allowedTools auto-approves listed tools. disallowed_tools / disallowedTools blocks listed tools regardless of other settings. permission_mode / permissionMode handles everything else: "default" calls canUseTool and denies when no callback exists; "acceptEdits" auto-approves edits and common filesystem commands; "plan" keeps source edits behind approval; "dontAsk" runs only pre-approved tools; "auto" uses a model classifier; and "bypassPermissions" removes ordinary prompts. TypeScript also requires allowDangerouslySkipPermissions: true for bypass mode, which cannot run as root on Unix.
When a tool is denied, Claude receives a rejection message and typically attempts a different approach.
Parallel execution
When Claude requests multiple tool calls in a single turn, read-only tools (Read, Glob, Grep, MCP tools marked read-only) run concurrently. Tools that modify state (Edit, Write, Bash) run sequentially to avoid conflicts. Custom tools default to sequential; set readOnlyHint in their annotations to enable parallel execution.
Control surfaces
Four parameters shape how the loop runs. All are fields on ClaudeAgentOptions (Python) or Options (TypeScript).
| Parameter | What it controls | Default |
|---|---|---|
max_turns / maxTurns |
Maximum tool-use round trips before stopping | No limit |
max_budget_usd / maxBudgetUsd |
Maximum cost before stopping | No limit |
effort |
Reasoning depth per turn ("low" to "max") |
Unset; model default |
model |
Which Claude model to use | SDK default |
When a limit is hit, the ResultMessage carries an error subtype (error_max_turns or error_max_budget_usd). Without limits, the loop runs until Claude finishes on its own — fine for well-scoped tasks but potentially expensive for open-ended prompts. Setting a budget is a good default for production agents.
The effort parameter trades latency and token cost for reasoning depth within each response. The current levels are "low", "medium", "high", "xhigh", and "max"; support depends on the model. Both SDKs leave it unset unless the application chooses a level. Effort and extended thinking are independent.
The context window
The context window does not reset between turns. Everything accumulates: system prompt, tool definitions, conversation history, tool inputs, and tool outputs. Content that stays the same across turns (system prompt, CLAUDE.md, tool schemas) is automatically prompt-cached, reducing cost for repeated prefixes.
Large tool outputs are the fastest way to fill context. Reading a big file or running a verbose command can consume thousands of tokens in a single turn.
Automatic compaction
When the context window approaches its limit, the SDK automatically compacts the conversation — it summarizes older history to free space while keeping recent exchanges and key decisions intact. A "compact_boundary" system event fires after compaction.
Compaction replaces older messages with a summary, so specific instructions from early in the conversation may not survive. Persistent rules belong in CLAUDE.md (loaded via settingSources), which is re-injected on every request rather than carried through history.
Keeping context lean
Long-running agents keep the main context lean in three ways. Use subagents for subtasks — each starts with a fresh context, and only the summary returns to the parent. Scope subagent tools to the minimum set they need with the tools field on AgentDefinition. Use ToolSearch for MCP servers to load tools on demand instead of preloading all schemas.
Hooks as interception points
Hook callbacks run in the application process rather than as model turns. A hook can still inject content into Claude’s context, so callback execution and callback output are separate concerns.
| Hook | When it fires | Common uses |
|---|---|---|
PreToolUse |
Before a tool executes | Validate inputs, block dangerous commands |
PostToolUse |
After a tool returns | Audit outputs, trigger side effects |
UserPromptSubmit |
When a prompt is sent | Inject additional context |
Stop |
When the agent finishes | Validate result, save session state |
PreCompact |
Before context compaction | Archive full transcript |
A PreToolUse hook that rejects a tool call prevents execution — Claude receives the rejection as the tool result and adapts. Other events expose prompt submission, tool completion, compaction, and stop boundaries without moving that policy into the prompt.
Handling the result
When the loop ends, the ResultMessage’s subtype field is the primary way to check termination state.
| Subtype | Meaning | result field |
|---|---|---|
success |
Task completed normally | Yes |
error_max_turns |
Hit turn limit before finishing | No |
error_max_budget_usd |
Hit budget limit before finishing | No |
error_during_execution |
API failure or canceled request | No |
error_max_structured_output_retries |
Structured output validation failed | No |
Always check the subtype before reading result. All subtypes carry total_cost_usd, usage, num_turns, and session_id for cost tracking and resumption. The stop_reason field indicates why the model stopped on its final turn — end_turn (normal), max_tokens (output limit), or refusal (declined request).
The session ID captured from ResultMessage enables resuming the full context later or forking into a different approach.
Takeaways
The loop is the engine
Tool selection, parallel execution, and context management all emerge from the five-step evaluate-execute-repeat cycle.
Messages are the observability surface
Five core message categories (System, Assistant, User, StreamEvent, Result) expose each lifecycle stage while observability events cover additional status.
Turns accumulate context
Context does not reset between turns. Long sessions with many tool calls build up history quickly; subagents and tool scoping keep the main thread lean.
Control surfaces prevent runaway agents
max_turns and max_budget_usd are hard stops; effort tunes reasoning depth. Without explicit limits, open-ended prompts can run long and consume more tokens.
Hooks are the interception layer
PreToolUse, PostToolUse, Stop, and PreCompact callbacks run in the application process, where they can enforce policy without hiding it in the prompt.