Skip to main content

Architecture

Async Hermes Agent is a library-focused derivative of Hermes Agent v2026.8.18. It keeps upstream file locations and public names where possible, then converts I/O-bearing runtime boundaries to coroutines. This makes upstream changes easier to compare without maintaining a parallel async module tree.

System overview​

The host owns the event loop and service boundary. There is no bundled HTTP server, interactive UI, messaging gateway, or scheduler in this distribution.

Primary entry points​

AIAgent​

run_agent.py exports AIAgent, the stateful agent runtime. Construction is synchronous because it records configuration and creates no external connection. The external lifecycle starts at the first awaited operation:

from run_agent import AIAgent


async with AIAgent(...) as agent:
result = await agent.run_conversation("Review this change")

__aenter__() resolves the provider runtime, discovers plugins, and starts MCP discovery. run_conversation() also performs lazy initialization, so explicit context-manager use is recommended but not required. close() releases MCP, provider, database, browser, and child-task resources and is idempotent.

BatchRunner​

batch_runner.py consumes JSONL prompts and runs bounded concurrent agent turns. Each prompt receives an isolated agent instance. Batch shards, checkpoints, statistics, and a merged trajectory file are written with async file operations. See Trajectory Format.

Code map​

PathResponsibility
run_agent.pyAIAgent public API, lifecycle, and compatibility surface
agent/conversation_loop.pyModel/tool iteration and provider recovery
agent/turn_context.pyOnce-per-turn setup, prompt restore/build, prefetch, and initial persistence
agent/tool_executor.pySequential and parallel-safe tool scheduling
agent/turn_finalizer.pyResult construction, trajectory save, cleanup, and final persistence
agent/prompt_builder.pyStable system-prompt assembly
agent/context_compressor.pyContext pressure handling and summarization
agent/transports/Native async model transports
model_tools.pyTool discovery, schemas, and dispatch entry points
tools/registry.pyTool registration and availability checks
tools/mcp_tool.pyMCP discovery, calls, reconnection, and teardown
toolsets.pyStatic and dynamic tool group resolution
hermes_state.pySessionDB, SQLite persistence, FTS search, and maintenance
agent/trajectory.pyPer-turn trajectory serialization
batch_runner.pyConcurrent dataset execution and checkpointing

The reduced hermes_cli/, gateway/, plugins/, and providers/ packages contain retained configuration, context, and extension contracts used by the library. They do not constitute the removed product applications.

Turn data flow​

Concurrency model​

  • A per-instance asyncio.Lock serializes turns on one AIAgent. This keeps mutable history, prompt-cache state, and persistence ordered.
  • Different agent instances can make progress concurrently on the same event loop.
  • Parallel-safe tool calls run in an asyncio.TaskGroup; observations are appended in the model's original tool-call order.
  • Tools requiring interaction, ordering, or a safety barrier remain sequential.
  • CPU-only parsing, token estimates, schema normalization, and message transformations remain synchronous.

Native async means active I/O is awaited. It is a concurrency contract, not a claim that every helper is a coroutine or that CPU work becomes cheaper.

Behavior-preservation contracts​

The async conversion keeps these upstream invariants:

  • The system-prompt prefix remains stable for the life of a conversation.
  • Provider message alternation and tool-call/tool-result pairing remain valid.
  • Tool observations retain model-issued order even when execution overlaps.
  • Reasoning, tool calls, observations, and final answers retain trajectory order and shape.
  • Cancellation finalizes durable partial state before propagating CancelledError to the host.
  • Hermes' internal interrupt path returns a partial result rather than being confused with task cancellation.

Live acceptance verification​

The default test suite is hermetic. Before a release, run the opt-in acceptance paths with an authenticated provider to exercise the real async chain rather than only mocked transports:

HERMES_LIVE_TESTS=1 HERMES_LIVE_PROVIDER=copilot \
uv run pytest -q \
tests/e2e/test_live_provider_tool_path.py \
tests/e2e/test_live_provider_stream_path.py \
tests/e2e/test_live_provider_extensions_path.py \
tests/e2e/test_live_provider_state_path.py \
tests/e2e/test_live_provider_timeout_path.py \
tests/e2e/test_live_provider_concurrency_path.py \
tests/e2e/test_live_provider_subagent_path.py \
tests/e2e/test_live_provider_compression_path.py \
tests/e2e/test_live_single_runner_path.py

These tests cover provider-to-tool observations and trajectory ordering, a real stdio MCP server and skill loading, persistent memory and cross-instance session resume, timeout cleanup and next-turn recovery. They also verify native streaming, live context compression and continuation, the retained single-task runner, overlapping requests across agent instances, per-agent turn serialization, and background subagent reinjection and cleanup. Every path fails on event-loop blocking or leaked tasks. Set HERMES_LIVE_MODEL to override the provider's test default; OpenRouter runs also require OPENROUTER_API_KEY.

A reasoning-capable provider is a separate release gate because the default Copilot acceptance model does not expose reasoning. Point this test at an already-running provider and model; for example, an LM Studio model loaded as async-hermes-reasoning on its default port:

HERMES_LIVE_REASONING_TESTS=1 \
HERMES_LIVE_REASONING_PROVIDER=lmstudio \
HERMES_LIVE_REASONING_MODEL=async-hermes-reasoning \
uv run pytest -q \
tests/e2e/test_live_reasoning_trajectory_path.py \
tests/e2e/test_live_batch_runner_path.py

Those paths require reasoning on both model turns and verify the saved reasoning → tool call → observation → reasoning → final answer trajectory, BatchRunner checkpoint/resume and merged JSONL output, and event-loop and task cleanup. The BatchRunner gate intentionally has no non-reasoning default: upstream data-generation behavior discards samples with zero reasoning.

Persistence and ownership​

SessionDB uses one lazily opened aiosqlite connection and async locks for connection and write serialization. A SessionDB attached to an agent, including one passed through session_db=, is closed by AIAgent.close(); passing it transfers lifecycle ownership to that agent. Host callbacks and configuration remain host-owned.

For details, continue with Agent Loop Internals, Upstream Differences, Session Storage, and Programmatic Integration.

Scope boundary​

The original Hermes Agent product also includes CLI/TUI, desktop, dashboard, messaging, ACP, and cron surfaces. They are intentionally absent here. Restore them from upstream only when their complete behavior and native-async I/O path can be carried together; do not add placeholders to the core.