Friday, July 24, 2026

Mastering Hermes Agent: A Guide to Configuration, Extension, and Goal-Oriented Deployment



INTRODUCTION

There is a particular kind of frustration that comes from using AI tools that forget everything the moment you close the window. You explain your project's conventions, your preferred coding style, the quirks of your deployment environment — and the next day you start from scratch. Every session is a first date. The AI is charming, capable, and completely amnesiac.

Hermes Agent, developed by Nous Research and released in February 2026, is built on the premise that this does not have to be the case. It treats the AI not as a stateless oracle you consult and dismiss, but as a persistent collaborator that accumulates knowledge, refines its own procedures, and grows more capable the longer it works with you. The name fits: in Greek mythology, Hermes was the messenger of the gods, the guide of souls, and the patron of travelers, merchants, and anyone who needed to get something done cleverly and quickly. The Hermes Agent aspires to something similar in the digital realm — it receives your goals, figures out how to reach them, executes the necessary steps across a wide range of tools, and remembers what worked so it can do it better next time.

This tutorial is written for two audiences who are more alike than they might initially think. The first is the practitioner who wants to harness Hermes to accomplish real goals: automating research workflows, managing files and code, orchestrating web browsing, building personal automation pipelines. The second is the developer who wants to extend Hermes by writing custom tools, building MCP servers, crafting skills, or wiring up adapters to external systems. Both audiences need to understand the same underlying architecture, because in Hermes the line between using the system and extending it is deliberately thin. A skill you write to help yourself becomes a reusable artifact. A plugin you build for one workflow can be activated or deactivated depending on the goal at hand.

The red thread running through everything that follows is goal-oriented configuration. Hermes is not a one-size-fits-all tool. A Hermes instance configured to help a data scientist analyze genomics datasets should look very different from one configured to help a software team manage a CI/CD pipeline, which in turn should look very different from one configured to run as a personal research assistant. Understanding how to shape Hermes around a specific goal — rather than leaving it in its default state and hoping for the best — is the central skill this tutorial aims to build.


Part One: Understanding What You Are Dealing With

Chapter 1: The Idea Behind Hermes, and Why It Changes Things

Most AI tools you have used so far are, at their core, amnesiac. You open a chat window, you type something, the model responds, and when you close the window and come back tomorrow, the model has no idea who you are, what you were working on, or what it learned from your last conversation. This is not a bug in the traditional sense — it is a deliberate architectural choice made for simplicity, scalability, and privacy. But it is also a profound limitation when you want an AI that genuinely gets better at helping you over time.

Hermes is built on a fundamentally different premise. It maintains persistent memory across sessions, builds a library of reusable procedures called skills, and runs a closed learning loop where successful task completions feed back into improved future performance. When you spend an afternoon teaching Hermes how your project's deployment pipeline works, that knowledge does not evaporate when you close the terminal. It is there the next morning, and the morning after that, and it gets refined each time the agent uses it.

The July 2026 "Judgment Release" of Hermes brought particularly significant improvements to this loop, including enhanced completion with evidence, first-class Mixture-of-Agents support, and visible self-improvement features that let you watch the agent update its own skill library in real time. These are not gimmicks — they represent a genuine shift in what it means for software to learn from experience.

Chapter 2: The Architecture in Your Head Before the Code on Your Screen

Before you install anything or write a single line of configuration, it is worth building a clear mental model of what Hermes actually is. This model will save you enormous amounts of confusion later, because Hermes has several moving parts that interact in non-obvious ways.

At the center of everything is the LLM — the large language model that acts as the agent's brain. Hermes does not ship with a model baked in; it is a shell that connects to whichever model you choose, whether that model lives on your local machine via Ollama, on a remote server via OpenAI or Anthropic, or on Nous Research's own Nous Portal. The model receives a carefully constructed system prompt that includes the agent's memory, its loaded skills, its available tool schemas, and the current conversation history. It then decides what to do next: either respond directly to the user, or call one or more tools.

Surrounding the LLM is the tool layer. Hermes ships with over forty built-in tools organized into logical groups called toolsets. These tools cover web search, browser automation, terminal execution, file reading and editing, image generation, text-to-speech, memory management, task scheduling, and more. Tools are the agent's hands — they let it reach out and affect the world beyond the conversation window.

Wrapping around the tool layer is the memory system. Hermes maintains two special files, MEMORY.md and USER.md, in a directory at ~/.hermes/memories/. The MEMORY.md file stores facts about the environment, coding conventions, and things the agent has learned about the tasks it performs. The USER.md file stores facts about you: your preferences, your communication style, your expectations. At the start of every session, these files are injected into the system prompt as a frozen snapshot, giving the agent immediate context about who you are and what it already knows.

Beyond the built-in memory, Hermes supports external memory providers — systems like Mem0, Honcho, and OpenViking — which offer capabilities like semantic search, knowledge graphs, and automatic fact extraction. These providers run alongside the built-in memory and can dramatically extend the depth and richness of what the agent remembers across sessions.

The skills system sits above the memory system in the abstraction hierarchy. A skill is not a memory of a fact; it is a memory of a procedure. When Hermes successfully completes a complex task, it can distill what it learned into a SKILL.md file: a structured document that describes, step by step, how to accomplish that kind of task. The next time a similar task comes up, the agent loads the relevant skill into context and follows its own previously written playbook. This is the closed learning loop that makes Hermes genuinely self-improving.

The MCP layer — which stands for Model Context Protocol — is the bridge between Hermes and the outside world of tools that were not built specifically for Hermes. MCP is an open standard, originally introduced by Anthropic in November 2024, that defines how AI agents communicate with external tool servers using JSON-RPC 2.0. An MCP server is a process that exposes a set of tools over a standardized interface, and Hermes can connect to any number of MCP servers simultaneously. This tutorial targets the stable MCP 2025-11-25 specification, which introduced OpenID Connect Discovery support, metadata for tools and resources, and experimental Tasks support. Because MCP is an open standard, tools built for Claude Code or Cursor can be used directly in Hermes without modification.

Finally, the plugin system allows developers to extend Hermes using Python. A plugin is a directory containing a manifest file and Python code that registers new tools, hooks into the agent's lifecycle events, or modifies the agent's behavior in other ways. Plugins are the deepest level of customization available without modifying Hermes's core source code.

All of these layers — the LLM, the tools, the memory, the skills, the MCP servers, and the plugins — are configured through a single YAML file at ~/.hermes/config.yaml, supplemented by a .env file for secrets. Understanding how to configure each layer for a given goal is what this tutorial is about.


Part Two: Getting Hermes Running

Chapter 3: Installation Across Platforms

Hermes is designed to run on Linux, macOS, WSL2 on Windows, and even Android via Termux. The installation process is deliberately simple, handled by a single shell script that takes care of all dependencies including Python 3.11, Node.js, ripgrep, and ffmpeg.

On Linux, macOS, or WSL2, open a terminal and run the following. It fetches the installer script over HTTPS and pipes it directly to bash, which is the standard pattern for this kind of tool:

curl -fsSL https://hermes-agent.nousresearch.com/install.sh | bash

After the installer finishes, reload your shell environment so that the hermes command becomes available. If you use bash:

source ~/.bashrc

If you use zsh (the default on modern macOS):

source ~/.zshrc

On native Windows without WSL2, open PowerShell as an administrator and run:

