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.

a tool that asks first
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}."
📌This is why you write specific tools

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:

agent.py — catch and report
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:

agent.py — a ceiling on steps
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.")
⚠️These aren't optional in production

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.