The AI landscape has shifted dramatically. We've moved beyond simple prompt-response interactions into the era of AI agents—autonomous systems that can reason, plan, and execute complex multi-step tasks. If you've tried to build one yourself, you've probably discovered it's not as simple as calling an API. There are loops to manage, tools to define, errors to handle, and architectural decisions that can make or break your agent's reliability.
This guide takes you from zero to production-ready AI agents. We'll cover the fundamental concepts, implement working code in TypeScript, and explore the patterns that separate toy demos from robust systems.
What Makes an AI Agent Different from a Chatbot?
A chatbot responds. An agent acts.
The key distinction lies in the agentic loop—the ability to:
- Observe the current state or user request
- Think about what action to take
- Act by calling external tools or APIs
- Reflect on the result and decide the next step
- Repeat until the task is complete
This loop is what enables an AI agent to perform tasks like "research competitors and create a summary report" or "find flights under $500 and book the cheapest one." The agent doesn't just generate text—it orchestrates a series of actions to achieve a goal.
┌─────────────────────────────────────────────────────┐
│ AGENTIC LOOP │
├─────────────────────────────────────────────────────┤
│ │
│ ┌──────────┐ │
│ │ OBSERVE │ ◄── User Request / Environment │
│ └────┬─────┘ │
│ │ │
│ ▼ │
│ ┌──────────┐ │
│ │ THINK │ ◄── LLM Reasoning │
│ └────┬─────┘ │
│ │ │
│ ▼ │
│ ┌──────────┐ │
│ │ ACT │ ◄── Tool Execution │
│ └────┬─────┘ │
│ │ │
│ ▼ │
│ ┌──────────┐ │
│ │ REFLECT │ ◄── Evaluate Result │
│ └────┬─────┘ │
│ │ │
│ ▼ │
│ ┌──────────────┐ │
│ │ Task Done? │──── Yes ──► Return Result │
│ └──────┬───────┘ │
│ │ No │
│ └────────────► Loop Back to OBSERVE │
│ │
└─────────────────────────────────────────────────────┘
Understanding Function Calling: The Foundation of Tool Use
Function calling (also called "tool use") is the mechanism that allows LLMs to request the execution of external functions. Instead of generating a text response, the model outputs a structured request to call a specific function with specific arguments.
How Function Calling Works
When you send a request to an LLM with function definitions, the model can choose to:
- Respond with text (traditional chatbot behavior)
- Request a function call (agentic behavior)
Here's the anatomy of a function definition for OpenAI's API:
const tools = [
{
type: "function",
function: {
name: "get_weather",
description: "Get the current weather for a specific location",
parameters: {
type: "object",
properties: {
location: {
type: "string",
description: "City name, e.g., 'San Francisco, CA'"
},
unit: {
type: "string",
enum: ["celsius", "fahrenheit"],
description: "Temperature unit"
}
},
required: ["location"]
}
}
}
];
The description fields are critical—they're what the LLM uses to decide when and how to call the function.
Function Calling with Claude (Anthropic)
Claude uses a similar but slightly different structure:
const tools = [
{
name: "get_weather",
description: "Get the current weather for a specific location",
input_schema: {
type: "object",
properties: {
location: {
type: "string",
description: "City name, e.g., 'San Francisco, CA'"
},
unit: {
type: "string",
enum: ["celsius", "fahrenheit"],
description: "Temperature unit"
}
},
required: ["location"]
}
}
];
The key difference is input_schema instead of parameters, but the concept is identical.
Building Your First AI Agent: A Complete Implementation
Let's build a practical AI agent that can search the web, read documents, and perform calculations. We'll use TypeScript and the OpenAI API, but the patterns apply to any LLM.
Step 1: Define Your Tool Interface
First, create a type-safe interface for your tools:
interface Tool {
name: string;
description: string;
parameters: {
type: "object";
properties: Record<string, {
type: string;
description: string;
enum?: string[];
}>;
required: string[];
};
execute: (args: Record<string, unknown>) => Promise<string>;
}
Step 2: Implement Your Tools
Here's a set of practical tools an agent might use:
const webSearchTool: Tool = {
name: "web_search",
description: "Search the web for current information. Use this for recent events, news, or data not in training.",
parameters: {
type: "object",
properties: {
query: {
type: "string",
description: "The search query"
}
},
required: ["query"]
},
execute: async (args) => {
const { query } = args as { query: string };
// In production, integrate with Google Search API, Bing, or Tavily
const response = await fetch(`https://api.search.example/search?q=${encodeURIComponent(query)}`);
const data = await response.json();
return JSON.stringify(data.results.slice(0, 5));
}
};
const calculatorTool: Tool = {
name: "calculator",
description: "Perform mathematical calculations. Supports basic arithmetic, percentages, and common math functions.",
parameters: {
type: "object",
properties: {
expression: {
type: "string",
description: "Mathematical expression to evaluate, e.g., '(100 * 1.15) + 50'"
}
},
required: ["expression"]
},
execute: async (args) => {
const { expression } = args as { expression: string };
try {
// In production, use a safe math parser like mathjs
const result = Function(`"use strict"; return (${expression})`)();
return `Result: ${result}`;
} catch (error) {
return `Error: Invalid expression`;
}
}
};
const readUrlTool: Tool = {
name: "read_url",
description: "Read and extract text content from a URL. Use for reading articles, documentation, or web pages.",
parameters: {
type: "object",
properties: {
url: {
type: "string",
description: "The URL to read"
}
},
required: ["url"]
},
execute: async (args) => {
const { url } = args as { url: string };
try {
const response = await fetch(url);
const html = await response.text();
// In production, use a proper HTML-to-text converter
const text = html.replace(/<[^>]*>/g, ' ').slice(0, 5000);
return text;
} catch (error) {
return `Error reading URL: ${error}`;
}
}
};
Step 3: The Agentic Loop
This is where the magic happens. The agentic loop orchestrates the entire interaction:
import OpenAI from 'openai';
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
interface Message {
role: "system" | "user" | "assistant" | "tool";
content: string;
tool_calls?: Array<{
id: string;
type: "function";
function: { name: string; arguments: string };
}>;
tool_call_id?: string;
}
async function runAgent(
userMessage: string,
tools: Tool[],
maxIterations: number = 10
): Promise<string> {
const toolDefinitions = tools.map(tool => ({
type: "function" as const,
function: {
name: tool.name,
description: tool.description,
parameters: tool.parameters
}
}));
const messages: Message[] = [
{
role: "system",
content: `You are a helpful AI assistant with access to tools.
Use tools when needed to answer questions accurately.
Always explain your reasoning before using a tool.
After getting tool results, synthesize the information into a helpful response.`
},
{ role: "user", content: userMessage }
];
for (let i = 0; i < maxIterations; i++) {
const response = await openai.chat.completions.create({
model: "gpt-4-turbo-preview",
messages: messages,
tools: toolDefinitions,
tool_choice: "auto"
});
const assistantMessage = response.choices[0].message;
messages.push(assistantMessage as Message);
// Check if we're done (no tool calls)
if (!assistantMessage.tool_calls || assistantMessage.tool_calls.length === 0) {
return assistantMessage.content || "I couldn't generate a response.";
}
// Execute each tool call
for (const toolCall of assistantMessage.tool_calls) {
const tool = tools.find(t => t.name === toolCall.function.name);
if (!tool) {
messages.push({
role: "tool",
tool_call_id: toolCall.id,
content: `Error: Tool '${toolCall.function.name}' not found`
});
continue;
}
try {
const args = JSON.parse(toolCall.function.arguments);
const result = await tool.execute(args);
messages.push({
role: "tool",
tool_call_id: toolCall.id,
content: result
});
} catch (error) {
messages.push({
role: "tool",
tool_call_id: toolCall.id,
content: `Error executing tool: ${error}`
});
}
}
}
return "Maximum iterations reached. The task may be incomplete.";
}
Step 4: Using Your Agent
const tools = [webSearchTool, calculatorTool, readUrlTool];
const result = await runAgent(
"What's the current population of Tokyo, and what percentage is that of Japan's total population?",
tools
);
console.log(result);
The agent will:
- Search for Tokyo's current population
- Search for Japan's total population
- Use the calculator to compute the percentage
- Synthesize a final answer
The ReAct Pattern: Reasoning and Acting
The ReAct (Reasoning + Acting) pattern is a powerful framework that makes agents more reliable. Instead of just acting, the agent explicitly reasons about each step.
Implementing ReAct
const REACT_SYSTEM_PROMPT = `You are an AI assistant that follows the ReAct pattern.
For each step, you must:
1. THOUGHT: Reason about the current situation and what to do next
2. ACTION: Choose an action (use a tool or provide final answer)
3. OBSERVATION: Analyze the result of the action
Format your response exactly like this:
THOUGHT: [Your reasoning here]
ACTION: [tool_name with arguments OR "FINAL_ANSWER"]
OBSERVATION: [Will be filled by tool result]
Continue this loop until you can provide a final answer.
When ready to answer, use:
THOUGHT: I now have enough information to answer.
ACTION: FINAL_ANSWER
[Your complete answer here]`;
This structured approach makes debugging easier and improves the agent's decision-making.
Error Handling and Reliability Patterns
Production agents need robust error handling. Here are critical patterns:
1. Retry with Exponential Backoff
async function executeWithRetry<T>(
fn: () => Promise<T>,
maxRetries: number = 3,
baseDelay: number = 1000
): Promise<T> {
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
return await fn();
} catch (error) {
if (attempt === maxRetries - 1) throw error;
const delay = baseDelay * Math.pow(2, attempt);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
throw new Error("Retry failed");
}
2. Tool Execution Timeout
async function executeWithTimeout<T>(
fn: () => Promise<T>,
timeoutMs: number = 30000
): Promise<T> {
return Promise.race([
fn(),
new Promise<T>((_, reject) =>
setTimeout(() => reject(new Error("Tool execution timeout")), timeoutMs)
)
]);
}
3. Graceful Degradation
async function safeToolExecute(tool: Tool, args: Record<string, unknown>): Promise<string> {
try {
return await executeWithTimeout(
() => executeWithRetry(() => tool.execute(args)),
30000
);
} catch (error) {
return `Tool "${tool.name}" failed: ${error}. Please try a different approach.`;
}
}
Advanced Pattern: Multi-Tool Orchestration
Complex tasks often require multiple tools working together. Here's a pattern for coordinating tools:
interface ToolResult {
toolName: string;
input: Record<string, unknown>;
output: string;
timestamp: Date;
}
class AgentContext {
private history: ToolResult[] = [];
private cache: Map<string, string> = new Map();
async executeToolWithCache(tool: Tool, args: Record<string, unknown>): Promise<string> {
const cacheKey = `${tool.name}:${JSON.stringify(args)}`;
if (this.cache.has(cacheKey)) {
return this.cache.get(cacheKey)!;
}
const result = await tool.execute(args);
this.cache.set(cacheKey, result);
this.history.push({
toolName: tool.name,
input: args,
output: result,
timestamp: new Date()
});
return result;
}
getHistory(): ToolResult[] {
return [...this.history];
}
getSummary(): string {
return this.history
.map(r => `${r.toolName}(${JSON.stringify(r.input)}) → ${r.output.slice(0, 100)}...`)
.join('\n');
}
}
Parallel Tool Execution
When tools are independent, execute them in parallel for better performance:
async function executeToolsInParallel(
toolCalls: Array<{ tool: Tool; args: Record<string, unknown> }>
): Promise<Map<string, string>> {
const results = new Map<string, string>();
const promises = toolCalls.map(async ({ tool, args }) => {
const result = await safeToolExecute(tool, args);
results.set(tool.name, result);
});
await Promise.all(promises);
return results;
}
Monitoring and Observability
Production agents need comprehensive logging:
interface AgentTrace {
traceId: string;
startTime: Date;
endTime?: Date;
steps: Array<{
type: "thought" | "action" | "observation";
content: string;
timestamp: Date;
metadata?: Record<string, unknown>;
}>;
totalTokens: number;
status: "running" | "completed" | "failed";
}
function createTracer(): {
trace: AgentTrace;
addStep: (type: "thought" | "action" | "observation", content: string) => void;
complete: () => void;
} {
const trace: AgentTrace = {
traceId: crypto.randomUUID(),
startTime: new Date(),
steps: [],
totalTokens: 0,
status: "running"
};
return {
trace,
addStep: (type, content) => {
trace.steps.push({
type,
content,
timestamp: new Date()
});
},
complete: () => {
trace.endTime = new Date();
trace.status = "completed";
}
};
}
Common Pitfalls and How to Avoid Them
1. Infinite Loops
Problem: Agent keeps calling tools without making progress.
Solution: Implement iteration limits and detect repeated actions:
function detectLoop(messages: Message[], threshold: number = 3): boolean {
const recentToolCalls = messages
.filter(m => m.tool_calls)
.slice(-threshold)
.map(m => JSON.stringify(m.tool_calls));
return new Set(recentToolCalls).size === 1 && recentToolCalls.length === threshold;
}
2. Context Window Overflow
Problem: Conversation history exceeds the model's context window.
Solution: Implement smart summarization:
async function summarizeHistory(messages: Message[]): Promise<Message[]> {
if (messages.length <= 10) return messages;
const toSummarize = messages.slice(1, -5); // Keep system + last 5
const summary = await openai.chat.completions.create({
model: "gpt-4-turbo-preview",
messages: [
{ role: "system", content: "Summarize this conversation concisely." },
{ role: "user", content: JSON.stringify(toSummarize) }
]
});
return [
messages[0], // System prompt
{ role: "assistant", content: `Previous context: ${summary.choices[0].message.content}` },
...messages.slice(-5) // Recent messages
];
}
3. Ambiguous Tool Descriptions
Problem: LLM doesn't use tools correctly because descriptions are unclear.
Solution: Write descriptions from the LLM's perspective:
// ❌ Bad
description: "Weather API"
// ✅ Good
description: "Get current weather conditions for a city. Use when user asks about weather, temperature, or climate. Returns temperature, humidity, and conditions. Only works for cities, not countries or regions."
Security Considerations
When building production agents, security is paramount:
1. Input Validation
function validateToolArgs(schema: Tool['parameters'], args: Record<string, unknown>): boolean {
for (const required of schema.required) {
if (!(required in args)) return false;
}
for (const [key, value] of Object.entries(args)) {
const propSchema = schema.properties[key];
if (!propSchema) continue;
if (propSchema.enum && !propSchema.enum.includes(value as string)) {
return false;
}
}
return true;
}
2. Sandboxed Execution
Never execute arbitrary code. Use sandboxed environments for code execution tools:
// Use libraries like isolated-vm or run in separate containers
import ivm from 'isolated-vm';
async function safeEval(code: string): Promise<string> {
const isolate = new ivm.Isolate({ memoryLimit: 128 });
const context = await isolate.createContext();
try {
const result = await context.eval(code, { timeout: 5000 });
return String(result);
} finally {
isolate.dispose();
}
}
3. Rate Limiting
class RateLimiter {
private requests: number[] = [];
constructor(
private limit: number,
private windowMs: number
) {}
async check(): Promise<boolean> {
const now = Date.now();
this.requests = this.requests.filter(t => t > now - this.windowMs);
if (this.requests.length >= this.limit) {
return false;
}
this.requests.push(now);
return true;
}
}
Conclusion
Building AI agents is about mastering the agentic loop: observe, think, act, reflect, repeat. The key takeaways:
- Start simple: Begin with basic function calling before adding complexity.
- Write clear tool descriptions: The LLM can only use tools it understands.
- Implement robust error handling: Agents will fail; handle it gracefully.
- Monitor everything: Traces and logs are essential for debugging.
- Set boundaries: Iteration limits, timeouts, and rate limiting prevent runaway agents.
- Security first: Never trust LLM outputs for sensitive operations without validation.
The patterns in this guide form the foundation for building production-ready AI agents. Whether you're automating customer support, building research assistants, or creating autonomous coding tools, these principles remain constant.
The next frontier—multi-agent systems, persistent memory, and learning from execution—builds on everything covered here. Master these fundamentals, and you'll be ready to build agents that don't just respond, but truly act.
💡 Note: This article was originally published on the Pockit Blog.
Check out Pockit.tools for 50+ free developer utilities (JSON Formatter, Diff Checker, etc.) that run 100% locally in your browser.
Top comments (1)
"🤖 AhaChat AI Ecosystem is here!
💬 AI Response – Auto-reply to customers 24/7
🎯 AI Sales – Smart assistant that helps close more deals
🔍 AI Trigger – Understands message context & responds instantly
🎨 AI Image – Generate or analyze images with one command
🎤 AI Voice – Turn text into natural, human-like speech
📊 AI Funnel – Qualify & nurture your best leads automatically"