C.W.K.
Stream
Lesson 01 of 05 · published

The Minimal Agent: Loop Before Framework

~32 min · python, minimal-agent, loop

Level 0Observer
0 XP0/40 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete

Build the loop once by hand

Before you use any framework, build the loop yourself. Not because frameworks are bad, but because frameworks hide the thing you must understand. The provider returns either final text or tool calls. Your application executes tool calls and feeds the results back. That is the machine.

A hand-built loop teaches the three failure points frameworks cannot save you from: bad tool schemas, bad tool result formatting, and missing stop policies.

The loop shape

Start with a message list. Send it to the model with tool definitions. If the model returns tool calls, execute them in your trusted code. Append the model's tool request and your tool result to state. Repeat.

The implementation below is intentionally provider-neutral. Real APIs differ in message shape, but the control flow is the same.

Keep tool execution outside the model

The model requests a tool call; your application decides whether it is allowed, executes it, catches errors, logs it, and returns a concise result. That separation is what keeps the system debuggable and permissioned.

Code

Minimal loop with safe tool execution·python
import json
from dataclasses import dataclass

@dataclass
class ToolCall:
    id: str
    name: str
    arguments: dict

def safe_execute(tool_call, registry):
    if tool_call.name not in registry:
        return {"ok": False, "error": f"Unknown tool: {tool_call.name}"}
    try:
        return {"ok": True, "data": registry[tool_call.name](**tool_call.arguments)}
    except Exception as exc:
        return {"ok": False, "error": str(exc)}

def run_agent(task, model, tool_registry, max_steps=10):
    messages = [{"role": "user", "content": task}]

    for _ in range(max_steps):
        response = model(messages=messages, tools=list(tool_registry))
        if response.final_text:
            return response.final_text

        messages.append(response.as_message())
        for call in response.tool_calls:
            result = safe_execute(call, tool_registry)
            messages.append({
                "role": "tool",
                "tool_call_id": call.id,
                "content": json.dumps(result),
            })

    return "Stopped before completion: max_steps reached."

External links

Exercise

Implement a toy agent loop with two fake tools: get_time() and add(a, b). Do not call a real model; mock a response object with one tool call.
Hint
The goal is control flow, not model access. Make sure tool errors are returned as data, not uncaught exceptions.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.