Module 3: From Chatbots to Coding Agents

The Agentic Loop: Observe → Think → Act → Observe

Description

The agentic loop is the central mechanism of every coding agent. It's what turns an LLM that generates text into an agent that solves tasks. If you understand the loop, you understand how any agent works — Claude Code, Cursor, Copilot Agent, or the mini-agent you'll build in Module 07.

This capsule explains the loop step by step, with a real example traced in detail, and covers the key decisions: when to continue the loop, when to stop, and what happens when something goes wrong.


The Loop in Its Simplest Form

┌───────────────────────────────────────────────────────┐
│                    AGENTIC LOOP                        │
│                                                       │
│    ┌──────────┐                                       │
│    │ OBSERVE  │ ← Receives input/result               │
│    └────┬─────┘                                       │
│         │                                             │
│         ▼                                             │
│    ┌──────────┐                                       │
│    │  THINK   │ ← Reasons about what to do            │
│    └────┬─────┘                                       │
│         │                                             │
│         ▼                                             │
│    ┌──────────┐                                       │
│    │   ACT    │ ← Executes an action (tool call/text) │
│    └────┬─────┘                                       │
│         │                                             │
│         ▼                                             │
│    DID IT FINISH?                                     │
│    │                                                  │
│    ├─ YES → Returns the result to the developer       │
│    │                                                  │
│    └─ NO → Goes back to OBSERVE (with the ACT result) │
│                                                       │
└───────────────────────────────────────────────────────┘

The three steps

OBSERVE:
→ The agent receives new information
→ First time: the developer's prompt
→ Subsequent times: the result of the previous action
→ "What do I know now?"

THINK:
→ The agent reasons about what to do
→ It analyzes the available information
→ It decides the next action
→ "What should I do?"

ACT:
→ The agent executes an action
→ Tool call (file read, shell execute, etc.)
→ Or generate a final response
→ "I do this."

Real Example: Tracing the Loop Step by Step

Let's see exactly how an agent processes a real task, step by step.

The task

Developer: "The UserService test is failing.
           The error says 'Cannot read property email of undefined'.
           Fix it."

Iteration 1: Find the test

OBSERVE:
→ Developer input: test failing, error about 'email of undefined'
→ I need to find the test and the error

THINK:
→ I need to locate the UserService test file
→ I'm going to search for files that contain "UserService" and "test"

ACT:
→ Tool: search(pattern="UserService", file_type="test")
→ Result: src/__tests__/UserService.test.ts

Iteration 2: Read the test

OBSERVE:
→ The test is in src/__tests__/UserService.test.ts
→ Now I need to see the content

THINK:
→ I'm going to read the file to find the failing test
→ The error mentions 'email of undefined', I should look for accesses to .email

ACT:
→ Tool: file_read("src/__tests__/UserService.test.ts")
→ Result: [file content, 80 lines]

Iteration 3: Analyze the error

OBSERVE:
→ I see the test. Line 34: expect(result.user.email).toBe(...)
→ The error is "Cannot read property email of undefined"
→ That means result.user is undefined
→ I need to see what UserService returns

THINK:
→ The problem isn't in the test but in UserService
→ UserService probably doesn't return the object with .user
→ I need to read the service

ACT:
→ Tool: file_read("src/services/UserService.ts")
→ Result: [service content, 120 lines]

Iteration 4: Find the bug

OBSERVE:
→ In UserService.ts, line 45:
  return { data: userData }
→ It returns { data: userData }, not { user: userData }
→ The test expects result.user.email
→ But the service returns result.data.email
→ The bug: the key is "data" in the service but the test expects "user"

THINK:
→ There are two options:
  1. Change the service to return { user: userData }
  2. Change the test to use result.data
→ I need to see if other files use UserService
→ If many use .data, the test is wrong
→ If it's new, I can choose

ACT:
→ Tool: search(pattern="UserService", file_type="ts")
→ Result: 3 files use UserService, all with .user

Iteration 5: Apply the fix

OBSERVE:
→ 3 files use .user → the service should return { user: }
→ The bug is in the service, not in the tests
→ Someone changed "user" to "data" incorrectly

