Raw capture β raw/skill-retriever.md
url: https://github.com/ChonSong/skill-retriever title: Skill Retriever author: ChonSong / Hermes Skill Retriever Contributors date_fetched: 2026-07-08 date_published: 2025
AgentSkillOS-powered semantic skill retrieval plugin for Hermes Agent. Pre-filters 1,200+ skills (998 community + 211 Hermes) organized in a 10,000-category capability taxonomy to the top-5 most relevant per query, injected as natural-language hints into the user message.
Language: Python 3.10+, 4,400 lines of source License: MIT Version: 0.2.0 Dependencies: chromadb, litellm, pyyaml, python-dotenv, rich
User Query β pre_llm_call hook (plugin/__init__.py)
β Searcher.search() (search/searcher.py)
β Load YAML capability tree
β Recursive LLM-guided node selection
β Parallel child branch search (ThreadPoolExecutor)
β LLM pruning (dedup + rank by workflow stages)
β Hint injection into user message
Three major subsystems:
Plugin layer (plugin/__init__.py, 185 lines): Hermes pre_llm_call hook. Minimal β checks disable flag, skips short messages, lazy-loads Searcher singleton, formats top-5 results as [Skill Retrieval Hint] block.
Search engine (search/searcher.py, 733 lines): The core. Recursive tree descent with LLM node selection, ThreadPoolExecutor parallelism, LLM pruning with workflow-stage ranking.
Tree builder (tree/builder.py, 654 lines): Offline LLM-driven taxonomy construction from SKILL.md files. Queue-based parallel processing with FIRST_COMPLETED pattern. Outputs YAML + HTML visualization.
skill-retriever/
βββ plugin/__init__.py β Hermes pre_llm_call hook
βββ plugin/plugin.yaml β Plugin manifest
βββ src/skill_retriever/
β βββ __init__.py β Public API
β βββ __main__.py β CLI entry point
β βββ cli.py β search, build, list, info commands
β βββ config.py β Env-based configuration
β βββ scanner.py β Hermes skills scanner (for plugin)
β βββ search/searcher.py β Core search engine (733 lines)
β βββ tree/
β β βββ builder.py β Tree builder (654 lines)
β β βββ prompts.py β All LLM prompts (centralized)
β β βββ schema.py β TreeNode, Skill, DynamicTreeConfig
β β βββ skill_scanner.py β Corpus SKILL.md scanner
β β βββ visualizer.py β HTML tree visualization (1094 lines)
β βββ capability_tree/ β Pre-built YAML + HTML trees
βββ data/skill_seeds/skills.json β Skill metadata (gitignored, 240MB)
βββ tests/ β 40 tests across 4 files
βββ scripts/install.sh β One-click Hermes plugin install
Generic tree node supporting arbitrary depth:
- Intermediate nodes: Have children (no skills directly)
- Leaf nodes: Have skills (no children)
- is_leaf property: checks len(self.children) == 0
- Recursive operations: count_all_skills(), collect_all_skills(), get_leaf_nodes()
- Supports two deserialization formats:
- from_recursive_tree(): New format ({id, name, description, children, skills})
- from_capability_tree(): Legacy format ({domains: {domain: {types: {skills}}}})
Flat dataclass with id, name, description, path, skill_path, content, plus metadata fields (github_url, stars, is_official, author) populated from skills.json at tree load time.
Single-parameter configuration: branching_factor (default 8) derives all others via multiplication:
- max_skills_per_node = branching_factor Γ 1.5 (12)
- expand_threshold = branching_factor Γ 0.7 (5)
- early_stop_skill_count = branching_factor Γ 1.7 (13)
- lazy_split_threshold = max_skills_per_node Γ 1.3
- classification_batch_size = branching_factor Γ 6
- structure_sample_size = branching_factor Γ 12
This proportional scaling ensures derived values stay coherent as tree size changes.
_search_node, line 251)At each node:
1. Leaf node (has skills, no children): Call LLM to select relevant skills from the leaf's skill list
2. Intermediate node (has children):
- If children β€ expand_threshold: Auto-expand all children (no LLM cost)
- Else: Call LLM to select relevant children (NODE_SELECTION_PROMPT)
3. Early stop: If exactly 1 child selected and total skills in subtree β€ early_stop_skill_count, collect all without further recursion
4. Parallel search: Multiple selected children searched concurrently via ThreadPoolExecutor(max_workers=min(len(children), self.max_parallel))
Uses litellm.completion() with temperature 0.3, caching enabled, and JSON array output format:
["child_id_1", "child_id_2"] β picks relevant branches["skill_id_1", "skill_id_2"] β picks relevant skills_parse_selection, line 603): Handles markdown code blocks, regex extraction, and both legacy (string IDs) and new (dict with id+reason) formatsThe SKILL_PRUNE_PROMPT (prompts.py:188-246) decomposes tasks into:
- Upstream (0-2 skills): Gather & prepare β what input/content does the user need first?
- Production (1-5 skills): Create & build β what tangible deliverables need to be created?
- Downstream (0-2 skills): Deliver & distribute β how does created content reach its audience?
This model deliberately fights keyword-matching: "promote" β only marketing/SEO skills β effective promotion requires assets to promote. The prompt inclues explicit anti-pattern warnings.
Output format: {workflow_analysis, upstream: [{id, role}], production: [{id, role}], downstream: [{id, role}], eliminated: [{id, reason}]}
The Searcher has an event_callback mechanism emitting events at every stage: search_start, node_enter, children_selected, skills_selected, early_stop, prune_start, prune_complete, search_complete. Enables WebUI integration and debugging.
Root assignment: LLM assigns all skills to 5 fixed root categories via FIXED_CATEGORY_ASSIGNMENT_PROMPT. The 5 categories are hardcoded in schema.py:
- content-creation, data-processing, development, automation, domain-specific
Recursive splitting: For each oversized category (> max_skills_per_node), LLM splits into sub-groups using RECURSIVE_SPLIT_PROMPT. Continues until all groups β€ threshold or max_depth reached.
Uses ThreadPoolExecutor with wait(futures, return_when=FIRST_COMPLETED) (line 182):
- A queue feeds nodes to a thread pool
- When any split completes, its children immediately join the queue
- This maximizes parallelism β no waiting for all siblings to finish before continuing
_validate_split_quality() (line 480) detects:
- Oversized groups (> 2.5Γ average)
- Singleton groups (1 skill)
- Missing descriptions
- Low coverage (< 90% skills assigned)
LLM credentials cascade through environment variables:
1. SKILL_RETRIEVER_LLM_API_KEY β OPENAI_API_KEY
2. SKILL_RETRIEVER_LLM_BASE_URL β OPENAI_BASE_URL
3. SKILL_RETRIEVER_LLM_MODEL β OPENAI_MODEL β "gpt-4o"
Zero additional configuration for users already running Hermes with OpenAI.
_on_pre_llm_call():
1. Check SKILL_RETRIEVER_DISABLE=1 β return None
2. Skip messages < 10 chars (greetings, follow-ups)
3. Lazy-load Searcher singleton (first call only)
4. Run search, format top-5 as [Skill Retrieval Hint] block
5. Return {"context": hint_block} β prepended to user message
The LLM sees hints before the user's message and may call skill_view(name) to load any suggested skill.
Each hint carries source + safety badges:
- πhermes β installed via Hermes, trusted
- πcommunity β from AgentSkillOS corpus, unreviewed
- β οΈ suffix β flagged by safety scan
tree/skill_scanner.py)SkillScanner class reads SKILL.md files from a corpus directory:
- Parses YAML frontmatter for name, description
- Falls back to first paragraph of body if no description
- Loads metadata (github_url, stars, author) from skills.json
- Used by TreeBuilder for offline taxonomy construction
scanner.py)scan_hermes_skills() reads the user's local Hermes install:
- Scans ~/.hermes/skills/ and ~/.hermes/hermes-agent/skills/
- Extracts category from parent directory name
- Parses frontmatter for description and triggers
- Used by the plugin for local skill context
Four subcommands via argparse:
- search "query" β run tree search, print top skills
- build β rebuild capability tree from corpus
- list β enumerate all skills in corpus
- info β system info, tree stats, tree structure summary
LLM-as-classifier instead of embeddings: The central architectural bet. Vector similarity is narrow; LLM tree navigation applies semantic reasoning at each decision point. This is how the system finds skills that embedding space hides.
Build-once, search-many: The expensive tree construction (many LLM calls) happens offline. Search uses cheap per-level calls (select N options, output JSON).
Pre-built static tree: Skills added after build time won't appear. Tradeoffs freshness for speed. The data is gitignored (240MB) and downloaded separately.
Borrow-mode coupling: Piggybacking on Hermes' LLM credentials eliminates setup friction but ties the retrieval gate to Hermes' model provider.
Fixed root categories: Hardcoded top-level taxonomy provides stability, at the cost of flexibility for skill types that don't fit the 5 categories.
Workflow-stage pruning: More sophisticated than typical dedup β the LLM reasons about complete workflows (upstreamβproductionβdownstream), not just relevance scoring.
40 tests across 4 files:
Tests are structural/integration rather than mocking at the boundary β they test against real bundled tree files and create real temp directories for skills.
| Aspect | skill-retriever | Pure Embedding Retrieval | Flat System Prompt |
|---|---|---|---|
| Discovery | LLM-navigated taxonomy | Vector similarity | Human scans list |
| Context cost | Small per-query | Per-query embedding | Every turn (all skills) |
| Finds non-obvious skills | Yes (semantic reasoning) | Limited (similarity-based) | Depends on human |
| Latency | +1-3 LLM calls | ~1 embedding lookup | 0 (always visible) |
| Scalability | 10K+ skills | Millions | ~200 before bloat |
| Freshness | Rebuild required | Real-time | Real-time |