What Are AI Agents? Loops, Tools, and Guardrails
An AI agent is a program where an LLM sits in a loop: it observes the current state, decides what action to take, executes that action using a tool, observes the result, and repeats until the task is done or a stop condition is met. The "agent" part is the loop and the decision-making, not the LLM itself. Without the loop, tools, and stop conditions, you just have a chatbot.
Cutting through the hype
The word "agent" gets thrown around to mean everything from a chatbot with a system prompt to a fully autonomous software engineer. Strip away the marketing, and the core pattern is simple:
- The LLM receives a goal or task.
- It looks at the current state (conversation history, tool outputs, environment).
- It decides what to do next: call a tool, ask the user a question, or declare the task done.
- If it called a tool, the result is added to the context, and the loop repeats from step 2.
That is it. An agent is a while loop with an LLM as the decision engine and tools as the actions. The power comes from the LLM's ability to reason about which tool to use next and when to stop.
Anatomy of an agent loop
Here is the minimal agent loop in TypeScript:
import OpenAI from 'openai';
const openai = new OpenAI();
interface Tool {
name: string;
description: string;
parameters: Record<string, unknown>;
execute: (args: Record<string, string>) => Promise<string>;
}
async function runAgent(
goal: string,
tools: Tool[],
maxSteps: number = 10
): Promise<string> {
const messages: OpenAI.ChatCompletionMessageParam[] = [
{
role: 'system',
content: 'You are a helpful agent. Use tools to accomplish the goal. ' +
'When the task is complete, respond with your final answer.',
},
{ role: 'user', content: goal },
];
const toolDefs: OpenAI.ChatCompletionTool[] = tools.map(t => ({
type: 'function',
function: {
name: t.name,
description: t.description,
parameters: t.parameters,
},
}));
for (let step = 0; step < maxSteps; step++) {
const response = await openai.chat.completions.create({
model: 'gpt-4o',
messages,
tools: toolDefs,
});
const message = response.choices[0].message;
messages.push(message);
// If no tool calls, the agent is done
if (!message.tool_calls || message.tool_calls.length === 0) {
return message.content ?? 'Task complete.';
}
// Execute each tool call
for (const toolCall of message.tool_calls) {
const tool = tools.find(t => t.name === toolCall.function.name);
const args = JSON.parse(toolCall.function.arguments);
const result = tool
? await tool.execute(args)
: 'Error: tool not found';
messages.push({
role: 'tool',
tool_call_id: toolCall.id,
content: result,
});
}
}
return 'Agent stopped: maximum steps reached.';
}
The maxSteps parameter is a guardrail. Without it, a confused agent could loop forever, burning through your API budget.
Tools are what make agents useful
The LLM provides the reasoning. Tools provide the capabilities. An agent without tools is just a chatbot with extra steps.
Common tool categories:
- Data retrieval: search a database, query an API, read a file
- Data mutation: create a record, send an email, update a spreadsheet
- Computation: run a calculation, execute code, parse a document
- Communication: send a Slack message, post to a webhook
The tools you expose define what the agent can do. An agent with read-only database tools can answer questions. An agent with write tools can take actions. Be deliberate about which capabilities you give it.
Example tools for a Kenyan e-commerce agent:
const tools: Tool[] = [
{
name: 'search_products',
description: 'Search the product catalog by keyword',
parameters: {
type: 'object',
properties: { query: { type: 'string' } },
required: ['query'],
},
execute: async (args) => {
const results = await db.products.search(args.query);
return JSON.stringify(results.slice(0, 5));
},
},
{
name: 'check_delivery_status',
description: 'Check delivery status by order number',
parameters: {
type: 'object',
properties: { orderNumber: { type: 'string' } },
required: ['orderNumber'],
},
execute: async (args) => {
const status = await logistics.getStatus(args.orderNumber);
return JSON.stringify(status);
},
},
];Guardrails: preventing agents from going off the rails
An agent without guardrails is a liability. Here are the ones that matter:
- Step limit. Cap the number of loop iterations. If the agent has not solved the task in 10 to 15 steps, it is probably stuck. Stop it and surface what it has so far.
- Budget limit. Track token usage across the loop. Set a maximum spend per agent run. When the limit is reached, stop the loop.
- Tool validation. Validate every argument the model passes to a tool. The model generates arguments from user input, so treat them as untrusted.
- Confirmation gates. For destructive or expensive actions (sending money, deleting records, sending emails), require user confirmation before the tool executes.
- Scope constraints. Tell the model in the system prompt what it should not do. "Never modify production data." "Never send messages without user approval."
- Logging. Log every step: what the model decided, what tool it called, what arguments it used, what the tool returned. This is your debugging trail and your audit log.
// Example: confirmation gate for a payment tool
async function executeWithConfirmation(
toolName: string,
args: Record<string, string>
): Promise<string> {
if (toolName === 'send_payment') {
const approved = await askUserConfirmation(
`Send KES ${args.amount} to ${args.recipient}?`
);
if (!approved) return 'Payment cancelled by user.';
}
return tools[toolName].execute(args);
}Agents vs chains vs simple prompts
Not every AI feature needs an agent. Here is when to use what:
- Simple prompt (no loop, no tools): Use when the model can answer from its training data or from context you provide. Examples: summarizing text, translating, generating a product description.
- Chain (fixed steps, no loop): Use when the task has a predictable sequence of steps. Example: extract entities from text, then look them up in a database, then format a report. Each step is hardcoded; the LLM does not choose what to do next.
- Agent (loop with tools): Use when the number and sequence of steps depends on the data. The LLM decides what to do next based on what it learns at each step. Example: "Research this company and write a summary," where the agent might need to search, read multiple pages, and synthesize.
Start with the simplest approach that works. If a chain handles your use case, do not build an agent. Agents add latency (multiple LLM calls), cost (tokens per step), and complexity (debugging non-deterministic loops). Use them when the flexibility is worth it.
Frequently Asked Questions
- Are AI agents autonomous?
- Only within the boundaries you set. An agent can decide which tools to call and in what order, but it can only use the tools you provide, and you control the stop conditions. True autonomy (an agent that writes its own tools and sets its own goals) is a research topic, not a production pattern.
- What frameworks exist for building agents?
- LangChain, LlamaIndex, CrewAI, and Anthropic's agent SDK are popular options. But the core pattern is simple enough to build without a framework. If you understand the loop (observe, decide, act, repeat), you can write it in plain TypeScript. Use a framework when you need advanced features like multi-agent coordination or complex memory management.
- How much do agents cost to run?
- Each step in the agent loop is at least one LLM API call. A 5-step agent run costs 5x a single prompt. Add tool execution costs (database queries, API calls). For high-traffic applications, agent costs can add up quickly. Set budget limits per run and monitor usage closely.
Ready to build real-world apps?
Join the McTaba Labs full-stack marathon. Ship 8 production apps with M-Pesa, USSD, and WhatsApp integrations, and get career support until placement.
See Programs