SMRT Agents
The Agent framework provides a base class for building autonomous actors in the SMRT ecosystem. Agents extend SmrtObject, inheriting automatic database persistence, AI-powered methods, and code generation capabilities.
Agent Class
Agents are designed for long-running processes, scheduled tasks, and autonomous operations requiring state management.
import { Agent } from '@happyvertical/smrt-agents';
import { smrt } from '@happyvertical/smrt-core';
@smrt()
class DataProcessor extends Agent {
protected config = {
batchSize: 100,
maxRetries: 3
};
// State properties (auto-persisted)
lastProcessedId: string = '';
itemsProcessed: number = 0;
async run(): Promise<void> {
// Main agent logic
}
} Key characteristics:
- Abstract
configproperty must be defined by subclasses - All properties auto-persist to database via SmrtObject
- Uses Single Table Inheritance (STI) - all agents share the
agentstable - Must apply
@smrt()decorator on subclasses
Agent Lifecycle
┌──────────────────────────────────────────────────────────┐
│ execute() │
│ │
│ initialize() ──► validate() ──► run() ──► [idle] │
│ │ │ │
│ │ ▼ │
│ │ [error] │
│ │
│ shutdown() ◄─── SIGTERM/SIGINT │
└──────────────────────────────────────────────────────────┘ | Method | Purpose |
|---|---|
initialize() | Setup signal handlers, prepare resources |
validate() | Check configuration and dependencies |
run() | Main agent logic (abstract, must implement) |
shutdown() | Cleanup resources, handle graceful termination |
execute() | Orchestrates full lifecycle |
@smrt()
class WebScraper extends Agent {
protected config = { targetUrl: 'https://example.com' };
async initialize(): Promise<this> {
await super.initialize();
this.logger.info('Initializing scraper');
return this;
}
async validate(): Promise<void> {
if (!this.config.targetUrl) {
throw new Error('targetUrl is required');
}
}
async run(): Promise<void> {
// Scraping logic
await this.save(); // Persist state
}
async shutdown(): Promise<void> {
this.logger.info('Cleaning up');
await super.shutdown();
}
}
// Execute
const agent = new WebScraper({ name: 'scraper-1' });
await agent.execute(); Agent Status
type AgentStatusType = 'idle' | 'initializing' | 'running' | 'error' | 'shutdown'; Status transitions automatically during lifecycle:
idle- Agent created, not runninginitializing- initialize() in progressrunning- run() executingerror- Exception occurredshutdown- Graceful shutdown in progress
Agent State
Agent state is persisted via SmrtObject inheritance:
@smrt()
class Crawler extends Agent {
protected config = { maxPages: 50 };
// These persist to database
lastCrawledUrl: string = '';
pagesProcessed: number = 0;
errors: Array<{ url: string; error: string }> = [];
async run(): Promise<void> {
this.lastCrawledUrl = 'https://example.com';
this.pagesProcessed += 1;
await this.save(); // Persist changes
}
} Configuration Storage
Agents support slot-based configuration:
// Save config for a UI slot
await agent.saveSlotConfig('sources', {
scrapers: ['civicweb', 'govstack'],
refreshInterval: 3600
});
// Load merged config (file + db)
const config = await agent.getMergedConfig('sources');
// Export all config (for static builds)
const exported = await agent.exportConfig({ includeSecrets: false }); Agent Communication
Agents communicate via the DispatchBus. Built-in methods:
class Fiscus extends Agent {
protected config = {};
async processIncomingDispatches(): Promise<void> {
const bus = await this.getDispatch();
// Subscribe to events
await bus.subscribe({
signalType: 'campaign.*',
subscriber: this.constructor.name
});
// Process pending dispatches
await this.processDispatches();
}
// Override to handle dispatches
async handleDispatch(payload: unknown, metadata: DispatchMetadata): Promise<void> {
if (metadata.type === 'campaign.completed') {
await this.recordRevenue(payload);
}
}
} | Method | Purpose |
|---|---|
getDispatch() | Get or create DispatchBus instance |
handleDispatch(payload, metadata) | Override to process incoming dispatches |
processDispatches() | Process all pending dispatches for this agent |
Agent Interests
Agents can declaratively query objects they're interested in:
const agent = new MyAgent({
name: 'my-agent',
interests: {
filter: { status: 'active' },
sort: 'created_at DESC',
objects: {
Meeting: {
filter: { 'scheduled_at >': new Date() },
sort: 'scheduled_at ASC',
limit: 10,
handler: async (meeting, agent) => ({
action: 'recap',
meeting
})
},
Document: [
{
name: 'needs-review',
filter: { status: 'pending' }
},
{
name: 'expired',
query: (t) => [
`${t}.expires_at < datetime('now')`,
[]
]
}
]
}
}
});
// Query all interesting items
const items = await agent.interesting();
for (const { type, data, name, handled } of items) {
console.log(`${type} from "${name}": action=${handled?.action}`);
} Interest Filter Options
| Option | Type | Purpose |
|---|---|---|
filter | Record<string, any> | SQL filter with operators |
query | (tableName) => [sql, params] | Custom SQL for complex patterns |
sort | string | string[] | ORDER BY clause |
limit | number | Max results |
qualify | (items) => Promise<items> | Post-filter async processing |
handler | (item, agent) => any | Action for each matched item |
Agent UI Slots
Agents declare admin panel slots for host applications:
@smrt()
class Praeco extends Agent {
static override uiSlots: AgentUISlots = {
sources: {
id: 'sources',
label: 'News Sources',
description: 'Configure scrapers and data sources',
icon: 'database',
order: 1
},
settings: {
id: 'settings',
label: 'Agent Settings',
icon: 'settings',
order: 2
}
};
protected config = {};
async run(): Promise<void> {}
}
// Host app registers Svelte components
import { AgentUIRegistry } from '@happyvertical/smrt-agents/ui';
AgentUIRegistry.register('Praeco', 'sources', SourcesPanel); Internal Tables
agent_configs
Stores agent slot configurations:
| Column | Type | Description |
|---|---|---|
id | TEXT | Unique identifier |
agentId | TEXT | Agent instance ID |
agentClass | TEXT | Agent class name |
slotId | TEXT | UI slot ID |
configData | JSON | Configuration data |
schemaVersion | INTEGER | Schema version |
Best Practices
1. Always Call super Methods
async initialize(): Promise<this> {
await super.initialize(); // Important!
// Your initialization...
return this;
} 2. Persist State Regularly
async run(): Promise<void> {
for (const item of items) {
await this.process(item);
this.itemsProcessed += 1;
await this.save(); // Persist after each item
}
} 3. Handle Errors Gracefully
async run(): Promise<void> {
try {
await this.doWork();
} catch (error) {
this.errors.push({ message: error.message, at: new Date() });
await this.save();
throw error; // Re-throw to set status to 'error'
}
}