Building an AI agent from scratch: the tool-calling loop
Published
Short answer
The core of an agent is a small loop: send the messages and the tool definitions to the model, run a tool in your own code if the model asks for one, append the result to the messages, and repeat until the model stops asking for tools. Keep the number of tools low, their names clear and their outputs short, and always put a step limit on the loop.
The core loop
The skeleton below adapts to any model provider. Replace the model.generate call with your provider's SDK; the shape of a tool call differs slightly between providers.
type ToolCall = { id: string; name: string; args: unknown };
type Message =
| { role: "user" | "assistant"; content: string; toolCalls?: ToolCall[] }
| { role: "tool"; toolCallId: string; content: string };
type Tools = Record<string, (args: unknown) => Promise<string>>;
const MAX_STEPS = 8;
export async function runAgent(goal: string, tools: Tools): Promise<string> {
const messages: Message[] = [{ role: "user", content: goal }];
for (let step = 0; step < MAX_STEPS; step++) {
const reply = await model.generate({ messages, tools: describe(tools) });
messages.push(reply);
if (!reply.toolCalls?.length) return reply.content;
for (const call of reply.toolCalls) {
const result = await runTool(tools, call);
messages.push({ role: "tool", toolCallId: call.id, content: result });
}
}
throw new Error("Agent did not finish within the step limit");
}Designing good tools
- Give it few tools. Too many make it harder for the model to pick the right one.
- Make the tool name and description say clearly what it does and when to use it.
- Define inputs with a schema (for example zod) and validate them before running.
- Keep outputs short. Long outputs fill the context and raise cost; summarize or trim if needed.
- Separate read-only tools from tools that change data.
Give errors back to the model
If a tool fails, don't crash the loop. Return the error message to the model as the tool result; it usually fixes its input and tries again. Still put a timeout on every tool, and catch the same error repeating so you can end the loop.
Limits and safety
- Step limit: the loop must not run forever.
- Permissions: each tool runs with the least access it needs.
- Approval: put irreversible actions such as sending email, paying or deleting behind human approval.
- Outside content is untrusted. Text from a web page or an email can try to act like an instruction.
Testing and monitoring
Log every step: the model's request, the tool called, its input, its output and its duration. Build a small evaluation set from real tasks, and compare results against it whenever you change the prompt or a tool. Agents can get worse silently; you won't notice what you don't measure.
Questions
- Do I need a framework to build an agent?
- No. The core loop is a few dozen lines. Starting with a plain loop and adding a framework only when you really need one usually gives clearer results.
- How do I control the cost of an agent?
- Set a step limit, keep tool outputs short, and consider a smaller model for simple steps. Record the cost of every run.
- What should I do when a tool fails?
- Return the error to the model as the tool result and set a retry limit. The model usually corrects its input and tries again.
Written by Mert Yılmaz, freelance fullstack developer.