Once you see it, you can’t unsee it: every AI agent, no matter how complex, is running the same loop under the hood.
The Big Picture
Think about a conversation with another person. The flow is:
- You say something
- They listen, think, respond
- You say something again → repeat from the start
The agent loop is exactly that — except the “thinking” is done by an LLM API.
┌─────────────────────────────────────────┐
│ AGENT LOOP │
│ │
│ ┌──────┐ send ┌───────────┐ │
│ │ User │ ─────────> │ LLM API │ │
│ └──────┘ └─────┬─────┘ │
│ ↑ │ │
│ │ stop_reason? │
│ │ / \ │
│ │ end_turn tool_use │
│ │ | | │
│ │ Print execute tools│
│ │ | │
│ └────────────┘ loop again │
└─────────────────────────────────────────┘
(Tool use is covered in Chapter 2: Tool Use)
Three Key Concepts
1. messages[] — The Only Memory
The LLM has no memory of its own. It can only “remember” because you send the entire conversation history on every API call.
type Message struct {
Role string
Content string
}
messages := []Message{}
// Turn 1
messages = append(messages, Message{Role: "user", Content: "What is the capital of France?"})
// → LLM replies "Paris"
messages = append(messages, Message{Role: "assistant", Content: "Paris"})
// Turn 2
messages = append(messages, Message{Role: "user", Content: "What is its population?"})
// → LLM "knows" the context is France because messages[] is sent in full
Analogy: Every time you talk to the LLM, you hand it a notebook with every previous exchange written inside. It reads from the top, every time.
2. stop_reason — The Only Decision Point
After the LLM finishes, it tells you why it stopped via stop_reason:
| stop_reason | Meaning | What to do |
|---|---|---|
"end_turn" |
LLM finished its response | Print text, wait for next user input |
"tool_use" |
LLM wants to call a tool | Execute the tool, return result to LLM |
"max_tokens" |
Response was cut off (too long) | Print what you got, handle partial |
3. The Loop Never Changes
This is the most important insight. From this chapter through Chapter 10 (Concurrency), the loop structure stays identical. What changes is only what happens inside the tool_use branch.
scanner := bufio.NewScanner(os.Stdin)
for {
fmt.Print("You > ")
scanner.Scan()
userInput := scanner.Text()
messages = append(messages, Message{Role: "user", Content: userInput})
response, err := client.Messages.Create(ctx, anthropic.MessageNewParams{
Model: anthropic.F(MODEL_ID),
System: anthropic.F([]anthropic.TextBlockParam{{Type: "text", Text: SYSTEM_PROMPT}}),
Messages: anthropic.F(toParams(messages)), // converts []Message to SDK-expected format
})
if err != nil {
log.Fatal(err) // simplified — production code should handle errors gracefully
}
switch response.StopReason {
case "end_turn":
// print text
messages = append(messages, Message{Role: "assistant", Content: response.Content[0].Text})
case "tool_use":
// Chapter 2 (Tool Use): execute tools
// Chapter 3 (Sessions): persist to disk
// Chapter 7 (Autonomy): check heartbeat
// ... all of that goes here — the loop stays the same
}
}
Takeaway
- Agent — a simple loop. All the complexity is just layers on top of this.
messages[]— the only state / memorystop_reason— the only decision point- Tool use — the entry point for all advanced features (covered in Chapter 2)
Series Overview
Chapter 1: while True + stop_reason → loop
Chapter 2: TOOLS + TOOL_HANDLERS → execution
Chapter 3: JSONL + ContextGuard → persistence
Chapter 4: Channel ABC + InboundMessage → channels
Chapter 5: BindingTable + session key → routing
Chapter 6: 8-layer prompt + hybrid search → intelligence
Chapter 7: Heartbeat + Cron → autonomy
Chapter 8: DeliveryQueue + backoff → reliability
Chapter 9: 3-layer retry onion → resilience
Chapter 10: Named lanes + Future → scale
Don’t worry about these names yet — each one is introduced step by step in its own chapter.
Each chapter = 1 production concern. Stack all 10 = a full agent gateway.