# The agentic loop, playable

> An AI coding agent runs a real task across 6 turns — reason, act, observe, adjust — with the context window filling as it works and 3 moves left to the reader.

- **Source:** https://ainativesoftware.engineering/agentic-loop
- **Site:** AI-Native Software Engineering — https://ainativesoftware.engineering/book

An agent is an LLM with tools, context, and a loop. The page at https://ainativesoftware.engineering/agentic-loop plays that loop as an interactive animation: one real coding task, step by step, with the context window filling as it runs and 3 decision points where the reader chooses the next move before seeing the agent's. This mirror is the same session as a readable trace.

- **The task:** Replace the deprecated getUserById with fetchUser everywhere. Keep the tests green.
- **The repo:** a Node.js service with an API layer, an auth service, and a test suite
- **Turns:** 6
- **Context spent:** about 5,640 of an illustrative 8,000-token window
- **Play it:** https://ainativesoftware.engineering/agentic-loop

## The four phases

### Reason — What do I still need to know?

The model reads everything in the window — goal, rules, tool descriptions, every result so far — and picks the next action. There is no planner outside the loop: the plan lives in the tokens it generates.

### Act — Call a tool.

The model never executes anything itself. It emits a structured tool call — a function name and JSON arguments — and the harness runs it. That gap between declaring and executing is where permission checks and human approval live.

### Observe — What did it return?

The tool result is appended to the message history. That is the only reason the agent “remembers” anything: the model is stateless, and every call replays the whole transcript from the top.

### Adjust — Goal met? Stop. If not, loop.

The model compares where it is against the goal. Done means replying with no tool call, which exits the loop. Not done means another pass — and real agents also carry iteration caps and token budgets, so a stuck loop cannot run forever.

## The toolbox

The agent starts with four tools. The model chooses between them by reading their descriptions — nothing else — which is why each description is written the way it is.

### search_codebase()

_"Search every file for a pattern. Returns matching paths and line numbers, not file contents."_

Saying what it returns — paths, not contents — stops the model treating one search as a substitute for reading the file. The description is the interface: the model chooses tools by reading it, nothing else.

### read_file()

_"Read the full text of the file at an absolute path. Not for binaries or files over 1 MB."_

Absolute paths are demanded on purpose: Anthropic's SWE-bench team found that requiring them eliminated a whole class of errors where the agent moved into a subdirectory and got relative paths wrong.

### write_file()

_"Replace the contents of the file at an absolute path with the given text."_

The whole new file travels inside the call's arguments — which is why a write shows up in the window as a big block, not a small one. Tool design decides how fast the context fills.

### run_tests()

_"Run the test suite, optionally filtered to one path. Returns the full pass/fail output."_

The most important tool in the box. Test output is how the agent checks its own work: a failure message with detail in it steers the next step, and a vague one leaves the model guessing.

## The session, step by step

### Before the first token

**setup** · `runtime › load system prompt` _(+700 tokens: System prompt)_

The system prompt lands first and the model reads it before anything else: identity, behavioural rules, output expectations. It is the highest-leverage text in the whole window — and it is there on every single call, relevant or not.

**setup** · `runtime › inject tool schemas` _(+900 tokens: Tool schemas × 4)_

Every tool's name, description and parameter schema goes into the window as text. This is the agent's entire map of what it can do — it will choose tools by reading these descriptions, so their wording directly sets the quality of every action that follows.

**setup** · `runtime › read AGENTS.md + rules` _(+600 tokens: Rules · AGENTS.md)_

Project conventions load next: build commands, banned libraries, testing standards. Always-on context like this is why the agent can follow rules nobody typed into the prompt — and every token of it is a token the session no longer has for work.

**setup** · `user › "Replace the deprecated getUserById with fetchUser everywhere. Keep the tests green."` _(+60 tokens: User goal)_

The only strictly mandatory piece of context arrives last: your message. Everything above it was a deliberate choice, made before the model generated a single token. The window already holds over 2,200 tokens and no work has happened yet.

### Turn 1

**reason** · `Thought: I need every usage of getUserById. Search first.` _(+40 tokens: Thought)_

First pass through the loop. The model has never seen this repo — its training data holds patterns, not your files — so the only honest first move is to look. Reasoning tokens like this one are the model thinking out loud, and they stay in the history as working memory.

**act** · `Action: search_codebase(pattern="getUserById")` _(+25 tokens: search_codebase())_

The model emits JSON — a function name and arguments — and stops. The harness validates it, runs the real search, and only then does anything touch the filesystem. Nothing the model outputs executes by itself.

