How AI Agents WorkCourse map

Chapter 3 · The core loop

State

An agent's task state lives outside the model: in your runtime, in saved files, and in the world it works on. Each model call sees only a slice of it.

Five kinds of state

Halfway through fixing the auth test, Patch has a conversation, a step counter, files on disk, and a saved checkpoint. It also builds a fresh input for each model call. These are five kinds of state, and they differ in who holds them and how long they last:

Five kinds of state, with examples from Patch
KindWhat it isHeld byLastsExample
ConversationThe log of messagesThe runtime, sometimes the providerThe sessionThe request, tool calls, tool results
ContextWhat one model call receivesThe runtime, while it builds and sends the callOne call (a trace may keep a copy)Instructions, tools, recent messages
Runtime stateVariables the loop tracksThe runtime processThe runstep = 7, files changed
World stateFiles, databases, servicesThe worldIts own lifetimeauth/session.py on disk
Persistent stateWhatever survives a restartFiles, databases, storesAcross runsA checkpoint after step 7
World state
(files, databases, services)
   │ tools read it and change it
   ▼
Conversation + runtime state
(held by the runtime)
   │ save ──▶ Persistent state
   │          (checkpoints, logs)
   │ build context
   ▼
Context (one call's view)
   │
   ▼
Model

Conversation

After one tool call, Patch's conversation looks like this:

messages = [
    {"role": "user", "content": "The auth test fails. Fix it."},
    {"role": "assistant", "tool_calls": [
        {"id": "call_1", "name": "run_command",
         "arguments": {"cmd": "pytest tests/test_auth.py"}}]},
    {"role": "tool", "tool_call_id": "call_1",
     "content": "FAILED test_login_expired_token ... 1 failed, 11 passed"},
]

The conversation is the log of messages. It holds the user's request, the model's replies and tool calls, and the tool results, each tagged with its call id. New entries go on the end. Usually your runtime keeps this list, although some provider APIs can store it for you (chapter 0).

The conversation is not the same as the context. Early in a run they look identical, because the runtime sends everything. They drift apart once the log no longer fits in one model call, or once the runtime starts choosing what to send.

Context: a view rebuilt for every call

Before each model call, Patch's runtime runs a function like this:

def build_context(state, budget):
    parts = [system_prompt(), tool_definitions()]
    parts += render_task(state)                  # status, plan, last test result
    parts += recent_messages(state["messages"], budget)
    return parts                                 # a view, not a copy

The context is exactly what one model call receives. Your runtime builds it from state, sends it, and then discards it, much like a database view that is computed when you query it. So the same state can produce different contexts. A summary can stand in for old messages, and a large file can appear as a short preview plus a path.

If something is not in the context, the model cannot use it on that call. Chapter 4 is about choosing what goes in.

Runtime state

Halfway through a run, Patch's runtime holds variables like these:

state = {
    "messages": [...],
    "status": "running",        # running | waiting_for_approval | done | failed
    "step": 7, "max_steps": 30, # one counter for the whole run
    "cost_usd": 0.42, "max_cost_usd": 2.00,
    "todo": [{"task": "fix the expiry check", "status": "in_progress"}],
    "pending_approval": None,
    "files_read": {"auth/session.py": "sha:9f2c41"},   # path -> hash of the version read
    "files_changed": ["auth/session.py"],
    "last_edit_step": 5,
    "last_test": {"cmd": "pytest tests/test_auth.py", "exit": 0, "after_step": 6},
}

Runtime state is what the loop itself keeps track of. Some fields steer the loop, such as the step count, the budgets, and a pending approval. Almost every agent has these. Some record evidence, such as the files changed and the last test run. These come from the agent's done check, so they differ from agent to agent. Patch is judged by its tests, so it tracks the last test run. An expense agent would track the receipt, the policy check, and the filing instead. Some fields exist for the model: the todo list, for example, gets copied into each context.

The model never writes this object directly. It asks for tool calls, and your runtime updates state from what the tools returned, not from what the model claims happened. You can still give the model fields of its own. A todo tool lets it rewrite its list, and a notes or file tool lets it save anything under names it picks. Your runtime stores what the model writes there, but never treats it as evidence. If the model could set last_test, it could mark its own tests as passed.

The three file fields show why evidence fields exist. Each one lets your code check something instead of trusting it:

The message log holds the same facts, but in a form code cannot easily use. Compaction can summarize old reads away, and your code should not have to search a conversation to enforce a rule.

External world state

At step 3, Patch reads auth/session.py. At step 6, a teammate pushes a change to the same file. Patch's copy is now out of date, and nothing tells it so.

World state is everything the agent acts on: files, databases, tickets, and deployed services. It exists without the agent, and other people change it too. The agent only ever holds observations of it, taken at some moment through its tools. Even a write tool's report, such as "1 replacement made", is a claim that a later read can confirm.

Persistent state

If Patch's process crashes at step 7, only what it saved survives: the session log of the conversation, a checkpoint, and saved files such as the full test output.

Persistent state is whatever outlives the process. A checkpoint is a snapshot of runtime state written after a step, so the run can continue where it stopped. Around actions with side effects, such as sending an email, record the intent before the action and the outcome after it. A crash between the two then leaves an "outcome unknown" record you can check, instead of no record at all (chapter 12).

Which copy is true?

Patch's done check reads last_test, which says the tests passed at step 6. But Patch edited auth/session.py again at step 8, so that result no longer proves anything.

The same fact often lives in several places: the file on disk, an old read in the conversation, a line in a summary. A good default is that the world is the source of truth for facts about the world, and runtime state is the source of truth for facts about the run. Everything in the context is a copy that can go stale. So re-read a file before editing it, and record which step produced each result:

def tests_still_pass(state):
    t = state["last_test"]
    last_edit = state["last_edit_step"] or 0
    return t is not None and t["exit"] == 0 and t["after_step"] > last_edit

This check refuses "done" when the last edit came after the last passing test run.

The check uses step numbers instead of clock times because it needs only the order of events. Your loop owns the step counter, so the counter never jumps, and it moves with the checkpoint when another worker resumes the run. A clock can be reset, and two machines' clocks can disagree. Changes from outside the run, such as a teammate's push, never get a step number, so files_read compares hashes instead.

Events and reducers

This function updates Patch's state from one event at a time:

def apply(state, event):                   # old state + one event -> new state
    if event.type == "file_edited":
        if event.path not in state["files_changed"]:
            state["files_changed"].append(event.path)
        state["last_edit_step"] = event.step
    elif event.type == "tests_ran":
        state["last_test"] = {"cmd": event.cmd, "exit": event.exit_code,
                              "after_step": event.step}
    elif event.type == "approval_decided":
        state["approvals"].append((event.call_id, event.decision))
    return state

state = reduce(apply, events, initial_state())   # replay to rebuild

A dependable runtime records events like these and computes the current state by applying them in order. Front-end developers call such a function a reducer, and backend developers call the pattern event sourcing. Because the log keeps everything, your runtime can replay it after a crash, and you can step through it to debug a bad run.

State in a coding agent

For a coding agent, state usually includes these items:

Coding-agent state: where it lives and what the model sees
ItemWhere it livesShown to the model?Why
Conversation historyRuntime message log, saved as a session fileYes: recent turns in full, older ones summarizedThe model needs the request and what it has tried
Task statusRuntime stateRarelyIt controls the loop. "Running" tells the model nothing.
Current planRuntime state, or a plan fileYes, rendered each callIt keeps the model on track over many steps
Files inspectedRuntime state: each path, with a hash of the version readAs a short listYour code can refuse edits to files the model never read, or that changed since. The contents stay in the log.
Files changedRuntime state, plus git in the worldYes, as a list or a diffReview, scope checks, and undo need it. The model needs a reminder of its own edits.
Test resultsLatest run in state, full output saved to a fileYes: the summary and the failuresThey drive the next step. The full log is one tool call away.
Outstanding todosRuntime stateYesSo nothing gets dropped
ApprovalsRuntime state, checkpointed while waitingOnly the decisions, as tool resultsThe runtime enforces them. The model needs only the outcome.
Artifacts producedFiles or object storage, referenced from stateReferences onlyLarge outputs stay out until needed

So the model sees what changes its next decision. Your runtime keeps what it needs to run the loop, enforce rules, and check results.

What the model sees, and what it does not

Patch's state includes a cost counter and an API token for the issue tracker. Neither helps the model choose its next step, and the token would be dangerous in its context.

So decide, for each field, whether to show it, summarize it, point to it, or keep it out. Show what changes the next decision, such as the request, recent results, and the plan. Summarize old turns. Point to big files by path instead of pasting them: a path costs a few tokens, and a tool can fetch the file when needed. Keep out what your runtime enforces, and above all keep out credentials. A secret in the context can end up in a log, a commit message, or a reply (chapter 10).

Budgets are a judgment call. Showing "step 27 of 30" can help the model wrap up, but it can also make it rush.

The kinds overlap. A checkpoint is a saved copy of runtime state, and files on disk survive restarts too. So for each item below, pick the role it plays here:

Pick an answer for each card.

The model's input on step 7

  1. Conversation
  2. Context
  3. Runtime
  4. World
  5. Persistent
Why

Context. The runtime built it for that one call. A trace may save a copy, but its role here is the input to that call.

The step counter

  1. Conversation
  2. Context
  3. Runtime
  4. World
  5. Persistent
Why

Runtime state. The loop uses it to enforce the step limit. A checkpoint can save a copy, which is then persistent state.

A file a teammate just changed

  1. Conversation
  2. Context
  3. Runtime
  4. World
  5. Persistent
Why

World state. The file also survives restarts, but its role here is the outside world: other people change it, so any copy the agent holds can go stale.

The result of call_3

  1. Conversation
  2. Context
  3. Runtime
  4. World
  5. Persistent
Why

Conversation. It is an entry in the message log, and it reaches a context only if the runtime includes it.

A snapshot saved after step 7

  1. Conversation
  2. Context
  3. Runtime
  4. World
  5. Persistent
Why

Persistent state. It is a saved copy of runtime state, so the run can resume after a crash.

State vs memory

Patch's step counter matters only during this run. The fact "this project uses PostgreSQL" matters next week too.

State serves the current run, and it is done when the run ends, although you may keep it for audits. Memory is information you choose to keep for future runs, along with a way to find it again. The two overlap: a saved session is persistent state, and when a later run searches it for a similar bug, it works as memory (chapter 5).

Running example: Patch gets a state object

Patch version 3 keeps a state object next to its message list. Its runtime fills it from tool results, and its done check now reads evidence from it: a passing test run that came after the last edit.

# Patch v2: the message list was the only state
- messages = [...]
# Patch v3: a state object the runtime owns
+ state = {
+     "messages": [...],
+     "status": "running",          # running | waiting_for_approval | done | failed
+     "step": 0, "max_steps": 30,
+     "files_read": {}, "files_changed": [],   # {path: hash}, [paths]
+     "last_edit_step": None,
+     "last_test": None,            # {"cmd", "exit", "after_step"}
+     "approvals": [],
+ }

Patch still sends the whole message list on every call, which stops fitting as runs get longer. Chapter 4 gives it a context builder.

What goes wrong

Acting on a stale view

Why it happens
The model edits a file based on a read from many steps ago, and someone changed the file since.
How to spot it
Edits fail because the old text no longer matches, or diffs touch lines the model never saw.
What to do
Make edit tools check the old text, and re-read files before writing.

State that lives only in the context

Why it happens
The plan exists only as old messages, so when compaction drops them, the agent loses the plan.
How to spot it
Behavior changes right after a compaction.
What to do
Keep task state in runtime state, and render it into each context.

Stale evidence

Why it happens
"Tests passed" was true at step 6, but the model edited the code again at step 8.
How to spot it
The done check passes, but CI fails.
What to do
Record which step produced each result, and expire it when anything it depends on changes.

You decide, AI builds

Decide yourself

  • What state exists, and which copy is the truth for each fact.
  • Which fields the model sees, which it gets as references, and which it never sees.
  • What must be saved so a run can resume.
  • When a result stops counting as evidence.

Delegate to AI

  • The state schema and its serialization.
  • Reducers that apply tool results to state.
  • Session logs and checkpoint storage.
  • Rendering state into sections of the context.

Check yourself

What is state in an agent, and who holds it? (Q6)

Answer

Everything the system tracks across steps: the conversation, runtime variables such as the step count and the plan, the world the agent acts on, and whatever it saves to resume. Your runtime and its storage hold it, and the world holds itself. The model holds none of it.

How are state and memory different? (Q8)

Answer

State serves the current run and ends with it. Memory is information kept on purpose for future runs, with a way to find it again. A saved session is state, and it works as memory when a later run searches it.

The model's final message says "all tests pass". What should the runtime check before it agrees?

  1. Nothing. The message is clear.
  2. The last test result in state, and whether that run came after the last edit
  3. A summary of the conversation
Answer

The message is a claim. The evidence is the recorded test run, and it counts only if no edit came after it.

Takeaways

Going deeper