Open Claude Code, type “Help me investigate this bug,” and press Enter.
The window starts scrolling. Claude reads auth.py, then login.py, runs a search, edits one line, runs a test, and finally says: “Done—the cause was X.”
It feels like a conversation with something that remembers what was said, can use tools, and waits for you when it needs input.
But an LLM API is stateless. Each request is independent; the server does not remember your previous prompt. To make the model appear to remember a conversation, the application must send the full message history again on every call.
That creates an intermediate layer: the harness. It retains the history and packages it every time it calls the LLM. What looks like a continuous chat is closer to this:
Turn 1: harness sends [1 message] → LLM returns a response
Turn 2: harness sends [3 messages] → LLM returns a response
Turn 3: harness sends [5 messages] → LLM returns a response
...
The message array only grows. The LLM remembers nothing; the harness remembers everything.
One user message, how many model calls?
Consider the bug fix above. Claude read two files, searched the codebase, made an edit, and ran a test: five tool calls. After each tool completes, the harness puts its tool_result back into messages and calls the LLM again so it can decide the next action.
One user message can therefore mean five to ten LLM calls:
User presses Enter
→ Call A: “I should read this file”
→ harness reads the file
→ Call B: “I should inspect another file”
→ ...
→ a response with no tool request
Only then does the user-visible turn finish.
That is the loop.
The minimal tool-use loop is deceptively small:
while True:
response = call_llm(messages)
if response.has_tool_use:
results = execute_tools(response.tool_use)
messages.append(response)
messages.append(results)
else:
break
In plain language:
-
while True: keep going until something explicitly ends the turn. -
call_llm(messages): send the complete current history and get the next response. -
has_tool_use: check whether the model asked to use tools. -
execute_toolsplus two appends: run the requested tools, then retain both the model’s request and the result. -
break: if there is no tool request, the turn is over.
The core is only five actions: call the model, inspect the response, execute tools when requested, append the results, and stop when no more tools are needed.
The user is only at the beginning and the end
In this loop, the user appears at two points: pressing Enter at the start and seeing the final answer at the end. The middle—model call, tool execution, result append, and next model call—runs automatically. The harness does not ask the user to approve each ordinary step.
This is the deepest difference between a chatbot and an agent. A chatbot normally waits for a person every turn. An agent waits for a person only at the boundaries; it drives itself through the middle of the task.
That self-driving property makes several mechanisms necessary:
- Interrupts: a user needs a way to stop a loop that will otherwise keep running.
- Permission approval: dangerous actions, such as destructive shell commands, must be able to pause the loop.
-
A
maxTurnscircuit breaker: an accidental loop needs a hard ceiling. - Hooks: custom logic must be inserted through lifecycle hooks because the user is not manually operating the intermediate steps.
Without automatic iteration, none of these would be particularly important.
The five-line happy path is not the product
The toy loop assumes everything works. A real product must handle a much harsher world:
- the context window fills up;
- the API fails due to a network problem, rate limit, or overload;
- the model refuses a request;
- the user interrupts while a tool is running;
- a tool crashes;
- output reaches
max_tokensand is cut off; - output is too long, requiring a fallback model or recovery path;
- several tool uses may need parallel or serialized execution;
- the user sends another message mid-turn, so it must be queued;
- hooks in
.claude/settings.jsonmust run at session and tool boundaries; - a first-use permission prompt blocks the loop until the UI answers;
- a subagent starts its own loop while remaining distinct from the main one.
Every item is outside the five-line example.
From a loop to an agent runtime
Claude Code weaves these cases into its main loop. Conceptually it looks more like this:
while (true) {
choose a route from state.transition.reason:
next_turn → normal LLM call
collapse_drain_retry → context collapse
reactive_compact_retry → compact and retry
max_output_tokens_escalate → increase the token ceiling
max_output_tokens_recovery → inject a “continue” message
stop_hook_blocking → a stop hook requires continuation
token_budget_continuation → continue under a token budget
call the LLM and process its stream
handle stop_reason:
end_turn → complete when there is no tool use
tool_use → runTools
max_tokens → output-token recovery
refusal → explain how to change model settings
context_window_exceeded → reactive compaction
execute tools:
batch by isConcurrencySafe for parallel or serial execution
convert every tool failure into an is_error tool_result
optionally start tools while streaming JSON is still being parsed
append tool results to messages
check terminal conditions:
AbortController.aborted → aborted_tools
turnCount > maxTurns → max_turns
PostToolUse hook blocks → hook_stopped
auto-compact threshold → compact
prompt_too_long → withhold and recover
continue to the next iteration
}
The simple loop has not disappeared. It is still the heart of the system. The production runtime wraps it with recovery paths, safety gates, queueing, streaming, concurrency control, lifecycle hooks, and context management.
What this series will examine
This series studies those pieces one by one: how history is retained, how state transitions choose a path, how tools are executed and scheduled, how context is compacted, how approval and interrupts work, and how subagents reuse the same machinery.
The key idea to keep in mind is simple: Claude Code is not a chat UI with a few tools attached. It is a harness that repeatedly runs an LLM, observes whether it wants an action, performs that action, and feeds the result back into the next decision. Everything else in the product exists to make that loop reliable in the real world.
Top comments (1)
The useful operational boundary here is not just “no more tool calls,” but “the claimed outcome has evidence.” A harness that finishes after a successful test run but before checking the diff, exit status, and intended file set can still report a very convincing wrong result. Treating completion as a small verification phase keeps the loop from optimizing only for its own stopping condition.