@mate_mattt: https://x.com/mate_mattt/status/2074313623523271010

X AI KOLs Timeline Tools

Summary

This article provides a detailed breakdown of the Hermes Agent architecture, including the event bus, adapter layer, GatewayRunner core scheduler layer, etc., revealing the design pattern of the Agent as a stateful, event-driven runtime.

https://t.co/8Q0c06LQdC
Original Article
View Cached Full Text

Cached at: 07/07/26, 05:34 PM

Detailed Breakdown of Hermes Agent Architecture - Unveiling the Runtime Underneath an Industry-Grade Agent Framework

AI Agents are Stateful, event-driven, and often need to run persistently in the background, constantly looping on their own.
This is exactly the pattern that frontend and client-side developers have been writing for ages (managing complex UI state machines, handling high-frequency user interaction events, RunLoop daemon processes).
If you look at Agent architecture through the lens of traditional programming knowledge, you’ll find it hasn’t invented any new technical frameworks — it simply injects LLM capabilities at certain event nodes.
That’s where the classic formula comes from:
Agent = Harness + Model
By the end of this article, you’ll see that the Harness is a composite of widely used technical architectures, all employing familiar framework concepts.

This is the third article in the Agent architecture series. The previous one analyzed Agent architecture evolution, focusing on LangGraph and Pi-Agent (reading that first will reinforce this one):

MateMatt@mate_mattt·Jul 5
Article
Agent Underlying State Machine Orchestration Evolution – Build a Big-Company Standard Agent Architecture
The previous article introduced the graph representation and 2D matrix representation of state machines, which got good reception:
It seems you folks are quite interested in such articles. Striking while the iron is hot, today we dive deeper into the agent’s underlying state orchestration system, comparing AutoGPT, LangGraph, Pi-Agent…
41 143 18K
If interested, remember to follow me — I will keep publishing in-depth analysis articles in this series.


Hermes Agent Overview

Hermes is not as simple as “throwing an LLM into a ReAct loop”. It is a full-fledged runtime — receiving external events, routing through lifecycle scaffolding, safely scheduling agent work, reusing local agent controllers when possible, and then calling a stateless remote LLM with reconstructed context.

Overall Architecture Panorama:

(Architecture diagram: from left to right)

  • User/Platform Entry – Feishu, Telegram, etc.
  • Adapter Layer – Normalizes input from different platforms into internal MessageEvent structure.
  • Event Bus Layer – Manages full lifecycle listeners and event dispatch.
  • GatewayRunner Core Scheduling Layer – Concurrency control, thread management and scheduling (this is the core layer).
  • AI Agent – The familiar ReAct implementation/execution layer.
  • Stateless LLM Interface Layer – Calls various LLM provider APIs.

We’ll now break down each layer according to this architecture diagram.


Layer 1: User / Platform Entry

The outermost layer is the real-world entry point: a user sends a message on some platform — Telegram, Discord, Slack, CLI, or other integration.

At this point, the message is still a platform-specific “semi-finished product”:

  • Telegram update
  • Discord message
  • CLI input
  • Webhook payload

These raw platform payloads are not yet agent tasks. They must first be “sanitized” (standardized) before the core runtime can process them.


Layer 2: Adapter Layer

The adapter layer converts platform-specific input into Hermes internal events.

It answers questions like:

  • Who sent it?
  • Which Session does it belong to?
  • What is the message text?
  • Are there images, files, audio attachments?
  • What is the session_key?
  • Is this Session already busy?

Conceptually, the adapter produces something like this:

MessageEvent + session_key + source metadata

The session_key is extremely important. It is the identity of a local session, used to route work to the correct session channel.

session_key = Session ID card

The adapter also participates in session-level “access control”:

active_sessions[session_key] = busy state guard
pending_messages[session_key] = next turn / interrupt

This means Hermes tries to avoid running two independent agent loops on the same session simultaneously — that would be chaos.


Layer 3: Event Bus / Scaffolding Layer

The Event Bus / Scaffolding layer is the most core conceptual layer of the entire architecture.

This layer is often misunderstood. The Event Bus does not run the ReAct loop, it does not “think”, and it does not call tools as an agent. Its job is to coordinate the lifecycle flow of events throughout the runtime.

You can think of it as a stitched-together combination of:

  • An event emitter.
  • A middleware pipeline.
  • A lifecycle hooks system.
  • An asynchronous coordination layer.
  • A place to trigger side effects before/after the main agent run.

What the Event Bus does

