@happyvertical/smrt-agents
Build autonomous actors with persistent state, inter-agent communication via DispatchBus, and comprehensive lifecycle management.
Overview
The @happyvertical/smrt-agents package provides a base Agent class for building
autonomous actors in the SMRT ecosystem. Agents are persistent, state-managing objects that extend
SmrtObject with automatic database persistence, lifecycle management, inter-agent communication,
and admin panel UI integration.
Key Features
- Persistent State: Automatic database persistence via SmrtObject
- Lifecycle Management: Initialize, validate, run, and shutdown hooks
- Inter-Agent Communication: DispatchBus for async messaging with wildcard patterns
- Interest-Based Queries: Declarative object discovery and filtering
- Configuration Management: Three-layer config (file + database + defaults)
- Status Tracking: Five states (idle, initializing, running, error, shutdown)
- UI Slots: Admin panel component declarations for configuration
- Graceful Shutdown: Signal handlers for SIGTERM/SIGINT
Architecture
┌─────────────────────────────────────────────────────────────┐ │ SMRT Agent Framework │ ├─────────────────────────────────────────────────────────────┤ │ ┌──────────────────────────────────────────────────────┐ │ │ │ Agent (Abstract Base Class) │ │ │ ├──────────────────────────────────────────────────────┤ │ │ │ • Extends SmrtObject (database persistence) │ │ │ │ • Status tracking (5 states) │ │ │ │ • Lifecycle hooks (initialize, validate, run) │ │ │ │ • Signal handlers (graceful shutdown) │ │ │ │ • Logger integration │ │ │ │ • DispatchBus communication │ │ │ │ • Interest-based querying │ │ │ └──────────────────────────────────────────────────────┘ │ │ ▲ │ │ │ extends │ │ ┌────────┴─────────────────────────────────────┐ │ │ │ DataProcessor │ Scraper │ BillingAgent │ ... │ │ │ └──────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────────┘
Use Cases
- Scheduled batch processing (ETL, data migrations)
- Web scraping and content aggregation
- Event-driven workflows (billing, notifications)
- Background jobs and long-running tasks
- Report generation and analytics
- Data synchronization between systems
- Content moderation and analysis
Installation
Using pnpm (recommended)
pnpm add @happyvertical/smrt-agentsUsing npm
npm install @happyvertical/smrt-agentsUsing bun
bun add @happyvertical/smrt-agentsPeer Dependencies
svelte@^5.0.0(optional, for UI components)
Quick Start (5 Minutes)
1. Create Your First Agent
import { Agent } from '@happyvertical/smrt-agents';
import { smrt } from '@happyvertical/smrt-core';
@smrt()
class DataProcessorAgent extends Agent {
protected config = {
batchSize: 100,
maxRetries: 3
};
itemsProcessed: number = 0;
lastRunAt?: Date;
async run(): Promise<void> {
this.logger.info('Starting data processing');
const items = await this.fetchDataBatch();
for (const item of items) {
await this.processItem(item);
this.itemsProcessed++;
}
this.lastRunAt = new Date();
await this.save(); // Persist state
this.logger.info('Processed ' + this.itemsProcessed + ' items');
}
private async fetchDataBatch() {
// Your implementation
return [];
}
private async processItem(item: any) {
// Your implementation
}
}2. Execute the Agent
const agent = new DataProcessorAgent({
name: 'data-processor-1'
});
await agent.execute();
// initialize() → validate() → run()3. Query Agent State
// Agents are persisted as SmrtObjects
const agent = await DataProcessorAgent.findBy({ name: 'data-processor-1' });
console.log(agent.status); // 'idle', 'running', 'error', etc.
console.log(agent.itemsProcessed); // 150
console.log(agent.lastRunAt); // 2026-01-12T10:30:00.000ZCore Concepts
1. Agent Lifecycle
execute() calls:
initialize() ──► validate() ──► run() ──► [idle]
(idle) (validates) (running) │
│ │
└─► [error] ◄───────────────────────── ┘
shutdown() ◄── (on SIGTERM/SIGINT)
(shutdown)
Lifecycle Methods
- initialize(): Setup phase - connect to external services, load dependencies
- validate(): Validate configuration and prerequisites
- run(): Main execution logic (abstract - must implement)
- shutdown(): Cleanup - close connections, clear timers
2. Agent Status (5 States)
| Status | Description |
|---|---|
idle | Agent created, not running |
initializing | initialize() in progress |
running | run() executing |
error | Exception occurred during execution |
shutdown | Graceful shutdown in progress |
3. Configuration Management
Three-layer configuration with priority order:
- Database-persisted config (highest): User-modified via admin panels
- File-based config: From smrt.config.js or environment
- Agent class defaults: Hardcoded defaults in constructor
import { getModuleConfig } from '@happyvertical/smrt-config';
@smrt()
class MyAgent extends Agent {
// Layer 3: Defaults
protected config = getModuleConfig('my-agent', {
enabled: true,
timeout: 30000
});
async run() {
// Layer 1+2+3 merged
const merged = await this.getMergedConfig();
console.log(merged.timeout); // From DB, file, or default
}
}4. State Persistence
- Any public property on Agent is automatically persisted to database
- Agents share a single
agentstable using Single Table Inheritance (STI) - Call
await this.save()to persist state changes
@smrt()
class TrackingAgent extends Agent {
// These properties auto-persist
totalItems: number = 0;
lastRunAt?: Date;
errors: Array<{ message: string; at: Date }> = [];
async run() {
this.totalItems = 100;
this.lastRunAt = new Date();
await this.save(); // Write to database
}
}DispatchBus Communication
Agents communicate asynchronously via DispatchBus, an event-driven messaging system with persistent subscriptions and wildcard pattern matching.
Dispatch Lifecycle
emit() ──► pending ──► process() ──► processing ──► completed
│
└──► failed ──► retry() ──► pending
Subscription Types
1. In-Memory Handlers (immediate)
const bus = await agent.getDispatch();
// Called synchronously when dispatch is emitted
bus.on('campaign.completed', async (payload, metadata) => {
console.log('Campaign completed:', payload);
});
// Fire-and-forget (errors logged, not propagated)
await bus.emit('campaign.completed', { campaignId: '123' });2. Persistent Subscriptions (deferred)
const bus = await agent.getDispatch();
// Create persistent subscription (stored in database)
await bus.subscribe({
signalType: 'campaign.*',
subscriber: 'BillingAgent'
});
// Process pending dispatches for this agent
await agent.processDispatches();
// Implement handler in agent class
async handleDispatch(payload: unknown, metadata: DispatchMetadata) {
if (metadata.type === 'campaign.completed') {
const data = payload as { campaignId: string; revenue: number };
await this.recordRevenue(data);
}
}Wildcard Pattern Matching
| Pattern | Matches |
|---|---|
campaign.* | campaign.started, campaign.completed |
agent.*.completed | agent.suasor.completed, agent.fiscus.completed |
* | All events (one segment only, not dots) |
*.*.completed | Multi-level events with 'completed' suffix |
CLI Commands
# List dispatches with filters
smrt dispatch:list --status pending --source Suasor
# Process pending for specific agent
smrt dispatch:process --subscriber Fiscus
# Retry failed dispatches
smrt dispatch:retry --max-attempts 3
# Cleanup old dispatches
smrt dispatch:cleanup --completed-older-than 30Interest-Based Queries
The interests system provides a declarative way to query objects the agent is
interested in, with filters, sorting, limiting, and custom handlers.
Basic Interest Configuration
constructor(options: AgentOptions = {}) {
super({
...options,
interests: {
filter: { status: 'active' },
sort: 'createdAt DESC',
objects: {
Meeting: {
name: 'upcoming',
filter: { 'scheduledAt >': new Date() },
sort: 'scheduledAt ASC',
limit: 10,
handler: async (meeting, agent) => ({
action: 'analyze',
meeting,
url: 'https://example.com/meetings/' + meeting.id
})
}
}
}
});
}
async run() {
const items = await this.interesting();
for (const { type, data, name, handled } of items) {
this.logger.info('Processing ' + type, {
name,
action: handled?.action,
id: data.id
});
if (handled?.action === 'analyze') {
await this.analyzeItem(data);
}
}
}Multiple Filters per Class
objects: {
Document: [
{
name: 'needs-review',
filter: { status: 'pending' }
},
{
name: 'expired',
query: (t) => [
t + '.expires_at < datetime(',
[]
]
},
{
name: 'high-priority',
filter: { priority: 'high' },
sort: 'createdAt DESC',
limit: 5
}
]
}Custom SQL Queries
objects: {
Invoice: {
query: (t) => [
t + '.amount > ? AND ' + t + '.status = ?',
[1000, 'unpaid']
]
}
}Integration with Other Modules
smrt-core
- SmrtObject: Agent extends SmrtObject for database persistence
- DispatchBus: Inter-agent communication via core/dispatch
- ObjectRegistry: Class discovery for interest queries
smrt-config
import { getModuleConfig } from '@happyvertical/smrt-config';
@smrt()
class MyAgent extends Agent {
protected config = getModuleConfig('my-agent', {
enabled: true,
timeout: 30000
});
}smrt-cli
# Dispatch management
smrt dispatch:list --status pending
smrt dispatch:process --subscriber MyAgent
smrt dispatch:cleanup --completed-older-than 7
# Agent execution (if registered)
smrt agent:run --name MyAgentIntegration Pattern: Agent + DispatchBus + Collection
DataProcessorAgent
├─► emits: data.processed
└─► subscribed to: data.ready
BillingAgent
├─► listens for: data.processed
└─► emits: billing.updated
ReportAgent
├─► listens for: billing.updated
├─► queries Meetings (interests)
└─► emits: report.generated
Best Practices
1. Always Call Super Methods
async initialize(): Promise<this> {
await super.initialize(); // Critical!
// Your initialization...
return this;
}
async shutdown(): Promise<void> {
this.logger.info('Cleaning up');
await super.shutdown(); // Clean up signal handlers
}2. Persist State Regularly
async run(): Promise<void> {
for (const item of items) {
await this.process(item);
this.itemsProcessed++;
await this.save(); // Save after each batch/item
}
}3. Handle Errors Gracefully
async run(): Promise<void> {
try {
await this.riskyOperation();
} catch (error) {
this.logger.error('Operation failed', { error });
this.errors.push({ message: error.message, at: new Date() });
await this.save();
throw error; // Let status change to 'error'
}
}4. Use Type-Safe Config
interface MyAgentConfig {
batchSize: number;
apiUrl: string;
}
@smrt()
class MyAgent extends Agent {
protected config!: MyAgentConfig; // Override base type
protected override getDefaultConfig(): MyAgentConfig {
return {
batchSize: 100,
apiUrl: 'https://api.example.com'
};
}
}5. Leverage Interests for Reactive Behavior
constructor(options: AgentOptions = {}) {
super({
...options,
interests: {
objects: {
Invoice: {
filter: { status: 'unpaid' },
handler: async (invoice) => ({
action: 'send-reminder',
priority: invoice.amount > 1000 ? 'high' : 'normal'
})
}
}
}
});
}
async run(): Promise<void> {
const items = await this.interesting();
for (const { data, handled } of items) {
if (handled?.action === 'send-reminder') {
await this.sendReminder(data, handled.priority);
}
}
}