s-m-r-t

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

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, 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 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 = { 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);
    }
  }
}
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

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