Post 01

The Smallest Coding Agent Loop

If you care about your craft, sooner or later you will build your own tools. So let's explore one of the most fun tools in modern software engineering: the coding agent. In a nutshell, it is a model inside a loop that can ask for tools, receive results, and decide what to do next. Get some coffee!

Hand-drawn anatomy diagram of the agent loop inside a sandbox.

If you ever wondered what actually sits inside a coding agent, the answer is surprisingly small.

At the center of every coding agent is a loop:

messages = [system_prompt, user_task]

while True:
    response = model.chat(messages, tools=tools)
    messages.append(response.message)

    if not response.tool_calls:
        break

    for call in response.tool_calls:
        result = run_tool(call.name, call.arguments)
        messages.append(tool_result(call.id, result))

That is the primitive.

The model looks at the conversation and decides either:

  • "I can answer now"
  • or "I need to use a tool"

If it asks for a tool, the harness runs that tool and feeds the result back into the conversation. Then the model gets another chance.

Building the loop

You can reproduce this with one Python file and a local model running in LM Studio (serving and inference endpoint). The goal is not to build a polished agent yet. The goal is to see a model ask for a tool, run that tool yourself, and feed the result back.


Install LM Studio

  1. Install LM Studio from lmstudio.ai.
  2. Open the model search and download a model that supports tool calling. A simple coder model is all you need. In LM Studio, search for qwen3-coder-30b and download a file your machine can run.
  3. I use qwen/qwen3-coder-30b, but a smaller Qwen coder or instruct model is fine for this first loop.
  4. Load the model in LM Studio.
  5. Open the Developer or Local Server tab and start the local server.
  6. Keep the default OpenAI-compatible endpoint: http://localhost:1234/v1.
LM Studio model search showing qwen/qwen3-coder-30b with Tool Use capability and download options.
Search for the model name, check that the result advertises Tool Use, then download a format and size that fits your machine.

If you are not sure what model id to use, once server is running ask LM Studio directly:

curl http://localhost:1234/v1/models

Copy the model id from that response. You will pass it to the script below as LOOP_MODEL.


Create a new Python Project

mkdir the-loop
cd the-loop
python3 -m venv .venv
.venv/bin/python -m pip install openai

Create a file the agent can inspect:

echo "Loop is a tiny coding agent experiment." > notes.txt

Then create loop.py:

import json
import os
from pathlib import Path

from openai import OpenAI


BASE_URL = os.getenv("LOOP_BASE_URL", "http://localhost:1234/v1")
API_KEY = os.getenv("LOOP_API_KEY", "lm-studio")
MODEL = os.getenv("LOOP_MODEL", "qwen/qwen3-coder-30b")
WORKSPACE = Path.cwd()

client = OpenAI(base_url=BASE_URL, api_key=API_KEY)


tools = [
    {
        "type": "function",
        "function": {
            "name": "list_dir",
            "description": "List files in the current workspace.",
            "parameters": {
                "type": "object",
                "properties": {
                    "path": {
                        "type": "string",
                        "description": "Directory path relative to the workspace.",
                    }
                },
                "required": ["path"],
            },
        },
    },
    {
        "type": "function",
        "function": {
            "name": "read_file",
            "description": "Read a text file from the current workspace.",
            "parameters": {
                "type": "object",
                "properties": {
                    "path": {
                        "type": "string",
                        "description": "File path relative to the workspace.",
                    }
                },
                "required": ["path"],
            },
        },
    },
]


def safe_path(path: str) -> Path:
    resolved = (WORKSPACE / path).resolve()
    if not resolved.is_relative_to(WORKSPACE):
        raise ValueError("Path escapes the workspace")
    return resolved


def list_dir(path: str) -> str:
    target = safe_path(path)
    return "\n".join(sorted(p.name for p in target.iterdir()))


def read_file(path: str) -> str:
    return safe_path(path).read_text()


def run_tool(name: str, arguments: dict) -> str:
    if name == "list_dir":
        return list_dir(arguments["path"])
    if name == "read_file":
        return read_file(arguments["path"])
    return f"Unknown tool: {name}"


messages = [
    {
        "role": "system",
        "content": (
            "You are a tiny coding agent. Use tools when you need to inspect "
            "the workspace. When you have enough information, answer clearly."
        ),
    },
    {
        "role": "user",
        "content": "What files are here, and what does notes.txt say?",
    },
]


while True:
    response = client.chat.completions.create(
        model=MODEL,
        messages=messages,
        tools=tools,
        tool_choice="auto",
    )

    message = response.choices[0].message
    messages.append(message.model_dump(exclude_none=True))

    if message.content:
        print("\nassistant:")
        print(message.content)

    if not message.tool_calls:
        break

    for tool_call in message.tool_calls:
        name = tool_call.function.name
        arguments = json.loads(tool_call.function.arguments or "{}")

        print(f"\ntool call: {name}({arguments})")
        result = run_tool(name, arguments)
        print(f"tool result:\n{result}")

        messages.append(
            {
                "role": "tool",
                "tool_call_id": tool_call.id,
                "content": result,
            }
        )

3. Run It

If your model id is different, set it before running:

export LOOP_MODEL="paste-your-lm-studio-model-id-here"
.venv/bin/python loop.py

You should see something like this:

tool call: list_dir({'path': '.'})
tool result:
notes.txt
loop.py

tool call: read_file({'path': 'notes.txt'})
tool result:
Loop is a tiny coding agent experiment.

assistant:
The workspace contains notes.txt and loop.py. notes.txt says:
"Loop is a tiny coding agent experiment."

That is the most basic and most important primitive for us. Notice the model did not read the file. It asked for read_file. Your Python harness read the file, appended the tool result, and gave the model another turn.

If the model answers without calling tools, try a model with stronger tool-calling support or make the task more explicit: "Use your tools to inspect the workspace before answering." Local models vary a lot here.

So now you know why this project is called The Loop.

Key takeaway is that the model is not really "doing" file operations. The harness is. The model emits structured intent:

{
  "name": "read_file",
  "arguments": {
    "path": "loop_cli/agent.py"
  }
}

Then our code decides what that means. The model reasons and proposes.

Everything after that is engineering:

  • tools
  • permissions
  • context management
  • memory
  • workspace boundaries
  • CLI UX
  • tests
  • evals

In harness terms, this first loop is only the heartbeat. It can call a model, receive a tool request, run a little code, and feed the result back. It does not yet have a serious environment, durable state, verification, or product-level permissions.

Now that we know the model cannot read a file, write code, or run a command by itself. It can only ask. In the next post, we will make that boundary explicit and look at tools as the model's hands.