Hermes

Trace the NousResearch Hermes agent with Neatlogs (Python).

Hermes (NousResearch) can be traced two ways, depending on how you run it:

  • Plugin mode — you run the standalone hermes CLI / gateway. Neatlogs plugs into Hermes' native observer hooks; no code changes at all. This is the recommended path.
  • Library mode — you import run_agent in your own Python code. Use init(instrumentations=[...]) + wrap().

Plugin mode — the standalone hermes CLI

The SDK ships a hermes_agent.plugins entry point, so Hermes discovers it as soon as both packages live in the same environment. Enable it once:

pip install neatlogs
hermes plugins enable neatlogs

Then set the API key (env, or ~/.hermes/.env) and just use Hermes normally:

export NEATLOGS_API_KEY=your_key
hermes

You never call neatlogs.init() in this mode — the observer hooks are the tracer. If the API key is missing they no-op silently (fail-open), and traces are grouped under the workflow name hermes.

The span tree is one turn = one trace, with all of a session's turns grouped into one multi-turn session:

WORKFLOW  hermes.turn                 ← one trace per user message / run_conversation
  LLM     hermes.api_request          ← one per provider attempt (usage + I/O)
  TOOL    hermes.tool.<name>          ← one per tool dispatch
  AGENT   hermes.subagent.<role>      ← a delegated child, under the spawning turn
    AGENT hermes.turn                 ← the child agent's own turns, nested in this trace

Every turn's root carries the session id, so the dashboard stitches the turns together. Kanban tasks (which often run in a worker subprocess) each become their own trace inside the same session. Trace/span ids are derived deterministically from Hermes' per-turn id, so children exported live still parent correctly under a root that is emitted at turn close.

Library mode — importing run_agent yourself

Hermes' public surface is the AIAgent class in the top-level run_agent module. Wrapping it patches AIAgent.run_conversation (an AGENT span) and ToolRegistry.dispatch (a TOOL span per tool call). Hermes' LLM calls go through the openai SDK (pointed at OpenRouter), so enable the openai instrumentation alongside hermes to capture LLM spans.

import os
import neatlogs

neatlogs.init(
    api_key=os.environ["NEATLOGS_API_KEY"],
    workflow_name="hermes-demo",
    # Hermes' LLM calls flow through the openai SDK — enable it for LLM spans.
    instrumentations=["hermes", "openai"],
)

from run_agent import AIAgent

# AIAgent defaults to OpenRouter (OPENROUTER_API_KEY); wrap patches the class.
agent = neatlogs.wrap(AIAgent(model="openai/gpt-4o-mini", max_iterations=4))

result = agent.run_conversation("Explain distributed tracing in one paragraph.")
print(result)

neatlogs.flush()
neatlogs.shutdown()

This produces an AGENT root (hermes.run_conversation) with nested LLM (chat.completions.create) and TOOL (hermes.tool.<name>) spans — no WORKFLOW wrapper needed, since the agent span is itself a valid root.

Note

Hermes is installed from GitHub (it isn't on PyPI): pip install "git+https://github.com/NousResearch/hermes-agent.git". If you use a non-OpenAI provider adapter (anthropic / bedrock / gemini / codex), enable that provider's instrumentation alongside hermes.

Sessions & end-user

Say one of your users has a whole thread of back-and-forths with your Hermes agent and you'd like to see them stitched together under a single session — Neatlogs handles that. Just wrap the identify() block around the run and the Hermes agent's root picks up the session and end-user you set:

# Same session_id every turn → one session; end_user_id attributes it to your user.
with neatlogs.identify(session_id=f"conv_{conversation_id}", end_user_id=user_id):
    result = agent.run_conversation(message)

Not every run is a conversation. A standalone workflow — a one-off task, not a back-and-forth — is a single trace with no turns to group. You still attribute it to the customer it ran for, sourcing the id and any metadata from your own user or request object:

Warning

A standalone workflow must not carry a session_id. Omit it entirely — Neatlogs sets session_id = trace_id, so the run is its own single-turn session, fully attributed to the end-user. Reusing one session_id across unrelated runs wrongly folds them into a single multi-turn conversation.

def run_task(task, user):
    # Standalone workflow — attribute to the end-user; do NOT pass session_id.
    with neatlogs.identify(
        end_user_id=str(user.id),
        end_user_metadata={"plan": user.plan},
    ):
        return agent.run_conversation(task)

The full picture lives in Sessions and End-User Identity.

On this page

Ask Neatlogs AI

Answers from the docs

How can I help?

Ask anything about instrumenting, tracing, or the Neatlogs dashboard.