DEV Community

Yuriy U
Yuriy U

Posted on

An AI agent is a loop. Here it is in 40 lines of plain Python

"AI agent" sounds like something that needs a framework. It doesn't. Strip away the libraries and every agent is the same small loop:

  1. Send the conversation to a model.
  2. If the model asks to use a tool, run it and add the result to the conversation.
  3. Repeat until the model gives a final answer — or you hit a step limit.

Here is that loop in plain Python. It runs without an API key because the "model" is a fake one that returns the same two shapes a real model does.

import json

# 1. Tools are plain Python functions.
def add(a, b):
    return a + b

def word_count(text):
    return len(text.split())

TOOLS = {"add": add, "word_count": word_count}

# 2. A fake model: it reads the conversation and either asks for a tool
#    or gives a final answer. A real LLM API returns the same two shapes.
def fake_model(messages):
    last = messages[-1]
    if last["role"] == "user":
        return {"type": "tool_call", "name": "add", "args": {"a": 19, "b": 23}}
    if last["role"] == "tool":
        return {"type": "answer", "text": f"The sum is {last['content']}."}

# 3. The agent loop: call the model, run tools it asks for,
#    feed results back, stop on an answer or after max_steps.
def run_agent(question, model, max_steps=5):
    messages = [{"role": "user", "content": question}]
    for step in range(max_steps):
        reply = model(messages)
        if reply["type"] == "answer":
            return reply["text"]
        tool = TOOLS.get(reply["name"])
        if tool is None:
            result = f"error: unknown tool {reply['name']}"
        else:
            result = tool(**reply["args"])
        print(f"step {step + 1}: {reply['name']}({json.dumps(reply['args'])}) -> {result}")
        messages.append({"role": "tool", "name": reply["name"], "content": str(result)})
    return "Stopped: step limit reached."

print(run_agent("What is 19 + 23?", fake_model))
Enter fullscreen mode Exit fullscreen mode

Output:

step 1: add({"a": 19, "b": 23}) -> 42
The sum is 42.
Enter fullscreen mode Exit fullscreen mode

The four ideas hiding in those lines

Tools are just functions. The model never runs code. It returns a request — a tool name and arguments — and your program decides whether to run it. That is also where safety lives: an unknown tool name becomes an error message, not a crash.

The conversation is the memory. messages is a list of dicts. The tool result goes back into it, so on the next call the model "sees" what happened. Everything an agent knows about the current task is in that list.

The step limit is not optional. A model can keep asking for tools forever. max_steps is the cheapest guardrail you will ever write, and it saves real money once each step is a paid API call.

Errors are observations. When a tool fails, don't raise — put the error text into the conversation. A good model reads it and tries something else.

Swapping in a real model

Replace fake_model with a function that calls an LLM API. Every major API (OpenAI, Anthropic, Google) accepts a list of tool definitions — name, description, JSON schema for the arguments — and answers with either text or a tool-call request. Your job is to translate that response into the same two shapes: tool_call or answer. The loop doesn't change.

Where to go from here

The things that turn this toy into something useful are all small additions to the same loop: a second and third tool, trimming old messages so the conversation fits, retries when a tool fails, and test cases that check the agent's answers.

I wrote these up as a free interactive course where you build each piece yourself in the browser: AI Agents Foundations — 25 short lessons, Python runs in the page, no sign-up. If you'd rather read than type, there's also a step-by-step guide.

Top comments (0)