Jeff's Journal

Building Hermes Mission Control: Real-Time Visual Observability for Agent Swarms

How we built a cinematic, real-time command center for Hermes agent systems using Next.js, React Flow, and Server-Sent Events. From the data model to the live agent graph, here's the full architecture.

J

Jeff

Windows & Microsoft Ecosystem

Building Hermes Mission Control: Real-Time Visual Observability for Agent Swarms

Introduction

When you're running a swarm of autonomous AI agents, the hardest question isn't "can it work?" — it's "what is it doing right now?"

In traditional software, you have logs, metrics, and dashboards. In agent systems, you have a black box making decisions, calling tools, evolving skills, and communicating with other agents — all at a pace and complexity that's nearly impossible to track through log files alone.

Hermes Mission Control is our answer to that problem. It's a real-time, visual command center that makes the invisible work of agent swarms visible, inspectable, and steerable. This post walks through the complete architecture, data model, and implementation details — and shows you how to build your own.

The Problem: Agent Observability Gap

Consider a typical Hermes deployment:

  • 8-12 active agents working in parallel across different domains (coding, research, testing, orchestration)
  • 50+ skills that are continuously created, improved, branched, and deprecated
  • Thousands of tool calls per hour, with complex dependency chains
  • Real-time memory updates as agents learn and share knowledge
  • Dynamic task allocation where agents pick up, complete, and spawn new tasks

Traditional observability tools — Prometheus dashboards, log aggregators, tracing systems — are designed for deterministic, request-response systems. They break down when applied to agent swarms because:

  1. Non-linear causality: An agent's decision may depend on memory written 10 minutes ago by a different agent
  2. Emergent behavior: The system's overall behavior can't be predicted from individual agent behavior
  3. Continuous evolution: Skills and strategies change over time, making static dashboards obsolete
  4. Human-in-the-loop: Operators need to understand, intervene, and guide — not just monitor

Architecture Overview

Mission Control follows a local-first, real-time architecture:

┌─────────────────────────────────────────────────────────┐
│                    Browser (Next.js)                    │
│                                                         │
│  ┌─────────────┐  ┌─────────────┐  ┌─────────────────┐ │
│  │ Agent Graph │  │ Skill Tree  │  │ Activity Feed   │ │
│  │ (React Flow)│  │ (Timeline)  │  │ (SSE Stream)    │ │
│  └─────────────┘  └─────────────┘  └─────────────────┘ │
│                                                         │
│  ┌─────────────────────────────────────────────────────┐│
│  │                 System Metrics                      ││
│  │  (Active agents, success rates, resource usage)      ││
│  └─────────────────────────────────────────────────────┘│
└─────────────────────────────────────────────────────────┘
         │                          │
         ▼                          ▼
┌─────────────────────┐  ┌──────────────────────┐
│  REST API Routes    │  │  SSE Event Stream    │
│  (Next.js API)      │  │  (Real-time updates) │
└─────────────────────┘  └──────────────────────┘
         │                          │
         ▼                          ▼
┌─────────────────────────────────────────────────────────┐
│                    Mock Data Store                       │
│  (Event emitter pattern for real-time simulation)        │
│  (Replaceable with live Hermes instance connection)      │
└─────────────────────────────────────────────────────────┘

Tech Stack

Layer Technology Why
Framework Next.js 16 (App Router) SSR, API routes, deployment simplicity
Visualization React Flow Production-grade graph rendering, zoom, pan
Real-time Server-Sent Events (SSE) Simpler than WebSockets, HTTP-compatible, auto-reconnect
Styling Tailwind CSS Rapid iteration, consistent design system
Icons lucide-react Clean, consistent iconography
Type Safety TypeScript Critical for data model integrity

Data Model: The Foundation

The heart of Mission Control is its data model. Everything — the graph, the timeline, the metrics — flows from these interfaces:

interface Agent {
  id: string;
  name: string;
  type: 'orchestrator' | 'worker' | 'researcher' | 'coder';
  status: 'idle' | 'thinking' | 'tool_calling' | 'success' | 'failed' | 'evolving';
  currentTask: string | null;
  skills: string[];
  createdAt: string;
  lastActive: string;
}

interface Skill {
  id: string;
  name: string;
  description: string;
  version: string;
  author: 'agent' | 'human';
  status: 'active' | 'evolving' | 'deprecated';
  performance: {
    usageCount: number;
    successRate: number;
    avgExecutionTime: number;
  };
  versions: SkillVersion[];
}

interface SystemEvent {
  id: string;
  timestamp: string;
  type: 'agent_started' | 'agent_completed' | 'skill_created' | 
        'skill_evolved' | 'task_started' | 'task_completed' | 
        'tool_called' | 'error' | 'goal_injected';
  agentId: string | null;
  skillId: string | null;
  taskId: string | null;
  message: string;
  details: Record<string, any>;
}

