Most explanations of AI agents stop at “a model with tools”. That is true, but it doesn't help when your agent forgets a preference, calls the wrong function or loops forever. To debug any of those, you need to know which part is misbehaving.
This post continues my series on building agents. The previous one covered LLMs versus agents.
The four components of an AI agent
An AI agent has four components: a brain (the language model that decides what to do next), memory (the conversation history and stored facts the model can read), tools (functions that let it act outside the model) and an action loop (the code that calls the model, runs the tools it asks for and feeds the results back). The brain is the only part that is a model. The other three are code and data that you write and control.
| Part | What it does | Without it |
|---|---|---|
| Brain | Reads everything and decides the next step | Nothing decides, so you have a fixed script |
| Memory | Holds the conversation and stored facts | The agent forgets between steps or sessions |
| Tools | Let it read or change things outside the model | It can only talk |
| Action loop | Runs the model, executes tools, returns results | One answer, then it stops |
Each part in a few lines
Brain. The language model. Each turn it gets the system prompt, the conversation, the tool list and any tool results, and it returns either text or a request to call a tool. It never runs anything itself.
Memory. Two layers. Short-term memory is the message history you send with every call. Long-term memory is anything written to storage and read back in a later run: a notes file, a database row, a vector store. The model remembers nothing between calls, so both layers are your code's job.
Tools. An ordinary function plus a description: a name, when to use it, and a JSON Schema for the inputs. The model never sees your code, only the description, so write it like API docs for a reader who can't ask questions.
Action loop. Send memory to the brain. Plain text means done. A tool request means run the tool, append the result to memory and go around again. Give the loop exits: a final answer, a step cap, unrecoverable failures, human approval.
All four in one file
This is an expense tracker with a notes file. Install the SDK with npm install @anthropic-ai/sdk, set ANTHROPIC_API_KEY and run npx tsx agent/four-parts.ts.
// agent/four-parts.ts
import Anthropic from '@anthropic-ai/sdk';
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
const client = new Anthropic();
const NOTES_FILE = 'notes.json';
const messages: Anthropic.MessageParam[] = [];
const expenses: { item: string; amount: number }[] = [];
function loadNotes(): string[] {
return existsSync(NOTES_FILE) ? JSON.parse(readFileSync(NOTES_FILE, 'utf8')) : [];
}
const tools: Anthropic.Tool[] = [
{
name: 'add_expense',
description: 'Record one expense. Use it whenever the user says they spent money.',
input_schema: {
type: 'object',
properties: {
item: { type: 'string' },
amount: { type: 'number', description: 'Amount in BDT' },
},
required: ['item', 'amount'],
},
},
{
name: 'get_total',
description: 'Return the exact total of all recorded expenses in BDT.',
input_schema: { type: 'object', properties: {} },
},
{
name: 'save_note',
description: 'Save a lasting fact about the user, such as a preference, for future sessions.',
input_schema: {
type: 'object',
properties: { note: { type: 'string' } },
required: ['note'],
},
},
];
const handlers: Record<string, (input: Record<string, unknown>) => unknown> = {
add_expense: (input) => {
expenses.push({ item: String(input.item), amount: Number(input.amount) });
return { ok: true, recorded: expenses.length };
},
get_total: () => ({ total: expenses.reduce((sum, e) => sum + e.amount, 0) }),
save_note: (input) => {
writeFileSync(NOTES_FILE, JSON.stringify([...loadNotes(), String(input.note)]));
return { ok: true };
},
};
function execute(name: string, input: Record<string, unknown>): unknown {
try {
const handler = handlers[name];
return handler ? handler(input) : { error: 'unknown tool' };
} catch (err) {
return { error: String(err) };
}
}
async function runAgent(userText: string): Promise<string> {
messages.push({ role: 'user', content: userText });
for (let step = 0; step < 6; step++) {
const response = await client.messages.create({
model: 'claude-sonnet-5',
max_tokens: 1024,
system: `You track expenses. Known about the user: ${loadNotes().join('; ') || 'nothing yet'}`,
tools,
messages,
});
messages.push({ role: 'assistant', content: response.content });
if (response.stop_reason !== 'tool_use') {
return response.content.flatMap((b) => (b.type === 'text' ? [b.text] : [])).join('');
}
const results: Anthropic.ToolResultBlockParam[] = [];
for (const block of response.content) {
if (block.type === 'tool_use') {
const output = execute(block.name, block.input as Record<string, unknown>);
results.push({ type: 'tool_result', tool_use_id: block.id, content: JSON.stringify(output) });
}
}
messages.push({ role: 'user', content: results });
}
return 'Stopped: step limit reached';
}
async function main() {
console.log(await runAgent('I spent 250 on lunch and 80 on a rickshaw. I prefer short answers.'));
console.log(await runAgent('What is my total so far?'));
}
main();
The brain is client.messages.create. Memory is messages plus notes.json. The tools are tools, handlers and execute. The action loop is the for block in runAgent.
The mistake that cost me an afternoon
The first version of this tracker forgot everything between messages. I had written const messages = [] inside the function that handled each user message, so every call started with a blank history. I blamed the system prompt and kept rewriting it until I noticed the array was being recreated on every call. Moving one line out of the function fixed it.
Common mistakes
- Vague tool descriptions. The brain picks tools by reading them, so say what the tool does, what it needs and when to use it.
- Sending the whole history forever. Cost and latency climb. Trim or summarize old turns and keep lasting facts in a separate store.
-
Swallowing tool errors. Return the error text as the result, as
executedoes, so the model can try again. -
Making the brain do work that code can do. Summing amounts inside the prompt invites slips. That is why
get_totalis a tool.
Read the full version
I write each post in English and Bangla. The full version, with a diagram, is on my blog: What's Inside an AI Agent?
Which of the four parts is hardest for you to debug?
Top comments (0)