**observe** · `Observation: 3 matches — src/api/users.ts:23, src/services/auth.ts:45, tests/users.test.ts:12` _(+120 tokens: Search result)_

The result is appended to the message history, and the next model call replays everything from the system prompt down to this line. What feels like memory is literally re-reading the transcript on every turn.

**Pause — Three call sites found. What would you do next?**

- Search-and-replace across all three files at once — Fast and blind. The model would be editing code it has never read — if fetchUser has a different signature or return type, all three edits are wrong at once.
- **The agent's move:** Read the first file before touching anything — The agent's move. Reading before writing keeps it grounded in what the code actually says, not what it statistically tends to say. One extra tool call buys a correct edit.
- Stop and ask which of the three files to change — A reasonable instinct, but the goal already says “everywhere”. Agents that ask about things the context already answers burn your time instead of theirs.

**adjust** · `Adjust: three files. Update them in order, starting with users.ts.` _(+30 tokens: Plan)_

Goal not met, so the loop continues — but now with a plan that exists only as tokens in the history. If this session were reset here, the plan would be gone with it. This is why long tasks write plans to files.

### Turn 2

**reason** · `Thought: read src/api/users.ts before editing it.` _(+35 tokens: Thought)_

Grounding: the action–observation cycle keeps the model tied to the actual state of the codebase rather than its internal guess about it. An agent that reads before it writes is much harder to lead astray.

**act** · `Action: read_file(path="/repo/src/api/users.ts")` _(+20 tokens: read_file())_

An absolute path, exactly as the tool description demands. The description is doing the work here — the model follows the contract it read in the schema, not any code.

**observe** · `Observation: 180 lines of users.ts land in the window` _(+1,400 tokens: users.ts contents)_

The single biggest block of the session so far — one file read costs more than thirty thoughts. Tool results, not reasoning, are what fill a context window, which is why tool output design decides how long a session stays coherent.

**adjust** · `Adjust: line 23 calls getUserById(req.params.id). Ready to edit.` _(+30 tokens: Plan)_

The model now knows something no amount of training data could tell it: what line 23 of your file actually says. In-context knowledge overriding in-weights guessing is the whole trick.

### Turn 3

**reason** · `Thought: swap the call. fetchUser returns null instead of throwing.` _(+45 tokens: Thought)_

A real difference between the two functions, noticed because the file contents are in the window. Whether this understanding survives into the edit is about to be tested — literally.

**act** · `Action: write_file(path="/repo/src/api/users.ts", content=…)` _(+260 tokens: write_file())_

Note the size of this block: a write_file call carries the entire new file in its arguments, so writes cost hundreds of tokens, not tens. The edit itself is the model's output — the harness just saves it.

**observe** · `Observation: File written.` _(+15 tokens: Write OK)_

Fifteen tokens. A good tool confirms and shuts up — imagine if every write echoed the whole file back. Every tool result is a context-budget decision someone made.

**adjust** · `Adjust: one down. Same change in auth.ts and the test file.` _(+30 tokens: Plan)_

The loop keeps its own progress in the history: which files are done, which remain. Every future call re-reads this bookkeeping along with everything else.

### Turn 4

**act** · `… the same read → edit cycle runs for auth.ts and users.test.ts` _(+620 tokens: 2 more read–edit cycles)_

Fast-forwarded here, but the agent does every pass in full: read, think, write, confirm. A real session is dozens of these cycles — and each one adds its results to the window whether they will matter later or not. Context cost compounds.

### Turn 5

**reason** · `Thought: all three edited. Run the tests to verify.` _(+35 tokens: Thought)_

The agent does not ask you whether the change works — it asks the test suite. Verification is a tool call away, and the answer will arrive as an observation it can reason from.

**act** · `Action: run_tests(path="tests/users.test.ts")` _(+20 tokens: run_tests())_

Scoped to the relevant file, not the whole suite — faster feedback and a smaller result in the window. Small habits like this keep long sessions inside budget.

**observe** · `Observation: FAIL — fetchUser › returns 404 when user not found. Expected 404, received 200.` _(+180 tokens: Test FAIL)_

The moment that separates an agent from an autocomplete. The failure names the test, the expectation and the actual value — that information density is what the next thought will be built from. A test that just said “failed” would leave the model guessing.

**Pause — The tests just failed. What would you do next?**

