Welcome to the first lesson in our series on building AI coding agents from scratch! Before we get into complex architectures or multi-agent teams, we need to strip away all the noise.
There is a fundamental truth in agent engineering: The model IS the agent. The code you write is just the “harness”.
In this lesson, we are going to build the most basic harness possible. Our motto for today is: “One loop & Bash is all you need”.
The Problem
A language model is brilliant at reasoning about code, but on its own, it cannot touch the real world. It cannot read your files, run your tests, or check for errors.
If you just use a standard chat interface, you have to manually copy the code it generates, run it in your terminal, and paste the errors back into the chat. Without an automated loop, you become the loop.
The Solution: The while Loop
The entire secret of an AI coding agent can be boiled down to a simple pattern:
- Send the prompt to the LLM.
- If the LLM wants to use a tool, execute it.
- Feed the result back to the LLM.
- Repeat until the LLM decides it is finished.
One single exit condition controls the entire flow.
How It Works (In Code)
Let’s look at how this is implemented in Python using the Anthropic API. The entire agent can be built in under 30 lines of code.
Step 1: The User Prompt The user’s prompt simply becomes the first message in our conversation array.
messages.append({"role": "user", "content": query})
Step 2: Call the Model
We send the entire message history, along with our tool definitions, to the LLM. In this minimal version, we only give the model one tool: bash.
response = client.messages.create(
model=MODEL,
system=SYSTEM,
messages=messages,
tools=TOOLS,
max_tokens=8000,
)
Step 3: Check the Stop Reason
We append the assistant’s response to our message history. Then, we check the stop_reason. If the model didn’t call a tool, it means it has finished its task and is talking directly to the user, so we break the loop.
messages.append({"role": "assistant", "content": response.content})
if response.stop_reason != "tool_use":
return
Step 4: Execute and Return Results
If the model did call a tool, we execute the requested Bash command, collect the output, and append it back to the history as a user message containing a tool_result. Then, the loop starts over at Step 2.
results = []
for block in response.content:
if block.type == "tool_use":
output = run_bash(block.input["command"])
results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": output,
})
messages.append({"role": "user", "content": results})
The Key Insight
By giving the model access to bash, it can effectively do anything a human developer can do in a terminal: read files using cat, write files using echo, and execute scripts.
Everything else we build in this series—planning, memory, concurrency, and teams—will layer on top of this exact same loop. The loop itself will never change.