THINK:
→ Clear fix: change "data" to "user" in UserService.ts line 45
→ I'm going to make the change

ACT:
→ Tool: file_write("src/services/UserService.ts", 
         line 45: return { user: userData })
→ Result: file modified

Iteration 6: Verify the fix

OBSERVE:
→ File modified. Now I need to verify

THINK:
→ I run the test to confirm the fix works

ACT:
→ Tool: shell_execute("npm test -- UserService")
→ Result: ✅ All tests passed (5/5)

Iteration 7: Report to the developer

OBSERVE:
→ Tests pass. The fix works.

THINK:
→ Task completed. I should report what I did.

ACT:
→ Response to the developer:
  "The bug was in src/services/UserService.ts, line 45.
   It was returning { data: userData } instead of { user: userData }.
   I fixed it and all the tests pass."
→ END OF LOOP

The whole loop visualized

Iteration   Observe              Think                Act
─────────   ───────              ─────                ───
1           Dev's prompt         "I search the test"  search()
2           Test found           "I read the test"    file_read()
3           Test read            "I need the service" file_read()
4           Service read         "Who uses what?"     search()
5           3 use .user          "I change the svc"   file_write()
6           File changed         "I verify"           shell_execute()
7           Tests pass           "Task complete"      → FINAL RESPONSE

Total: 7 iterations, 5 tool calls, 1 fix
Agent time: ~30 seconds

Stop Conditions

How does the agent know when to stop iterating?

The four conditions

1. TASK COMPLETED
   → The agent determines the task is done
   → It generates a final response instead of a tool call
   → "Tests pass, bug fixed. Done."

2. UNRECOVERABLE ERROR
   → The agent can't continue
   → "I don't have permission to modify this file"
   → "I can't find the module I need"
   → It reports the error to the developer

3. NEEDS DEVELOPER INPUT
   → The agent reaches a decision point it can't resolve alone
   → "I found two possible causes. Which one do I investigate?"
   → "Should I modify the database or just the API?"

4. ITERATION LIMIT
   → A safety net to avoid infinite loops
   → Each agent has a maximum number of iterations/tool calls
   → If reached: "I couldn't complete the task within the limit."

The "knows" in quotes

IMPORTANT:
The agent doesn't "know" the task is complete.

What really happens:
→ The LLM generates tokens
→ Instead of generating a tool call, it generates a response text
→ That means "I don't need more tools = task completed"

It's a probabilistic decision, not a conscious evaluation.
That's why sometimes:
→ The agent declares "done" when it isn't
→ The agent keeps iterating when it already finished
→ The developer needs to confirm (the circuit breaker from M05)

Single-Turn vs Multi-Turn

Single-Turn

SINGLE-TURN:
Developer → Prompt → Agent → [internal loop] → Response → END

The developer gives ONE instruction.
The agent runs the loop internally.
It returns ONE result.
There's no back-and-forth.

EXAMPLE:
Developer: "Add a .gitignore for a Node.js project"
Agent: [checks if it exists → no → creates the file → responds]
Developer: [receives the result]

Multi-Turn

MULTI-TURN:
Developer → Prompt → Agent → Response
Developer → Follow-up → Agent → Response
Developer → Adjustment → Agent → Response
...

The developer and the agent have A CONVERSATION.
Each message from the developer can start a new loop.
The context accumulates between turns.

EXAMPLE:
Developer: "Explore the structure of the auth project"
Agent: [explores → reports]
Developer: "Now show me how the tokens are handled"
Agent: [reads specific files → reports]
Developer: "Add refresh token rotation"
Agent: [plans → implements → reports]

When to use each

SINGLE-TURN works for:
→ Simple, well-defined tasks
→ "Create this file with this content"
→ "Run these tests"
→ "Format this code"

MULTI-TURN is necessary for:
→ Tasks that require exploration first
→ Complex tasks where you need to direct step by step
→ Iterative debugging
→ The R→P→E→V workflow from Module 06

The relationship with R→P→E→V

R→P→E→V naturally uses MULTI-TURN:

