Anthropic, the company that makes Claude, describes agents in its guide Building effective agents (December 2024) as "typically just LLMs using tools based on environmental feedback in a loop." The same guide recommends "finding the simplest solution possible" and warns that frameworks can add layers that "obscure the underlying prompts." It suggests starting with the model's API directly.
So this guide does exactly that. Below is a whole agent with no framework: one model, two tools, a loop that feeds the tools' results back to the model, and two ways to stop. You can read it in a few minutes and run it on your own machine.
Claude and the Anthropic SDK are not made by webridgeAI. We checked the loop on 18 October 2026 with the Anthropic Python SDK (version 1.15) against a simulated API: the tool results go back with the right ids, a path outside the folder is refused, and the step limit stops a model that never finishes. Run it once with your own key to see a live answer.
1. What the agent does
You give it a task in plain words. The model decides whether it needs a tool. If it does, your code runs the tool and hands the result back. The model looks at the result and decides again: another tool, or a final answer. That is the whole loop.
task -> model -> tool call? -> yes -> run the tool -> result back to the model -> (round again)
-> no -> final answer, stopThis agent has two tools: a calculator, because models are better at deciding what to add up than at adding, and read_file, which reads a text file from the folder you run it in. The model is claude-opus-5-5. To spend less per run, change MODEL to claude-sonnet-5-5.
2. The whole agent
"""A tiny working agent: one model, two tools, one loop.
Run: python agent.py "Read prices.txt and add up the prices."
"""
import ast
import operator
import pathlib
import sys
import anthropic
MODEL = "claude-opus-5-5"
MAX_STEPS = 10 # stop condition 1: a hard cap on trips round the loop
FOLDER = pathlib.Path.cwd().resolve() # the only folder the agent may read from
SYSTEM = "You are a careful assistant. Use the calculator for every sum. Read files only when the task needs them."
TOOLS = [
{
"name": "calculator",
"description": "Work out an arithmetic expression using + - * / ** and brackets, for example '(12.5 * 4) / 3'.",
"input_schema": {
"type": "object",
"properties": {"expression": {"type": "string"}},
"required": ["expression"],
"additionalProperties": False,
},
"strict": True,
},
{
"name": "read_file",
"description": "Read a text file from the current folder. Give a path relative to that folder, for example 'notes.txt'.",
"input_schema": {
"type": "object",
"properties": {"path": {"type": "string"}},
"required": ["path"],
"additionalProperties": False,
},
"strict": True,
},
]
# --- the tools: plain Python functions ------------------------------------
OPERATORS = {
ast.Add: operator.add, ast.Sub: operator.sub, ast.Mult: operator.mul,
ast.Div: operator.truediv, ast.Pow: operator.pow, ast.USub: operator.neg, ast.UAdd: operator.pos,
}
def calculator(expression):
"""Arithmetic only. Never eval() text that a model wrote."""
def walk(node):
if isinstance(node, ast.Constant) and isinstance(node.value, (int, float)):
return node.value
if isinstance(node, ast.BinOp) and type(node.op) in OPERATORS:
return OPERATORS[type(node.op)](walk(node.left), walk(node.right))
if isinstance(node, ast.UnaryOp) and type(node.op) in OPERATORS:
return OPERATORS[type(node.op)](walk(node.operand))
raise ValueError("only numbers and + - * / ** are allowed")
return str(walk(ast.parse(expression, mode="eval").body))
def read_file(path):
"""A permission check: refuse anything outside FOLDER."""
target = (FOLDER / path).resolve()
if not target.is_relative_to(FOLDER):
raise PermissionError(f"{path} is outside the folder this agent may read")
return target.read_text()[:20_000]
def run_tool(name, tool_input):
"""Run one tool call. Return (text for the model, whether it failed)."""
try:
if name == "calculator":
return calculator(tool_input["expression"]), False
if name == "read_file":
return read_file(tool_input["path"]), False
return f"unknown tool: {name}", True
except Exception as error: # the model sees the error and can try something else
return f"error: {error}", True
# --- the loop ---------------------------------------------------------------
def run_agent(task, client=None):
client = client or anthropic.Anthropic()
messages = [{"role": "user", "content": task}]
for step in range(1, MAX_STEPS + 1):
# 1. Ask the model what to do next, given everything so far.
response = client.messages.create(
model=MODEL, max_tokens=16000, system=SYSTEM, tools=TOOLS, messages=messages,
)
messages.append({"role": "assistant", "content": response.content})
# 2. Stop condition 2: the model has stopped asking for tools, so it is done.
if response.stop_reason != "tool_use":
answer = "".join(block.text for block in response.content if block.type == "text")
return answer or f"(stopped: {response.stop_reason})"
# 3. Run every tool it asked for.
results = []
for block in response.content:
if block.type == "tool_use":
output, failed = run_tool(block.name, block.input)
print(f" step {step}: {block.name}({block.input}) -> {output[:60]!r}")
results.append({
"type": "tool_result",
"tool_use_id": block.id, # ties the result to the request
"content": output,
"is_error": failed,
})
# 4. Feed the results back in, and go round again.
messages.append({"role": "user", "content": results})
return f"(stopped: hit the {MAX_STEPS}-step limit without a final answer)"
if __name__ == "__main__":
print(run_agent(" ".join(sys.argv[1:]) or "What is 17.5% of 2,340?"))3. The loop, step by step
Everything that makes it an agent is in run_agent, about 35 lines.
- Ask the model. Each trip round the loop sends the whole conversation so far (the task, every tool call, every result) along with the list of tools. The model has no memory between calls; the
messageslist is its memory. - Check whether it is done. The answer comes back with a
stop_reason. If it istool_use, the model wants a tool. Anything else (end_turn, or a cut-off such asmax_tokens) means this turn is over, so the loop returns what the model wrote. - Run the tools. The reply can ask for more than one tool at once. The model never runs anything itself: it only writes a request (a tool name and its input), and your code decides whether and how to run it.
- Feed the results back. Every result goes back in one message, each tagged with the
tool_use_idof the request it answers. If a tool failed, the error goes back too, markedis_error, so the model can try another way instead of the program crashing.
The tool definitions at the top (TOOLS) are what the model reads to decide which tool to use. Their description lines matter more than they look: a vague description gets a tool used at the wrong time. strict: True makes the model's input match the schema exactly.
4. Two ways to stop
A loop needs a way out. This one has two.
- The model stops asking for tools. That is the normal ending: it has what it needs and writes the answer.
- A hard cap,
MAX_STEPS = 10. If the model keeps calling tools without finishing, the loop gives up after ten trips. Anthropic's guide calls this out: "it's also common to include stopping conditions (such as a maximum number of iterations) to maintain control." Without it, a confused agent can loop until it runs up a bill.
5. What goes around the loop
The loop is the easy part. Whether an agent is safe to use depends on what you put around it. This small one shows three of those pieces.
What it is allowed to touch. The model asks; your code decides. read_file resolves the path and refuses anything outside the folder you started in, so a request for ../../somewhere-else comes back as an error, not a file. The calculator parses arithmetic only and never runs eval() on text a model wrote. Give an agent only the tools the task needs, and limit each tool to what it should reach. A tool that can delete, send or pay needs a person to approve it first (our guide A person in the loop covers how to design that step).
How it checks its work. Each tool result is real information from outside the model: the actual file, the actual sum. That is the "environmental feedback" in Anthropic's definition, and it is why an agent can correct itself. Return errors as results rather than hiding them, so the model sees what went wrong. Anthropic recommends testing agents in sandboxed environments before trusting them with anything real.
When it stops. The step cap above is the bluntest version. Real agents also stop to ask a person at checkpoints, or when they hit something they can't resolve, and they stop when a time or cost budget runs out.
6. Things to try
- Ask it something that needs no tool ("What is an agent?"). It should answer straight away, in one step.
- Ask for a file that doesn't exist. Watch the error go back and the model explain it.
- Lower
MAX_STEPSto 1 and give it the prices task. It stops before it can finish. - Add a third tool, for example one that lists the files in the folder. Write a function, add its definition to
TOOLS, and add one line torun_tool. The loop doesn't change.
7. Where to go next
- Building effective agents, Anthropic. The source of the definition above, and the patterns to use before you need a full agent.
- How to implement tool use, Claude documentation. Tool definitions, parallel calls and error results in detail, and the SDK's tool runner, which writes this loop for you once you understand it.
- Claude Agent SDK. When you want built-in tools such as file editing and a shell, with permissions, instead of writing every tool yourself.