An AI agent is a program that uses a language model to decide what to do next, executes an action, observes the result, and repeats until the task is complete. That loop — decide, act, observe — is what separates agents from simple chat interfaces. The model isn’t just answering a question; it’s running a process.
Agents sound complex, but the core mechanics are straightforward enough to build from scratch in an afternoon. This guide walks through building a working research agent in plain JavaScript. No framework, no SDK, just the OpenAI API and a loop.
The Core Loop
Every agent — regardless of the framework or model behind it — runs the same basic cycle:
- The model receives a task and a list of available tools (functions it can call).
- The model decides whether to call a tool or produce a final answer.
- If it calls a tool, the agent executes the function and appends the result to the conversation.
- The model sees the result and decides again. Repeat until done.
This is called a ReAct loop (Reason + Act). The model reasons about what to do, acts by calling a tool, and uses the observation to reason about the next step.
Defining Tools
Tools are plain functions with a JSON Schema description the model uses to understand what they do and what arguments they expect. Here are three simple tools for a research agent:
const tools = [
{
type: "function",
function: {
name: "search_web",
description: "Search the web for a query and return a summary of results.",
parameters: {
type: "object",
properties: {
query: { type: "string", description: "The search query" }
},
required: ["query"]
}
}
},
{
type: "function",
function: {
name: "read_url",
description: "Fetch and return the text content of a URL.",
parameters: {
type: "object",
properties: {
url: { type: "string", description: "The URL to fetch" }
},
required: ["url"]
}
}
},
{
type: "function",
function: {
name: "write_report",
description: "Write a final report summarising everything found. Call this when done.",
parameters: {
type: "object",
properties: {
content: { type: "string", description: "The report text" }
},
required: ["content"]
}
}
}
];
The Tool Executor
You need a function that takes the model’s tool call, runs the actual implementation, and returns the result. In a real agent these functions hit real APIs; for learning purposes, stub them:
async function executeTool(name, args) {
switch (name) {
case "search_web":
// Real: call a search API (Brave, Serper, Tavily, etc.)
return `Search results for "${args.query}": [result 1] [result 2]`;
case "read_url":
// Real: fetch and extract text from the URL
const res = await fetch(args.url);
const html = await res.text();
return html.slice(0, 3000); // truncate for context window
case "write_report":
return args.content; // the agent is done when this is called
}
}
The Agent Loop
Here is the complete agent loop. It sends messages to the model, checks whether the model wants to call a tool or finish, and repeats:
async function runAgent(task) {
const messages = [
{ role: "system", content: "You are a research assistant. Use tools to gather information, then write a report." },
{ role: "user", content: task }
];
while (true) {
const response = await fetch("https://api.openai.com/v1/chat/completions", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${API_KEY}`
},
body: JSON.stringify({
model: "gpt-4o",
messages,
tools,
tool_choice: "auto"
})
});
const data = await response.json();
const message = data.choices[0].message;
messages.push(message);
// No tool call: the model produced a final answer
if (!message.tool_calls) {
return message.content;
}
// Execute each tool call and append results
for (const call of message.tool_calls) {
const args = JSON.parse(call.function.arguments);
const result = await executeTool(call.function.name, args);
// If the model called write_report, we're done
if (call.function.name === "write_report") {
return result;
}
messages.push({
role: "tool",
tool_call_id: call.id,
content: String(result)
});
}
}
}
That’s the full agent. Call it with a task:
const report = await runAgent(
"Research the latest developments in browser-native AI inference and write a two-paragraph summary."
);
console.log(report);
What Makes a Good Tool Set
The quality of your agent is largely determined by the quality of its tools. A few design principles that matter:
- Tools should be atomic. Each tool does one thing. “Search and summarize” should be two tools, not one — the model might want to search, read a specific result, then decide whether to search again.
- Return structured output where possible. If you return raw HTML the model has to parse, you’re wasting context window. Strip HTML to text, truncate aggressively, and return only what’s useful.
- Give the model a stop tool.
write_reportin the example above is the termination signal. Without it, the model might loop indefinitely or end mid-thought. Always give the agent a clean way to declare it’s finished. - Cap iterations. Add a loop counter. If the agent makes more than 10–15 tool calls without finishing, something went wrong. Break the loop and return what you have.
Adding Memory
The agent above has no memory between runs. Each call to runAgent starts fresh. For tasks that span multiple sessions or require the agent to remember past actions, you need to persist the message history somewhere — a file, IndexedDB, or a vector store for semantic recall.
The simplest form of agent memory is just serializing the messages array to JSON and storing it. On the next run, deserialize and pass it back in as the starting context. For longer histories, trim old messages or summarize them to stay within the context window.
Beyond the Single Agent
Once you can build one agent, the next step is multi-agent architectures: an orchestrator agent that breaks a complex task into sub-tasks, spawns specialist agents (a researcher, a writer, a code reviewer), and aggregates their outputs. Each sub-agent has its own tool set scoped to its role. The orchestrator’s tools are just function calls to the sub-agents.
The same loop, nested. That is all multi-agent systems are at the core — loops calling loops, each with a different system prompt and tool set.
The primitives are simple enough to build yourself. Understanding them at this level makes every agentic framework — LangChain, Autogen, CrewAI — legible and debuggable. You know what the framework is doing because you’ve written it by hand.