The Event Bus / Scaffolding layer is responsible for routing and lifecycle control. It deals with concepts like:

  • publish(event) – publish an event
  • subscribe(handler) – subscribe a handler
  • lifecycle hooks – lifecycle hooks
  • async dispatch – asynchronous dispatch
  • side effects – side effects
  • interrupt signals – interrupt signals
  • pending-message triggers – pending message triggers

A simplified lifecycle looks something like:

on_message_received ——> on_processing_start ——> on_agent_run ——> on_tool_event ——> on_response_sent ——> on_processing_complete

In the source code, these “bus” responsibilities may be spread across adapters, hooks, background tasks, and GatewayRunner, rather than being implemented as a traditional single class called EventBus.

What the Event Bus does not do

The Event Bus does not own these things:

  • Worker thread pool.
  • AIAgent cache.
  • ReAct loop.
  • Remote LLM sessions.
  • Semantics of local tool execution.

Its role is more like:
“An event occurred. Who needs to be notified? Which lifecycle hooks should run? Should we trigger an interrupt? Should we start processing? Should we send a typing indicator, log, emit metrics, or clean up?”

Analogy: Node.js Event Loop

If you’re familiar with Node.js, this event bus should feel familiar:

Node.js EventEmitter ~= Publish/Subscribe
Express middleware ~= Lifecycle interception
Node Event Loop ~= Async dispatch face
libuv worker thread pool ~= Shared executor (for blocking tasks)
Application handlers ~= GatewayRunner + AIAgent

The Event Bus is not the same as the operating system’s event loop. It is an application-level event coordination pattern built on top of async runtime behavior.

Why does the Harness need this layer?

Without this layer, each platform adapter (tg, feishu, etc.) would have to implement the same logic repeatedly:

  • Toggle typing indicator.
  • Log message lifecycle events.
  • Trigger progress updates.
  • Handle interrupts.
  • Handle pending messages.
  • Run cleanup logic.
  • Notify observability or metric handlers.
  • Separate platform-specific code from agent logic.

The Event Bus/Scaffolding layer keeps these lifecycle concerns out of the core ReAct loop.

This separation is important:

Event Bus / Scaffolding = Event lifecycle + side effects
AIAgent ReAct loop = Reasoning + Tool calls + Observations + Final answer

Order and Concurrency in the Event Bus

For a single message, lifecycle stages are generally ordered:

Received -> Processing started -> Agent run -> Reply -> Completed

But across different sessions, events can proceed concurrently:

Session A event is being processed
Session B event can start processing at the same time
Session C event is queued waiting for a worker thread

For the same session, Hermes tries to ensure only one active run at a time:

Same session_key -> One active run
New messages during active run -> pending / interrupt / queuing behavior

This is a key design decision. The system allows concurrency across sessions, but protects individual sessions from running multiple conflicting agent loops simultaneously.

Event Bus pseudocode:

async def handle_raw_platform_message(raw):
    event = adapter.to_message_event(raw)
    session_key = event.session_key
    await publish("message_received", event)
    if adapter.is_session_active(session_key):
        adapter.store_pending_or_interrupt(session_key, event)
        await publish("message_pending", event)
        return
    await publish("processing_start", event)
    try:
        response = await gateway_runner.handle_message(event)
        await publish("response_sent", response)
    finally:
        await publish("processing_complete", event)

Layer 4: GatewayRunner

GatewayRunner is the global scheduler (Worker) of the runtime.

If the Event Bus is the lifecycle coordination layer, GatewayRunner is the layer that decides how agent work is prepared and executed.

What GatewayRunner manages

GatewayRunner owns or coordinates these main runtime resources:

ThreadPoolExecutor – thread pool
running_agents – active run tracking
agent_cache – AIAgent objects cached by session_key
session store – session storage
queued events – queued events
model/provider config – model/provider configuration
progress and interrupt monitoring – progress and interrupt monitoring
adapters – adapters

When a task arrives, GatewayRunner does not create a permanent worker thread for a session. Instead, it submits the blocking agent work to a shared executor.

Session A borrows worker thread 1
Session B borrows worker thread 2
Session C waits if pool is full

When the agent finishes, the worker thread returns to the pool.

Worker threads are not owned by sessions.

Shared Thread Pool

Hermes uses a shared thread pool to handle blocking agent work.

The practical reason: agent.run_conversation() may call remote APIs, run tools, interact with files, wait for subprocesses, stream output — these tasks should not block the async message processing loop.

So GatewayRunner conceptually does:

