hermes skill router

July 2, 2026 · View on GitHub

Hermes plugin that indexes skills with Jina embeddings and injects the most relevant skill pointers at the right time.

What it does

  • Embeds each user message with jina-embeddings-v5-text-small
  • Cosine-matches against a pre-built skill index
  • Optionally reranks ambiguous matches with jina-reranker-v3
  • Keeps reranking off the hot path unless the gate says cosine is uncertain
  • Gate decisions are logged at debug level so rerank frequency can be measured before changing defaults
  • Falls back safely if the index or API is unavailable

Why it exists

Hermes skill libraries grow fast. The model should not have to guess from a long list of unrelated skills every turn. This plugin narrows the candidate set before the LLM sees it.

Behavior

Default mode:

  • cosine-only
  • gated reranking only when scores are ambiguous

Overrides:

  • SKILL_ROUTER_RERANK=1 → always rerank
  • SKILL_ROUTER_RERANK=0 → never rerank

Files

  • __init__.py — hook entry point and gating logic
  • build_index.py — scans skills and builds skill_index.json
  • jina_client.py — embedding client
  • reranker_client.py — optional second-stage reranker
  • tests/test_router.py — regression tests

Local usage

The plugin is designed to be loaded by Hermes from ~/.hermes/plugins/hermes-skill-router/.

To rebuild the index manually from a shell:

python3 - <<'PY'
import importlib.util, os, re, sys

with open(os.path.expanduser('~/.hermes/.env')) as f:
    m = re.search(r'JINA_API_KEY=(.+)', f.read())
os.environ['JINA_API_KEY'] = m.group(1).strip().strip('"').strip("'")

pkg_path = os.path.expanduser('~/.hermes/plugins/hermes-skill-router')
spec = importlib.util.spec_from_file_location(
    'skill_router',
    os.path.join(pkg_path, '__init__.py'),
    submodule_search_locations=[pkg_path],
)
pkg = importlib.util.module_from_spec(spec)
sys.modules['skill_router'] = pkg
spec.loader.exec_module(pkg)

from skill_router.build_index import build_index, load_index
build_index(force=True)
idx = load_index()
print(f"rebuilt {len(idx['skills'])} skills with model {idx['model']}")
PY

Hermes also rebuilds the index automatically when the skills tree changes.

Release notes

  • Keep generated artifacts out of version control
  • Do not commit skill_index.json
  • Verify the test file runs without live API access

Changelog

v1.1.0

Fixes:

  • Runtime env override: SKILL_ROUTER_RERANK is now read at call time, not import time. Changing the env var mid-session takes effect immediately without restarting Hermes.
  • Thread safety: All session state (_suggested_skills, _turn_counts, _last_seen_turns) and index management (_index_cache) are now protected by threading.Lock. Prevents TOCTOU races in concurrent multi-platform gateway use.
  • Bounded session tracking: Added TTL sweep (inactive sessions cleaned after 50 turns) and hard cap (max 100 tracked sessions with oldest-eviction). Prevents unbounded memory growth in long-running processes.
  • Model/dimension validation: Index load now validates model and dimensions against current jina_client config. Stale indexes from a different embedding model are detected and rebuilt automatically.
  • Dedup window reduced: Suppression window reduced from 8 turns to 4 turns. Prevents skills from disappearing mid-task on multi-turn work that needs the same skill across turns.
  • Empty docs crash fix: _batch_cosine no longer crashes on empty document lists (numpy AxisError).

Tests:

  • Expanded from 4 tests to 83 tests across 14 test classes
  • Full coverage of: conversational filter branches, rerank gate logic (forced on/off at runtime, case-insensitive normalization), matching pipeline (synthetic index, rerank failure fallback, rerank success reordering, reranker out-of-bounds index filtering, all-identical-scores edge case, candidate cap, sorting), cosine similarity edge cases (zero vectors, empty docs, batch consistency), session tracking (suppression, expiry, TTL sweep, hard cap eviction), thread safety (concurrent turn increments, concurrent mark/get), index validation via load_index (model mismatch, dimension mismatch, missing model, missing dimensions, corrupt JSON, missing file — all with tempfile isolation), graceful degradation (embed failure, missing index, no candidates, successful match formatting), query builder (history, list content, truncation, None handling), embed batch boundaries (64 exact, 65 split, 128 split), embed/rerank client edge cases, _maybe_rebuild (throttle skip/allow, hash change trigger, failure, counter advance), and env var test hygiene.