interface GraphNode {
  id: string;
  type: 'agent' | 'skill';
  name: string;
  status: string;
  agentType?: string;
  description?: string;
}

interface GraphEdge {
  id: string;
  source: string;
  target: string;
  type: 'communication' | 'tool_use' | 'memory_access' | 'skill_inheritance';
  label?: string;
}

Key Design Decisions

  1. Separation of concerns: Agents, skills, tasks, memory, and events are separate entities with their own lifecycles. This makes the system extensible — you can add new entity types without breaking existing code.

  2. Graph as a derived view: The SkillGraph (nodes + edges) is generated from the agent/skill data, not stored independently. This ensures consistency — if an agent is deleted, its graph nodes are automatically removed.

  3. Event sourcing pattern: All state changes are recorded as events. This enables time-travel debugging, audit trails, and the activity feed.

  4. Versioned skills: Skills have a version field and can have multiple versions in their history. This supports the evolution timeline feature.

Real-Time Updates: SSE Over WebSockets

We chose Server-Sent Events (SSE) over WebSockets for real-time updates. Here's why:

  • Simpler implementation: SSE is just HTTP with a specific content type. No connection handshake, no message framing.
  • Automatic reconnection: Browsers automatically reconnect to SSE endpoints if the connection drops.
  • HTTP-compatible: Works with existing infrastructure (proxies, CDNs, load balancers).
  • Unidirectional: For Mission Control, the server only needs to push data to the client. Bidirectional communication isn't needed.

The SSE endpoint is a Next.js API route:

// src/app/api/events/stream/route.ts
export async function GET(request: NextRequest) {
  const stream = new ReadableStream({
    start(controller) {
      const send = (data: any) => {
        controller.enqueue(`data: ${JSON.stringify(data)}\n\n`);
      };

      // Send initial state
      send({ type: "init", data: getSystemState() });

      // Subscribe to state changes via event emitter
      const unsubscribe = subscribeToState((state) => {
        send({ type: "update", data: state });
      });

      // Heartbeat every 15 seconds
      const heartbeat = setInterval(() => {
        send({ type: "heartbeat", timestamp: Date.now() });
      }, 15000);

      // Cleanup on disconnect
      let cleanedUp = false;
      request.signal.addEventListener("abort", () => {
        if (cleanedUp) return;
        cleanedUp = true;
        unsubscribe();
        clearInterval(heartbeat);
        try { controller.close(); } catch (e) { /* already closed */ }
      });
    },
  });

  return new Response(stream, {
    headers: {
      "Content-Type": "text/event-stream",
      "Cache-Control": "no-cache",
      "Connection": "keep-alive",
    },
  });
}

On the client side, the React component connects via EventSource:

useEffect(() => {
  const es = new EventSource("/api/events/stream");
  
  es.onmessage = (event) => {
    const msg = JSON.parse(event.data);
    if (msg.type === "update" && msg.data) {
      setState(msg.data);
    }
  };

  return () => es.close();
}, []);

Activity Simulation

For development and demos, we simulate real-time agent activity:

export function startActivitySimulation(intervalMs: number = 3000): void {
  activityInterval = setInterval(() => {
    const skill = skills[Math.floor(Math.random() * skills.length)];
    const agent = agents[Math.floor(Math.random() * agents.length)];
    
    // Randomly update agent status
    agent.status = statuses[Math.floor(Math.random() * statuses.length)];
    agent.lastActive = new Date().toISOString();
    
    // Generate event
    const event: SystemEvent = {
      id: uuidv4(),
      timestamp: new Date().toISOString(),
      type: eventTypes[Math.floor(Math.random() * eventTypes.length)],
      agentId: agent.id,
      message: `Agent ${agent.name} ${type.replace("_", " ")}`,
      details: { duration: Math.floor(Math.random() * 5000), success: Math.random() > 0.2 },
    };
    
    store.events.unshift(event);
    broadcastChange();
  }, intervalMs);
}

The Live Agent Graph

The centerpiece of Mission Control is the interactive agent graph, built with React Flow. Here's how it works:

Custom Node Components

Each node type has a custom renderer:

function AgentNode({ data }: { data: { label: string; status: string; type: string } }) {
  const statusColors = {
    idle: "border-slate-500",
    thinking: "border-blue-400",
    tool_calling: "border-amber-400",
    success: "border-green-400",
    failed: "border-red-400",
    evolving: "border-purple-400",
  };

  const typeIcons = {
    orchestrator: Users,
    worker: Bot,
    researcher: Database,
    coder: Code,
  };

  return (
    <div className={`bg-slate-800 border-2 ${borderColor} rounded-xl p-3 min-w-[120px] text-center`}>
      <Handle type="target" position={Position.Top} />
      <Handle type="source" position={Position.Bottom} />
      <div className="flex items-center justify-center mb-1">
        <Icon className="w-4 h-4 text-blue-400" />
      </div>
      <div className="text-white text-sm font-medium">{data.label}</div>
      <div className="text-slate-400 text-xs capitalize">{data.status}</div>
      <div className="text-slate-500 text-xs">{data.type}</div>
    </div>
  );
}

