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.
References
- Building effective agents — Anthropic
- ReAct: Synergizing Reasoning and Acting in Language Models — arXiv
- Model Context Protocol specification — Model Context Protocol
About Jishu Labs
Jishu Labs is a software development company founded in 2016. We build custom software, AI/ML systems, and full-stack web and mobile applications for clients, and we make eight AI tools for software teams.