Building Always-On AI Agents: From Passive Chatbots to Autonomous Services

Building Always-On AI Agents: From Passive Chatbots to Autonomous Services

Learn how to build persistent, reactive AI agents that monitor events and take autonomous action. This tutorial covers architectural patterns for always-on agents using the Registry Broker's polling and session management capabilities.

5 min read1,047 words

Most agent tutorials build simple chatbots: send message, get response. This works for demos, but production infrastructure needs to run autonomously.

Service Agents are long-running processes that monitor data and take action without human input. They don't wait for prompts; they wait for events.

In this guide, we'll architect a production-ready service agent using the Registry Broker's session management and polling capabilities.

Understanding Agent Lifecycle Models

Before building, let's understand the fundamental difference between agent architectures:

Request-Response Agents

  • Wake up when called
  • Process one request
  • Return a response
  • Effectively "die" until the next request
  • This is how most web APIs work—stateless, ephemeral, horizontal-scalable but fundamentally passive.

    Service Agents (Always-On)

  • Boot once and run indefinitely
  • Maintain persistent state
  • Monitor for events continuously
  • React autonomously to triggers
  • Service agents act more like database servers or message brokers than web handlers. They require different architectural thinking.

    The Core Pattern: Event Loop with Polling

    The Registry Broker doesn't provide push notifications for new messages (WebSocket subscriptions are for real-time chat, not general event streaming). Instead, service agents use a polling pattern to check for new activity.

    Here's the foundational structure:

    
      RegistryBrokerClient,
      type ChatHistoryEntry 
    } from '@hashgraphonline/standards-sdk';
    

    interface SessionState {
    lastChecked: number;
    messageCount: number;
    }

    class ServiceAgent {
    private client: RegistryBrokerClient;
    private myUaid: string;
    private activeSessions: Map = new Map();
    private running: boolean = false;

    constructor(uaid: string, brokerUrl: string) {
    this.myUaid = uaid;
    this.client = new RegistryBrokerClient({ baseUrl: brokerUrl });
    }

    async start(): Promise {
    console.log(Service agent starting: ${this.myUaid});
    console.log(Online at ${new Date().toISOString()});

    this.running = true;

    while (this.running) {
    try {
    await this.pollActiveSessions();
    } catch (error) {
    console.error('Polling error:', error);
    // Continue running despite errors
    }

    // Wait before next poll cycle
    await this.delay(2000);
    }
    }

    stop(): void {
    console.log('Service agent shutting down...');
    this.running = false;
    }

    private async pollActiveSessions(): Promise {
    for (const [sessionId, state] of this.activeSessions) {
    const history = await this.client.chat.getHistory(sessionId);
    const allMessages = history.history ?? [];

    // Find messages newer than our last check
    const newMessages = allMessages.filter(
    entry => new Date(entry.timestamp).getTime() > state.lastChecked
    );

    // Process only user messages (we sent the agent ones)
    const userMessages = newMessages.filter(m => m.role === 'user');

    for (const message of userMessages) {
    await this.handleMessage(sessionId, message);
    }

    // Update state
    state.lastChecked = Date.now();
    state.messageCount = allMessages.length;
    }
    }

    async handleMessage(
    sessionId: string,
    message: ChatHistoryEntry
    ): Promise {
    // Override this in subclasses for custom behavior
    console.log(New message in ${sessionId}: ${message.content});
    }

    registerSession(sessionId: string): void {
    if (!this.activeSessions.has(sessionId)) {
    this.activeSessions.set(sessionId, {
    lastChecked: Date.now(),
    messageCount: 0,
    });
    console.log(Tracking session: ${sessionId});
    }
    }

    private delay(ms: number): Promise {
    return new Promise(resolve => setTimeout(resolve, ms));
    }
    }

    This base class provides the event loop infrastructure. Subclass it to add domain-specific behavior.

    Example: The Sentinel Agent

    Let's build a "Sentinel" agent that monitors for critical alerts and triggers external systems:

    class SentinelAgent extends ServiceAgent {
      private alertThreshold: number;
      
      constructor(
        uaid: string, 
        brokerUrl: string, 
        alertThreshold: number = 5
      ) {
        super(uaid, brokerUrl);
        this.alertThreshold = alertThreshold;
      }
    

    async handleMessage(
    sessionId: string,
    message: ChatHistoryEntry
    ): Promise {
    const content = message.content.toUpperCase();

    console.log([SENTINEL] Analyzing: ${content.substring(0, 50)}...);

    // Pattern matching for critical signals
    if (this.isCriticalSignal(content)) {
    console.warn('>>> CRITICAL SIGNAL DETECTED');
    await this.triggerEmergencyProtocol(sessionId, content);
    } else if (this.isRoutineCheck(content)) {
    await this.acknowledgeRoutine(sessionId);
    }
    }

    private isCriticalSignal(content: string): boolean {
    const criticalPatterns = [
    'CRITICAL_FAILURE',
    'SECURITY_BREACH',
    'PRICE_DUMP',
    'SYSTEM_DOWN',
    'UNAUTHORIZED_ACCESS',
    ];
    return criticalPatterns.some(pattern => content.includes(pattern));
    }

    private isRoutineCheck(content: string): boolean {
    return content.includes('STATUS') || content.includes('HEALTH_CHECK');
    }

    private async triggerEmergencyProtocol(
    sessionId: string,
    content: string
    ): Promise {
    // 1. Log the incident
    console.log(Emergency triggered at ${new Date().toISOString()});
    console.log(Content: ${content});

    // 2. Call external alerting system
    await this.notifyPagerDuty(content);

    // 3. Send acknowledgment through the broker
    await this.client.chat.sendMessage({
    sessionId: sessionId,
    message: 'ACK. Emergency protocols initiated. On-call team summoned.',
    });
    }

    private async acknowledgeRoutine(sessionId: string): Promise {
    const status = {
    status: 'healthy',
    uptime: process.uptime(),
    timestamp: new Date().toISOString(),
    activeSessions: this.activeSessions.size,
    };

    await this.client.chat.sendMessage({
    sessionId,
    message: Status report: ${JSON.stringify(status)},
    });
    }

    private async notifyPagerDuty(content: string): Promise {
    // In production, integrate with your actual alerting system
    console.log([PAGERDUTY] Sending alert: ${content.substring(0, 100)});

    // Example: fetch('https://events.pagerduty.com/v2/enqueue', { ... })
    }
    }

    Running the Service Agent

    Deploy the agent as a long-running process:

    
    

    async function main() {
    const agent = new SentinelAgent(
    process.env.AGENT_UAID!,
    process.env.REGISTRY_BROKER_BASE_URL ?? 'https://hol.org/registry/api/v1',
    );

    // Register sessions to monitor
    // In practice, these come from your session management layer
    const sessionsToMonitor = process.env.MONITOR_SESSIONS?.split(',') ?? [];
    for (const sessionId of sessionsToMonitor) {
    agent.registerSession(sessionId.trim());
    }

    // Handle shutdown gracefully
    process.on('SIGTERM', () => agent.stop());
    process.on('SIGINT', () => agent.stop());

    // Start the event loop
    await agent.start();
    }

    main().catch(console.error);

    Production Deployment Considerations

    Containerization

    Service agents should run in containers for reliability:

    FROM node:20-alpine
    WORKDIR /app
    COPY package*.json ./
    RUN npm ci --only=production
    COPY dist ./dist
    CMD ["node", "dist/sentinel.js"]
    

    Scaling

    Because the Registry Broker maintains session state, you can run multiple agent instances:

    1. Session sharding: Different instances monitor different session ranges
    2. Leader election: Use Redis or etcd to elect a primary instance
    3. Stateless processing: Each poll is independent, enabling horizontal scaling

    Monitoring

    Add observability to your agent:

    // Emit metrics
    setInterval(() => {
      console.log(JSON.stringify({
        type: 'agent_metrics',
        activeSessions: activeSessions.size,
        uptimeSeconds: process.uptime(),
        memoryMB: process.memoryUsage().heapUsed / 1024 / 1024,
        timestamp: new Date().toISOString(),
      }));
    }, 60000);
    

    Error Recovery

    Service agents must handle failures gracefully:

    async pollWithRetry(sessionId: string, retries: number = 3): Promise {
      for (let attempt = 0; attempt < retries; attempt++) {
        try {
          return await this.pollSession(sessionId);
        } catch (error) {
          console.warn(Poll attempt ${attempt + 1} failed:, error);
          await this.delay(1000 * (attempt + 1)); // Exponential backoff
        }
      }
      console.error(Session ${sessionId} unreachable after ${retries} attempts);
    }
    

    Use Cases for Service Agents

    DeFi Liquidation Bot

  • Monitors price feeds from oracles
  • Detects undercollateralized positions
  • Executes liquidation transactions automatically
  • Customer Support Triage

  • Listens to incoming support conversations
  • Classifies urgency using an LLM
  • Routes to appropriate human agents
  • Handles simple queries autonomously
  • Security Monitor

  • Analyzes logs from other agents
  • Detects anomalous patterns
  • Triggers lockdown procedures
  • Generates incident reports
  • Workflow Orchestrator

  • Monitors for task completion signals
  • Triggers next steps in pipelines
  • Handles retries and failures
  • Maintains workflow state
  • From Chatbot to Infrastructure

    Passive chatbots answer questions when asked. Service agents are infrastructure—they run continuously, monitor actively, and act autonomously. Moving from request-response to event-loop processing enables a new class of AI applications.

    The Registry Broker handles the session management and message routing. Your agent provides the intelligence. Together, they create autonomous systems.

    More from the blog