Chapter 9 · AI Engineer

A minimal agent loop

An agent is not one model call — it's a loop. A single call can answer a question; an agent can take *steps* toward a goal, using tools and reacting to what it finds. The cycle is think → act → observe:

  • Think — the model looks at the history so far and decides the next move: call a tool, or give a final answer.
  • Act — your code runs the tool it asked for.
  • Observe — you feed the tool's result back into the history, and the model thinks again with that new information.

This repeats until the model emits a final answer instead of another tool call. The history is the agent's memory: each tool result you append is something the next "think" can build on. That's how it chains steps — look up a value, then use that value in a calculation, then report the result.

One rule is non-negotiable: a max-steps cap. A model can get stuck calling tools forever. The loop must run a bounded number of times and raise (or stop) if it never reaches a final answer. A loop with no cap is a runaway bill and a hung process waiting to happen.

Syntax

history = [{"role": "user", "content": goal}]
for _ in range(max_steps):
    step = model(history)            # think
    if step["type"] == "final":
        return step["answer"]        # stop
    result = TOOLS[step["name"]](**step["input"])   # act
    history.append({"role": "tool", "name": step["name"], "content": result})  # observe
raise RuntimeError("agent did not finish within max_steps")

Worked examples

The loop: think, act, observe, stop

def lookup_tool(name):
    return {"london": 9, "paris": 11}.get(name.lower(), 0)

def calc_tool(a, b):
    return a + b

TOOLS = {"lookup": lookup_tool, "calc": calc_tool}

# Scripted model: step 1 lookup, step 2 calc, step 3 final.
def agent_model_stub(history):
    obs = [m for m in history if m.get("role") == "tool"]
    if len(obs) == 0:
        return {"type": "tool", "name": "lookup", "input": {"name": "London"}}
    if len(obs) == 1:
        return {"type": "tool", "name": "calc", "input": {"a": obs[0]["content"], "b": 100}}
    return {"type": "final", "answer": f'The result is {obs[1]["content"]}.'}

def run_agent(goal, max_steps=5):
    history = [{"role": "user", "content": goal}]
    for _ in range(max_steps):
        step = agent_model_stub(history)
        if step["type"] == "final":
            return step["answer"]
        result = TOOLS[step["name"]](**step["input"])
        history.append({"role": "tool", "name": step["name"], "content": result})
    raise RuntimeError("agent did not finish within max_steps")

print(run_agent("look up London then add 100"))
# Output:
# The result is 109.

Feeding results forward (the chain)

# Trace of the run above — each observation feeds the next think:
#   turn 1: think -> lookup("London")   act -> 9     observe -> history has 9
#   turn 2: think -> calc(9, 100)        act -> 109   observe -> history has 109
#   turn 3: think -> final "The result is 109."   (stop)
# The calc step could only use 9 because the lookup result was fed back in.

The max-steps cap stops a runaway

# (run_agent as above)
try:
    run_agent("go", max_steps=1)   # needs 3 turns, only allowed 1
except RuntimeError as e:
    print(e)
# Output:
# agent did not finish within max_steps

Common mistakes & gotchas

No max-steps cap = a runaway loop

Without a hard limit, a model that keeps proposing tool calls runs forever — burning tokens, money, and time, and hanging your process. The for _ in range(max_steps) bound and the raise at the end are mandatory, not optional polish. Treat an agent with no cap as a bug.

Forgetting to feed the observation back

If you run the tool but don't append its result to history, the next think is blind to what happened and the agent either repeats itself or stalls. The append step IS the "observe" — skip it and the loop loses its memory between turns.

Off-by-one on the step budget

Count the turns the task actually needs. The example needs three model turns (lookup, calc, final), so max_steps=1 or 2 can't finish but 3 can. When an agent fails to complete, check the cap is generous enough before assuming the logic is broken — and keep it tight enough to stop genuine runaways.

Why it matters

The think-act-observe loop is the engine inside every coding assistant, research agent, and autonomous workflow you've heard of. Strip away the branding and they're all this: a model deciding the next step, code running a tool, the result fed back, repeat until done. Once you've written the loop, you understand what an "AI agent" actually is — and that there's a hard cap protecting you from it.