Hermes
Trace the NousResearch Hermes agent with Neatlogs (Python).
Hermes (NousResearch) is a Python agentic loop whose 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.
