Agents vs Skills vs Rules: Understanding AI System Layers
Building effective AI systems requires understanding three distinct layers that work together: Agents (the thinkers), Skills (the doers), and Rules (the guardrails). Confusing these layers leads to brittle, hard-to-maintain systems.
The Three-Layer Architecture
┌─────────────────────────────────────┐
│ AGENTS │ ← The Thinkers
│ • Planning & Decision Making │
│ • Coordination & Orchestration │
│ • Context Management │
├─────────────────────────────────────┤
│ SKILLS │ ← The Doers
│ • Capabilities & Actions │
│ • API Integrations │
│ • Data Processing │
├─────────────────────────────────────┤
│ RULES │ ← The Guardrails
│ • Safety & Security │
│ • Business Logic │
│ • Compliance & Governance │
└─────────────────────────────────────┘
Each layer has a specific responsibility and shouldn’t overlap with others.
Agents: The Orchestration Layer
What agents do:
- Receive user requests and understand intent
- Plan multi-step workflows
- Decide which skills to use and when
- Handle errors and retry logic
- Manage conversation context
- Coordinate multiple skills together
What agents DON’T do:
- Implement actual business logic (that’s skills)
- Enforce security policies (that’s rules)
- Directly access external APIs (through skills only)
Agent Example
class EmailAgent:
def __init__(self):
self.skills = {
"search_emails": EmailSearchSkill(),
"compose_email": ComposeSkill(),
"send_email": SendSkill()
}
self.rules = EmailRules()
async def handle_request(self, user_input):
# Agent thinking/planning
if "find emails from" in user_input:
return await self.skills["search_emails"].search(user_input)
elif "send email to" in user_input:
# Check rules before acting
if self.rules.can_send_to(user_input.recipient):
return await self.skills["send_email"].send(user_input)
else:
return "Cannot send: Security policy violation"
Skills: The Capability Layer
What skills do:
- Implement specific business capabilities
- Interface with external systems (APIs, databases)
- Process and transform data
- Execute domain-specific logic
- Return structured results
What skills DON’T do:
- Make decisions about which actions to take
- Handle user interaction directly
- Enforce business rules (they can check rules, but not define them)
Skill Example
class SendEmailSkill:
def __init__(self):
self.email_client = EmailClient()
self.rules = EmailRules() # Can reference rules
async def send(self, to, subject, body):
# Skill implementation
try:
# Reference rules for validation
if not self.rules.is_valid_email(to):
return {"success": False, "error": "Invalid recipient"}
result = await self.email_client.send(to, subject, body)
return {"success": True, "message_id": result.id}
except Exception as e:
return {"success": False, "error": str(e)}
Rules: The Governance Layer
What rules do:
- Define security policies and constraints
- Enforce business logic and compliance
- Validate inputs and outputs
- Set rate limits and quotas
- Audit and log actions
- Handle authorization and permissions
What rules DON’T do:
- Implement any business logic
- Make decisions about workflow
- Directly interact with external systems
Rules Example
class EmailRules:
def __init__(self):
self.blocked_domains = ["spam.com", "malicious.net"]
self.rate_limits = {"hourly": 100, "daily": 1000}
self.usage_log = []
def can_send_to(self, recipient):
# Rule: Check blocked domains
domain = recipient.split("@")[1]
if domain in self.blocked_domains:
return False
# Rule: Check rate limits
if self.check_rate_limit():
return False
# Rule: Log for audit
self.log_action("send_attempt", recipient)
return True
def is_valid_email(self, email):
# Rule: Email format validation
return "@" in email and "." in email.split("@")[1]
Why Separation Matters
1. Reusability
# Skills can be reused across agents
email_skill = SendEmailSkill()
customer_agent = CustomerServiceAgent(email_skill)
marketing_agent = MarketingAgent(email_skill)
2. Testability
# Each layer can be tested independently
def test_skill():
skill = SendEmailSkill()
result = skill.send("test@example.com", "Subject", "Body")
assert result["success"] == True
def test_rules():
rules = EmailRules()
assert rules.can_send_to("good@example.com") == True
assert rules.can_send_to("bad@spam.com") == False
3. Maintainability
- Bug in email sending? Fix the skill, not the agent
- New security policy? Update rules, not skills
- Change workflow logic? Modify agent, not skills
4. Security
- Skills can’t bypass security (rules enforce it)
- Agents can’t directly access external systems
- Clear audit trail through rule layer
Common Anti-Patterns
❌ Mixing Responsibilities
# BAD: Agent implementing business logic
class BadAgent:
async def send_email(self, to, subject):
# Agent shouldn't implement email logic
if "spam" in subject.lower():
return "Cannot send spam" # This is a rule!
# Agent shouldn't call APIs directly
smtp = SMTPClient()
return smtp.send(to, subject) # This is a skill!
✅ Proper Separation
# GOOD: Clear separation of concerns
class GoodAgent:
def __init__(self):
self.skills = {"email": SendEmailSkill()}
self.rules = EmailRules()
async def handle_request(self, request):
if self.rules.validate(request):
return await self.skills["email"].send(request)
else:
return "Request blocked by policy"
Interaction Patterns
Pattern 1: Agent → Skill → Rules
Agent decides to send email
↓
Skill implements email sending
↓
Skill checks rules before sending
↓
Rules validate and allow/deny
Pattern 2: Agent → Rules → Skill
Agent wants to send email
↓
Agent checks rules first
↓
Rules validate the request
↓
If allowed, agent calls skill
Pattern 3: Skill → Rules (Internal Validation)
Skill receives request from agent
↓
Skill internally validates against rules
↓
Skill executes or rejects based on rules
Real-World Example: Document Processing Agent
# Agent Layer
class DocumentAgent:
def __init__(self):
self.skills = {
"ocr": OCRSkill(),
"summarize": SummarySkill(),
"classify": ClassificationSkill(),
"store": StorageSkill()
}
self.rules = DocumentRules()
async def process_document(self, file_path):
# Agent: Plan the workflow
if not self.rules.can_process(file_path):
return "File rejected by policy"
# Agent: Coordinate skills
text = await self.skills["ocr"].extract(file_path)
summary = await self.skills["summarize"].create(text)
category = await self.skills["classify"].determine(text)
# Agent: Store results
result = await self.skills["store"].save({
"summary": summary,
"category": category,
"original": file_path
})
return result
# Skill Layer
class OCRSkill:
async def extract(self, file_path):
# Skill: Implement OCR logic
ocr_service = OCRService()
return await ocr_service.extract_text(file_path)
# Rules Layer
class DocumentRules:
def __init__(self):
self.allowed_formats = ["pdf", "docx", "jpg"]
self.max_size_mb = 50
def can_process(self, file_path):
# Rules: Validate file
if not self.is_allowed_format(file_path):
return False
if not self.is_under_size_limit(file_path):
return False
return True
Implementation Best Practices
1. Clear Interfaces
# Define interfaces between layers
class AgentInterface:
async def process(self, request: Request) -> Response:
pass
class SkillInterface:
async def execute(self, params: dict) -> Result:
pass
class RulesInterface:
def validate(self, action: Action) -> bool:
pass
2. Dependency Injection
# Inject dependencies, don't create them
class Agent:
def __init__(self, skills: dict, rules: RulesInterface):
self.skills = skills
self.rules = rules
3. Configuration-Driven Rules
# rules.yaml
email:
blocked_domains: ["spam.com"]
rate_limits:
hourly: 100
daily: 1000
allowed_senders: ["@company.com"]
4. Observability
# Log at each layer
class Skill:
async def execute(self, params):
logger.info(f"Skill executing: {self.__class__.__name__}")
result = await self._do_work(params)
logger.info(f"Skill completed: {result}")
return result
Testing Strategy
Unit Tests (Per Layer)
# Test agent logic
def test_agent_planning():
agent = Agent(mock_skills, mock_rules)
plan = agent.plan_workflow("send email to user")
assert plan.steps == ["validate", "compose", "send"]
# Test skill implementation
def test_skill_execution():
skill = EmailSkill()
result = skill.send("test@example.com", "Subject", "Body")
assert result.success == True
# Test rule validation
def test_rule_validation():
rules = EmailRules()
assert rules.can_send_to("good@example.com") == True
assert rules.can_send_to("bad@spam.com") == False
Integration Tests (Cross-Layer)
def test_agent_to_skill_integration():
# Test agent correctly calls skill
agent = Agent(real_skills, mock_rules)
result = agent.handle_request("send email to test@example.com")
assert "message_id" in result
Evolution of the Architecture
As your system grows, you might add:
Additional Layers
- Monitoring Layer: Track performance, errors, usage
- Caching Layer: Improve performance with smart caching
- Security Layer: Advanced threat detection and response
Advanced Patterns
- Skill Composition: Combine multiple skills into meta-skills
- Rule Hierarchies: Global, team, and individual rule sets
- Agent Networks: Multiple agents coordinating across systems
Key Takeaway: The separation of agents, skills, and rules isn’t just architecture — it’s about creating maintainable, secure, and scalable AI systems. Each layer has a clear responsibility, and respecting those boundaries is key to success.
Next: Learn about common architecture patterns for implementing these layers effectively.
Related Articles
Deepen your understanding with these curated continuations.
7 AI Agent Architecture Patterns: Simple to Multi-Agent Systems
Master AI agent design with these 7 architecture patterns. From simple request-response to complex multi-agent orchestration, event-driven, and hybrid systems.
What Are AI Agents? Beyond Chatbots to Autonomous Systems
AI agents are autonomous systems that can perceive, reason, and act. Learn how they differ from chatbots, what makes them powerful, and when to use them.
Open Source Agent Skill Repositories 2026: Complete Guide
Discover the best open-source agent skill repositories with proper tagging, categorization, and integration patterns. Build agents faster with pre-built skills.