Intermediate 25 min

The Agent Loop: Where It All Comes Together

This is the heart of your agent. The loop does four things:

  1. Send messages + tool definitions to the model
  2. Receive response (text or tool call request)
  3. Execute any tool calls
  4. Repeat until done or hit max iterations

Let’s build it step by step.

Understanding Conversation State

The key to agents is maintaining state. Each iteration adds to the conversation:

input_list = [
    {"role": "user", "content": "Create a weekly digest from ./notes"},
    # Agent responds with a plan...
    {"role": "assistant", "content": "I'll create a plan..."},
    # Agent calls a tool...
    {"type": "function_call", "name": "list_files", ...},
    # Tool result goes back...
    {"type": "function_call_output", "output": '{"files": [...]}'},
    # Agent uses result and continues...
]

The input_list grows with each step. The model sees the full history and can make informed decisions.

The Core Agent Function

Here’s the complete agent loop:

🐍 Python Complete Agent Loop
📟 Console Output
Run code to see output...

Breaking Down the Loop

Let’s understand each part:

1. Initialize Conversation

input_list = [{"role": "user", "content": goal}]

Start with the user’s goal. This is the first message in the conversation.

2. Set Instructions

instructions = (
    "You are a helpful ops agent.\\n"
    "First, write a short plan (3-7 steps).\\n"
    "Then execute the plan by calling tools when needed.\\n"
    ...
)

Instructions guide the agent’s behavior. This is where you define the agent’s personality and approach.

3. Call the Model

resp = client.chat.completions.create(
    model="gpt-4",
    messages=[{"role": "system", "content": instructions}] + input_list,
    tools=tools,
    tool_choice="auto"
)

Send the conversation + tools to the model. tool_choice="auto" lets the model decide when to use tools.

4. Check for Tool Calls

if not message.tool_calls:
    return message.content  # Agent is done

If there are no tool calls, the agent has finished its work.

5. Execute Tools

for tool_call in message.tool_calls:
    name = tool_call.function.name
    args = json.loads(tool_call.function.arguments)
    
    if needs_approval(name) and not approve(name, args):
        tool_output = {"status": "denied_by_user"}
    else:
        tool_output = dispatch_tool(name, args)

For each tool call:

  • Extract name and arguments
  • Check if approval is needed
  • Execute the tool
  • Capture the result

6. Add Results to Conversation

input_list.append({
    "role": "tool",
    "tool_call_id": tool_call.id,
    "content": json.dumps(tool_output)
})

Tool results go back into the conversation. The model will see them in the next iteration.

Complete Working Example

Let’s put it all together with a full runnable script:

🐍 Python Complete agent.py Script
📟 Console Output
Run code to see output...

Save this as agent.py and run it:

python agent.py

Try It: Experiment with MAX_ITERS

Exercise 1: Set MAX_ITERS=2 and see what happens.

The agent won’t have enough iterations to complete the task. You’ll see it stop mid-execution.

Why this matters: In production, you need safety limits. Without them, a buggy agent could loop forever and cost you money.

Exercise 2: Deny an approval and watch the agent recover.

When you deny write_file, the agent should see {"status": "denied_by_user"} and explain that it couldn’t complete the task.

Why this matters: Agents need to handle failures gracefully. They should explain what went wrong, not just crash.

Common Issues and Fixes

Issue 1: Agent loops forever

Symptom: Agent keeps calling the same tool repeatedly

Fix: Check your tool implementations. Make sure they return useful results. If a tool returns an error, the agent might retry indefinitely.

Issue 2: Agent doesn’t call tools

Symptom: Agent just talks about what it would do

Fix: Check your tool schemas. Make sure descriptions are clear and parameters are well-defined. The model needs to understand when to use each tool.

Issue 3: Tool calls fail with JSON errors

Symptom: json.loads() raises an exception

Fix: Wrap tool execution in try/except. Return {"error": "..."} instead of crashing.

Key Takeaways

You’ve built a complete agent loop! You now understand:

  1. Conversation state - How input_list grows with each iteration
  2. Tool calling flow - Model proposes, code executes, results return
  3. Approval gates - How to add human-in-the-loop control
  4. Safety limits - Why MAX_ITERS prevents infinite loops
  5. Error handling - How to make agents resilient

What’s Next?

In the next page, we’ll add more controls: budgets, rate limits, and dry-run mode. These are essential for production agents.