How the AI Agent Loop Pattern Works: The Core of Every AI Agent
I spent weeks trying to build an AI agent. I kept overcomplicating it with state machines, complex workflows, and endless abstractions. Then I finally understood the core pattern.
It’s a while True loop.
That’s it. The entire agent architecture in under 30 lines of code.
The Problem: Over-Engineering Agents
When I first started building AI agents, I thought I needed:
- Complex state management
- Workflow orchestration frameworks
- Multi-stage pipelines
- Event-driven architectures
I was wrong. Every AI agent - from simple coding assistants to complex multi-agent systems - is built on one fundamental pattern: the agent loop.
The Pattern Revealed
Here’s what every AI agent actually does:
User --> messages[] --> LLM --> response | stop_reason == "tool_use"? / \ yes no | | execute tools return text append results loop back -----------------> messages[]The model decides when to call tools and when to stop. The code just executes what the model asks for.
The Core Implementation
Let me show you the actual code. This is from a real agent implementation:
def agent_loop(messages: list): while True: response = client.messages.create( model=MODEL, system=SYSTEM, messages=messages, tools=TOOLS, max_tokens=8000, )
# Append the assistant's response to conversation history messages.append({"role": "assistant", "content": response.content})
# Check if the model wants to stop or use tools if response.stop_reason != "tool_use": return
# Execute all requested tools results = [] for block in response.content: if block.type == "tool_use": print(f"\033[33m$ {block.input['command']}\033[0m") output = run_bash(block.input["command"]) results.append({ "type": "tool_result", "tool_use_id": block.id, "content": output })
# Inject results back as user message messages.append({"role": "user", "content": results})Under 30 lines. Everything else in agent engineering layers on top of this - without changing the loop.
How the Loop Actually Works
Let me walk through each step:
Step 1: User Input Becomes the First Message
messages = [{"role": "user", "content": "List all Python files in the src directory"}]The user’s prompt initializes the message array.
Step 2: Send Messages + Tool Definitions to LLM
response = client.messages.create( model=MODEL, system=SYSTEM, messages=messages, tools=TOOLS, # Tell the model what tools it can use max_tokens=8000,)The LLM receives:
- The conversation history
- Available tool definitions (name, description, parameters)
- System prompt
Step 3: Check the stop_reason
if response.stop_reason != "tool_use": return # Model is done, return the text responseThe stop_reason tells you what the model wants:
"tool_use"- The model wants to execute tools"end_turn"- The model is done and returning text
Step 4: Execute Tools and Loop Back
# Execute each tool the model requestedresults = []for block in response.content: if block.type == "tool_use": output = run_bash(block.input["command"]) results.append({ "type": "tool_result", "tool_use_id": block.id, "content": output })
# Append results as a user messagemessages.append({"role": "user", "content": results})
# Loop back to step 2The tool results become a user message, and the loop continues.
Why This Matters
I used to think I needed to control when tools were called. I’d write complex logic like:
# DON'T DO THIS - Over-engineeredif "file" in user_input: call_file_tool()elif "search" in user_input: call_search_tool()else: ask_llm()This is wrong. The model controls the flow. Your code just:
- Sends messages to the LLM
- Checks if it wants tools
- Executes what it asks for
- Appends results
- Loops back
The motto: “One loop & Bash is all you need”
Common Mistakes I Made
Mistake 1: Adding Exit Conditions Beyond stop_reason
I tried to add my own exit logic:
# WRONG - Don't add your own exit conditionswhile iteration < MAX_ITERATIONS: # ... agent loop if response_contains_answer(response): breakThe model knows when it’s done. Trust stop_reason.
Mistake 2: Not Appending All Message Types
I forgot that every response needs to be in the conversation:
# WRONG - Missing assistant responseif response.stop_reason == "tool_use": # Execute tools messages.append({"role": "user", "content": results}) # Forgot to append assistant response!You must append both:
- The assistant’s response (with tool use blocks)
- The tool results as a user message
Mistake 3: Complex Tool Selection Logic
I thought I needed to decide which tools to offer:
# WRONG - Don't filter toolsif "code" in user_input: tools = [bash_tool, read_tool, write_tool]else: tools = [search_tool]Just pass all tools. The model knows what it needs.
How This Scales
All 12 sessions of advanced agent courses preserve this core loop. What they add:
- Planning: A tool that writes plans
- Memory: Tools that read/write memory
- Teams: Multiple agents with shared message history
- Delegation: One agent calling another as a tool
But the loop never changes. It’s always:
while True: response = call_llm(messages, tools) messages.append(assistant_response) if response.stop_reason != "tool_use": return messages.append(user_message_with_tool_results)The Key Insight
“The MODEL decides when to call tools and when to stop. The CODE just executes what the model asks for.”
This shifted my entire mental model. I stopped trying to build complex control flows and started trusting the LLM’s reasoning.
The loop belongs to the agent. The mechanisms belong to the harness.
What the Loop Provides
- State management: The messages array IS your state
- Tool orchestration: The model decides what to call and when
- Conversation history: Just append, never mutate
- Exit conditions:
stop_reasonhandles it all
Related Knowledge
- Tool Calling: How LLMs request function execution through structured outputs
- Message History: Why conversation context matters for multi-turn reasoning
- Stop Sequences: How models signal completion vs continuation
Summary
The AI agent loop is the heartbeat of every AI agent:
- Send messages + tools to LLM
- Check
stop_reason - Execute tools if needed
- Append results
- Loop back
Everything else - planning, context, teams, memory - is built on this foundation. The pattern is elegant because it’s simple. The power comes from the model, not the code.
Next time you’re building an agent, start with the loop. Add complexity only when you need it.
Final Words + More Resources
My intention with this article was to help others share my knowledge and experience. If you want to contact me, you can contact by email: Email me
Here are also the most important links from this article along with some further resources that will help you in this scope:
Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!
Comments