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
hermesCLI / gateway. Neatlogs plugs into Hermes' native observer hooks; no code changes at all. This is the recommended path. - Library mode — you import
run_agentin your own Python code. Useinit(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 neatlogsThen set the API key (env, or ~/.hermes/.env) and just use Hermes normally:
export NEATLOGS_API_KEY=your_key
hermesYou 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 traceEvery 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.
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:
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.