Edge Types and Color Coding

Edges represent different types of relationships:

  • Communication (blue): Agent-to-agent communication
  • Tool use (amber): Agent using a skill/tool
  • Memory access (purple): Agent accessing shared memory
  • Skill inheritance (green): Agent inherits from a skill

Circular Layout

Nodes are positioned in a circle for initial rendering:

useEffect(() => {
  if (nodes.length === 0) return;
  
  const centerX = 400;
  const centerY = 300;
  const radius = 200;
  const nodeCount = nodes.length;

  const updatedNodes = nodes.map((node, index) => {
    const angle = (2 * Math.PI * index) / nodeCount;
    return {
      ...node,
      position: {
        x: centerX + radius * Math.cos(angle) - 60,
        y: centerY + radius * Math.sin(angle) - 30,
      },
    };
  });

  setNodes(updatedNodes);
}, [graph, agents, skills]);

Goal Injection: Steering the System

One of the most powerful features is the ability to inject new goals from the UI:

const handleInjectGoal = async () => {
  if (!goalInput.trim()) return;
  setInjecting(true);
  try {
    await fetch("/api/goals", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ goal: goalInput }),
    });
    setGoalInput("");
  } finally {
    setInjecting(false);
  }
};

The goal is added as a new task and an goal_injected event appears in the activity feed. In a production system, this would trigger the agent system to pick up the new task.

Extending Mission Control

Connecting to a Live Hermes Instance

The current implementation uses mock data. To connect to a live Hermes instance:

  1. Replace the mock data store with a Hermes API client:
// Replace getSystemState() with:
export async function getSystemState(): Promise<SystemState> {
  const [agents, skills, tasks, events] = await Promise.all([
    hermesClient.getAgents(),
    hermesClient.getSkills(),
    hermesClient.getTasks(),
    hermesClient.getEvents(),
  ]);
  
  return {
    agents,
    skills,
    tasks,
    events,
    graph: generateGraph(agents, skills),
    metrics: calculateMetrics(agents, skills, tasks, events),
  };
}
  1. Update the SSE endpoint to poll the Hermes API instead of simulating activity:
// In the SSE endpoint, replace startActivitySimulation with:
const pollInterval = setInterval(async () => {
  const state = await getSystemState();
  send({ type: "update", data: state });
}, 5000);
  1. Add authentication to the API routes:
export async function GET(request: NextRequest) {
  const token = request.headers.get("Authorization");
  if (!token || !validateToken(token)) {
    return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
  }
  // ... rest of handler
}

Adding New Visualization Types

The modular component structure makes it easy to add new visualizations:

// Add a new component: src/lib/components/MemoryMap.tsx
export default function MemoryMap({ memory }: { memory: MemoryEntry[] }) {
  // Visualize memory as a force-directed graph
  // Nodes = memory entries, edges = references between entries
}

// Add to the dashboard:
<section className="mb-8">
  <h2 className="text-lg font-semibold text-white mb-4">Memory Map</h2>
  <MemoryMap memory={state.memory} />
</section>

Custom Edge Types

React Flow supports custom edge types for specialized visualizations:

const edgeTypes: EdgeTypes = {
  dependency: DependencyEdge,
  dataFlow: DataFlowEdge,
  causal: CausalEdge,
};

// Use in the graph:
<ReactFlow
  edgeTypes={edgeTypes}
  // ...
/>

Performance Considerations

For large agent swarms (100+ agents), consider:

  1. Virtualized rendering: Use react-window or similar for large lists
  2. Clustering: Group agents by domain or status in the graph
  3. Sampling: Only show events from the last N minutes in the feed
  4. Web Workers: Offload graph layout calculations to a background thread

Conclusion

Hermes Mission Control demonstrates that agent observability doesn't have to be complex. By combining:

  • A clean, versioned data model
  • Real-time updates via SSE
  • Interactive visualizations with React Flow
  • A local-first architecture

...we've built a tool that makes the invisible work of agent swarms visible and actionable.

The full source code is available at github.com/smfworks/hermes-mission-control. Whether you're running Hermes, OpenClaw, or your own agent framework, the patterns and architecture described here can be adapted to your needs.

Next in this series: Hermes Skill Forge: A Visual Studio for Skill Evolution — how we built a tool for inspecting, editing, and steering the self-improvement of Hermes skills.