iex (irm https://hermes-agent.nousresearch.com/install.ps1)

The WSL2 path is strongly recommended for Windows users who plan to do any serious work with Hermes, especially if that work involves terminal automation, file system operations, or running local LLMs. The native Windows installation works for basic use but lacks some of the shell integration that makes Hermes genuinely powerful.

If you prefer to run Hermes in a container to keep it isolated from your host system, Docker is supported as well. The following command mounts your Hermes configuration directory into the container and passes your OpenAI API key as an environment variable:

docker run --rm -it \
    -v ~/.hermes:/root/.hermes \
    -e OPENAI_API_KEY=$OPENAI_API_KEY \
    nousresearch/hermes-agent:latest

Once installed, the first thing you should do is run the health check command, which verifies that all required dependencies are present and that your configuration is not obviously broken:

hermes doctor

If hermes doctor reports any issues, address them before proceeding. Common problems include missing API keys, Python version mismatches, or network connectivity issues that prevent Hermes from reaching its configured LLM provider.

For a quick start that gets you up and running with a sensible default configuration, Nous Research provides a setup wizard:

hermes setup --portal

This command walks you through connecting to the Nous Portal, which is Nous Research's own hosted model service offering over three hundred models under a single subscription. It also configures all four Tool Gateway tools — web search, image generation, text-to-speech, and browser automation — without requiring you to manage separate third-party API keys. If you prefer to use a different provider from the start, you can skip this and configure your provider manually, which is what the next chapter covers in detail.


Part Three: Configuring the Brain — LLM Providers

Chapter 4: The Configuration System and Its Layers

Before diving into specific provider configurations, you need to understand how Hermes's configuration system works, because it has a layered precedence model that can cause confusion if you are not aware of it.

Configuration values are resolved in the following order, from highest priority to lowest. Command-line arguments passed directly to the hermes command override everything else. The main configuration file at ~/.hermes/config.yamloverrides the secrets file. The secrets file at ~/.hermes/.env provides defaults for API keys and tokens. Finally, Hermes's built-in hardcoded defaults apply when nothing else specifies a value.

This layering means you can have a stable base configuration in config.yaml and override specific values on a per-invocation basis using CLI arguments — which is extremely useful when you want to test a different model without permanently changing your configuration.

The config.yaml file is plain YAML and can be edited directly with any text editor, or managed through the hermes config subcommand. The most useful config subcommands are:

hermes config edit          # Opens config.yaml in your default editor
hermes config get KEY       # Prints the current value of a specific key
hermes config set KEY VAL   # Sets a value, routing API keys to .env automatically
hermes config check         # Checks for missing or outdated options
hermes config migrate       # Adds new fields and bumps the _config_version key

The .env file follows standard dotenv format: one KEY=VALUE pair per line, with no spaces around the equals sign. This file should never be committed to version control, and its permissions should be set to 600 so that only your user account can read it:

chmod 600 ~/.hermes/.env

One practical note: hermes config set is smarter than it looks. When you use it to set a value that looks like an API key or secret, it automatically routes the value to .env rather than config.yaml. This means you rarely need to edit .envdirectly — just use hermes config set for everything and let Hermes sort out where each value belongs.

Chapter 5: Connecting to a Remote LLM Provider

The most common starting point for new Hermes users is connecting to a remote LLM provider. Hermes supports a wide range of providers out of the box, including OpenAI, Anthropic, OpenRouter, Nous Portal, Fireworks AI, NovitaAI, and several others.

Let us start with OpenAI, since it is the most familiar. First, add your API key to the secrets file:

# ~/.hermes/.env
OPENAI_API_KEY=sk-your-actual-key-goes-here

Then configure the model section of your config.yaml. The model section tells Hermes which provider to use, what the base URL of the API is, and which model to use by default:

# ~/.hermes/config.yaml
model:
  provider: openai
  base_url: https://api.openai.com/v1
  default: gpt-4o

For Anthropic's Claude models, the pattern is similar. Add the key to .env:

# ~/.hermes/.env
ANTHROPIC_API_KEY=sk-ant-your-actual-key-goes-here

And update config.yaml:

# ~/.hermes/config.yaml
model:
  provider: anthropic
  base_url: https://api.anthropic.com
  default: claude-opus-4-5

OpenRouter is particularly interesting because it acts as a unified gateway to dozens of providers, including OpenAI, Anthropic, Google, Mistral, and many open-source models. It also supports provider routing, which lets you specify preferences about cost, speed, and quality. The configuration looks like this:

# ~/.hermes/.env
OPENROUTER_API_KEY=sk-or-your-actual-key-goes-here
# ~/.hermes/config.yaml
model:
  provider: openrouter
  base_url: https://openrouter.ai/api/v1
  default: anthropic/claude-opus-4-5

provider_routing:
  sort: throughput
  ignore:
    - azure
    - bedrock

The provider_routing section is optional but powerful. Setting sort to throughput tells OpenRouter to prefer whichever backend currently offers the fastest token generation speed. Setting it to price minimizes cost. Setting it to latency minimizes time-to-first-token, which matters when you want the agent to feel responsive. The ignore list lets you exclude specific infrastructure providers if you have compliance or latency reasons to avoid them.

Chapter 6: Running Hermes Against a Local LLM

This is where things get genuinely exciting for privacy-conscious users, researchers, and anyone who wants to run Hermes without incurring per-token API costs. Hermes can connect to any LLM that exposes an OpenAI-compatible API, and Ollama is the most popular way to run such models locally.

First, install Ollama from https://ollama.ai and pull a model that has sufficient context length. Hermes requires at least 64,000 tokens of context for its multi-step tool-calling workflows, so you need a model that can handle that. Qwen 2.5 Coder 32B is an excellent choice for coding-heavy workloads, while Llama 3.3 70B performs well for general-purpose tasks:

ollama pull qwen2.5-coder:32b

Start the Ollama server if it is not already running. On most systems, Ollama runs as a background service after installation, but you can start it manually:

ollama serve

By default, Ollama listens on port 11434 and exposes an OpenAI-compatible API at http://localhost:11434/v1. Now configure Hermes to use it. There is an important subtlety here: you must use provider: custom rather than provider: ollama, because provider: ollama tells Hermes to connect to Ollama's cloud service, which requires an API key. For the local Ollama server, use custom and point the base_url at your local instance:

# ~/.hermes/config.yaml
model:
  provider: custom
  base_url: http://127.0.0.1:11434/v1
  default: qwen2.5-coder:32b
  api_key: local-no-key-needed

The api_key field is set to an arbitrary non-empty string because Hermes validates that an API key is present even for local providers. The actual value does not matter — Ollama ignores it entirely.

Configuring the context window. Ollama's default context window is only 4,096 tokens, which is far too small for Hermes's multi-step tool-calling workflows. You have two good options for increasing it.

The first option is the OLLAMA_CONTEXT_LENGTH environment variable, which sets the default for every model on the server. This is the simplest approach if you run Ollama as a dedicated service:

# Set context length globally for all models on this Ollama server.
OLLAMA_CONTEXT_LENGTH=65536 ollama serve

To make this permanent on Linux with systemd, add the environment variable to the Ollama service override:

sudo systemctl edit ollama

Add the following in the editor that opens:

[Service]
Environment="OLLAMA_CONTEXT_LENGTH=65536"

Then reload and restart:

sudo systemctl daemon-reload
sudo systemctl restart ollama

The second option is a custom Modelfile, which lets you set the context for a specific model without affecting others. This is the right choice when you run multiple models with different context requirements:

# Save this as Modelfile.hermes
FROM qwen2.5-coder:32b
PARAMETER num_ctx 65536

Build and name the custom model:

ollama create hermes-qwen -f Modelfile.hermes

Verify the context window was applied:

ollama show --modelfile hermes-qwen
# Look for: PARAMETER num_ctx 65536

ollama ps
# The CONTEXT column shows tokens allocated for currently loaded models.

Update your config.yaml to use this custom model name:

# ~/.hermes/config.yaml
model:
  provider: custom
  base_url: http://127.0.0.1:11434/v1
  default: hermes-qwen
  api_key: local-no-key-needed

Now you can run hermes and all inference will happen entirely on your local machine. The trade-off compared to hosted models is speed: a 32B parameter model on consumer hardware will generate tokens significantly more slowly than a hosted API, but the privacy, cost, and offline capabilities often make this trade-off worthwhile.

For users who want to run a local model for most tasks but fall back to a hosted model for particularly complex reasoning, the auxiliary model system provides an elegant solution. You can configure a lightweight local model for tasks like context compression and session title generation, while using a powerful hosted model for the main reasoning loop:

# ~/.hermes/config.yaml
model:
  provider: openrouter
  base_url: https://openrouter.ai/api/v1
  default: anthropic/claude-opus-4-5

auxiliary:
  compression:
    provider: custom
    base_url: http://127.0.0.1:11434/v1
    model: qwen2.5-coder:7b
    api_key: local-no-key-needed
  vision:
    provider: openrouter
    model: google/gemini-flash-1.5
    base_url: https://openrouter.ai/api/v1

This configuration uses Claude Opus for the main agent loop, a small local Qwen model for compressing long conversation histories (a relatively simple task that does not require a frontier model), and Gemini Flash for vision tasks like analyzing screenshots or images.

Chapter 7: Writing a Python Client That Talks to Both Local and Remote LLMs

Beyond the CLI, you can build Python scripts that talk directly to whichever LLM backend Hermes is configured to use. This is useful for embedding LLM calls in your own automation scripts, testing prompts programmatically, or building lightweight wrappers around the same OpenAI-compatible API that Hermes uses internally.

Install the httpx library first. As of July 2026, the current stable release is 0.28.1:

pip install "httpx>=0.28.1"

If you are using uv for dependency management, which is recommended for its significantly faster resolution:

uv add "httpx>=0.28.1"

The following client supports both a local Ollama backend and a remote OpenAI-compatible backend, selected via an environment variable. It demonstrates synchronous completion and streaming, which matters especially with local models where the first token can take several seconds to arrive:

# hermes_client.py
#
# A flexible LLM client that supports both local (Ollama) and
# remote (OpenAI-compatible) backends. The backend is selected
# via the USE_LOCAL_LLM environment variable.
#
# Requirements:
#   pip install "httpx>=0.28.1"
#   # or: uv add "httpx>=0.28.1"
#
# Usage:
#   USE_LOCAL_LLM=true python hermes_client.py
#   USE_LOCAL_LLM=false OPENAI_API_KEY=sk-... python hermes_client.py

import os
import json
import httpx
from typing import Optional, Generator


# ---------------------------------------------------------------------------
# Configuration: reads from environment variables with sensible defaults.
# ---------------------------------------------------------------------------

USE_LOCAL_LLM = os.getenv("USE_LOCAL_LLM", "false").lower() == "true"

LOCAL_BASE_URL  = os.getenv("LOCAL_BASE_URL",  "http://127.0.0.1:11434/v1")
LOCAL_MODEL     = os.getenv("LOCAL_MODEL",     "hermes-qwen")
LOCAL_API_KEY   = os.getenv("LOCAL_API_KEY",   "local-no-key-needed")

REMOTE_BASE_URL = os.getenv("REMOTE_BASE_URL", "https://api.openai.com/v1")
REMOTE_MODEL    = os.getenv("REMOTE_MODEL",    "gpt-4o")
REMOTE_API_KEY  = os.getenv("OPENAI_API_KEY",  "")


def build_client_config() -> dict:
    """
    Returns a dictionary with the base_url, model, and api_key
    appropriate for the currently selected backend.
    """
    if USE_LOCAL_LLM:
        return {
            "base_url": LOCAL_BASE_URL,
            "model":    LOCAL_MODEL,
            "api_key":  LOCAL_API_KEY,
        }
    else:
        if not REMOTE_API_KEY:
            raise EnvironmentError(
                "OPENAI_API_KEY is not set. "
                "Set it in your environment or switch to local mode."
            )
        return {
            "base_url": REMOTE_BASE_URL,
            "model":    REMOTE_MODEL,
            "api_key":  REMOTE_API_KEY,
        }


def chat_completion(
    messages:    list[dict],
    system:      Optional[str] = None,
    temperature: float = 0.7,
    max_tokens:  int   = 2048,
) -> str:
    """
    Sends a list of messages to the configured LLM backend and returns
    the assistant's response as a plain string.

    Args:
        messages:    A list of {"role": ..., "content": ...} dicts.
        system:      Optional system prompt prepended to the conversation.
        temperature: Sampling temperature (0.0 = deterministic, 1.0 = creative).
        max_tokens:  Maximum number of tokens in the response.

    Returns:
        The assistant's reply as a string.
    """
    config = build_client_config()

    full_messages: list[dict] = []
    if system:
        full_messages.append({"role": "system", "content": system})
    full_messages.extend(messages)

    headers = {
        "Authorization": f"Bearer {config['api_key']}",
        "Content-Type":  "application/json",
    }

    payload = {
        "model":       config["model"],
        "messages":    full_messages,
        "temperature": temperature,
        "max_tokens":  max_tokens,
    }

    response = httpx.post(
        f"{config['base_url']}/chat/completions",
        headers=headers,
        json=payload,
        timeout=120.0,  # Local models can be slow; give them time.
    )
    response.raise_for_status()

    data = response.json()
    return data["choices"][0]["message"]["content"]


def streaming_chat_completion(
    messages:    list[dict],
    system:      Optional[str] = None,
    temperature: float = 0.7,
) -> Generator[str, None, None]:
    """
    Streams the assistant's response token by token. Yields each text
    chunk as it arrives, allowing the caller to display output in real time.

    This is especially useful with local models, where the first token
    may take several seconds to arrive.
    """
    config = build_client_config()

    full_messages: list[dict] = []
    if system:
        full_messages.append({"role": "system", "content": system})
    full_messages.extend(messages)

    headers = {
        "Authorization": f"Bearer {config['api_key']}",
        "Content-Type":  "application/json",
    }

    payload = {
        "model":       config["model"],
        "messages":    full_messages,
        "stream":      True,
        "temperature": temperature,
    }

    with httpx.stream(
        "POST",
        f"{config['base_url']}/chat/completions",
        headers=headers,
        json=payload,
        timeout=120.0,
    ) as response:
        response.raise_for_status()
        for line in response.iter_lines():
            if not line or line == "data: [DONE]":
                continue
            if line.startswith("data: "):
                chunk_json = line[len("data: "):]
                try:
                    chunk = json.loads(chunk_json)
                    delta = chunk["choices"][0].get("delta", {})
                    text  = delta.get("content", "")
                    if text:
                        yield text
                except (json.JSONDecodeError, KeyError):
                    # Malformed chunk; skip it gracefully.
                    continue


# ---------------------------------------------------------------------------
# Example usage
# ---------------------------------------------------------------------------

if __name__ == "__main__":
    backend = "LOCAL (Ollama)" if USE_LOCAL_LLM else "REMOTE (OpenAI)"
    print(f"Using backend: {backend}")
    print("-" * 50)

    conversation = [
        {
            "role":    "user",
            "content": "Explain the concept of agentic AI in three sentences.",
        }
    ]

    system_prompt = (
        "You are a concise technical writer. "
        "Answer questions clearly and without unnecessary padding."
    )

    print("Response (streaming):")
    for token in streaming_chat_completion(conversation, system=system_prompt):
        print(token, end="", flush=True)
    print("\n")

The streaming_chat_completion function is worth examining closely. Without streaming, your application would appear frozen while a local model generates its response. With streaming, you display each token as it arrives, which makes the experience feel much more responsive even when the underlying model is slow. The timeout=120.0 is intentionally generous — large local models can take a while to produce the first token, and you do not want the client to give up prematurely.


Part Four: Tools, Toolsets, and the Art of Giving an Agent Hands

Chapter 8: Understanding Toolsets and Why They Matter for Goal Configuration

One of the most important configuration decisions you will make for any Hermes deployment is which toolsets to enable. This decision has consequences for security, performance, token consumption, and the agent's ability to accomplish its goal. Enabling too few tools leaves the agent unable to complete its tasks. Enabling too many tools wastes tokens on tool schemas the agent will never use, and it increases the surface area for unintended or harmful actions.

Every tool in Hermes belongs to exactly one toolset. A toolset is simply a named bundle of related tools. When you enable a toolset, all of its tools become available to the agent. When you disable a toolset, none of its tools appear in the system prompt — which means the LLM does not know they exist and cannot call them.

The built-in toolsets cover the following broad categories:

The web toolset provides web_search and web_extract, which let the agent search the internet and retrieve the content of specific URLs. The browser toolset provides a full suite of browser automation tools including browser_navigatebrowser_snapshotbrowser_clickbrowser_vision, and browser_console, which let the agent control a headless browser to interact with websites that require JavaScript or authentication. The terminal toolset provides the terminaland process tools, which let the agent execute shell commands and manage background processes. The file toolset provides read_file and patch, which let the agent read files from disk and make targeted edits to them. The memorytoolset provides the memory tool, which lets the agent update its persistent MEMORY.md and USER.md files. The skills toolset provides skill_viewskill_manage, and skills_list, which let the agent discover, load, and manage its skill library. The delegation toolset provides delegate_task, which lets the agent spawn sub-agents for parallel workstreams. The media toolset provides vision_analyzeimage_generatetext_to_speech, and video_generate for multimodal work. The cron toolset provides the cronjob tool for scheduling automated tasks. The todo toolset provides task planning and tracking capabilities.

In config.yaml, toolsets are configured under the toolsets key. The simplest possible configuration enables everything:

# ~/.hermes/config.yaml
toolsets:
  - all

But for production deployments or goal-specific configurations, you should be explicit about which toolsets you need. A configuration for a coding assistant that should not have internet access or browser control might look like this:

# ~/.hermes/config.yaml
toolsets:
  - terminal
  - file
  - memory
  - skills
  - todo

A configuration for a research assistant that needs web access but should not execute arbitrary shell commands might look like this:

# ~/.hermes/config.yaml
toolsets:
  - web
  - browser
  - memory
  - skills
  - file

The principle is straightforward: give the agent exactly the tools it needs to accomplish its goal, and no more. This is not just a security principle — it is also a performance principle. Every tool schema that appears in the system prompt consumes tokens. A configuration with thirty enabled tools will have a significantly larger system prompt than one with eight enabled tools, which means less room for conversation history and more frequent context compression.

Chapter 9: The Tool Search Layer for Large Tool Collections

When you connect Hermes to multiple MCP servers, you can easily end up with hundreds of tools available. Loading all of their schemas into the system prompt simultaneously would be prohibitively expensive in terms of tokens and would likely confuse the model with too many choices. Hermes addresses this with an opt-in feature called Tool Search.

When Tool Search is enabled, Hermes maintains an index of all available tools but only loads the schemas of tools that are relevant to the current task. At the start of each tool-calling step, a small auxiliary model performs a semantic search over the tool index and selects the most relevant schemas to inject into the context. This means the main model only sees the tools it is likely to need, which saves tokens and improves accuracy.

Tool Search is configured in config.yaml under the tool_search key:

# ~/.hermes/config.yaml
tool_search:
  enabled: true
  top_k: 10
  model: google/gemini-flash-1.5

The top_k parameter controls how many tool schemas are loaded at once. A value of 10 is a reasonable starting point — you may want to increase it if the agent frequently fails to find the right tool, or decrease it if you are hitting context length limits.


Part Five: Memory — Teaching Hermes to Remember

Chapter 10: The Built-In Memory System

The built-in memory system is one of Hermes's most distinctive features, and understanding how it works will help you configure it effectively for different goals.

As mentioned in the architecture overview, Hermes maintains two memory files: MEMORY.md and USER.md. These live in ~/.hermes/memories/ and are injected into the system prompt at the start of every session. The agent can update them during a session using the memory tool, which supports three operations: adding a new entry, replacing an existing entry, and removing an entry.

The MEMORY.md file is intended for facts about the environment and task domain. Good entries include things like the location of important directories, the coding conventions used in a project, the names and purposes of key scripts, and any quirks of the system that the agent has discovered through experience. For example, after a session where the agent discovers that a particular project uses a non-standard test runner, it might add an entry like: "Project X uses pytest-xdist for parallel test execution; always pass -n auto when running tests."

The USER.md file is intended for facts about you. Good entries include your preferred programming language, your communication style preferences (do you want verbose explanations or terse summaries?), your working hours, your preferred output formats, and any recurring tasks you do frequently.

Both files have character limits to prevent them from growing without bound. When an agent tries to write an entry that would push the file over its limit, the memory tool returns an error and prompts the agent to consolidate or remove existing entries before adding new ones. This keeps the memory focused on the most important and current information.

Chapter 11: External Memory Providers

For use cases that require deeper memory capabilities, Hermes ships with several external memory provider plugins. These providers run alongside the built-in memory system and offer features that the simple file-based system cannot provide, such as semantic search over past conversations, knowledge graph construction, and automatic fact extraction.

You can set up an external memory provider using the hermes memory subcommand:

hermes memory setup    # Interactive setup wizard for a provider
hermes memory status   # Shows which provider is currently active
hermes memory off      # Disables the external provider

Alternatively, you can configure the provider directly in config.yaml:

# ~/.hermes/config.yaml
memory:
  provider: mem0
  mem0:
    api_key: m0-your-mem0-api-key

When an external provider is active, Hermes automatically injects provider-specific context into the system prompt at session start, prefetches memories that are relevant to the current conversation, syncs conversation turns to the provider's storage, and extracts new memories at session end. It also adds provider-specific tools to the agent's toolset, allowing the agent to explicitly search, store, and manage memories through the provider's interface.

The choice of external memory provider depends on your goal. If you are building a personal assistant that needs to remember a rich web of facts about your life and preferences, a knowledge graph provider like OpenViking may be the best choice. If you are building a coding assistant that needs to remember past debugging sessions and code patterns, a semantic search provider like Mem0 may be more appropriate. If you are building a customer-facing agent that needs to model individual users' preferences and history, Honcho's user modeling capabilities may be the right fit.


Part Six: Skills — The Self-Improving Loop

Chapter 12: How Skills Work and Why They Are Transformative

The skills system is what separates Hermes from a sophisticated but ultimately static tool executor. A skill is a structured knowledge document that encodes a procedure: not just what to do, but how to do it, in enough detail that the agent can follow the procedure reliably on future occasions without having to rediscover it through trial and error.

Skills are stored as directories in ~/.hermes/skills/, and each skill directory contains at minimum a SKILL.md file. The SKILL.md format is compatible with the agentskills.io open standard, which means skills written for Hermes can potentially be shared with and used by other compatible agents.

The agent loads skills in a three-stage process designed to minimize token consumption. At startup, the agent reads only the name and description fields from every installed skill's YAML frontmatter. This is the discovery stage, and it costs very few tokens because only the metadata is loaded, not the full skill body. When the agent encounters a task that matches a skill's description, it loads the full SKILL.md body into context. This is the activation stage. Finally, if the skill body references external files in scripts/references/, or assets/ subdirectories, those files are loaded only when the agent actually needs them. This is the execution stage.

This progressive disclosure architecture means you can have dozens or even hundreds of skills installed without significantly impacting the agent's context usage during sessions where those skills are not relevant.

Chapter 13: Writing Your First Skill

Let us walk through the process of writing a skill from scratch. Suppose you frequently need to generate a structured project report from a Git repository — something that summarizes recent commits, identifies the most active contributors, lists open issues, and produces a formatted document. This is exactly the kind of task that benefits from being encoded as a skill.

First, create the skill directory:

mkdir -p ~/.hermes/skills/git-project-report

Now create the SKILL.md file. The file begins with YAML frontmatter enclosed in triple-dash delimiters, followed by the skill body in plain text. Note that the triple-dash delimiters are required YAML syntax, not decoration:

---
name: git-project-report
description: >
  Generates a structured project status report from a Git repository.
  Use this skill when asked to summarize project activity, recent commits,
  contributor statistics, or open issues for a Git-based project.
version: "1.0"
metadata:
  author: your-name
  tags: git, reporting, project-management
compatibility:
  system_packages:
    - git
allowed-tools: terminal read_file web_search
---

Git Project Report Skill

Overview

This skill produces a comprehensive project status report for any Git
repository. The report covers recent commit history, contributor activity,
branch status, and optionally fetches open issues from GitHub if the
repository is hosted there.

When to Use This Skill

Use this skill whenever the user asks for a project summary, a sprint
review document, a contributor report, or any kind of structured overview
of a Git repository's recent activity.

Step-by-Step Procedure

Step 1: Identify the repository path. Ask the user for the path to the
repository if it is not already clear from context. Verify that the path
exists and contains a .git directory before proceeding.

Step 2: Gather commit history for the last 30 days using the following
command pattern:

    git -C <repo_path> log --since="30 days ago" \
        --pretty=format:"%h|%an|%ae|%ad|%s" \
        --date=short

Step 3: Gather contributor statistics:

    git -C <repo_path> shortlog -sn --since="30 days ago"

Step 4: Check the current branch and any divergence from the main branch:

    git -C <repo_path> status --short --branch

Step 5: If the repository has a GitHub remote, extract the owner and repo
name from the remote URL and use web_search or the GitHub MCP server to
fetch open issues.

Step 6: Assemble the report in the following structure:
  - Executive Summary (2-3 sentences)
  - Commit Activity (table of commits with date, author, message)
  - Top Contributors (ranked by commit count)
  - Branch Status
  - Open Issues (if available)
  - Recommendations (based on activity patterns)

Step 7: Write the report to a file named project-report-YYYY-MM-DD.md
in the repository root, and also display a summary in the chat.

Common Edge Cases

If the repository has no commits in the last 30 days, extend the window
to 90 days and note this in the report.

If the git command fails because git is not installed, inform the user
and stop.

If the GitHub API rate limit is exceeded, skip the issues section and
note this in the report.

The YAML frontmatter is the part that matters most for skill discovery. The description field is what the agent reads during the discovery stage to decide whether this skill is relevant to the current task. Write it as if you are writing a search engine query: include the key terms that a user might use when asking for this kind of help, and be specific enough that the agent will not accidentally activate the skill for unrelated tasks.

The allowed-tools field is an experimental feature that pre-approves specific tools for use within this skill, which can speed up execution by reducing the need for the agent to reason about tool selection from scratch.

The body of the skill is written for the agent, not for a human reader, though it should be clear enough that a human can understand and audit it. Write in clear, imperative language. Specify exact command patterns where possible. Anticipate edge cases and provide explicit instructions for handling them. The agent will follow these instructions more reliably if they are unambiguous.

Chapter 14: Teaching Hermes a New Skill Interactively

You do not always have to write skills by hand. Hermes can create skills from observation. After completing a complex task successfully, you can ask Hermes to encode what it just did as a reusable skill:

/learn

The /learn slash command opens an interactive flow where Hermes reviews the current session's tool calls and reasoning steps, distills them into a structured SKILL.md file, and saves it to the skills directory. You can also point /learn at an external document or URL:

/learn https://docs.example.com/deployment-procedure

In this case, Hermes reads the document and converts it into a skill that encodes the procedure described in the document. This is an extremely powerful way to onboard Hermes to your organization's existing documentation — point it at your runbooks and watch it internalize them.


Part Seven: MCP Servers — Connecting Hermes to the World

Chapter 15: What MCP Is and Why It Matters

The Model Context Protocol is an open standard for communication between AI agents and external tool servers. Think of it as a universal adapter: just as USB-C lets you connect many different devices to many different computers without needing a different cable for each combination, MCP lets AI agents connect to many different tool servers without needing a custom integration for each one.

An MCP server is a process that exposes a set of tools over a standardized JSON-RPC 2.0 interface. The server can be a local process communicating over standard input/output (stdio transport), or a remote HTTP server. When Hermes connects to an MCP server, it automatically discovers all the tools that server exposes, creates schemas for them, and makes them available to the agent. The tools appear in the agent's tool list with a prefix of mcp_<server_name>_, so a tool called get_file_contents on an MCP server named filesystem would appear as mcp_filesystem_get_file_contents.

This tutorial targets the MCP 2025-11-25 specification — the stable release that marked MCP's one-year anniversary and introduced experimental Tasks support for asynchronous workflows, simplified OAuth with Client ID Metadata Documents, OpenID Connect Discovery, and metadata for tools and resources. This is the version you will encounter in the wild today, and the version that FastMCP 3.0 targets by default.

The MCP ecosystem is growing rapidly. There are already MCP servers for GitHub, GitLab, Jira, Confluence, Slack, Google Drive, Notion, PostgreSQL, SQLite, and many other services. Because MCP is an open standard, tools built for Claude Code or Cursor can be used directly in Hermes without modification.

Chapter 16: Configuring MCP Servers in Hermes

MCP servers are configured in the mcp_servers section of config.yaml. Each entry specifies either a local stdio server (using the command and args keys) or a remote HTTP server (using the url key).

Here is a configuration that connects Hermes to three MCP servers simultaneously: a filesystem server for local file access, a GitHub server for repository management, and a PostgreSQL server for database access:

# ~/.hermes/config.yaml
mcp_servers:

  # Filesystem server: gives Hermes access to a specific directory.
  # The path argument restricts access to only that directory,
  # which is an important security boundary.
  filesystem:
    command: npx
    args:
      - "-y"
      - "@modelcontextprotocol/server-filesystem"
      - "/home/youruser/projects"

  # GitHub server: allows Hermes to read and write to GitHub repos,
  # manage issues, pull requests, and more.
  github:
    command: npx
    args:
      - "-y"
      - "@modelcontextprotocol/server-github"
    env:
      GITHUB_PERSONAL_ACCESS_TOKEN: "${GITHUB_TOKEN}"

  # PostgreSQL server: allows Hermes to query a database.
  # The connection string is passed as an argument.
  postgres:
    command: npx
    args:
      - "-y"
      - "@modelcontextprotocol/server-postgres"
      - "postgresql://user:password@localhost:5432/mydb"

The env key in the github server configuration shows how to pass environment variables to an MCP server. The ${GITHUB_TOKEN} syntax tells Hermes to substitute the value of the GITHUB_TOKEN environment variable from your shell or from ~/.hermes/.env. This keeps secrets out of the config.yaml file itself.

For remote HTTP-based MCP servers, the configuration uses a url key instead of command and args:

# ~/.hermes/config.yaml
mcp_servers:
  google_drive:
    url: https://drivemcp.googleapis.com/mcp/v1
    auth: oauth
    oauth:
      client_id: your-oauth-client-id
      client_secret: your-oauth-client-secret

Chapter 17: Building Your Own MCP Server in Python

Building a custom MCP server is the most powerful way to extend Hermes with tools that are specific to your organization's systems, internal APIs, or proprietary data sources. FastMCP 3.0, released in January 2026, makes this remarkably straightforward. FastMCP 3.0 is a major architectural overhaul that rebuilt the core around Providers and Transforms for enhanced extensibility, observability, and composability. It also introduced component versioning, granular authorization, and OpenTelemetry integration — while keeping the surface API familiar for anyone who has used earlier versions.

Install FastMCP 3.0. The recommended approach uses uv for fast dependency resolution, but pip works equally well:

# Recommended: uv (significantly faster dependency resolution)
uv add "fastmcp>=3.0"

# Alternative: pip
pip install "fastmcp>=3.0"

# Verify the installation:
fastmcp version

The following example builds a complete MCP server that exposes tools for interacting with a hypothetical internal project management system. It uses the FastMCP 3.0 @mcp.tool decorator (without parentheses for the simple case where no metadata overrides are needed) and demonstrates input validation, error handling, structured return values, and documentation that helps the LLM understand when and how to use each tool:

# internal_pm_mcp_server.py
#
# An MCP server that exposes tools for an internal project management system.
# Targets the MCP 2025-11-25 specification via FastMCP 3.0.
#
# Requirements:
#   uv add "fastmcp>=3.0"
#   # or: pip install "fastmcp>=3.0"
#
# Run standalone for testing:
#   python internal_pm_mcp_server.py
#
# Add to Hermes by placing the following in ~/.hermes/config.yaml:
#   mcp_servers:
#     internal_pm:
#       command: python
#       args: ["/path/to/internal_pm_mcp_server.py"]

import json
import datetime
from typing import Optional
from fastmcp import FastMCP

# Initialize the MCP server with a descriptive name.
# This name appears in Hermes's tool list as the prefix: mcp_internal_pm_*
mcp = FastMCP("internal_pm")


# ---------------------------------------------------------------------------
# Simulated data store. In a real implementation, replace these dicts
# with calls to your actual project management API or database.
# ---------------------------------------------------------------------------

PROJECTS: dict[str, dict] = {
    "PROJ-001": {
        "name":   "Hermes Integration",
        "status": "active",
        "owner":  "alice@example.com",
        "tasks":  ["TASK-101", "TASK-102", "TASK-103"],
    },
    "PROJ-002": {
        "name":   "Data Pipeline Refactor",
        "status": "planning",
        "owner":  "bob@example.com",
        "tasks":  ["TASK-201"],
    },
}

TASKS: dict[str, dict] = {
    "TASK-101": {
        "title":    "Set up MCP server scaffold",
        "status":   "done",
        "assignee": "alice@example.com",
        "due":      "2026-07-01",
    },
    "TASK-102": {
        "title":    "Write tool handlers",
        "status":   "in_progress",
        "assignee": "alice@example.com",
        "due":      "2026-07-15",
    },
    "TASK-103": {
        "title":    "Integration testing",
        "status":   "todo",
        "assignee": None,
        "due":      "2026-07-30",
    },
    "TASK-201": {
        "title":    "Design new pipeline architecture",
        "status":   "todo",
        "assignee": "bob@example.com",
        "due":      "2026-08-01",
    },
}


@mcp.tool
def list_projects(status_filter: Optional[str] = None) -> str:
    """
    Lists all projects in the project management system.

    Use this tool when the user asks about available projects, wants to
    see what projects exist, or needs to find a project by status.

    Args:
        status_filter: Optional. Filter projects by status. Valid values
                       are 'active', 'planning', 'completed', or 'archived'.
                       If omitted, all projects are returned.

    Returns:
        A JSON string containing a list of project summaries.
    """
    results = []
    for project_id, project in PROJECTS.items():
        if status_filter and project["status"] != status_filter:
            continue
        results.append({
            "id":         project_id,
            "name":       project["name"],
            "status":     project["status"],
            "owner":      project["owner"],
            "task_count": len(project["tasks"]),
        })
    return json.dumps(results, indent=2)


@mcp.tool
def get_project_details(project_id: str) -> str:
    """
    Retrieves full details for a specific project, including all its tasks.

    Use this tool when the user asks about a specific project's status,
    tasks, or ownership. Requires a valid project ID (e.g., 'PROJ-001').

    Args:
        project_id: The unique identifier of the project (e.g., 'PROJ-001').

    Returns:
        A JSON string with full project details and task summaries,
        or an error message if the project ID is not found.
    """
    project = PROJECTS.get(project_id)
    if not project:
        return json.dumps({
            "error": (
                f"Project '{project_id}' not found. "
                "Use list_projects to see available project IDs."
            )
        })

    task_details = []
    for task_id in project["tasks"]:
        task = TASKS.get(task_id, {})
        task_details.append({
            "id":       task_id,
            "title":    task.get("title",    "Unknown"),
            "status":   task.get("status",   "unknown"),
            "assignee": task.get("assignee", "unassigned"),
            "due":      task.get("due",      "no due date"),
        })

    return json.dumps({
        "id":     project_id,
        "name":   project["name"],
        "status": project["status"],
        "owner":  project["owner"],
        "tasks":  task_details,
    }, indent=2)


@mcp.tool
def update_task_status(task_id: str, new_status: str) -> str:
    """
    Updates the status of a specific task.

    Use this tool when the user wants to mark a task as done, move it
    to in-progress, or change its status for any other reason.

    Args:
        task_id:    The unique identifier of the task (e.g., 'TASK-102').
        new_status: The new status. Must be one of: 'todo', 'in_progress',
                    'done', 'blocked', 'cancelled'.

    Returns:
        A JSON string confirming the update, or an error message.
    """
    valid_statuses = {"todo", "in_progress", "done", "blocked", "cancelled"}

    if new_status not in valid_statuses:
        return json.dumps({
            "error": (
                f"Invalid status '{new_status}'. "
                f"Valid values are: {', '.join(sorted(valid_statuses))}"
            )
        })

    task = TASKS.get(task_id)
    if not task:
        return json.dumps({"error": f"Task '{task_id}' not found."})

    old_status     = task["status"]
    task["status"] = new_status

    # Use timezone-aware UTC timestamp.
    # datetime.utcnow() is deprecated in Python 3.12+; use this form instead.
    now_utc = datetime.datetime.now(datetime.timezone.utc).isoformat()

    return json.dumps({
        "success":    True,
        "task_id":    task_id,
        "title":      task["title"],
        "old_status": old_status,
        "new_status": new_status,
        "updated_at": now_utc,
    }, indent=2)


if __name__ == "__main__":
    # Run the server using stdio transport, which is what Hermes expects
    # for locally-configured MCP servers.
    # FastMCP 3.0: mcp.run() defaults to stdio; being explicit is clearer.
    mcp.run(transport="stdio")

To connect this server to Hermes, add the following to your config.yaml:

# ~/.hermes/config.yaml
mcp_servers:
  internal_pm:
    command: python
    args:
      - "/path/to/internal_pm_mcp_server.py"

After restarting Hermes, the tools mcp_internal_pm_list_projectsmcp_internal_pm_get_project_details, and mcp_internal_pm_update_task_status will be available to the agent. You can verify this by asking Hermes: "What tools do you have for project management?"

The docstrings on each tool function are critically important. FastMCP 3.0 uses them as the tool descriptions that are sent to the LLM. The LLM reads these descriptions to decide which tool to call and how to call it. Write them as if you are explaining the tool to a smart colleague who has never seen your system before: describe what the tool does, when to use it, what the parameters mean, and what the return value looks like.

When you need to override the default inferred metadata — for example, to give a tool a human-readable display title, mark it as destructive, or add categorization tags — use @mcp.tool() with explicit arguments:

from mcp.types import ToolAnnotations

@mcp.tool(
    name="archive_project",
    title="Archive a Project",
    description="Permanently archives a project and all its tasks. This action cannot be undone.",
    annotations=ToolAnnotations(readOnlyHint=False, destructiveHint=True),
    tags={"projects", "destructive"},
)
def archive_project(project_id: str, confirm: bool = False) -> str:
    """Internal implementation — description above overrides this docstring."""
    if not confirm:
        return json.dumps({"error": "Set confirm=true to proceed with archival."})
    # ... archival logic ...
    return json.dumps({"success": True, "archived": project_id})

Part Eight: Plugins — Extending Hermes from the Inside

Chapter 18: The Plugin Architecture

While MCP servers extend Hermes from the outside by adding tools that run in separate processes, plugins extend Hermes from the inside by running Python code within the Hermes process itself. This gives plugins access to capabilities that MCP servers cannot have: they can hook into the agent's lifecycle events, modify the system prompt, observe every tool call and response, and register tools that have direct access to Hermes's internal state.

A plugin lives in a directory under ~/.hermes/plugins/<name>/. The directory must contain a plugin.yaml manifest file and at least one Python file that implements a register(ctx) function.

Note: The plugin API shown in this chapter reflects Hermes's documented plugin architecture as of the July 2026 "Judgment Release." Because Hermes is actively developed, specific method names on the context object may evolve between releases. Always run hermes config migrate after updating Hermes to ensure your configuration schema is current, and check hermes plugins --help to confirm the current API surface before building production plugins.

The plugin.yaml manifest is minimal:

# ~/.hermes/plugins/my_analytics/plugin.yaml
name: my_analytics
version: "1.0"
description: >
  Tracks tool usage and session metrics, writing them to a local
  analytics file for later analysis.

The Python file implements the register function, which receives a context object that provides the API for registering tools and hooks:

# ~/.hermes/plugins/my_analytics/plugin.py
#
# A Hermes plugin that tracks tool usage metrics across sessions.
# It demonstrates both tool registration and lifecycle hook usage.

import json
import datetime
import pathlib
from typing import Any


# The analytics log file lives in a dedicated subdirectory.
ANALYTICS_DIR  = pathlib.Path.home() / ".hermes" / "analytics"
ANALYTICS_FILE = ANALYTICS_DIR / "tool_usage.jsonl"


def _ensure_analytics_dir() -> None:
    """Creates the analytics directory if it does not already exist."""
    ANALYTICS_DIR.mkdir(parents=True, exist_ok=True)


def _log_event(event_type: str, data: dict) -> None:
    """
    Appends a single event record to the analytics log file.
    Uses JSON Lines format (one JSON object per line) for easy parsing.
    """
    _ensure_analytics_dir()
    # Use timezone-aware UTC timestamp throughout.
    now_utc = datetime.datetime.now(datetime.timezone.utc).isoformat()
    record = {
        "timestamp":  now_utc,
        "event_type": event_type,
        **data,
    }
    with open(ANALYTICS_FILE, "a", encoding="utf-8") as f:
        f.write(json.dumps(record) + "\n")


def _on_session_start(ctx: Any) -> None:
    """
    Lifecycle hook called when a new Hermes session begins.
    Logs the session start event.
    """
    _log_event("session_start", {
        "session_id": getattr(ctx, "session_id", "unknown"),
    })


def _on_session_end(ctx: Any) -> None:
    """
    Lifecycle hook called when a Hermes session ends.
    Logs the session end event.
    """
    _log_event("session_end", {
        "session_id": getattr(ctx, "session_id", "unknown"),
    })


def _post_tool_call(ctx: Any, tool_name: str, result: Any) -> None:
    """
    Lifecycle hook called after every tool execution.
    Logs the tool name and whether it succeeded or returned an error.
    """
    result_str = str(result) if result is not None else ""
    is_error   = (
        "error" in result_str.lower()
        or "failed" in result_str.lower()
    )

    _log_event("tool_call", {
        "tool_name": tool_name,
        "success":   not is_error,
    })


def get_analytics_summary() -> str:
    """
    Tool handler: reads the analytics log and returns a summary of
    tool usage statistics across all sessions.
    """
    if not ANALYTICS_FILE.exists():
        return "No analytics data found yet. Use some tools first!"

    tool_counts: dict[str, int] = {}
    session_count = 0

    with open(ANALYTICS_FILE, "r", encoding="utf-8") as f:
        for line in f:
            line = line.strip()
            if not line:
                continue
            try:
                record = json.loads(line)
                if record.get("event_type") == "session_start":
                    session_count += 1
                elif record.get("event_type") == "tool_call":
                    tool_name = record.get("tool_name", "unknown")
                    tool_counts[tool_name] = tool_counts.get(tool_name, 0) + 1
            except json.JSONDecodeError:
                continue

    sorted_tools = sorted(
        tool_counts.items(),
        key=lambda item: item[1],
        reverse=True,
    )

    lines = [
        f"Total sessions tracked: {session_count}",
        f"Total tool calls tracked: {sum(tool_counts.values())}",
        "",
        "Top tools by usage:",
    ]
    for tool_name, count in sorted_tools[:10]:
        lines.append(f"  {tool_name}: {count} calls")

    return "\n".join(lines)


def register(ctx: Any) -> None:
    """
    Entry point called by Hermes when the plugin is loaded.
    Registers tools and lifecycle hooks with the provided context object.
    """
    # Register lifecycle hooks to track session and tool events.
    ctx.register_hook("on_session_start", _on_session_start)
    ctx.register_hook("on_session_end",   _on_session_end)
    ctx.register_hook("post_tool_call",   _post_tool_call)

    # Register a tool that lets the agent (and user) query the analytics.
    ctx.register_tool(
        name="get_analytics_summary",
        toolset="analytics",
        schema={
            "name":        "get_analytics_summary",
            "description": (
                "Returns a summary of tool usage statistics across all "
                "Hermes sessions. Use this when the user asks about which "
                "tools have been used most frequently, or to get an overview "
                "of agent activity."
            ),
            "parameters": {
                "type":       "object",
                "properties": {},
                "required":   [],
            },
        },
        handler=get_analytics_summary,
    )

After creating the plugin directory and files, enable the plugin by adding it to your config.yaml under the plugins key, then restart Hermes:

# ~/.hermes/config.yaml
plugins:
  - my_analytics

You can verify the plugin loaded by running /plugins inside a Hermes session. The analytics tool will now appear in the agent's toolset, and the lifecycle hooks will silently log events to ~/.hermes/analytics/tool_usage.jsonl in the background.


Part Nine: Profiles and Goal-Oriented Configuration

Chapter 19: The Concept of Profiles

A profile in Hermes is a named configuration that defines a specific agent persona, toolset, memory configuration, and model selection. Profiles let you maintain multiple distinct Hermes configurations and switch between them depending on what you are trying to accomplish.

Think of profiles as different modes for the same underlying system. Your "coding assistant" profile might use a code-specialized local model, enable only the terminal and file toolsets, and have a MEMORY.md full of your project's coding conventions. Your "research assistant" profile might use a frontier reasoning model, enable the web and browser toolsets, and have a MEMORY.md full of your research domain's key concepts and sources. Your "devops" profile might use a mid-tier model for cost efficiency, enable the terminal and MCP server toolsets for your cloud infrastructure, and have a MEMORY.md full of your deployment procedures.

Profiles are defined in config.yaml under the profiles key, and you switch between them using the /profile slash command inside a session, or the --profile flag when launching Hermes from the command line:

hermes --profile coding-assistant
hermes --profile research-assistant
hermes --profile devops-automation

Chapter 20: Configuring Profiles for Different Goals

Let us build out a complete config.yaml that defines three profiles for three distinct goals. This example demonstrates how dramatically different two Hermes configurations can look when they are optimized for different purposes.

Note: The profiles key and system_prompt_extra field shown below reflect the Hermes configuration patterns as of the July 2026 "Judgment Release." Run hermes config migrate after updating Hermes to ensure your config.yaml schema is current with the latest version.

# ~/.hermes/config.yaml
#
# Hermes configuration with three goal-oriented profiles.
# Switch profiles with: hermes --profile <name>
# or inside a session with: /profile <name>
#
# Run "hermes config migrate" after updating Hermes to keep this
# file's schema version current.

# Default model (used when no profile overrides it)
model:
  provider: openrouter
  base_url: https://openrouter.ai/api/v1
  default: anthropic/claude-opus-4-5

# Memory configuration (default: no external provider)
memory:
  provider: none

# Compression: use a cheap fast model to summarize long contexts
compression:
  enabled: true
  threshold: 0.60

auxiliary:
  compression:
    provider: openrouter
    base_url: https://openrouter.ai/api/v1
    model: google/gemini-flash-1.5

# -----------------------------------------------------------------------
# Profile definitions
# -----------------------------------------------------------------------
profiles:

  # Profile: coding-assistant
  # Optimized for software development tasks on a local codebase.
  # Uses a local Ollama model for privacy and cost efficiency.
  coding-assistant:
    model:
      provider: custom
      base_url: http://127.0.0.1:11434/v1
      default: hermes-qwen
      api_key: local-no-key-needed
    toolsets:
      - terminal
      - file
      - memory
      - skills
      - todo
    mcp_servers:
      github:
        command: npx
        args:
          - "-y"
          - "@modelcontextprotocol/server-github"
        env:
          GITHUB_PERSONAL_ACCESS_TOKEN: "${GITHUB_TOKEN}"
    system_prompt_extra: >
      You are a senior software engineer. You write clean, well-tested,
      well-documented code. You always check existing code before writing
      new code. You prefer to make small, targeted changes rather than
      rewriting large sections. You ask clarifying questions before
      starting any significant implementation task.

  # Profile: research-assistant
  # Optimized for deep research tasks requiring web access and synthesis.
  # Uses a frontier reasoning model for complex analysis.
  research-assistant:
    model:
      provider: openrouter
      base_url: https://openrouter.ai/api/v1
      default: anthropic/claude-opus-4-5
    toolsets:
      - web
      - browser
      - file
      - memory
      - skills
      - todo
    memory:
      provider: mem0
      mem0:
        api_key: "${MEM0_API_KEY}"
    tool_search:
      enabled: true
      top_k: 8
    system_prompt_extra: >
      You are a meticulous research analyst. You always cite your sources.
      You distinguish clearly between established facts and your own
      interpretations. You present multiple perspectives on contested
      questions. You flag when information may be outdated.

  # Profile: devops-automation
  # Optimized for infrastructure management and deployment automation.
  # Uses multiple MCP servers for cloud and project management tools.
  devops-automation:
    model:
      provider: openrouter
      base_url: https://openrouter.ai/api/v1
      default: anthropic/claude-sonnet-4-5
    toolsets:
      - terminal
      - file
      - memory
      - skills
      - delegation
      - cron
    mcp_servers:
      internal_pm:
        command: python
        args:
          - "/opt/hermes-tools/internal_pm_mcp_server.py"
      filesystem:
        command: npx
        args:
          - "-y"
          - "@modelcontextprotocol/server-filesystem"
          - "/opt/deployments"
    tool_search:
      enabled: true
      top_k: 12
    system_prompt_extra: >
      You are a senior DevOps engineer. You prioritize reliability and
      safety above speed. Before executing any destructive operation,
      you always confirm with the user. You prefer idempotent operations.
      You document every significant action in the session notes.

This configuration demonstrates several important principles. The coding-assistant profile uses a local model because code generation does not require internet access and privacy is important for proprietary codebases. The research-assistant profile uses a frontier model and an external memory provider because research tasks benefit from the highest-quality reasoning and from remembering past research sessions. The devops-automation profile enables the delegationtoolset because infrastructure tasks often benefit from running multiple checks in parallel, and it enables the cron toolset because scheduled health checks and deployment verifications are common in DevOps workflows.

The system_prompt_extra field in each profile injects additional instructions into the agent's system prompt that shape its personality and behavior for that specific goal. This is a powerful lever: the same underlying model will behave quite differently depending on what persona it is given.


Part Ten: Sub-Agents and Parallel Workstreams

Chapter 21: When One Agent Is Not Enough

For complex goals that can be decomposed into independent subtasks, Hermes supports spawning sub-agents: isolated agent instances that each have their own conversation history, terminal session, and toolset, but share the main agent's skill library. Sub-agents can run in parallel, which can dramatically reduce the time needed to complete multi-part tasks.

The delegate_task tool is the mechanism for spawning sub-agents. When the main agent calls delegate_task, it provides a description of the subtask and optionally a set of constraints (such as which tools the sub-agent should use). Hermes spawns a new agent instance, runs it to completion, and returns the result to the main agent.

The key insight about sub-agents is that they are best suited for tasks that are both independent (they do not depend on each other's results) and complex (they require multiple tool calls and reasoning steps). For simple one-shot tasks, the overhead of spawning a sub-agent is not worth it. For tasks that would generate a lot of intermediate data that would clutter the main agent's context, sub-agents are ideal because their intermediate steps are isolated.

A practical example: suppose your goal is to produce a competitive analysis report that compares five different software products across eight dimensions. You could have the main agent research all five products sequentially, but that would take a long time and would fill the main context with a lot of intermediate research data. Instead, you could spawn five sub-agents in parallel, each responsible for researching one product, and then have the main agent synthesize their findings into the final report.

Chapter 22: Orchestrating Parallel Research with Python

The following script demonstrates how to orchestrate a multi-agent research workflow using direct OpenAI-compatible API calls — the same API that Hermes uses internally. Each research task runs in its own thread, calling the LLM independently, and the results are collected and passed to a synthesis step. This pattern works with any OpenAI-compatible backend, including local Ollama models.

Install the required library first:

pip install "httpx>=0.28.1"
# or: uv add "httpx>=0.28.1"
# multi_agent_research.py
#
# Demonstrates a parallel research workflow using direct OpenAI-compatible
# API calls. Each topic is researched concurrently in its own thread,
# and a final synthesis step produces a unified comparative report.
#
# This pattern mirrors what Hermes does internally when the delegate_task
# tool spawns sub-agents for parallel workstreams.
#
# Requirements:
#   pip install "httpx>=0.28.1"
#   # or: uv add "httpx>=0.28.1"
#
# Usage:
#   # Remote (OpenAI):
#   OPENAI_API_KEY=sk-... python multi_agent_research.py
#
#   # Local (Ollama — ensure OLLAMA_CONTEXT_LENGTH=65536 or Modelfile is set):
#   USE_LOCAL_LLM=true python multi_agent_research.py

import os
import json
import textwrap
import concurrent.futures
import httpx
from typing import Optional


# ---------------------------------------------------------------------------
# Backend configuration — mirrors the hermes_client.py pattern.
# ---------------------------------------------------------------------------

USE_LOCAL_LLM = os.getenv("USE_LOCAL_LLM", "false").lower() == "true"

if USE_LOCAL_LLM:
    BASE_URL = os.getenv("LOCAL_BASE_URL", "http://127.0.0.1:11434/v1")
    MODEL    = os.getenv("LOCAL_MODEL",    "hermes-qwen")
    API_KEY  = os.getenv("LOCAL_API_KEY",  "local-no-key-needed")
else:
    BASE_URL = os.getenv("REMOTE_BASE_URL", "https://api.openai.com/v1")
    MODEL    = os.getenv("REMOTE_MODEL",    "gpt-4o")
    API_KEY  = os.getenv("OPENAI_API_KEY",  "")
    if not API_KEY:
        raise EnvironmentError(
            "OPENAI_API_KEY is not set. "
            "Set it in your environment or use USE_LOCAL_LLM=true."
        )


# ---------------------------------------------------------------------------
# Research configuration
# ---------------------------------------------------------------------------

RESEARCH_TOPICS = [
    "LangChain agent framework capabilities and limitations",
    "AutoGen multi-agent framework capabilities and limitations",
    "CrewAI framework capabilities and limitations",
    "Hermes Agent by Nous Research capabilities and limitations",
    "LlamaIndex agent capabilities and limitations",
]

EVALUATION_DIMENSIONS = [
    "memory and persistence",
    "tool integration",
    "multi-agent support",
    "local LLM support",
    "ease of configuration",
    "community and ecosystem",
    "production readiness",
    "self-improvement capabilities",
]


# ---------------------------------------------------------------------------
# Core LLM call — synchronous, used from worker threads.
# ---------------------------------------------------------------------------

def call_llm(
    messages:    list[dict],
    system:      Optional[str] = None,
    temperature: float = 0.3,
    max_tokens:  int   = 3000,
) -> str:
    """
    Sends a chat completion request to the configured backend and returns
    the assistant's response as a plain string.

    Args:
        messages:    Conversation history as a list of role/content dicts.
        system:      Optional system prompt inserted before the conversation.
        temperature: Sampling temperature. Lower values produce more focused
                     and consistent output, which is desirable for research.
        max_tokens:  Maximum tokens in the response.

    Returns:
        The assistant's reply as a string, or an error message if the
        request fails.
    """
    full_messages: list[dict] = []
    if system:
        full_messages.append({"role": "system", "content": system})
    full_messages.extend(messages)

    headers = {
        "Authorization": f"Bearer {API_KEY}",
        "Content-Type":  "application/json",
    }
    payload = {
        "model":       MODEL,
        "messages":    full_messages,
        "temperature": temperature,
        "max_tokens":  max_tokens,
    }

    try:
        response = httpx.post(
            f"{BASE_URL}/chat/completions",
            headers=headers,
            json=payload,
            timeout=180.0,  # Research prompts can be long; be patient.
        )
        response.raise_for_status()
        data = response.json()
        return data["choices"][0]["message"]["content"]
    except httpx.HTTPStatusError as exc:
        return f"HTTP error {exc.response.status_code}: {exc.response.text[:200]}"
    except httpx.RequestError as exc:
        return f"Request error: {exc}"


# ---------------------------------------------------------------------------
# Research worker — one per topic, runs in its own thread.
# ---------------------------------------------------------------------------

def research_topic(topic: str) -> tuple[str, str]:
    """
    Researches a single topic and evaluates it across all defined dimensions.

    Each call to this function is independent: it creates its own HTTP
    connection and has no shared state with other concurrent calls.
    This mirrors the isolation guarantee of Hermes sub-agents.

    Args:
        topic: The topic to research (e.g., a framework name and focus area).

    Returns:
        A tuple of (topic, findings) where findings is the LLM's structured
        assessment of the topic across all evaluation dimensions.
    """
    dimensions_list = "\n".join(
        f"  - {dim}" for dim in EVALUATION_DIMENSIONS
    )

    prompt = textwrap.dedent(f"""
        Research the following topic and provide a structured assessment:

        Topic: {topic}

        For each dimension listed below, write a concise assessment of
        2 to 4 sentences based on publicly available documentation,
        community discussions, and recent releases. Be specific and
        cite concrete examples where possible. Note the approximate
        date of any time-sensitive information.

        Dimensions to evaluate:
        {dimensions_list}

        Format your response with one clearly labeled section per dimension.
    """).strip()

    system = (
        "You are a technical analyst specializing in AI developer tooling. "
        "You write precise, evidence-based assessments. "
        "You do not speculate beyond what the available evidence supports."
    )

    findings = call_llm(
        messages=[{"role": "user", "content": prompt}],
        system=system,
        temperature=0.2,  # Low temperature for factual consistency.
        max_tokens=2500,
    )
    return (topic, findings)


# ---------------------------------------------------------------------------
# Synthesis step — runs after all research workers complete.
# ---------------------------------------------------------------------------

def synthesize_report(topic_findings: list[tuple[str, str]]) -> str:
    """
    Takes the individual research findings for each topic and synthesizes
    them into a unified comparative analysis report.

    Args:
        topic_findings: A list of (topic, findings) tuples from research_topic.

    Returns:
        A formatted comparative analysis report as a string.
    """
    findings_text = "\n\n".join(
        f"Research findings for: {topic}\n\n{findings}"
        for topic, findings in topic_findings
    )

    synthesis_prompt = textwrap.dedent(f"""
        You have received research reports on five AI agent frameworks.
        Synthesize these into a unified comparative analysis report with
        the following structure:

        1. Executive Summary (one paragraph summarizing the landscape)
        2. Comparison Table (frameworks as columns, dimensions as rows,
           brief cell entries of 5 to 10 words each)
        3. Dimension-by-Dimension Analysis (one section per dimension,
           discussing trade-offs across all five frameworks)
        4. Recommendations (which framework suits which use case, and why)

        Here are the individual research reports:

        {findings_text}
    """).strip()

    system = (
        "You are a senior technical writer producing a professional "
        "comparative analysis report. Write clearly and objectively. "
        "Structure the report so that a busy engineering manager can "
        "extract the key insights in under five minutes."
    )

    return call_llm(
        messages=[{"role": "user", "content": synthesis_prompt}],
        system=system,
        temperature=0.3,
        max_tokens=4000,
    )


# ---------------------------------------------------------------------------
# Main orchestration
# ---------------------------------------------------------------------------

def main() -> None:
    backend = "LOCAL (Ollama)" if USE_LOCAL_LLM else f"REMOTE ({BASE_URL})"
    print(f"Backend: {backend}")
    print(f"Model:   {MODEL}")
    print(f"Topics:  {len(RESEARCH_TOPICS)}")
    print()
    print("Starting parallel research...")

    topic_findings: list[tuple[str, str]] = []

    # Run all research tasks concurrently. Each thread is independent,
    # mirroring the isolation of Hermes sub-agents.
    with concurrent.futures.ThreadPoolExecutor(
        max_workers=len(RESEARCH_TOPICS)
    ) as executor:
        future_to_topic = {
            executor.submit(research_topic, topic): topic
            for topic in RESEARCH_TOPICS
        }

        for future in concurrent.futures.as_completed(future_to_topic):
            topic = future_to_topic[future]
            try:
                result = future.result()
                topic_findings.append(result)
                short_topic = topic[:55] + "..." if len(topic) > 55 else topic
                print(f"  [done] {short_topic}")
            except Exception as exc:
                print(f"  [error] {topic[:55]}: {exc}")
                topic_findings.append((topic, f"Research failed: {exc}"))

    print()
    print("Synthesizing final report...")
    final_report = synthesize_report(topic_findings)

    output_path = "agent_framework_comparison.md"
    with open(output_path, "w", encoding="utf-8") as f:
        f.write(final_report)

    print(f"Report written to: {output_path}")
    print()
    preview = final_report[:600].rstrip()
    print("Preview:")
    print(preview)
    if len(final_report) > 600:
        print(f"\n... ({len(final_report) - 600} more characters in the file)")


if __name__ == "__main__":
    main()

This script is genuinely runnable against any OpenAI-compatible backend. The ThreadPoolExecutor runs all five research tasks concurrently, collecting results as they complete. The synthesis step then receives all findings and produces a unified report. This is exactly the pattern that Hermes's delegate_task tool implements internally — the difference is that Hermes manages the sub-agent lifecycle automatically, while this script manages it explicitly.


Part Eleven: Putting It All Together — Goal-Oriented Configuration Recipes

Chapter 23: Recipe 1 — The Personal Research Assistant

Goal: An agent that helps you conduct deep research on any topic, remembers your past research sessions, builds on previous findings, and produces well-structured reports.

The key requirements for this goal are strong web access, a high-quality reasoning model, persistent memory that can recall past research, and the ability to browse JavaScript-heavy websites. The agent should be conservative about executing code or modifying files.

# ~/.hermes/config.yaml (research-assistant profile)
model:
  provider: openrouter
  base_url: https://openrouter.ai/api/v1
  default: anthropic/claude-opus-4-5

toolsets:
  - web
  - browser
  - file
  - memory
  - skills
  - todo
  - media

memory:
  provider: mem0
  mem0:
    api_key: "${MEM0_API_KEY}"

compression:
  enabled: true
  threshold: 0.55

auxiliary:
  compression:
    provider: openrouter
    base_url: https://openrouter.ai/api/v1
    model: google/gemini-flash-1.5
  vision:
    provider: openrouter
    base_url: https://openrouter.ai/api/v1
    model: google/gemini-flash-1.5

tool_search:
  enabled: true
  top_k: 8

The corresponding MEMORY.md for this profile should be seeded with your research domain:

## Research Domain
Primary research areas: agentic AI, LLM architectures, multi-agent systems.
Secondary areas: distributed systems, knowledge graphs.

## Output Preferences
Reports should use academic citation style (Author, Year).
Always distinguish between primary sources and secondary summaries.
Flag any information older than 12 months as potentially outdated.

## Trusted Sources
Prefer: arxiv.org, ACL Anthology, NeurIPS proceedings, official documentation.
Treat with caution: blog posts without citations, social media claims.

Chapter 24: Recipe 2 — The Autonomous DevOps Engineer

Goal: An agent that can manage infrastructure, run deployments, monitor systems, and respond to alerts, with the ability to schedule recurring tasks and spawn sub-agents for parallel checks.

This goal requires terminal access, file access, MCP servers for cloud infrastructure, the delegation toolset for parallel workstreams, and the cron toolset for scheduled automation. The model does not need to be the most powerful available, since most DevOps tasks are procedural rather than requiring deep reasoning, so a mid-tier model is appropriate for cost efficiency.

# ~/.hermes/config.yaml (devops-automation profile)
model:
  provider: openrouter
  base_url: https://openrouter.ai/api/v1
  default: anthropic/claude-sonnet-4-5

toolsets:
  - terminal
  - file
  - memory
  - skills
  - delegation
  - cron
  - todo

mcp_servers:
  filesystem:
    command: npx
    args:
      - "-y"
      - "@modelcontextprotocol/server-filesystem"
      - "/opt/deployments"
      - "/etc/hermes-configs"
  internal_pm:
    command: python
    args:
      - "/opt/hermes-tools/internal_pm_mcp_server.py"

compression:
  enabled: true
  threshold: 0.65

The MEMORY.md for this profile should encode your infrastructure's key facts:

## Infrastructure Overview
Production cluster: k8s-prod.internal (3 nodes, 96 CPU, 384 GB RAM)
Staging cluster: k8s-staging.internal (1 node, 32 CPU, 128 GB RAM)
Database: PostgreSQL 15 on db-prod.internal:5432, db name: appdb

## Deployment Conventions
Always run tests before deploying to production.
Use blue-green deployment strategy for all stateful services.
Rollback command: kubectl rollout undo deployment/<name> -n production

## Alert Thresholds
CPU above 80% for 5 minutes: investigate and report.
Memory above 90%: immediate escalation required.
Disk above 85%: schedule cleanup within 24 hours.

Chapter 25: Recipe 3 — The Self-Improving Coding Assistant

Goal: An agent that helps you write, review, debug, and test code, learns your coding style and project conventions over time, and builds up a library of project-specific skills.

This goal benefits enormously from a local LLM for privacy and cost reasons, since code often contains proprietary logic. It needs terminal access for running tests and build tools, file access for reading and editing code, and the skills toolset so the agent can build up project-specific procedures over time.

# ~/.hermes/config.yaml (coding-assistant profile)
model:
  provider: custom
  base_url: http://127.0.0.1:11434/v1
  default: hermes-qwen
  api_key: local-no-key-needed

toolsets:
  - terminal
  - file
  - memory
  - skills
  - todo

mcp_servers:
  github:
    command: npx
    args:
      - "-y"
      - "@modelcontextprotocol/server-github"
    env:
      GITHUB_PERSONAL_ACCESS_TOKEN: "${GITHUB_TOKEN}"

compression:
  enabled: true
  threshold: 0.70

auxiliary:
  compression:
    provider: custom
    base_url: http://127.0.0.1:11434/v1
    model: qwen2.5-coder:7b
    api_key: local-no-key-needed

The MEMORY.md for this profile should encode your project's conventions:

## Project: MyApp
Language: Python 3.11
Test framework: pytest with pytest-xdist; run with: pytest -n auto
Linter: ruff; format with: ruff format . && ruff check --fix .
Type checker: mypy --strict
Build system: uv (not pip); install deps with: uv sync

## Coding Conventions
All public functions must have Google-style docstrings.
Maximum line length: 100 characters.
Prefer dataclasses over plain dicts for structured data.
All database queries go through the repository layer in src/db/repositories/.

## Common Commands
Run all tests: pytest -n auto --tb=short
Start dev server: uvicorn src.main:app --reload --port 8000
Generate migrations: alembic revision --autogenerate -m "description"

Part Twelve: Advanced Topics for Developers

Chapter 26: Designing Tools That the LLM Will Actually Use Correctly

One of the most common mistakes developers make when building custom tools — whether as Hermes plugins or MCP servers — is writing tool descriptions that are too vague, too technical, or too similar to other tools. The LLM decides which tool to call based entirely on the tool's name and description. If the description is unclear, the LLM will either call the wrong tool or fail to call any tool when it should.

Several principles consistently produce well-used tools. The description should begin with a verb that describes what the tool does: "Retrieves", "Creates", "Updates", "Searches", "Analyzes". This immediately tells the LLM the action category. The description should include the phrase "Use this tool when" followed by a concrete description of the situation that calls for this tool — this is the most important part because it teaches the LLM the tool's trigger condition. The description should also state what the tool returns, not just what it takes as input, because the LLM needs to know what it will get back in order to reason about whether calling this tool will advance its goal.

Parameter descriptions are equally important. Each parameter should explain not just what the parameter is, but what values are valid, what the default behavior is when the parameter is omitted, and what happens if an invalid value is provided. Consider the difference between these two descriptions of the same parameter:

# Vague — the LLM does not know what to put here:
"status": {
    "type": "string",
    "description": "The status."
}

# Clear — the LLM knows exactly what to put here:
"status": {
    "type": "string",
    "description": (
        "The task status to filter by. Must be one of: 'todo', "
        "'in_progress', 'done', 'blocked', 'cancelled'. "
        "If omitted, tasks of all statuses are returned."
    )
}

The second description is longer, but it eliminates ambiguity entirely. The LLM will produce valid calls to this tool on the first try, rather than requiring a retry after an error.

Chapter 27: Handling Context Length and Compression

As Hermes sessions grow longer, the conversation history, tool call results, and loaded skills can push against the model's context limit. Hermes handles this through context compression: when the context reaches a configurable threshold (expressed as a fraction of the model's maximum context length), Hermes uses an auxiliary model to summarize older parts of the conversation and replace them with a compact summary.

The compression threshold is configured in config.yaml:

compression:
  enabled: true
  threshold: 0.60

A threshold of 0.60 means compression triggers when the context is 60% full. Lower thresholds compress more aggressively, keeping more room for new content but potentially losing detail from older parts of the conversation. Higher thresholds preserve more history but risk hitting the context limit during long sessions.

For local models with smaller context windows, you may need to set a lower threshold and use a more aggressive compression model. The following configuration is appropriate for a local 7B model with a 32K context window:

model:
  provider: custom
  base_url: http://127.0.0.1:11434/v1
  default: my-local-7b-model
  api_key: local-no-key-needed

compression:
  enabled: true
  threshold: 0.45

auxiliary:
  compression:
    provider: custom
    base_url: http://127.0.0.1:11434/v1
    model: my-local-7b-model
    api_key: local-no-key-needed

Chapter 28: Debugging and Observability

When Hermes is not behaving as expected, the first step is always to check what the agent actually sees. The most common causes of unexpected behavior are: the agent does not have the tool it needs (because the toolset is not enabled), the agent has too many tools and is choosing the wrong one (because the descriptions are ambiguous), the agent's memory contains outdated or incorrect information (because an old session wrote bad data), or the model is not powerful enough for the task (because a local model is being asked to do something that requires frontier-level reasoning).

The hermes doctor command checks for obvious configuration problems:

hermes doctor

Inside a session, the /plugins command shows which plugins are loaded. The /skill command shows which skills are available. You can ask the agent directly: "What tools do you have available?" and it will list them.

For deeper debugging, enable verbose logging by setting the HERMES_LOG_LEVEL environment variable:

HERMES_LOG_LEVEL=debug hermes

This produces detailed logs of every tool call, every LLM request, and every lifecycle hook invocation. The logs are written to ~/.hermes/logs/ and can be inspected with any text editor or log analysis tool.

For MCP server debugging, you can test a server independently before connecting it to Hermes. The MCP 2025-11-25 protocol requires an initialize request followed by a notifications/initialized notification before any other request. A correct test harness must send both. The following script handles this properly:

# test_mcp_server.py
#
# A test harness for MCP servers targeting the 2025-11-25 specification.
# Correctly implements the JSON-RPC 2.0 initialize handshake required
# by MCP 2025-11-25 before any tools/list or tools/call request.
#
# Usage:
#   python test_mcp_server.py
#   python test_mcp_server.py python my_other_server.py
#
# The script starts the server as a subprocess, performs the required
# initialize handshake, then sends a tools/list request and prints
# the tools the server exposes.

import subprocess
import json
import sys


def send_request(proc: subprocess.Popen, request: dict) -> dict | None:
    """
    Writes a JSON-RPC request to the server's stdin and reads one
    line of response from its stdout.

    Args:
        proc:    The running MCP server subprocess.
        request: A JSON-RPC 2.0 request dict.

    Returns:
        The parsed JSON-RPC response dict, or None if parsing fails
        or if the server produces no output.
    """
    line = json.dumps(request) + "\n"
    proc.stdin.write(line.encode("utf-8"))
    proc.stdin.flush()

    response_line = proc.stdout.readline()
    if not response_line:
        return None
    try:
        return json.loads(response_line.decode("utf-8").strip())
    except json.JSONDecodeError:
        return None


def send_notification(proc: subprocess.Popen, notification: dict) -> None:
    """
    Writes a JSON-RPC notification to the server's stdin.
    Notifications have no id field and expect no response.

    Args:
        proc:         The running MCP server subprocess.
        notification: A JSON-RPC 2.0 notification dict (no 'id' field).
    """
    line = json.dumps(notification) + "\n"
    proc.stdin.write(line.encode("utf-8"))
    proc.stdin.flush()


def test_mcp_server(server_command: list[str]) -> None:
    """
    Starts an MCP server as a subprocess, performs the MCP 2025-11-25
    initialize handshake, then sends a tools/list request to verify
    that the server starts correctly and exposes tools.

    The MCP 2025-11-25 protocol requires:
      1. Client sends an 'initialize' request.
      2. Server responds with its capabilities and agreed protocol version.
      3. Client sends a 'notifications/initialized' notification.
      4. Normal tool calls may now proceed.

    Skipping any of these steps will cause most MCP servers to reject
    subsequent requests or produce no output.

    Args:
        server_command: The command to start the MCP server as a list of
                        strings (e.g., ['python', 'internal_pm_mcp_server.py']).
    """
    print(f"Starting server: {' '.join(server_command)}")

    proc = subprocess.Popen(
        server_command,
        stdin=subprocess.PIPE,
        stdout=subprocess.PIPE,
        stderr=subprocess.PIPE,
    )

    try:
        # Step 1: Send the initialize request.
        # protocolVersion must match the spec version the server implements.
        # This harness targets MCP 2025-11-25.
        initialize_request = {
            "jsonrpc": "2.0",
            "id":      1,
            "method":  "initialize",
            "params":  {
                "protocolVersion": "2025-11-25",
                "capabilities":    {},
                "clientInfo": {
                    "name":    "hermes-test-harness",
                    "version": "1.0.0",
                },
            },
        }
        init_response = send_request(proc, initialize_request)
        if init_response is None:
            print("ERROR: No response to initialize request.")
            stderr_output = proc.stderr.read().decode("utf-8")
            if stderr_output:
                print(f"Server stderr:\n{stderr_output[:500]}")
            return

        if "error" in init_response:
            print(f"ERROR during initialize: {init_response['error']}")
            return

        result      = init_response.get("result", {})
        server_info = result.get("serverInfo", {})
        agreed_ver  = result.get("protocolVersion", "unknown")
        print(f"Initialize OK.")
        print(f"  Server: {server_info.get('name', 'unknown')} "
              f"v{server_info.get('version', 'unknown')}")
        print(f"  Agreed protocol version: {agreed_ver}")

        # Step 2: Send the initialized notification.
        # This is required by the MCP 2025-11-25 spec before any tool calls.
        # Notifications have no 'id' and expect no response.
        send_notification(proc, {
            "jsonrpc": "2.0",
            "method":  "notifications/initialized",
            "params":  {},
        })

        # Step 3: Request the list of available tools.
        tools_request = {
            "jsonrpc": "2.0",
            "id":      2,
            "method":  "tools/list",
            "params":  {},
        }
        tools_response = send_request(proc, tools_request)
        if tools_response is None:
            print("ERROR: No response to tools/list request.")
            return

        if "error" in tools_response:
            print(f"ERROR from tools/list: {tools_response['error']}")
            return

        tools = tools_response.get("result", {}).get("tools", [])
        print(f"\nServer exposes {len(tools)} tool(s):\n")
        for tool in tools:
            name        = tool.get("name", "unknown")
            description = tool.get("description", "(no description)")
            if len(description) > 80:
                description = description[:77] + "..."
            print(f"  {name}")
            print(f"    {description}")

    finally:
        proc.terminate()
        try:
            proc.wait(timeout=5)
        except subprocess.TimeoutExpired:
            proc.kill()


if __name__ == "__main__":
    # Default: test the internal PM server from Chapter 17.
    # Pass a different command as CLI arguments if needed:
    #   python test_mcp_server.py python my_other_server.py
    command = sys.argv[1:] if len(sys.argv) > 1 else [
        "python", "internal_pm_mcp_server.py"
    ]
    test_mcp_server(command)

Run it against your MCP server before connecting it to Hermes:

# Test the internal PM server:
python test_mcp_server.py

# Test a different server:
python test_mcp_server.py python my_other_server.py

Part Thirteen: A Complete Worked Example

Chapter 29: From Goal to Running Agent — End to End

Let us tie everything together with a complete worked example. The goal: configure a Hermes agent that helps a small software team manage their sprint workflow. The agent should be able to check the status of tasks in the internal project management system, update task statuses, search GitHub for related pull requests, run the test suite, and produce a daily sprint summary report.

This goal requires the internal_pm MCP server we built in Chapter 17, the GitHub MCP server, terminal access for running tests, file access for reading configuration files, and memory for tracking sprint conventions.

Step 1: Install the required MCP servers.

npm install -g @modelcontextprotocol/server-github

Step 2: Deploy the internal PM MCP server.

sudo mkdir -p /opt/hermes-tools
cp internal_pm_mcp_server.py /opt/hermes-tools/

Step 3: Create the sprint-manager profile in config.yaml.

# ~/.hermes/config.yaml
model:
  provider: openrouter
  base_url: https://openrouter.ai/api/v1
  default: anthropic/claude-sonnet-4-5

profiles:
  sprint-manager:
    model:
      provider: openrouter
      base_url: https://openrouter.ai/api/v1
      default: anthropic/claude-sonnet-4-5
    toolsets:
      - terminal
      - file
      - memory
      - skills
      - todo
      - delegation
    mcp_servers:
      internal_pm:
        command: python
        args:
          - "/opt/hermes-tools/internal_pm_mcp_server.py"
      github:
        command: npx
        args:
          - "-y"
          - "@modelcontextprotocol/server-github"
        env:
          GITHUB_PERSONAL_ACCESS_TOKEN: "${GITHUB_TOKEN}"
    system_prompt_extra: >
      You are a sprint manager assistant for a software development team.
      You help track task progress, coordinate with GitHub, run tests,
      and produce clear status reports. You are proactive about flagging
      blocked tasks and overdue items. You always confirm before marking
      a task as done.

Step 4: Seed the memory files.

Save the following content to ~/.hermes/memories/MEMORY.md:

## Sprint Conventions
Current sprint: Sprint 42 (ends 2026-08-01)
Active project: PROJ-001 (Hermes Integration)
Test command: pytest -n auto --tb=short
GitHub repo: myorg/hermes-integration
Daily standup: 9:00 AM UTC

## Team Members
alice@example.com: Tech Lead (backend)
bob@example.com: Senior Engineer (infrastructure)
carol@example.com: Engineer (frontend)

Step 5: Create a sprint-report skill.

mkdir -p ~/.hermes/skills/sprint-report

Save the following as ~/.hermes/skills/sprint-report/SKILL.md:

---
name: sprint-report
description: >
  Generates a daily sprint status report for the current sprint.
  Use when asked for a sprint summary, daily standup notes,
  sprint status, or progress report.
version: "1.0"
metadata:
  author: sprint-team
allowed-tools: mcp_internal_pm_list_projects mcp_internal_pm_get_project_details terminal read_file
---

Sprint Report Skill

This skill generates a structured daily sprint status report.

Procedure

1. Retrieve the current sprint's project details using
   mcp_internal_pm_get_project_details with the project ID from memory.

2. Categorize tasks by status: done, in_progress, todo, blocked.

3. Run the test suite to get the current pass/fail status:
   pytest -n auto --tb=short -q

4. Check for any pull requests opened in the last 24 hours using
   the GitHub MCP server.

5. Produce a report with the following sections:
   - Sprint Progress (tasks done / total tasks, percentage)
   - In Progress (list of tasks currently being worked on)
   - Completed Today (tasks marked done since yesterday)
   - Blocked Items (tasks with blocked status, with notes)
   - Test Suite Status (pass/fail counts)
   - Recent Pull Requests
   - Risks and Recommendations

6. Save the report to sprint-report-YYYY-MM-DD.md in the current directory.

Step 6: Launch Hermes with the sprint-manager profile.

hermes --profile sprint-manager

Step 7: Ask for a sprint report.

Once inside the Hermes session, simply ask:

Generate today's sprint status report.

Hermes will activate the sprint-report skill, call the internal_pm MCP server to get task data, run the test suite in the terminal, query GitHub for recent pull requests, and produce a structured report. The entire workflow — from the user's request to the finished report — requires no manual intervention.


Conclusion

Chapter 30: The Philosophy of Goal-Oriented Configuration

If there is one idea to take away from everything in this tutorial, it is that Hermes is not a product you install and then use — it is a platform you configure and then grow. The difference between a Hermes instance that feels magical and one that feels frustrating is almost always a configuration difference, not a capability difference. The capability is there. The question is whether you have shaped the configuration to match your goal.

The process of configuring Hermes for a new goal follows a natural sequence. You start by identifying the goal clearly: not "help me with work" but "help me manage a Python microservices codebase, run tests, review pull requests, and track sprint progress." From that clear goal, you derive the required toolsets: terminal and file for code execution, GitHub MCP for pull requests, memory for project conventions. From the toolsets, you derive the appropriate model: a code-specialized model for coding tasks, a frontier reasoning model for complex analysis, a local model when privacy matters. From the model choice, you derive the memory and compression configuration: how much context do you need, how aggressively should you compress, what facts should be pre-loaded into MEMORY.md? And from all of that, you derive the skills: what procedures does this agent need to know how to execute reliably?

This process is iterative. Your first configuration will not be perfect. You will discover that the agent is missing a tool it needs, or that it has too many tools and is getting confused, or that its memory contains outdated information. Each of these discoveries is an opportunity to refine the configuration. Over time, as the agent builds up a library of skills and a rich memory of your environment and preferences, it becomes genuinely more capable. That is the closed learning loop that makes Hermes different from every other AI tool you have used.

The developers reading this tutorial have an additional opportunity: every custom MCP server you build, every plugin you write, every skill you craft is a contribution to a growing ecosystem. The agentskills.io standard means that skills are portable. An MCP server you build for your organization's internal tools can be shared with the broader community. The tools you create today become the foundation on which future agents — and future users — will build.

Hermes is, in the end, a bet on a particular vision of what AI assistance should look like: not a stateless oracle you consult and dismiss, but a persistent collaborator that grows more capable the longer it works with you, that remembers what you care about, that learns from its own experience, and that can be extended and customized to fit the specific contours of your work. This tutorial has given you the tools to realize that vision. The rest is up to you and the agent you build together.

No comments: