Skip to main content

Running the loop for real

We now have all the pieces:

This lesson connects them to a real model and runs the whole thing end to end — the actual shopbot.py from our agent-harness project. No more pseudocode.

The setup

A few lines before the loop. We create a client for the Anthropic API, pick a model, and set a guardrail.

from anthropic import Anthropic

from tools import TOOL_FUNCTIONS, TOOL_SCHEMAS

MODEL = "claude-opus-4-8"
MAX_ROUNDS = 10 # a guardrail: never loop forever

client = Anthropic()

TOOL_SCHEMAS and TOOL_FUNCTIONS are exactly what we built in What is a tool? — the menu the model reads, and the kitchen that maps a tool name to a real function.

Starting the conversation

The conversation begins as a list with one entry: the customer's question. This is the growing list from the harness loop that the harness re-sends every round.

conversation = [
{
"role": "user",
"content": "Where's my order? I bought running shoes last week.",
}
]

ask_model becomes a real API call

In the pseudocode from the harness loop we wrote ask_model(conversation, tools). Here's the real thing — one call to the SDK, handing it the conversation and the tool schemas:

response = client.messages.create(
model=MODEL,
max_tokens=1024,
tools=TOOL_SCHEMAS,
messages=conversation,
)

That tools=TOOL_SCHEMAS argument is how the model learns the tools exist — it gets the descriptions we wrote in What is a tool? on every call.

Final answer, or tool request?

After each call we add the model's turn to the conversation, then check what kind of turn it was. The SDK tells us with response.stop_reason:

conversation.append({"role": "assistant", "content": response.content})

if response.stop_reason != "tool_use":
# A plain answer → leave the loop.
final_text = "".join(
block.text for block in response.content if block.type == "text"
)
print(final_text)
return

If stop_reason is anything other than "tool_use", the model is done — it gave us a final answer, so we print it and stop. This is the natural exit from the harness loop.

A reply is a list of "blocks"

response.content isn't a plain string — it's a list of blocks. A block is either text (block.type == "text") or a tool request (block.type == "tool_use"). That's why we filter by type instead of just reading a string. One reply can even contain several blocks.

Running the tools

If we didn't return, the model asked for at least one tool. We walk its blocks, run each tool_use through the harness, and collect the results:

tool_results = []
for block in response.content:
if block.type == "tool_use":
function = TOOL_FUNCTIONS[block.name] # name → real function
result = function(**block.input) # run it with the model's args
tool_results.append(
{
"type": "tool_result",
"tool_use_id": block.id,
"content": str(result),
}
)

Three things to notice:

  • block.name is the tool the model chose; TOOL_FUNCTIONS[block.name] is the What is a tool? lookup turning that name into the real function.
  • block.input is the arguments the model filled in, shaped by the schema.
  • Each result carries a tool_use_id that matches the request — that's how the model knows which request this answer is for.

Closing the loop

The results go back in as the next turn, and we go around again — now with more information than before:

conversation.append({"role": "user", "content": tool_results})

Wrap all of that in for round_number in range(1, MAX_ROUNDS + 1): and you have the complete harness. The loop runs until the model returns a final answer, or until it hits MAX_ROUNDS — whichever comes first.

Watching it run

With ANTHROPIC_API_KEY set, uv run shopbot.py prints each trip around the loop. It looks like this:

--- Round 1: asking the model ---
[harness] running look_up_orders({'customer': 'current customer'})

--- Round 2: asking the model ---
[harness] running get_tracking_status({'tracking_number': '1Z999'})

--- Round 3: asking the model ---

=== ShopBot's final answer ===
Your running shoes are out for delivery — they arrive today!

That's the exact three-round walkthrough we traced by hand in the harness loop — look up the order, check tracking, answer — except now a real model is choosing each step and a real harness is carrying it out.

Run it yourself

The full code is in the agent-harness/ folder (shopbot.py + tools.py). Set ANTHROPIC_API_KEY, then uv run shopbot.py. Try editing the mock data in tools.py and watch the final answer change.

Recap

  • A real ask_model is one client.messages.create(...) call, passed both the messages (the conversation) and the tools (the schemas from What is a tool?).
  • The reply is a list of blocks; stop_reason == "tool_use" means the model wants a tool, anything else means it gave a final answer.
  • For each tool_use block, the harness maps the name → function, runs it with the model's arguments, and sends back a tool_result carrying the matching tool_use_id.
  • Loop until a final answer or MAX_ROUNDS. That's a working agent harness.

You've now built the working loop. But run it and ask "where's my order?" and the model will ask you who you are — because we never told it. Next we give ShopBot a role and context with a system prompt.