await loop.run_in_executor(
    shared_thread_pool,
    run_sync,
)

Inside run_sync, Hermes eventually calls:

agent.run_conversation(message, conversation_history=history)

Key idea:

Async event handling stays responsive
Blocking agent runs are offloaded to worker threads

From current research, GatewayRunner uses coroutines: _handle_message, _run_agent_inner are async def, and it uses asyncio.create_task() for progress, stream, interrupt monitor tasks. But the core AIAgent loop is not a coroutine — it’s a synchronous function run in the thread pool.

Reference source: gateway/run.py

So the call flow is roughly:

A worker thread is occupied by Session A’s agent run -> agent is waiting for LLM / reading files / running tools -> this worker thread will NOT yield to Session B -> only when the run returns does the thread return to the pool

You can see that when the agent reads a file, it’s obviously an IO blocking operation. But the thread still waits and does not yield; this doesn’t align with the efficiency of modern coroutines. I suspect this is to reduce code complexity, since this is an Agent running on a user machine with very low concurrency requirements. (This is a complex area; please correct me if I’m wrong.)

What happens when the thread pool is full?

If all worker threads are busy, new executor tasks will wait in the executor’s queue.

That’s normal backpressure:

Pool has idle thread -> start immediately
Pool full -> wait

This does NOT mean a session owns a worker thread. It only means the task is waiting for a shared resource to become available.

One Active Run Per Session

GatewayRunner works together with the adapter layer to protect sessions from duplicate runs.

For a single session_key:

Only one active agent run at a time

If a new message arrives while a session is active, Hermes may:

  • Store it as a pending next turn.
  • Treat it as an interrupt signal.
  • Route it as a command.
  • If the user uses queue semantics, place it in an explicit FIFO queue.

Important distinction:

Default pending message = usually one next-turn/interrupt slot
Explicit queue = FIFO semantics

So don’t assume every ordinary message goes into a big FIFO queue. The default behavior is more like “protect this session, note what comes next, possibly interrupt it.”

GatewayRunner pseudocode:

async def gateway_handle_message(event):
    session_key = event.session_key
    mark_running_or_pending(session_key)
    def run_sync():
        history = load_history(session_key)
        config = resolve_model_and_tools(event)
        agent = agent_cache.get(session_key, config)
        if agent is None:
            agent = AIAgent(config=config, session_key=session_key)
            agent_cache[session_key] = agent
        return agent.run_conversation(
            event.text,
            conversation_history=history,
        )
    result = await run_in_executor(shared_thread_pool, run_sync)
    persist_result(session_key, result)
    clear_running(session_key)
    drain_pending_if_any(session_key)
    return result.final_response

This shows the core scheduling relationship:

GatewayRunner is responsible for scheduling.
ThreadPoolExecutor provides temporary execution threads.
AIAgent executes the ReAct loop.


Layer 5: AIAgent

AIAgent is the local agent controller. It is a local runtime object that knows:

  • Which model/provider to use.
  • Which tools are available.
  • How to construct messages.
  • How to manage session history.
  • How to run the ReAct loop.
  • How to handle interrupts.
  • How to execute tool calls.
  • How to produce the final reply.

Internal loop:

  • Call LLM -> model returns final answer -> stop
  • Call LLM -> model returns tool_calls -> execute tools -> append observations -> call LLM again

This is the ReAct pattern:

Reason -> Act -> Observe -> Repeat

In code:

while not done and iteration_count < max_iterations:
    assistant_message = call_llm(messages, tools=tool_schemas)
    if assistant_message.tool_calls:
        tool_results = execute_tools(assistant_message.tool_calls)
        messages.append(tool_results)
        continue
    return assistant_message.final_text

The loop is local. Intelligence comes from the remote model, but the control structure is local.


Layer 6: Remote LLM API

The remote LLM API is stateless. The remote model doesn’t remember your session.

Each time Hermes calls the model, it sends the entire context needed for that call:

  • system prompt
  • conversation history
  • latest user message
  • tool schemas
  • tool observations
  • runtime instructions

So the diagram shows:

messages/history → reconstruct context

The remote LLM appears to “remember” — only because Hermes re-assembles the session state and sends it again.


Layer 7: Local Tools

Tools are local capabilities exposed to the model, described by structured tool schemas. For example:

  • Read file
  • Write file
  • Run terminal command
  • Query memory
  • Search context
  • Call API
  • Generate media

The LLM does not execute these tools directly. It only emits structured tool calls. The local runtime is responsible for validation and execution, then injects the observation back into the message list.

