Skip to main content
The agent loop is the beating heart of Claude Code. Every interaction — interactive or non-interactive — runs through the same while-loop in src/query.ts. The loop calls the Claude API, inspects the stop_reason, executes any requested tools, appends the results to the message history, and calls the API again. It stops when the model returns a text response with no pending tool calls.

The core loop

                    THE CORE LOOP
                    =============

    User --> messages[] --> Claude API --> response
                                          |
                                stop_reason == "tool_use"?
                               /                          \
                             yes                           no
                              |                             |
                        execute tools                    return text
                        append tool_result
                        loop back -----------------> messages[]
That is the minimal agent loop. Claude Code wraps this loop with a production-grade harness: permissions, streaming, concurrency, compaction, sub-agents, persistence, and MCP.

Single query lifecycle

1

processUserInput()

Parse slash commands (e.g., /compact, /memory), build the UserMessage, and attach any file context. Slash commands that produce an immediate response return early here without touching the API.
2

fetchSystemPromptParts()

Assemble the system prompt from tool descriptions, permission context, and any CLAUDE.md memory files discovered lazily under the current working directory. The result is a SystemPrompt branded string array.
3

recordTranscript()

Persist the user message to disk as an append-only JSONL entry under ~/.claude/projects/<hash>/sessions/<session-id>.jsonl. This write is blocking (await) to protect against crash loss.
4

normalizeMessagesForAPI()

Strip UI-only fields from the messages array and, if the context window is near capacity, trigger autoCompact() to summarize older messages before the API call.
5

Claude API (streaming)

POST to /v1/messages with the full tool list and assembled system prompt. Stream events arrive as message_startcontent_block_deltamessage_stop. Text blocks are yielded immediately to the consumer (SDK or REPL).
6

StreamingToolExecutor

When a tool_use block arrives, the executor partitions tool calls into two buckets: concurrency-safe tools that can run in parallel (isConcurrencySafe() === true), and serial tools that must run one at a time. The partition is determined per-tool per-input.
7

canUseTool() — permission check

Before executing each tool, the permission system runs: pre-tool hooks → allow/deny rules → interactive prompt (if needed). A DENY result appends a tool_result error and continues the loop without executing the tool.
8

tool.call()

Execute the tool (BashTool, FileReadTool, FileEditTool, WebFetchTool, etc.) and collect the result. Long-running tools stream progress events back to the UI via the onProgress callback.
9

append tool_result and loop

Push the tool result onto messages[], fire off a recordTranscript() write (fire-and-forget for assistant messages, order-preserving queue), and loop back to step 4 for another API call with the updated history.
10

yield result message

When stop_reason is not "tool_use", yield the final SDKMessage to the consumer, including the response text, token usage, accumulated cost, and session ID.

Architecture layers

┌─────────────────────────────────────────────────────────────────────┐
│                         ENTRY LAYER                                 │
│  cli.tsx ──> main.tsx ──> REPL.tsx (interactive)                   │
│                     └──> QueryEngine.ts (headless/SDK)              │
└──────────────────────────────┬──────────────────────────────────────┘


┌─────────────────────────────────────────────────────────────────────┐
│                       QUERY ENGINE                                  │
│  submitMessage(prompt) ──> AsyncGenerator<SDKMessage>               │
│    │                                                                │
│    ├── fetchSystemPromptParts()    ──> assemble system prompt       │
│    ├── processUserInput()          ──> handle /commands             │
│    ├── query()                     ──> main agent loop              │
│    │     ├── StreamingToolExecutor ──> parallel tool execution      │
│    │     ├── autoCompact()         ──> context compression          │
│    │     └── runTools()            ──> tool orchestration           │
│    └── yield SDKMessage            ──> stream to consumer           │
└─────────────────────────────────────────────────────────────────────┘

The 12 progressive harness mechanisms

The source code demonstrates 12 layered mechanisms that a production AI agent harness needs beyond the minimal loop. Each builds on the previous.
#MechanismWhat it adds
s01The loopquery.ts: the while-loop that calls the API, checks stop_reason, executes tools, appends results. One loop and Bash is all you need to start.
s02Tool dispatchTool.ts + tools.ts: every tool registers into the dispatch map. The loop stays identical. buildTool() factory provides safe defaults. Adding a tool = adding one handler.
s03PlanningEnterPlanModeTool / ExitPlanModeTool + TodoWriteTool: list steps first, then execute. Tracked in AppState as mode: 'plan'.
s04Sub-agentsAgentTool + forkSubagent.ts: each child gets a fresh messages[], keeping the parent conversation clean. Four spawn modes: default, fork, worktree, remote.
s05Knowledge on demandSkillTool + memdir/: inject knowledge via tool_result, not the system prompt. CLAUDE.md files loaded lazily per directory.
s06Context compressionservices/compact/: three-layer strategy — autoCompact (summarize) + snipCompact (trim) + contextCollapse (restructure).
s07Persistent tasksTaskCreateTool / TaskUpdateTool / TaskGetTool / TaskListTool: file-based task graph with status tracking and persistence across restarts.
s08Background tasksDreamTask + LocalShellTask: daemon threads run slow operations in the background; completions are injected as notifications.
s09Agent teamsTeamCreateTool / TeamDeleteTool + InProcessTeammateTask: persistent teammates with async mailboxes for parallel workstreams.
s10Team protocolsSendMessageTool: one request-response pattern drives all inter-agent negotiation.
s11Autonomous agentscoordinator/coordinatorMode.ts (feature-gated COORDINATOR_MODE): idle-scan-and-claim loop — workers self-assign tasks without the lead assigning each one.
s12Worktree isolationEnterWorktreeTool / ExitWorktreeTool: sub-agents bound to isolated git worktrees; tasks manage goals, worktrees manage directories.

Key source files

FileRole
src/query.tsMain agent loop (~785 KB, the largest file in the repo)
src/QueryEngine.tsSDK/headless query lifecycle, AsyncGenerator<SDKMessage>
src/main.tsxREPL bootstrap, 4,683 lines
src/Tool.tsTool<Input, Output, Progress> interface and buildTool() factory
src/tools.tsTool registry, presets, and feature-gated conditional imports
src/services/tools/StreamingToolExecutor.tsParallel tool runner with concurrency partitioning
src/services/tools/toolOrchestration.tsBatch tool orchestration (runTools())
src/query.ts is ~785 KB — the largest file in the repository. It contains the streaming tool executor, context compaction logic, permission orchestration, and the main agent loop all in one place.

Build docs developers (and LLMs) love