DEV Community

Cover image for How to Build AI Agents in 2026: The Actually Simple Guide
Raju Dandigam
Raju Dandigam

Posted on

How to Build AI Agents in 2026: The Actually Simple Guide

Building an AI agent sounds complicated. It's not. By the end of this guide, you'll have a working agent that can search the web, remember conversations, and handle multi-step tasks. No frameworks, just TypeScript and an LLM API.

What We're Building

A research assistant agent that:

  • Takes questions from users
  • Uses tools (web search) when needed
  • Remembers conversation history
  • Handles errors without crashing
  • Runs in about 150 lines of TypeScript

This won't be production-ready, but it'll work and you'll understand every line.

Prerequisites

You need:

  • Node.js 18 or higher
  • Basic TypeScript knowledge
  • An Anthropic API key (get one free)

That's it. No prior AI experience needed.

Setup (5 minutes)

# Create project
mkdir research-agent
cd research-agent
npm init -y

# Install dependencies
npm install @anthropic-ai/sdk dotenv

# Install dev dependencies
npm install -D typescript @types/node tsx

# Initialize TypeScript
npx tsc --init
Enter fullscreen mode Exit fullscreen mode

Create .env:

ANTHROPIC_API_KEY=your-key-here
Enter fullscreen mode Exit fullscreen mode

Step 1: Define Your Types

Create src/types.ts:

export interface Message {
 role: 'user' | 'assistant';
 content: string;
}

export interface Tool {
 name: string;
 description: string;
 input_schema: {
   type: 'object';
   properties: Record<string, any>;
   required?: string[];
 };
 execute: (input: any) => Promise<string>;
}
Enter fullscreen mode Exit fullscreen mode

Why these types matter:
Strong typing prevents bugs. If you change how a tool works, TypeScript tells you everywhere that breaks.

Step 2: Create a Simple Tool

Create src/tools/search.ts:

import { Tool } from '../types';

export const searchTool: Tool = {
 name: 'search_web',
 description: 'Search the internet for current information. Use this when you need facts, recent events, or data you do not know.',
 input_schema: {
   type: 'object',
   properties: {
     query: {
       type: 'string',
       description: 'The search query',
     },
   },
   required: ['query'],
 },
 execute: async (input: { query: string }) => {
   console.log(`[Tool] Searching for: ${input.query}`);

   // For now, return mock data
   // In production, call a real search API (SerpAPI, Brave Search, etc.)
   return `Search results for "${input.query}":

1. Recent advances in AI agents show improved reasoning capabilities
2. Multi-step task completion is now standard in 2026
3. Tool-calling interfaces have become more reliable

(This is mock data - replace with real search API)`;
 },
};
Enter fullscreen mode Exit fullscreen mode

Real-world note:
For production, integrate with SerpAPI, Brave Search API, or Tavily. The structure stays the same.

Step 3: Build the Agent Runner

Create src/agent.ts:

import Anthropic from '@anthropic-ai/sdk';
import { Message, Tool } from './types';

export class Agent {
 private client: Anthropic;
 private conversationHistory: Message[] = [];

 constructor(
   private systemPrompt: string,
   private tools: Tool[],
   private model: string = 'claude-3-5-sonnet-20241022'
 ) {
   const apiKey = process.env.ANTHROPIC_API_KEY;
   if (!apiKey) {
     throw new Error('ANTHROPIC_API_KEY not found in environment');
   }
   this.client = new Anthropic({ apiKey });
 }

 async chat(userMessage: string): Promise<string> {
   // Add user message to history
   this.conversationHistory.push({
     role: 'user',
     content: userMessage,
   });

   // Create the API request
   const response = await this.client.messages.create({
     model: this.model,
     max_tokens: 4096,
     system: this.systemPrompt,
     messages: this.conversationHistory,
     tools: this.tools.map(tool => ({
       name: tool.name,
       description: tool.description,
       input_schema: tool.input_schema,
     })),
   });

   // Check if agent wants to use tools
   if (response.stop_reason === 'tool_use') {
     return await this.handleToolUse(response);
   }

   // No tools needed - return direct response
   const content = response.content.find(block => block.type === 'text');
   const assistantMessage = content?.type === 'text' ? content.text : '';

   this.conversationHistory.push({
     role: 'assistant',
     content: assistantMessage,
   });

   return assistantMessage;
 }

