s-m-r-t

@happyvertical/smrt-agents

Build autonomous actors with persistent state, inter-agent communication via DispatchBus, and comprehensive lifecycle management.

v0.19.0AgentsAutomation

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)

bash
pnpm add @happyvertical/smrt-agents

Using npm

bash
npm install @happyvertical/smrt-agents

Using bun

bash
bun add @happyvertical/smrt-agents

Peer Dependencies

  • svelte@^5.0.0 (optional, for UI components)

Quick Start (5 Minutes)

1. Create Your First Agent

typescript
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

typescript
const agent = new DataProcessorAgent({
  name: 'data-processor-1'
});

await agent.execute();
// initialize() → validate() → run()

3. Query Agent State

typescript
// 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.000Z

Core 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)

StatusDescription
idleAgent created, not running
initializinginitialize() in progress
runningrun() executing
errorException occurred during execution
shutdownGraceful shutdown in progress

3. Configuration Management

Three-layer configuration with priority order:

  1. Database-persisted config (highest): User-modified via admin panels
  2. File-based config: From smrt.config.js or environment
  3. Agent class defaults: Hardcoded defaults in constructor
typescript
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 agents table using Single Table Inheritance (STI)
  • Call await this.save() to persist state changes
typescript
@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)

typescript
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)

typescript
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

PatternMatches
campaign.*campaign.started, campaign.completed
agent.*.completedagent.suasor.completed, agent.fiscus.completed
*All events (one segment only, not dots)
*.*.completedMulti-level events with 'completed' suffix

CLI Commands

bash
# 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 30

Interest-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

typescript
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

typescript
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

typescript
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

typescript
import { getModuleConfig } from '@happyvertical/smrt-config';

@smrt()
class MyAgent extends Agent {
  protected config = getModuleConfig('my-agent', {
    enabled: true,
    timeout: 30000
  });
}

smrt-cli

bash
# 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 MyAgent

Integration 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

typescript
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

typescript
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

typescript
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

typescript
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

typescript
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);
    }
  }
}