AI agents represent the next evolution in AI applications - systems that can autonomously plan, execute, and adapt to complete complex tasks. Unlike simple chatbots, agents use tools, maintain memory, and reason about multi-step problems. This guide covers practical patterns for building production AI agents in 2026.
Agent Architecture Overview
A complete AI agent consists of four core components: a reasoning engine (LLM), tools for taking actions, memory for context, and a planning system for complex tasks.
// Core Agent Architecture
import Anthropic from '@anthropic-ai/sdk';
interface Tool {
name: string;
description: string;
parameters: Record<string, any>;
execute: (params: any) => Promise<any>;
}
interface AgentMemory {
shortTerm: Message[]; // Conversation history
longTerm: VectorStore; // Persistent knowledge
working: Map<string, any>; // Current task context
}
interface AgentConfig {
model: string;
systemPrompt: string;
tools: Tool[];
maxIterations: number;
temperature: number;
}
class Agent {
private client: Anthropic;
private config: AgentConfig;
private memory: AgentMemory;
constructor(config: AgentConfig) {
this.client = new Anthropic();
this.config = config;
this.memory = {
shortTerm: [],
longTerm: new VectorStore(),
working: new Map(),
};
}
async run(task: string): Promise<string> {
// Add task to memory
this.memory.shortTerm.push({ role: 'user', content: task });
let iteration = 0;
while (iteration < this.config.maxIterations) {
// Get relevant context from long-term memory
const context = await this.memory.longTerm.search(task, 5);
// Call LLM with tools
const response = await this.client.messages.create({
model: this.config.model,
max_tokens: 4096,
system: this.buildSystemPrompt(context),
tools: this.formatTools(),
messages: this.memory.shortTerm,
});
// Process response
const { content, stopReason } = this.processResponse(response);
// Check if agent wants to use a tool
if (response.stop_reason === 'tool_use') {
const toolResults = await this.executeTools(response.content);
this.memory.shortTerm.push(
{ role: 'assistant', content: response.content },
{ role: 'user', content: toolResults }
);
iteration++;
continue;
}
// Agent is done
if (response.stop_reason === 'end_turn') {
// Store interaction in long-term memory
await this.memory.longTerm.add({
task,
response: content,
timestamp: new Date(),
});
return content;
}
}
return 'Max iterations reached without completion';
}
private async executeTools(content: any[]): Promise<any[]> {
const results = [];
for (const block of content) {
if (block.type === 'tool_use') {
const tool = this.config.tools.find(t => t.name === block.name);
if (!tool) {
results.push({
type: 'tool_result',
tool_use_id: block.id,
content: `Error: Tool ${block.name} not found`,
});
continue;
}
try {
const result = await tool.execute(block.input);
results.push({
type: 'tool_result',
tool_use_id: block.id,
content: JSON.stringify(result),
});
} catch (error) {
results.push({
type: 'tool_result',
tool_use_id: block.id,
content: `Error: ${error.message}`,
});
}
}
}
return results;
}
private formatTools() {
return this.config.tools.map(tool => ({
name: tool.name,
description: tool.description,
input_schema: {
type: 'object',
properties: tool.parameters,
required: Object.keys(tool.parameters),
},
}));
}
private buildSystemPrompt(context: any[]): string {
let prompt = this.config.systemPrompt;
if (context.length > 0) {
prompt += '\n\nRelevant context from memory:\n';
context.forEach((c, i) => {
prompt += `${i + 1}. ${c.content}\n`;
});
}
return prompt;
}
}Tool Implementation
Tools extend agent capabilities beyond text generation. Well-designed tools are atomic, composable, and handle errors gracefully.
// Tool Implementations
// Web Search Tool
const webSearchTool: Tool = {
name: 'web_search',
description: 'Search the web for current information. Use for recent events, documentation, or facts.',
parameters: {
query: { type: 'string', description: 'Search query' },
num_results: { type: 'number', description: 'Number of results (1-10)' },
},
async execute({ query, num_results = 5 }) {
const response = await fetch(
`https://api.search.example/search?q=${encodeURIComponent(query)}&n=${num_results}`
);
const data = await response.json();
return data.results.map((r: any) => ({
title: r.title,
snippet: r.snippet,
url: r.url,
}));
},
};
// Code Execution Tool (sandboxed)
const codeExecutionTool: Tool = {
name: 'execute_code',
description: 'Execute Python code in a sandboxed environment. Returns stdout, stderr, and return value.',
parameters: {
code: { type: 'string', description: 'Python code to execute' },
timeout: { type: 'number', description: 'Timeout in seconds (max 30)' },
},
async execute({ code, timeout = 10 }) {
const response = await fetch('https://sandbox.example/execute', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ code, timeout: Math.min(timeout, 30) }),
});
return response.json();
},
};
// Database Query Tool
const databaseTool: Tool = {
name: 'query_database',
description: 'Query the database. Only SELECT queries are allowed.',
parameters: {
query: { type: 'string', description: 'SQL SELECT query' },
},
async execute({ query }) {
// Validate query is SELECT only
const normalized = query.trim().toLowerCase();
if (!normalized.startsWith('select')) {
throw new Error('Only SELECT queries are allowed');
}
// Check for dangerous patterns
const dangerous = ['drop', 'delete', 'update', 'insert', 'alter', '--'];
if (dangerous.some(d => normalized.includes(d))) {
throw new Error('Query contains forbidden keywords');
}
const result = await db.query(query);
return {
rows: result.rows.slice(0, 100), // Limit results
rowCount: result.rowCount,
};
},
};
// File Operations Tool
const fileOperationsTool: Tool = {
name: 'file_operations',
description: 'Read, write, or list files in the workspace.',
parameters: {
operation: { type: 'string', enum: ['read', 'write', 'list'] },
path: { type: 'string', description: 'File path relative to workspace' },
content: { type: 'string', description: 'Content for write operation' },
},
async execute({ operation, path, content }) {
const workspacePath = '/workspace';
const fullPath = `${workspacePath}/${path}`;
// Security: prevent path traversal
if (fullPath.includes('..') || !fullPath.startsWith(workspacePath)) {
throw new Error('Invalid path');
}
switch (operation) {
case 'read':
return await fs.readFile(fullPath, 'utf-8');
case 'write':
await fs.writeFile(fullPath, content);
return { success: true, path };
case 'list':
const files = await fs.readdir(fullPath);
return files;
default:
throw new Error(`Unknown operation: ${operation}`);
}
},
};Planning and Reasoning
Complex tasks require planning. The ReAct (Reasoning + Acting) pattern helps agents break down problems and track progress.
// ReAct Agent with Planning
interface Plan {
goal: string;
steps: PlanStep[];
currentStep: number;
status: 'planning' | 'executing' | 'completed' | 'failed';
}
interface PlanStep {
id: number;
description: string;
status: 'pending' | 'in_progress' | 'completed' | 'failed';
result?: string;
}
class ReActAgent extends Agent {
private plan: Plan | null = null;
async run(task: string): Promise<string> {
// Phase 1: Planning
this.plan = await this.createPlan(task);
console.log('Plan created:', this.plan);
// Phase 2: Execution
for (let i = 0; i < this.plan.steps.length; i++) {
this.plan.currentStep = i;
this.plan.steps[i].status = 'in_progress';
try {
const result = await this.executeStep(this.plan.steps[i]);
this.plan.steps[i].result = result;
this.plan.steps[i].status = 'completed';
// Check if we should replan based on results
if (await this.shouldReplan(result)) {
this.plan = await this.replan(this.plan, result);
i = this.plan.currentStep - 1; // Reset to new current step
}
} catch (error) {
this.plan.steps[i].status = 'failed';
// Try to recover
const recovery = await this.attemptRecovery(error, this.plan.steps[i]);
if (!recovery.success) {
this.plan.status = 'failed';
return `Failed at step ${i + 1}: ${error.message}`;
}
}
}
// Phase 3: Synthesize results
return this.synthesizeResults(this.plan);
}
private async createPlan(task: string): Promise<Plan> {
const response = await this.client.messages.create({
model: this.config.model,
max_tokens: 2048,
system: `You are a planning agent. Break down the task into concrete steps.
Output your plan as JSON:
{
"goal": "the overall goal",
"steps": [
{ "id": 1, "description": "step description" },
...
]
}
Keep steps atomic and actionable. Usually 3-7 steps is appropriate.`,
messages: [{ role: 'user', content: task }],
});
const planText = response.content[0].text;
const planJson = JSON.parse(planText);
return {
...planJson,
currentStep: 0,
status: 'executing',
steps: planJson.steps.map((s: any) => ({
...s,
status: 'pending',
})),
};
}
private async executeStep(step: PlanStep): Promise<string> {
const response = await this.client.messages.create({
model: this.config.model,
max_tokens: 4096,
system: `You are executing step ${step.id} of a plan.
Goal: ${this.plan!.goal}
Current step: ${step.description}
Previous steps completed:
${this.plan!.steps
.filter(s => s.status === 'completed')
.map(s => `- ${s.description}: ${s.result}`)
.join('\n')}
Use the available tools to complete this step. Be thorough but efficient.`,
tools: this.formatTools(),
messages: [{ role: 'user', content: `Execute: ${step.description}` }],
});
// Execute any tools and get final result
return this.processAgentResponse(response);
}
private async shouldReplan(result: string): Promise<boolean> {
const response = await this.client.messages.create({
model: this.config.model,
max_tokens: 256,
messages: [{
role: 'user',
content: `Given this step result: "${result}"
And the remaining plan:
${this.plan!.steps
.filter(s => s.status === 'pending')
.map(s => `- ${s.description}`)
.join('\n')}
Should we adjust the plan? Reply with just YES or NO.`,
}],
});
return response.content[0].text.trim().toUpperCase() === 'YES';
}
private async replan(currentPlan: Plan, newInfo: string): Promise<Plan> {
const response = await this.client.messages.create({
model: this.config.model,
max_tokens: 2048,
system: 'You are adjusting a plan based on new information.',
messages: [{
role: 'user',
content: `Current plan goal: ${currentPlan.goal}
Completed steps:
${currentPlan.steps
.filter(s => s.status === 'completed')
.map(s => `- ${s.description}: ${s.result}`)
.join('\n')}
New information: ${newInfo}
Provide updated remaining steps as JSON array.`,
}],
});
const newSteps = JSON.parse(response.content[0].text);
return {
...currentPlan,
steps: [
...currentPlan.steps.filter(s => s.status === 'completed'),
...newSteps.map((s: any, i: number) => ({
...s,
id: currentPlan.currentStep + i + 1,
status: 'pending',
})),
],
};
}
private synthesizeResults(plan: Plan): string {
const completedSteps = plan.steps
.filter(s => s.status === 'completed')
.map(s => `${s.description}: ${s.result}`)
.join('\n\n');
return `Completed goal: ${plan.goal}\n\nResults:\n${completedSteps}`;
}
}Memory Systems
// Advanced Memory System
import { Pinecone } from '@pinecone-database/pinecone';
import { OpenAIEmbeddings } from '@langchain/openai';
interface MemoryEntry {
id: string;
content: string;
type: 'conversation' | 'fact' | 'procedure' | 'preference';
metadata: {
timestamp: Date;
importance: number;
accessCount: number;
lastAccessed: Date;
source: string;
};
embedding?: number[];
}
class AgentMemorySystem {
private shortTermMemory: MemoryEntry[] = [];
private pinecone: Pinecone;
private embeddings: OpenAIEmbeddings;
private maxShortTermSize = 20;
constructor() {
this.pinecone = new Pinecone();
this.embeddings = new OpenAIEmbeddings();
}
// Add to short-term memory
async addShortTerm(content: string, type: MemoryEntry['type']) {
const entry: MemoryEntry = {
id: crypto.randomUUID(),
content,
type,
metadata: {
timestamp: new Date(),
importance: 0.5,
accessCount: 0,
lastAccessed: new Date(),
source: 'conversation',
},
};
this.shortTermMemory.push(entry);
// Consolidate if too large
if (this.shortTermMemory.length > this.maxShortTermSize) {
await this.consolidateMemory();
}
}
// Consolidate short-term to long-term memory
private async consolidateMemory() {
// Keep recent and important entries
const toKeep = this.shortTermMemory
.sort((a, b) => {
const recencyA = Date.now() - a.metadata.timestamp.getTime();
const recencyB = Date.now() - b.metadata.timestamp.getTime();
const scoreA = a.metadata.importance - recencyA / 1000000;
const scoreB = b.metadata.importance - recencyB / 1000000;
return scoreB - scoreA;
})
.slice(0, this.maxShortTermSize / 2);
const toConsolidate = this.shortTermMemory.filter(
e => !toKeep.includes(e)
);
// Store in long-term memory
await this.addToLongTerm(toConsolidate);
this.shortTermMemory = toKeep;
}
// Add to long-term vector memory
private async addToLongTerm(entries: MemoryEntry[]) {
const index = this.pinecone.index('agent-memory');
for (const entry of entries) {
const embedding = await this.embeddings.embedQuery(entry.content);
await index.upsert([{
id: entry.id,
values: embedding,
metadata: {
content: entry.content,
type: entry.type,
timestamp: entry.metadata.timestamp.toISOString(),
importance: entry.metadata.importance,
},
}]);
}
}
// Retrieve relevant memories
async recall(query: string, limit = 5): Promise<MemoryEntry[]> {
// Search short-term first (recency bias)
const shortTermResults = this.searchShortTerm(query);
// Search long-term
const queryEmbedding = await this.embeddings.embedQuery(query);
const index = this.pinecone.index('agent-memory');
const longTermResults = await index.query({
vector: queryEmbedding,
topK: limit,
includeMetadata: true,
});
// Combine and deduplicate
const combined = [
...shortTermResults,
...longTermResults.matches.map(m => ({
id: m.id,
content: m.metadata?.content as string,
type: m.metadata?.type as MemoryEntry['type'],
metadata: {
timestamp: new Date(m.metadata?.timestamp as string),
importance: m.metadata?.importance as number,
accessCount: 0,
lastAccessed: new Date(),
source: 'long-term',
},
})),
];
// Update access counts
combined.forEach(e => e.metadata.accessCount++);
return combined.slice(0, limit);
}
private searchShortTerm(query: string): MemoryEntry[] {
// Simple keyword matching for short-term
const queryWords = query.toLowerCase().split(' ');
return this.shortTermMemory
.filter(e => {
const contentWords = e.content.toLowerCase().split(' ');
return queryWords.some(qw => contentWords.some(cw => cw.includes(qw)));
})
.slice(0, 3);
}
// Forget irrelevant memories
async forget(criteria: { olderThan?: Date; type?: string }) {
const index = this.pinecone.index('agent-memory');
// Delete from Pinecone based on criteria
// (Implementation depends on your indexing strategy)
}
}Best Practices
AI Agent Best Practices
Design:
- Keep tools atomic and composable
- Implement robust error handling
- Add rate limiting and timeouts
- Log all actions for debugging
Safety:
- Sandbox code execution
- Validate all tool inputs
- Implement human-in-the-loop for critical actions
- Set spending/usage limits
Performance:
- Cache tool results where appropriate
- Use streaming for long-running tasks
- Implement parallel tool execution
- Monitor and optimize token usage
Conclusion
AI agents are transforming automation by combining LLM reasoning with real-world actions. Success requires careful attention to tool design, planning systems, and memory management. Start simple with a few well-designed tools and add complexity as needed.
Ready to build AI agents for your business? Contact Jishu Labs for expert guidance on designing and implementing autonomous AI systems.
About Sarah Johnson
Sarah Johnson is the CTO at Jishu Labs with deep expertise in AI systems. She has built production AI agents for enterprise automation and developer tools.