DEV Community

Cover image for Building Always-On AI Agents: From Passive Chatbots to Autonomous Services
Michael Kantor for HOL (Hashgraph Online)

Posted on Originally published at hol.org

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

Originally published at HOL

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<string, SessionState> = new Map();
  private running: boolean = false;
  constructor(uaid: string, brokerUrl: string) {
    this.myUaid = uaid;
    this.client = new RegistryBrokerClient({ baseUrl: brokerUrl });
  }
  async start(): Promise<void> {
    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(&#39;Polling error:&#39;, 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<void> {
    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 =&gt; new Date(entry.timestamp).getTime() &gt; state.lastChecked
  );

  // Process only user messages (we sent the agent ones)
  const userMessages = newMessages.filter(m =&gt; m.role === &#39;user&#39;);

  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<void> {
    // 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});
    }
  }
Enter fullscreen mode Exit fullscreen mode

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<void> {
    const content = message.content.toUpperCase();
console.log(`[SENTINEL] Analyzing: ${content.substring(0, 50)}...`);

// Pattern matching for critical signals
if (this.isCriticalSignal(content)) {
  console.warn(&#39;&gt;&gt;&gt; CRITICAL SIGNAL DETECTED&#39;);
  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<void> {
    // 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: &#39;ACK. Emergency protocols initiated. On-call team summoned.&#39;,
});

  }
  private async acknowledgeRoutine(sessionId: string): Promise<void> {
    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<void> {
    // In production, integrate with your actual alerting system
    console.log([PAGERDUTY] Sending alert: ${content.substring(0, 100)});
// Example: fetch(&#39;https://events.pagerduty.com/v2/enqueue&#39;, { ... })
Enter fullscreen mode Exit fullscreen mode

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&#39;,
  );
  // 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();
}
Enter fullscreen mode Exit fullscreen mode

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"]
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

Error Recovery

Service agents must handle failures gracefully:

async pollWithRetry(sessionId: string, retries: number = 3): Promise<void> {
  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`);
}
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (0)