LLM emits tool call -> AIAgent validates it -> local runtime executes it -> result becomes observation -> observation sent back to LLM


Complete Message Flow

End-to-end path:

  1. User sends a message.
  2. Platform adapter receives raw platform payload.
  3. Adapter normalizes it into MessageEvent + session_key.
  4. Event Bus / Scaffolding publishes lifecycle events.
  5. Adapter/Runtime checks if the session is already active.
  6. GatewayRunner prepares session history, configuration, tools, and agent object.
  7. GatewayRunner submits the blocking agent work to the shared ThreadPoolExecutor.
  8. A worker thread runs agent.run_conversation().
  9. AIAgent enters the ReAct loop.
  10. Remote LLM is called with messages/history/tool schemas.
  11. If tool calls are returned, local tools execute and observations are appended.
  12. Loop repeats until final reply or stop condition.
  13. GatewayRunner persists and cleans up running state.
  14. Event Bus / Scaffolding triggers reply and completion hooks.
  15. Adapter sends the final reply back to the user.

Ownership & Sharing Table

ComponentOwnerShared?LifespanPurpose
GatewayRunnerProcess/RuntimeYes, global within processLong-livedMain scheduler
ThreadPoolExecutorGatewayRunnerYesLong-livedRun blocking agent work
Worker threadsThread poolYes, borrowed per taskTemporary per submitted runExecute one submitted run
session_keyRuntime/Session systemNoLong-livedIdentity for routing state, pending messages, cached agents
active_sessions[session_key]Adapter/RuntimePer-sessionDuring active runPrevent duplicate runs on same session
pending_messages[session_key]Adapter/RuntimePer-sessionUntil consumedStore next-turn/interrupt
running_agents[session_key]GatewayRunnerPer active sessionCurrent runTrack active agent for interrupt/status
agent_cache[session_key]GatewayRunnerPer cached sessionAcross turns until invalidatedReuse local AIAgent
AIAgentRuntime-created objectTypically bound to a cached sessionAcross turnsReuse local ReAct controller
Remote LLM APIProviderShared external servicePer callStateless generation of assistant messages/tool calls
ToolsLocal runtimeShared definition, per-call executionVariesPerform tool operations

Summary

A simplified processing flow:

  • Event Bus / Scaffolding: Handles lifecycle, routing, hooks, side effects, interrupts
  • GatewayRunner: Schedules work, owns shared executor, tracks active agents, manages cache
  • ThreadPoolExecutor: Shared worker thread pool; sessions borrow threads temporarily
  • session_key: Session ID card; routes state, pending messages, cached agents
  • AIAgent: Local ReAct controller; cached by session_key when active
  • Remote LLM: Stateless API; context reconstructed via messages/history

This article is fairly long because the Hermes runtime framework itself is quite complex.

If you found it helpful, please like and follow. I will continue to publish in-depth articles on Agent architecture.

Similar Articles

@dotey: For the Hermes Agent architecture documentation, I recommend directly reading the official docs—they're fairly clear. Then use Codex or Claude Code to open the project codebase and let the Agent explain the code to you. If anything is unclear, you can ask follow-up questions. This approach is great because you can ask any questions you want...

X AI KOLs Timeline

It is recommended to directly view the official Hermes Agent architecture documentation and use Codex or Claude Code to open the project codebase, letting the Agent explain the code for deeper understanding.

@QingQ77: Provide an out-of-the-box skills and workflow layer for Hermes Agent, covering the entire application lifecycle from idea to deployment to operations. https://github.com/Salomondiei08/oh-my-hermes… Oh My Hermes is to…

X AI KOLs Timeline

Oh My Hermes is an open-source workflow layer that provides out-of-the-box skills and agent teams for Hermes Agent, covering the full application lifecycle from requirements to deployment and operations, turning Hermes into an autonomous operations and development partner.

@NFTCPS: Hermes just blew the Agent ceiling away! Programmers everywhere are building desktop marvels, running creative pipelines, and saving tokens like crazy — are you still not on board? First up, hermes-desktop, the CLI toy evolves into a native Mac/Windows desktop app, one-click install and ready to chat, multi-platform messaging + self-evolving loop — that's a true desktop companion. https://github.com/fathah/hermes-desktop

X AI KOLs Timeline

This article introduces five open-source tool projects built around the Hermes Agent, including a desktop application, creative workflow, shared memory layer, token compression tool, and monitoring dashboard, aimed at expanding the Agent ecosystem.