AI agents are everywhere in conversation right now, but most tutorials either hand-wave the implementation or bury you in a framework before you understand what's actually happening. This post builds a real agent from scratch β no LangChain, no abstractions β so you can see exactly how the loop works.
What is an AI agent, actually?
An AI agent is a loop. At each step, the model looks at the current state (conversation history + available tools), decides what to do next, and either calls a tool or returns a final answer. That's it.
User message β Model β Tool call OR final answer
β β
Tool result ββββββ
The model doesn't "think" between steps β you send a message, get a response, execute any tool it asked for, feed the result back, repeat.
Step 1: Define your tools
Tools are just functions with a JSON Schema description. The model reads the description and decides when to call them.
const tools = [
{
type: "function",
function: {
name: "search_web",
description: "Search the web for current information on a topic",
parameters: {
type: "object",
properties: {
query: {
type: "string",
description: "The search query"
}
},
required: ["query"]
}
}
},
{
type: "function",
function: {
name: "read_file",
description: "Read the contents of a local file",
parameters: {
type: "object",
properties: {
path: {
type: "string",
description: "Absolute path to the file"
}
},
required: ["path"]
}
}
}
]
Step 2: Implement the tool functions
These are the actual implementations the agent will call. Keep them simple and focused.
import { readFileSync } from 'fs'
async function executeTool(name: string, args: Record<string, string>) {
switch (name) {
case "search_web":
// Replace with your actual search API (Brave, Serper, Tavily, etc.)
const results = await searchAPI(args.query)
return JSON.stringify(results.slice(0, 3))
case "read_file":
try {
return readFileSync(args.path, 'utf-8')
} catch (e) {
return `Error reading file: ${e}`
}
default:
return `Unknown tool: ${name}`
}
}
Step 3: Write the agent loop
This is the core. Send messages, handle tool calls, repeat until the model gives a final answer.
import OpenAI from 'openai'
const client = new OpenAI({ apiKey: process.env.OPENAI_API_KEY })
async function runAgent(userMessage: string): Promise<string> {
const messages: OpenAI.ChatCompletionMessageParam[] = [
{
role: "system",
content: "You are a helpful assistant with access to tools. Use them when needed."
},
{
role: "user",
content: userMessage
}
]
// Agent loop β runs until model gives a final text response
while (true) {
const response = await client.chat.completions.create({
model: "gpt-4o",
messages,
tools,
tool_choice: "auto"
})
const message = response.choices[0].message
messages.push(message)
// No tool calls = final answer
if (!message.tool_calls || message.tool_calls.length === 0) {
return message.content ?? ""
}
// Execute each tool call and feed results back
for (const toolCall of message.tool_calls) {
const args = JSON.parse(toolCall.function.arguments)
const result = await executeTool(toolCall.function.name, args)
messages.push({
role: "tool",
tool_call_id: toolCall.id,
content: result
})
}
}
}
// Use it
const answer = await runAgent("What's the weather in Vancouver today and what should I wear?")
console.log(answer)
Step 4: Add a safety limit
Agents can loop forever if something goes wrong. Always add a max-steps guard.
async function runAgent(userMessage: string, maxSteps = 10): Promise<string> {
let steps = 0
// ... same setup ...
while (steps < maxSteps) {
steps++
const response = await client.chat.completions.create({ ... })
if (!message.tool_calls?.length) {
return message.content ?? ""
}
// execute tools...
}
return "Agent reached maximum steps without completing the task."
}
What makes a good agent tool?
- Focused β one thing, done well. Don't build a
do_everythingtool. - Descriptive β the description is what the model reads. Be specific about when to use it and what it returns.
- Error-safe β always return a string, even on failure. The model needs to read the error and recover.
- Fast β slow tools kill the agent experience. Cache aggressively.
Where to go from here
Once this pattern is solid, the interesting work is in:
- Memory β storing past conversations and retrieving relevant ones (RAG over your own history)
- Planning β having the model write a plan before executing it, rather than just reacting
- Parallelism β running multiple tool calls in parallel with
Promise.all - Streaming β streaming the final response back to the user so they see progress
The agent loop itself stays the same regardless of how complex your tools get. That's the insight β complexity lives in the tools, not the loop.
Building an AI agent for your product? Talk to us β we've shipped production agent systems across several client products.