 private async handleToolUse(response: any): Promise<string> {
   const toolUseBlocks = response.content.filter(
     (block: any) => block.type === 'tool_use'
   );

   // Execute all tool calls
   const toolResults = await Promise.all(
     toolUseBlocks.map(async (block: any) => {
       const tool = this.tools.find(t => t.name === block.name);
       if (!tool) {
         return {
           type: 'tool_result',
           tool_use_id: block.id,
           content: `Error: Tool ${block.name} not found`,
         };
       }


       try {
         const result = await tool.execute(block.input);
         return {
           type: 'tool_result',
           tool_use_id: block.id,
           content: result,
         };
       } catch (error) {
         return {
           type: 'tool_result',
           tool_use_id: block.id,
           content: `Error executing tool: ${error}`,
           is_error: true,
         };
       }
     })
   );

   // Add assistant's tool use to history
   this.conversationHistory.push({
     role: 'assistant',
     content: response.content,
   });

   // Send tool results back to Claude
   const followUpResponse = await this.client.messages.create({
     model: this.model,
     max_tokens: 4096,
     system: this.systemPrompt,
     messages: [
       ...this.conversationHistory,
       {
         role: 'user',
         content: toolResults,
       },
     ],
     tools: this.tools.map(tool => ({
       name: tool.name,
       description: tool.description,
       input_schema: tool.input_schema,
     })),
   });

   // Extract final response
   const textBlock = followUpResponse.content.find(
     block => block.type === 'text'
   );
   const finalMessage = textBlock?.type === 'text' ? textBlock.text : '';

   this.conversationHistory.push({
     role: 'assistant',
     content: finalMessage,
   });

   return finalMessage;
 }

 getHistory(): Message[] {
   return this.conversationHistory;
 }

 clearHistory(): void {
   this.conversationHistory = [];
 }
}
Enter fullscreen mode Exit fullscreen mode

What's happening here:

  1. Constructor: Sets up the Anthropic client and stores tools
  2. chat(): Main method - sends message, handles response
  3. handleToolUse(): Executes tools when Claude requests them
  4. History management: Keeps conversation context between turns

Step 4: Put It All Together

Create src/index.ts:

import { config } from 'dotenv';
import { Agent } from './agent';
import { searchTool } from './tools/search';

// Load environment variables
config();

const systemPrompt = `You are a helpful research assistant.

When users ask questions:
1. If you need current information, use the search_web tool
2. Provide clear, concise answers
3. Cite your sources when using search results
4. If you're unsure, say so - don't guess

Keep responses focused and practical.`;

async function main() {
 const agent = new Agent(systemPrompt, [searchTool]);

 console.log('Research Agent started. Ask me anything!\n');

 // Example conversation
 try {
   const response1 = await agent.chat(
     'What are the main AI trends in 2026?'
   );
   console.log('Agent:', response1);
   console.log('\n---\n');

   const response2 = await agent.chat(
     'Can you elaborate on the first trend?'
   );
   console.log('Agent:', response2);
   console.log('\n---\n');

   // Show conversation history
   console.log('Conversation history:');
   console.log(JSON.stringify(agent.getHistory(), null, 2));
 } catch (error) {
   console.error('Error:', error);
 }
}
main();
Enter fullscreen mode Exit fullscreen mode

Step 5: Run Your Agent

npx tsx src/index.ts
Enter fullscreen mode Exit fullscreen mode

You should see:

  1. The agent receives your question
  2. It decides to use the search tool
  3. The search executes
  4. The agent synthesizes a response
  5. Follow-up questions use conversation history

What Just Happened?

You built a functional AI agent with:

Memory:
The conversationHistory array maintains context across turns. The agent remembers what you've discussed.

Tool Use:
When Claude needs information it doesn't have, it calls the search tool. You can add more tools the same way.

Error Handling:
Tool failures return error messages instead of crashing. The agent can explain what went wrong.

