top of page

What Is an Agent (With Code) — Building AI Agents, Part 1

  • Shawn West
  • Jul 8
  • 3 min read

Updated: Aug 6

Building AI Agents · Part 1

An agent is an LLM that decides what to do next, takes an action, observes the result, and decides again. This tutorial shows the smallest working agent so the concept is concrete.

What You'll Build

A working agent in ~50 lines of code that solves a simple task.

Step 1: The Agent Loop (5 min)

1. Observe (current state)
2. Decide (what to do next)
3. Act (call a tool)
4. Observe (result)
5. Decide (next action or "done")
6. Loop until done

This is the core. Everything else is refinement.

Step 2: The Minimal Tools (10 min)

For our example: a calculator agent.

def add(a: float, b: float) -> float:
    return a + b

def multiply(a: float, b: float) -> float:
    return a * b

def subtract(a: float, b: float) -> float:
    return a - b

TOOLS = [
    {
        "name": "add",
        "description": "Add two numbers",
        "input_schema": {
            "type": "object",
            "properties": {
                "a": {"type": "number"},
                "b": {"type": "number"},
            },
            "required": ["a", "b"],
        }
    },
    # ... multiply, subtract similarly
]

TOOL_FN = {"add": add, "multiply": multiply, "subtract": subtract}

Step 3: The Agent Loop in Code (15 min)

def agent(task: str, max_steps: int = 10):
    messages = [{"role": "user", "content": task}]
    
    for step in range(max_steps):
        response = client.messages.create(
            model="claude-sonnet-4-6",
            max_tokens=1024,
            tools=TOOLS,
            messages=messages,
        )
        
        if response.stop_reason == "tool_use":
            # Find the tool call
            tool_use = next(c for c in response.content if c.type == "tool_use")
            
            # Execute
            result = TOOL_FN[tool_use.name](**tool_use.input)
            print(f"[Step {step}] Called {tool_use.name}({tool_use.input}) = {result}")
            
            # Add to conversation
            messages.append({"role": "assistant", "content": response.content})
            messages.append({
                "role": "user",
                "content": [{
                    "type": "tool_result",
                    "tool_use_id": tool_use.id,
                    "content": str(result),
                }]
            })
        else:
            # Final answer
            return response.content[0].text
    
    return "Max steps reached"

That's the agent. 30 lines.

Step 4: Run It (5 min)

result = agent("What is (5 + 3) * 7?")
print(f"\nFinal: {result}")

Watch the steps:

[Step 0] Called add({'a': 5, 'b': 3}) = 8
[Step 1] Called multiply({'a': 8, 'b': 7}) = 56

Final: (5 + 3) * 7 = 56

The agent:

  1. Decided to add 5 + 3 first

  2. Got the result

  3. Decided to multiply by 7

  4. Returned the final answer

Each step was the model deciding what to do.

Step 5: Try a More Complex Task (5 min)

result = agent("If I have 4 boxes of 12 apples each, how many do I have? Then if I give half away, how many remain?")
print(result)

Watch the multi-step reasoning unfold.

Step 6: Understand What's Different From Tools (5 min)

In Part 7 of Path 9, you used tools. The model called a tool, got a result, responded.

An agent does multi-step:

  • Tool → result → another tool → another result → another tool → ... → final answer

  • The model controls the loop

  • The number of steps is dynamic

That's the agent abstraction.

Step 7: Add a System Prompt (10 min)

For more reliable behavior:

SYSTEM_PROMPT = """
You are a math assistant. You have access to add, multiply, and subtract tools.

When given a math problem:
1. Break it into single-operation steps
2. Use the tools for each step (don't compute mentally)
3. Use intermediate results in subsequent steps
4. Give a clear final answer

If a task doesn't need calculation, answer directly without tools.
"""

response = client.messages.create(
    system=SYSTEM_PROMPT,
    # ...
)

The agent's behavior steers significantly.

Step 8: Log the Trace (10 min)

For each agent run, log:

trace = {
    "task": task,
    "steps": [],
    "final": None,
}

for step in range(max_steps):
    response = client.messages.create(...)
    
    if response.stop_reason == "tool_use":
        tool_use = ...
        result = ...
        
        trace["steps"].append({
            "step": step,
            "tool": tool_use.name,
            "input": tool_use.input,
            "output": str(result),
        })
        # ...
    else:
        trace["final"] = response.content[0].text
        break

log_trace(trace)

Traces are how you debug agents. Save every one in development.

Step 9: Add Error Handling (10 min)

Tools can fail:

def execute_tool(tool_use):
    try:
        return TOOL_FN[tool_use.name](**tool_use.input)
    except Exception as e:
        return f"Error: {str(e)}"

Return the error to the agent. It can recover (try different approach) or give up.

Step 10: The Iteration (varies)

This is the foundation. From here:

  • Part 2: more tools

  • Part 3-10: progressively more sophisticated

But the loop stays the same. Decide. Act. Observe. Repeat.

What You Just Did

You built a working AI agent. The loop is visible; the code fits on a screen; the concept is concrete.

Common Failure Modes

No max_steps. Agent loops forever on confused tasks.

No error handling. Tool failure breaks the loop.

No trace logging. Can't debug.

Single-step thinking. Tools defined but agent doesn't actually chain them.

Hidden mental math. Model computes 5+3 without calling the tool. Force tool use via prompt.

Continue the Building AI Agents path

Part of the Building AI Agents learning path.

bottom of page