Skip to content

Chapter 13 of 14 · Build agents · 5 min read

Build a simple agent with tools

AI Tool Garage editors·

You’ve used agents and taught them with skills. Now it’s time to see how small the core of an agent really is. In this chapter you’ll build one in outline: a research helper that can search notes, read a file and save a summary. The code is deliberately SDK-agnostic pseudo-code, close to Python, because every model provider names things slightly differently. Once the shape is clear, mapping it onto a real SDK is a matter of reading its tool-use documentation.

What we’re building

  • A model that receives a task, such as “Summarise what our notes say about pricing”.
  • Three tools: search_notes, read_note and save_summary.
  • A loop that runs tool calls, returns results and stops when the model is done or a limit is hit.
  • A human approval step before anything is written.

Step 1: define the tools

Each tool needs a name, a description the model will read, an input schema and a real function behind it. Write descriptions as carefully as skill descriptions: they’re how the model decides which tool to use.

Tool definitions (pseudo-code)

TOOLS = [ { "name": "search_notes", "description": "Search the notes folder for a keyword or phrase. " "Returns up to 10 matching file names with a one-line snippet each. " "Use this before read_note to find relevant files.", "input_schema": {"query": "string, required"}, }, { "name": "read_note", "description": "Read the full text of one note by file name, as returned by search_notes.", "input_schema": {"name": "string, required"}, }, { "name": "save_summary", "description": "Save a finished summary as a new note. Only call this once, at the end.", "input_schema": {"title": "string, required", "text": "string, required"}, }, ] NOTES_DIR = "./notes" def search_notes(query): hits = simple_text_search(NOTES_DIR, query, limit=10) return [{"name": h.name, "snippet": h.first_matching_line} for h in hits] def read_note(name): path = safe_join(NOTES_DIR, name) # refuse paths outside NOTES_DIR return truncate(read_text(path), max_chars=20_000) def save_summary(title, text): path = safe_join(NOTES_DIR, slugify(title) + ".md") write_text(path, text) return {"saved": path} HANDLERS = {"search_notes": search_notes, "read_note": read_note, "save_summary": save_summary}

Notice the small defences built into the tools themselves: safe_join refuses paths outside the notes folder, and truncate stops one huge file from flooding the context. Guardrails in tool code are more reliable than instructions asking the model to behave.

Step 2: write the loop

The loop sends the conversation and the tool list to the model, inspects the reply, runs any requested tools, appends the results and repeats. Real SDKs differ in names and message formats, but all of them follow this shape.

The agent loop (pseudo-code)

SYSTEM = """You are a research assistant for a folder of notes. Search before you read. Quote notes when you rely on them. When you have enough, write a concise summary and save it with save_summary. If the notes don't answer the question, say so instead of guessing.""" NEEDS_APPROVAL = {"save_summary"} MAX_STEPS = 15 def run_agent(task): messages = [{"role": "user", "content": task}] for step in range(MAX_STEPS): reply = model.generate(system=SYSTEM, messages=messages, tools=TOOLS) messages.append(reply.as_message()) if not reply.tool_calls: # no tool requested: we're done return reply.text results = [] for call in reply.tool_calls: results.append(execute(call)) messages.append(tool_results_message(results)) return "Stopped: reached the step limit before finishing." def execute(call): if call.name not in HANDLERS: return error_result(call, f"Unknown tool: {call.name}") if call.name in NEEDS_APPROVAL and not ask_user_to_approve(call): return error_result(call, "The user declined this action.") try: output = HANDLERS[call.name](**call.input) return ok_result(call, output) except Exception as e: return error_result(call, f"Tool failed: {e}")

Step 3: walk through a run

StepModel doesProgram does
1Requests search_notes("pricing")Runs the search, returns five file names with snippets
2Requests read_note for the two most relevant filesReads both, truncating if needed, and returns the text
3Requests save_summary with a title and textAsks the user to approve; on yes, writes the file
4Replies with a short message and no tool callsLoop ends and the reply is shown to the user

Two details in that run are worth noticing. The model chose to search before reading because the tool descriptions and the system prompt told it to; nothing in the loop forces that order. And the loop never needed to know what the task was about. The same dozen lines could drive a coding agent or a support agent with a different set of tools and instructions.

Design choices that matter

Errors are results, not crashes

When a tool fails, return a clear error message to the model instead of stopping the program. Models are good at recovering, for example by trying a different file name, if they can see what went wrong.

Limits keep runs bounded

A step limit stops a confused agent from looping forever. In production you’d add limits on total time, tokens and spend, and log every step so you can see what happened.

Approval for side effects

Reading is low risk; writing, sending and deleting are not. Gating actions with side effects behind human approval is one of the simplest and most effective safety measures. Chapter 14 goes further.

Keep tool output small

Every tool result goes into the context and is resent on every later step. Return what the model needs, such as snippets, summaries or the relevant part of a file, rather than everything you have.

Adding skills and MCP

This simple loop can grow without changing shape. Instead of writing every tool yourself, you can connect MCP servers and add their tools to the list. Instead of a long system prompt, you can give the agent skills: put the skill descriptions in the system prompt and give the model a tool to read a skill’s files when one is relevant. Agent frameworks, including the Claude Agent SDK, provide this loop, tool handling, permissions and skill loading ready-made, so for real projects you’ll usually start from one rather than writing the loop by hand. Building it once yourself is still the best way to understand what those frameworks do.

Key takeaways

  • An agent is a loop: call the model, run requested tools, append results, repeat.
  • Tool descriptions guide the model; guardrails in tool code protect you.
  • Return errors to the model, cap steps and cost, and gate side effects behind approval.
  • Frameworks package this loop; skills and MCP plug into it without changing its shape.

Put it into practice

New agent skills, repos and AI tools — every week

The best new skills and open-source projects, the fastest-growing repos, and one free prompt. Five minutes to read. Unsubscribe anytime.

Written by the AI Tool Garage editors. Tool details link to their catalogue pages, which are refreshed weekly; spotted something out of date? Tell us.