Type Safety:
TypeScript ensures tools match expected interfaces. If you change a tool's signature, TypeScript flags every usage.

Making It Interactive

Want a CLI instead of hardcoded messages? Add this to src/index.ts:

import * as readline from 'readline';


async function interactiveMode() {
 const agent = new Agent(systemPrompt, [searchTool]);
 const rl = readline.createInterface({
   input: process.stdin,
   output: process.stdout,
 });

 console.log('Research Agent (type "exit" to quit)\n');

 const askQuestion = () => {
   rl.question('You: ', async (input) => {
     if (input.toLowerCase() === 'exit') {
       rl.close();
       return;
     }

     try {
       const response = await agent.chat(input);
       console.log(`\nAgent: ${response}\n`);
     } catch (error) {
       console.error('Error:', error);
     }

     askQuestion();
   });
 };

 askQuestion();
}

interactiveMode();
Enter fullscreen mode Exit fullscreen mode

Now you can have real conversations with your agent.

Adding More Tools

Want to add database access, calculations, or API calls? Follow the same pattern:

// src/tools/calculator.ts
import { Tool } from '../types';

export const calculatorTool: Tool = {
 name: 'calculate',
 description: 'Perform mathematical calculations',
 input_schema: {
   type: 'object',
   properties: {
     expression: {
       type: 'string',
       description: 'Math expression to evaluate (e.g., "2 + 2", "sqrt(16)")',
     },
   },
   required: ['expression'],
 },
 execute: async (input: { expression: string }) => {
   try {
     // In production, use a safe math library like mathjs
     // This is just for demonstration
     const result = eval(input.expression);
     return `Result: ${result}`;
   } catch (error) {
     return `Error calculating: ${error}`;
   }
 },
};
Enter fullscreen mode Exit fullscreen mode

Add it to your agent:

const agent = new Agent(systemPrompt, [searchTool, calculatorTool]);
Enter fullscreen mode Exit fullscreen mode

That's it. The agent now has math capabilities.

Common Issues

"API key not found"
Make sure .env is in your project root and contains ANTHROPIC_API_KEY=...

"Module not found"
Run npm install to ensure all dependencies are installed.

Agent doesn't use tools
Your system prompt needs to encourage tool use. Try: "When you need information, always use available tools rather than guessing."

Conversation history grows too large
Add a limit:

if (this.conversationHistory.length > 20) {
 this.conversationHistory = this.conversationHistory.slice(-10);
}
Enter fullscreen mode Exit fullscreen mode

What's Missing?

This agent is functional but basic. For production, you'd add:

Budget limits:
Track token usage and stop when limits are reached.

Better memory:
Summarize old conversations or use vector storage for semantic recall.

Observability:
Log tool executions, track errors, measure response times.

Multiple agents:
Coordinate specialist agents for complex tasks.

Safety guardrails:
Validate tool inputs, sanitize outputs, handle malicious prompts.

But none of that matters until you've built something that works. Start here, then add complexity as needed.

Next Steps

Improve the search tool:
Replace mock data with a real search API. SerpAPI offers free tiers.

Add more tools:
Database queries, file operations, API integrations - all follow the same pattern.

Build a UI:
Connect this to a web interface using Next.js or a CLI using inquirer.

Deploy it:
This runs anywhere Node.js runs - Vercel, Railway, AWS Lambda.

Study tool calling:
Read Anthropic's tool use guide for advanced patterns.

Wrapping Up

You now have a working AI agent. The code is straightforward:

  • Define tools with a consistent interface
  • Let the LLM decide when to use them
  • Execute tools and send results back
  • Maintain conversation history for context

This pattern scales. Production agents are variations of this basic structure with more tools, better error handling, and monitoring.

The hard part isn't the code - it's designing good tools and prompts. But you can only learn that by building and iterating.

Start with this foundation and expand as you learn what your agent needs.

Questions? Drop a comment below.

Top comments (1)

Collapse
 
jacobfoster21 profile image
jacob foster

This is a great way to make AI agents feel less intimidating. Building it from scratch in a simple way really helps people understand what’s actually going on instead of treating it like a black box.