Chapter 5 · Part 2
Keep it on track
A chatbot only produces text. An agent acts — it runs your functions, which can change files, send messages, or spend money. That power is the point, and also the risk. Three guardrails turn a toy into something you'd actually trust to run.
1. Gate anything hard to undo
The model decides which tool to call; your code decides whether to let it. The useful test is reversibility. Reading a file is safe — let it run freely. Deleting one, sending an email, or charging a card is not — put a human in the loop.
You gate right inside the tool function: ask, and refuse if the answer's no. The agent simply sees the refusal and adapts.
def delete_file(path):
ok = input(f"Agent wants to delete {path!r}. Allow? [y/N] ")
if ok.lower() != "y":
return "The user declined to delete the file." # the agent reads this and moves on
os.remove(path)
return f"Deleted {path}."You could give the agent one all-powerful run_shell_command tool. But then every action is an
opaque command string — you can't tell a harmless ls from an rm -rf. Specific tools
(read_file, delete_file) give your code a place to check, confirm, and log each action. Start
broad if you must; gate the moment an action can't be taken back.
2. Let tools fail without crashing
Tools touch the real world, so they will fail — a missing file, a timeout, a bad argument. If
your code throws, the whole agent dies. Instead, hand the error back to the model as a
result flagged is_error, and it will typically apologize, adjust, and try another way:
for block in reply.content:
if block.type == "tool_use":
try:
result = TOOL_FUNCTIONS[block.name](**block.input)
is_error = False
except Exception as e:
result, is_error = f"Error: {e}", True # tell the agent, don't crash
tool_results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": result,
"is_error": is_error,
})3. Cap the loop
Your while True trusts the model to stop. Almost always it does — but a confused agent can get
stuck calling tools forever, and every pass costs a call. Give it a hard ceiling:
for step in range(10): # never more than 10 tool-using turns
reply = client.messages.create(model="claude-opus-4-8", max_tokens=1024,
tools=tools, messages=messages)
messages.append({"role": "assistant", "content": reply.content})
if reply.stop_reason != "tool_use":
print(reply.content[0].text)
break
# ... run tools, append results ...
else:
print("Stopped: agent hit the 10-step limit.")An agent runs on its own, so it fails on its own too. A gate on risky actions, error results
instead of crashes, and a loop cap are the minimum to run one unattended. The tool runner from
the last chapter honours the same ideas — you gate inside the tool function, and pass
max_iterations to bound the loop.
Your agent is capable and safe to run. Last chapter: what it costs, which model to use, and where to take it next.