- Revert everything and start the task over — Throws away three correct edits over one missing branch — and the fresh attempt would hit the same failure, with less context to reason from.
- **The agent's move:** Read the failure message and fix what it names — The agent's move. Expected 404, got 200: the handler never checks for the null that fetchUser returns. The failure output is an observation like any other — the loop treats it as fuel, not as a verdict.
- Re-run the tests — it might be flaky — Nothing here is flaky: same input, same missing null check, same failure. Re-running without a hypothesis is the agent equivalent of turning it off and on again.

**adjust** · `Adjust: getUserById threw on a missing user; fetchUser returns null. The handler needs a null check.` _(+50 tokens: Diagnosis)_

Self-correction, mid-loop: the failure became a hypothesis about the code, not a reason to stop. No human intervened between the red test and this diagnosis.

**act** · `Action: write_file(users.ts) — add the null check, return 404` _(+210 tokens: write_file())_

The fix is three lines, but the call still carries the whole file. The agent is now two write_file calls deep into the same file — and both versions sit in the history, which is one way sessions quietly bloat.

**observe** · `Observation: File written.` _(+15 tokens: Write OK)_

Confirmation only. The real observation — did the fix work? — needs another pass through the loop. The agent never assumes; it re-checks.

### Turn 6

**reason** · `Thought: run the tests again to confirm the fix.` _(+30 tokens: Thought)_

Edit → test → read failure → fix → test again. This inner cycle is the engine of every coding agent you will ever use, and its speed limit is the quality of your test suite's failure messages.

**act** · `Action: run_tests(path="tests/users.test.ts")` _(+20 tokens: run_tests())_

Same call as before — but the file it exercises has changed, so the observation will be new. Identical actions, different world: the loop's state lives in the repo as much as in the window.

**observe** · `Observation: PASS — 4 tests passed.` _(+90 tokens: Test PASS)_

The observation the whole session was steering toward. Green tests are the agent's proof of done — not its own confidence, which is worth exactly nothing as evidence.

**Pause — The tests pass. What would you do next?**

- Keep going — refactor the file while we're in here — Scope creep with an agent's stamina behind it. Nobody asked for a refactor, nothing verifies it, and every extra edit risks the green you just earned.
- **The agent's move:** Declare the goal met, reply, and stop — The agent's move. The goal was “replace everywhere, keep the tests green” — both are now true and provable. A reply with no tool call in it is how the loop ends.
- Run the full test suite one more time to be safe — Defensible in a big change — but this one touched three files, all covered by the suite just run. Paranoia is a budget item like everything else.

**adjust** · `Reply: replaced all 3 call sites, added a null check, tests pass. → no tool call → loop exits` _(+60 tokens: Final reply)_

The exit condition is structural: a response containing no tool call ends the while loop. The window closes at around 5,600 tokens of a budget that started empty — and none of it survives. Next session, the assembly starts again from the top.

## The 6 things worth remembering

### The model is stateless. The loop is not.

Every call starts from a blank model and replays the whole transcript. What feels like memory is the history you watched accumulate — which is why continuity belongs in files, not in the conversation.

### Tool results fill the window, not thoughts.

One file read cost more than every thought in the session combined. The context meter is a budget, and tool output is what spends it — so what a tool returns is a design decision, not a detail.

### The model declares; the harness executes.

Nothing the model outputs runs by itself. It emits a tool call, and your side of the loop decides whether to carry it out. Every permission prompt you have ever clicked lives in that gap.

### Tests are observations, not a final exam.

The red test did not end the session — it steered it. Failure output goes back into the window and becomes the next thought's raw material, which is why failure messages with detail in them make agents converge faster.

### Descriptions are the real API.

The agent picked every tool by reading its description, the same way it will pick yours. A vague description produces wrong calls and wasted turns; a precise one is invisible, which is what good infrastructure looks like.

### Stopping is a design decision.

The loop ended because the model replied without a tool call — and production agents back that up with iteration caps, token budgets and ask-the-human tools. An agent without exit conditions is a while loop with your API key.

## Where this comes from

This page animates the mental model from [AI-Native Software Engineering](https://ainativesoftware.engineering/book.md): what makes an agent an agent, and how context is assembled and spent while it works. For the seven-day version with practice exercises, take [the roadmap](https://ainativesoftware.engineering/roadmap.md); to make your own repo a place this loop works well, follow [the baby steps](https://ainativesoftware.engineering/baby-steps.md).

---

_AI-Native Software Engineering by Alfonso Graziano (O'Reilly Media, Early Release; print edition February 2027). Every page of ainativesoftware.engineering is also served as Markdown: append `.md` to any URL. Index: https://ainativesoftware.engineering/llms.txt — whole site in one file: https://ainativesoftware.engineering/llms-full.txt._
