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, type AgentOptions } from '@happyvertical/smrt-agents';
import { smrt } from '@happyvertical/smrt-core';
import { getModuleConfig } from '@happyvertical/smrt-config';

@smrt()
class DataProcessor extends Agent {
  protected config = getModuleConfig('data-processor', {
    batchSize: 100,
    maxRetries: 3
  });

  // State properties (auto-persisted)
  lastProcessedId: string = '';
  itemsProcessed: number = 0;

  async run(): Promise<void> {
    // Main agent logic
  }
}

Key characteristics:

  • Abstract config property must be defined by subclasses
  • All properties auto-persist to database via SmrtObject
  • Uses Single Table Inheritance (STI) - all agents share the agents table
  • Must apply @smrt() decorator on subclasses
  • Automatic SIGTERM/SIGINT signal handling for graceful shutdown

Agent Lifecycle

┌──────────────────────────────────────────────────────────┐
│                     execute()                            │
│                                                          │
│   initialize() ──► validate() ──► run() ──► [idle]     │
│        │                            │                    │
│        │                            ▼                    │
│        │                        [error]                  │
│                                                          │
│   shutdown() ◄─── SIGTERM/SIGINT                        │
└──────────────────────────────────────────────────────────┘
MethodPurpose
initialize()Setup signal handlers, prepare resources
validate()Check configuration and dependencies
run()Main agent logic (abstract, must implement)
shutdown()Cleanup resources, remove signal handlers
execute()Orchestrates full lifecycle
@smrt()
class WebScraper extends Agent {
  protected config = getModuleConfig('web-scraper', {
    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(); // Cleans up signal handlers
  }
}

// Execute
const agent = new WebScraper({ name: 'scraper-1' });
await agent.execute();

Signal Handling (Graceful Shutdown)

Agents automatically register SIGTERM and SIGINT handlers during initialize(). When a signal is received, the agent transitions to shutdown status and calls shutdown() for cleanup. Signal handlers are automatically removed during shutdown.

@smrt()
class LongRunningAgent extends Agent {
  protected config = {};

  async run(): Promise<void> {
    while (this.status !== 'shutdown') {
      await this.processNextBatch();
      await this.save(); // Persist progress
    }
  }

  async shutdown(): Promise<void> {
    this.logger.info('Graceful shutdown initiated');
    // Finish current work, flush buffers, etc.
    await super.shutdown(); // Always call super to clean up signal handlers
  }
}

Agent Status

type AgentStatusType = 'idle' | 'initializing' | 'running' | 'error' | 'shutdown';

Status transitions automatically during lifecycle:

  • idle - Agent created, not running
  • initializing - initialize() in progress
  • running - run() executing
  • error - Exception occurred
  • shutdown - Graceful shutdown in progress

Agent State

Agent state is persisted via SmrtObject inheritance:

@smrt()
class Crawler extends Agent {
  protected config = getModuleConfig('crawler', { 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
  }
}

TenantAgent -- Multi-Tenant Bindings

The TenantAgent model provides a junction table (tenant_agents) binding agents to tenants with permission overrides and hierarchy resolution:

  • Explicit binding: Row exists for tenant (source: 'explicit')
  • Inherited: Walks up tenant hierarchy (source: 'inherited')
  • Permissions: Manifest defaults merged with per-tenant overrides
import { TenantAgent, TenantAgentCollection } from '@happyvertical/smrt-agents';

// Check if agent is available for a tenant
const tenantAgents = await TenantAgentCollection.create({ db: 'app.db' });
const binding = await tenantAgents.list({
  where: { agentType: 'Praeco', tenantId: 'tenant-123' }
});

AgentSchedule

Cron-based scheduling stored in the _smrt_agent_schedules table. Executed by ScheduleRunner from smrt-jobs.

import { AgentSchedule, AgentScheduleCollection } from '@happyvertical/smrt-agents';

// Fields: agentType, cron, method (default: 'run'),
//         maxConcurrent, timeout

AgentConfig -- DB-Persisted Configuration

The AgentConfig model stores slot-based configuration in the database, merged with file-based config at runtime:

// 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);
    }
  }
}
MethodPurpose
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

OptionTypePurpose
filterRecord<string, any>SQL filter with operators
query(tableName) => [sql, params]Custom SQL for complex patterns
sortstring | string[]ORDER BY clause
limitnumberMax results
qualify(items) => Promise<items>Post-filter async processing
handler(item, agent) => anyAction 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:

ColumnTypeDescription
idTEXTUnique identifier
agentIdTEXTAgent instance ID
agentClassTEXTAgent class name
slotIdTEXTUI slot ID
configDataJSONConfiguration data
schemaVersionINTEGERSchema version

_smrt_agent_schedules

Stores agent schedule definitions:

ColumnTypeDescription
agentTypeTEXTAgent class name
cronTEXTCron expression
methodTEXTMethod to invoke (default: 'run')
maxConcurrentINTEGERMax concurrent executions
timeoutINTEGERExecution timeout (ms)

tenant_agents

Junction table binding agents to tenants:

ColumnTypeDescription
agentTypeTEXTAgent class name
tenantIdTEXTTenant ID
permissionsJSONPer-tenant permission overrides
statusTEXTBinding status

Best Practices

1. Always Call super Methods

async initialize(): Promise<this> {
  await super.initialize(); // Sets up signal handlers
  // Your initialization...
  return this;
}

async shutdown(): Promise<void> {
  // Your cleanup...
  await super.shutdown(); // Removes signal handlers
}

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'
  }
}

4. Use getModuleConfig() for Configuration

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

@smrt()
class MyAgent extends Agent {
  // Loads from smrt.config.ts modules.my-agent section
  protected config = getModuleConfig('my-agent', {
    cronSchedule: '0 2 * * *',
    maxRetries: 3
  });
}