Turn 1 (Research): "Explore src/auth/ and explain how it works"
Turn 2 (Plan):     "Design a plan to add refresh tokens"
Turn 3 (Execute):  "Implement step 1 of the plan"
Turn 4 (Execute):  "Now step 2"
Turn 5 (Validate): "Run the tests"

Each turn can have MULTIPLE ITERATIONS of the internal loop.
Turn 1 can be 5 iterations (read 5 files).
Turn 3 can be 10 iterations (write + test + fix + test).

Anatomy of an Iteration

Within each iteration, this is what happens technically:

┌─────────────────────────────────────────────────────┐
│ ANATOMY OF AN ITERATION                              │
│                                                     │
│ 1. INPUT TO THE LLM                                │
│    → System prompt (the agent's base instructions)  │
│    → Conversation history (previous messages)       │
│    → Result of the previous tool call (if any)      │
│    → Definitions of available tools                 │
│                                                     │
│ 2. THE LLM GENERATES A RESPONSE                    │
│    → Reasoning (sometimes visible as "thinking")    │
│    → Decision: tool call or final response?         │
│    → If tool call: the tool's name + arguments      │
│    → If response: text for the developer            │
│                                                     │
│ 3. EXECUTION                                        │
│    → If tool call: the system executes the tool     │
│    → The result is added to the history             │
│    → It goes back to step 1                         │
│    → If response: it's shown to the developer       │
│    → END of this loop sequence                      │
│                                                     │
└─────────────────────────────────────────────────────┘

What the developer sees vs what happens

WHAT YOU SEE:
"Searching files... reading UserService.ts... 
 found the bug on line 45... fixing...
 running tests... ✅ everything passes"

WHAT HAPPENS:
Iteration 1: LLM generates → tool_call(search)
             System executes → result
Iteration 2: LLM generates → tool_call(file_read)  
             System executes → result
Iteration 3: LLM generates → tool_call(file_read)
             System executes → result
Iteration 4: LLM generates → tool_call(search)
             System executes → result
Iteration 5: LLM generates → tool_call(file_write)
             System executes → result
Iteration 6: LLM generates → tool_call(shell_execute)
             System executes → result
Iteration 7: LLM generates → final_response(text)
             → It's shown to the developer

EACH "step" you see is a complete iteration of the loop:
input to the LLM → generation → execution → result → repeat

The Cost of the Loop

Each iteration has a cost:

COST PER ITERATION:

1. INPUT TOKENS
   → System prompt (repeated every time)
   → The complete history of the conversation
   → Result of the previous tool call
   → Tool definitions
   → GROWS with each iteration

2. OUTPUT TOKENS
   → The LLM's reasoning
   → Tool call or response
   → Relatively constant

3. LATENCY
   → Each iteration requires a call to the LLM
   → 1-5 seconds per iteration typically
   → 7 iterations = 7-35 seconds

4. CONTEXT WINDOW
   → The history grows with each iteration
   → Eventually it approaches the limit
   → If it fills up: the agent loses old context

Why the cost matters

A SIMPLE TASK:
→ 3-5 iterations → low cost → fast

A COMPLEX TASK:
→ 20-50 iterations → significant cost → slower
→ The context window can fill up
→ The agent can "forget" the first iterations

PRACTICAL IMPLICATION:
→ Well-defined tasks → fewer iterations → better result
→ Vague prompts → more iterations → more cost → worse result
→ R→P→E→V reduces iterations through better direction

Variants of the Loop

Loop with confirmation (Permission System)

Some agents ask for confirmation before acting:

OBSERVE → THINK → [ASKS FOR CONFIRMATION] → ACT → OBSERVE

Claude Code by default asks for confirmation for:
→ File write (modifying files)
→ Shell execute (running commands)
→ It doesn't ask for file read (just reading)

Cursor Agent by default:
→ Shows the plan and executes
→ You can configure the level of autonomy

WHY?
→ Safety: you don't want the agent to delete files without asking
→ Control: it gives you the chance to correct the course
→ The developer as circuit breaker (Module 05)

Loop with internal planning

More sophisticated agents add a planning phase:

OBSERVE → PLAN → THINK → ACT → OBSERVE

The agent creates an internal plan before starting:
1. "I need to do A, B, C, D"
2. "I start with A"
3. [executes A]
4. "A done. Next is B"
5. [executes B]
...

ADVANTAGE: more coherent on long tasks
DISADVANTAGE: the plan can be incorrect

Loop with self-correction

Some agents detect errors and correct themselves:

OBSERVE → THINK → ACT → [ERROR] → OBSERVE → THINK → FIX → ACT

Example:
1. Agent writes code
2. Agent runs tests → they fail
3. Agent reads the error
4. Agent fixes the code
5. Agent runs tests → they pass

THIS IS POWERFUL:
→ The agent iterates on its own work
→ It doesn't need the developer to point out the error
→ It "self-debugs" (with limitations)

BUT ALSO RISKY:
→ It can iterate indefinitely without resolving
→ It can "fix" a symptom and not the cause
→ The agent's sunk cost fallacy (Module 06)

The Loop and the Context Window

As the loop iterates, the context window fills up:

ITERATION 1:  [system prompt][user prompt][tools][think][act]
              ████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ (10% full)

ITERATION 5:  [system][user][tools][iter1][iter2][iter3][iter4][iter5]
              ██████████████░░░░░░░░░░░░░░░░░░░░░ (40% full)

ITERATION 15: [system][user][tools][iter1]...[iter15]
              ████████████████████████████████░░░░ (85% full)

ITERATION 20+: CONTEXT WINDOW FULL
              ████████████████████████████████████ (100%)
              → The agent loses old information
              → Or the system does "compaction" (summarizing)
              → The quality of the reasoning degrades

Practical implication

FOR THE DEVELOPER:

1. Long tasks → split into parts
   → Don't ask "implement the whole feature" in one turn
   → Split into smaller steps (R→P→E→V)
   → Each step = a new chance for fresh context

2. More iterations ≠ better result
   → After a certain point, quality drops
   → If the agent goes in circles, it's better to restart
   → The "rule of three" from Module 06

3. Context window is a limited resource
   → Don't waste it with context dumping
   → Curating the context is more effective

Practical Exercise

Exercise 1: Trace a real loop

Give your coding agent a task and trace each iteration:

TASK: _______________________________________________

ITERATION 1:
→ Observe: _________________________________________
→ Think: ___________________________________________
→ Act: _____________________________________________
→ Tool call: _______________________________________
→ Result: _______________________________________

ITERATION 2:
→ Observe: _________________________________________
→ Think: ___________________________________________
→ Act: _____________________________________________
→ Tool call: _______________________________________
→ Result: _______________________________________

ITERATION 3:
→ Observe: _________________________________________
→ Think: ___________________________________________
→ Act: _____________________________________________
→ Tool call: _______________________________________
→ Result: _______________________________________

[continue until the agent finishes]

TOTAL:
→ Iterations: ____
→ Tool calls: ____
→ Total time: ____
→ Was the result correct? ____
See guided reflection

Example of a real trace with the task "Add a .gitignore for Node.js":

IterationObserveThinkActTool call
1Developer's prompt"Does a .gitignore already exist?"Search for the filesearch(".gitignore")
2No .gitignore exists"I should create one with standard Node.js patterns"Create the filefile_write(".gitignore", "node_modules/\n.env\ndist/\n...")
3File created"I verify it was created correctly"Read the filefile_read(".gitignore")
4Content confirmed"Task completed"Final response

Total: 4 iterations, 3 tool calls, ~10 seconds.

The important thing when tracing: clearly identify what new information arrives in each OBSERVE, what decision it makes in THINK, and what concrete action it executes in ACT. If the agent repeats actions without progress, it's a sign it's lost.

Exercise 2: Identify the stop condition

In the task from the previous exercise:

How did the agent finish?
□ Declared the task completed
□ Unrecoverable error
□ Asked for input
□ Reached the iteration limit

Do you agree that it stopped?
□ Yes, it finished correctly
□ No, it should have done more
□ No, it should have stopped earlier
See solution

Guide for evaluating each stop condition:

  1. Declared the task completed → The most common. The agent generates a final response instead of a tool call. Verify: did it really finish? Sometimes it declares "done" prematurely without running tests or without verifying that the change works.

  2. Unrecoverable error → The agent reports it can't continue. Example: "I don't have permission to modify this file" or "Module X doesn't exist in the project." The key question: was it really unrecoverable or did the agent give up too soon?

  3. Asked for input → The agent found an ambiguity it can't resolve on its own. Example: "I found two possible causes of the bug, which one do I investigate first?" This is a good sign — it indicates the agent recognizes the limits of its autonomy.

  4. Reached the iteration limit → Safety net activated. If this happens frequently, your tasks are too broad or the agent is going in circles without progress.

Criterion for evaluating whether it should have stopped: Is the final result correct and complete? If yes, it stopped well. If it missed verifying (for example, it didn't run tests), it should have done more. If it made unnecessary changes after solving the problem, it should have stopped earlier.

Exercise 3: Predict the loop

BEFORE giving the task to the agent, predict:

TASK: _______________________________________________

MY PREDICTION:
→ Number of iterations: ____
→ Tools it will use: ____
→ Stop condition: ____
→ Expected result: ____

ACTUAL RESULT:
→ Number of iterations: ____
→ Tools it used: ____
→ Stop condition: ____
→ Result: ____

Did your prediction match?
→ If yes: you're understanding the loop
→ If no: where did your prediction fail and why?
See guided reflection

Example prediction for the task "Rename the getData function to fetchUserData across the whole project":

Reasonable prediction:

  • Iterations: 5-8 (search for usages → read each file → modify each one → verify)
  • Tools: search (find all usages), file_read (verify context), file_write (rename in each file), shell_execute (run tests)
  • Stop condition: task completed (after verifying with tests)
  • Result: all usages renamed, tests pass

Where predictions usually fail:

  • Underestimating iterations: The agent may find more files than expected, or need to read additional files to understand the context before renaming.
  • Not predicting self-correction: If the rename breaks something, the agent may do extra iterations to correct it (run tests → see error → fix → re-test).
  • Assuming determinism: The same prompt can produce different loops. One day the agent searches first; another day it reads the main file directly.

With practice, your predictions will improve — this indicates you're internalizing how the loop works.


Common Mistakes

MistakeReality
"The agent thinks like a human"The agent generates tokens — the "thinking" is next-token prediction
"More iterations = more work = better"More iterations can mean the agent is lost
"The agent knows when to stop"The agent generates a final response by probability, not by certainty
"The loop is deterministic"The same prompt can produce different loops (due to the nature of the LLM)
"The context window is infinite"It's finite and each iteration consumes it — design your usage with this in mind

Summary

THE AGENTIC LOOP:

OBSERVE → THINK → ACT → OBSERVE → ... → END

→ OBSERVE: receive information (prompt or tool result)
→ THINK: reason about what to do (next-token prediction)
→ ACT: execute an action (tool call) or generate a final response

STOP CONDITIONS:
1. Task completed → final response
2. Unrecoverable error → reports
3. Needs input → asks the developer
4. Iteration limit → safety net

SINGLE vs MULTI-TURN:
→ Single: one prompt → internal loop → result
→ Multi: continuous conversation, each turn can have its loop

COST:
→ Each iteration consumes tokens and context window
→ More iterations isn't always = a better result
→ Design your usage to minimize unnecessary iterations

FOR MODULE 07:
→ You'll implement this loop in Python
→ Your mini-agent will do exactly this: observe → think → act → loop

Next capsule: 04 - Tool use and the ReAct pattern — how the LLM "uses tools" and the Reason + Act pattern.


Resources

  1. Anthropic: Building Effective Agents — The agentic loop from Anthropic's perspective
  2. Yao et al.: ReAct Paper — The paper that formalized the loop with reasoning
  3. LangChain: Agent Loop — Practical implementation of the loop
  4. Lilian Weng: Autonomous Agents — Technical analysis of the loop and its variants