Anatomy of an Agent for Multi-Agent Work: Prompt, Tools, Loop, Memory
Every agent in a multi-agent system is built from the same four parts: a system prompt that defines its job, a set of tools it may call, a loop that keeps going until the job is done, and some memory of what has happened so far. If you can wrap these four parts in one small class, you can build teams by composing instances of it.
The four parts
System prompt
The prompt states the role, the goal, the output format and the rules. In a team, it should also say who the agent works for and what it should return. Keep it focused on one job.
Tools
Tools are functions the model can request. Each has a name, a description and a JSON schema for its input. A worker might have a search tool; a reviewer might have none. Fewer tools mean fewer wrong choices.
Loop
The loop calls the model, runs any requested tools, appends the results, and repeats until the model returns a final answer without a tool request. Always add a maximum number of turns.
Memory
Within a run, memory is the message list. Across runs it might be a database. For subagents, the message list is usually discarded after the run, and only the final answer is passed back.
A reusable Agent class
The code below implements all four parts with the Anthropic SDK. The tools argument maps tool names to Python functions plus their schemas. The loop checks stop_reason for tool_use.
import json
import anthropic
client = anthropic.Anthropic()
class Agent:
def __init__(self, name, system, tools=None, model="claude-sonnet-5-5", max_turns=8):
self.name = name
self.system = system
self.tools = tools or {} # {"tool_name": (schema_dict, python_function)}
self.model = model
self.max_turns = max_turns
def run(self, task: str) -> str:
messages = [{"role": "user", "content": task}]
schemas = [schema for schema, _ in self.tools.values()]
for _ in range(self.max_turns):
kwargs = dict(model=self.model, max_tokens=4000,
system=self.system, messages=messages)
if schemas:
kwargs["tools"] = schemas
response = client.messages.create(**kwargs)
messages.append({"role": "assistant", "content": response.content})
if response.stop_reason != "tool_use":
return "".join(b.text for b in response.content if b.type == "text")
results = []
for block in response.content:
if block.type == "tool_use":
_, fn = self.tools[block.name]
try:
output = str(fn(**block.input))
is_error = False
except Exception as exc:
output, is_error = f"Tool error: {exc}", True
results.append({"type": "tool_result",
"tool_use_id": block.id,
"content": output,
"is_error": is_error})
messages.append({"role": "user", "content": results})
return "Stopped: reached the turn limit."
add_schema = {
"name": "add",
"description": "Add two numbers.",
"input_schema": {
"type": "object",
"properties": {"a": {"type": "number"}, "b": {"type": "number"}},
"required": ["a", "b"],
},
}
calculator = Agent("calc", "You do arithmetic. Always use the add tool.",
tools={"add": (add_schema, lambda a, b: a + b)})
print(calculator.run("What is 1234 + 8766?"))
Running this should print an answer that contains 10000. The model requests the add tool, your function runs, the result goes back as a tool_result, and the model writes the final reply.
Why a class matters for teams
Once an agent is an object with a run(task) method that takes a string and returns a string, every multi-agent pattern in this course becomes ordinary code: call one agent, pass the result to another, run several in threads, wrap one inside a tool. The class hides the model details, so you can change the model or add logging in one place.
Design notes
- Give each agent a unique
name. You will need it for logs and traces. - Return strings or small structured objects from
run. Return values are your inter-agent protocol. - Return tool errors to the model with
is_errorso it can recover instead of crashing the loop. - Keep the turn limit low for subagents. A worker that needs thirty turns is probably doing too much.
Later posts reuse this shape. If you prefer a framework, the same ideas map onto the Agent Frameworks topic.
Stateless calls, stateful agents
The Messages API is stateless. The model remembers nothing between calls; the only memory is the list of messages you send. This is why the messages list is the heart of the class above, and why a subagent's memory disappears when run returns. That is a feature in multi-agent work: a fresh subagent starts clean, with no leftover assumptions from earlier tasks. If you do want an agent to remember something across runs, you must store it yourself and put it back into the prompt.
What you should log per agent
Even a tiny agent deserves a few log lines, because in a team you will need to know which agent did what. At minimum record the agent name, the task text, the number of turns used, the tools called, the token usage from response.usage, and the final output. Add a timestamp and a run id when you start composing agents. These records are the raw material for tracing, cost accounting and evaluation in later posts.
Exercise
Extend the class so that run returns an object with the answer, the number of turns and the total input and output tokens. Then create two agents with different system prompts and compare their token use on the same task. You will start to see how prompt and tool choices change cost, which matters when you multiply by many agents.
Comments
No comments yet. Have a question or a tip? Be the first to share it.