Skip to content

feat(skills): compact system prompt index with usage-driven pinned skills - #14319

Closed
sontianye wants to merge 1 commit into
NousResearch:mainfrom
sontianye:feat/compact-skill-index
Closed

feat(skills): compact system prompt index with usage-driven pinned skills#14319
sontianye wants to merge 1 commit into
NousResearch:mainfrom
sontianye:feat/compact-skill-index

Conversation

@sontianye

Copy link
Copy Markdown

Summary

  • Replace the full skill index in the system prompt (~2500 tokens) with a compact format (~500 tokens): pinned skills + category summary + search guidance
  • Add query parameter to skills_list tool for keyword search across name, description, and category
  • Pin frequently-used skills via usage data from state.db, with category-representative fallback for cold start

Problem

The skills system implements progressive disclosure across three tiers (skills_tool.py:9), but tier 0 (system prompt) was duplicating tier 1 — injecting the full name+description list for all 71 bundled skills on every turn.

Issue Impact
~2500 tokens/turn for skill index alone 15-25% of system prompt budget
Descriptions truncated to 60 chars in prompt skills_list returns 236 chars avg — better matching quality
Scales linearly with no cap Optional skills, plugins, external dirs make it worse

Solution

Make each tier do its own job:

Tier 0 (system prompt) — compact awareness trigger:

## Skills
You have access to 71 specialized skills across 19 categories.

<pinned_skills>
  - plan: Create and manage task plans for multi-step work
  - systematic-debugging: Structured approach to diagnosing bugs
  ...
</pinned_skills>

<skill_categories>
  software-development (6), github (6), creative (11), mlops (13), ...
</skill_categories>

For pinned skills above, load directly with skill_view(name).
For other tasks, search with skills_list(query="keyword") first...

Tier 1 (skills_list) — gains query param for precise search with full descriptions.

Key design decisions

  • Pinned skills are data-driven: queries state.db for skill_view call frequency in the last 30 days (reusing the insights.py extraction pattern), with a char budget cap (~300 tokens)
  • Cold start fallback: when no usage data exists, picks one representative per category (alphabetically first) so the agent has concrete examples across the full breadth
  • Cache-friendly: usage epoch (hourly granularity) in cache key — pinned skills refresh at most once/hour without thrashing
  • Zero config: no new settings, no mode switches — the compact format applies universally
  • Signature unchanged: build_skills_system_prompt() callers are unaffected

Results

Metric Before After
System prompt tokens ~2500/turn ~500/turn
Matching quality 60-char truncated descriptions Full descriptions via search
Scalability O(n) tokens Fixed budget

Changes

File Change
agent/prompt_builder.py Add _query_skill_usage(), _get_usage_epoch(), _get_pinned_skills(); rewrite rendering in build_skills_system_prompt()
tools/skills_tool.py Add query param to skills_list(); update schema and handler
tests/agent/test_prompt_builder.py Update 2 existing assertions; add TestCompactSkillsPrompt (4 tests)
tests/tools/test_skills_tool.py Add 6 query search tests

Test plan

  • All 10 existing TestBuildSkillsSystemPrompt tests pass
  • All 4 new TestCompactSkillsPrompt tests pass (usage-driven pinning, category fallback, budget cap, format structure)
  • All 6 new TestSkillsListQuery tests pass (name/description/category match, case insensitivity, combination with category filter)
  • Full suite: 3688 passed, 0 regressions (6 pre-existing failures in unrelated discord/gateway/minimax tests)

Checklist

  • Bug fix / feature (non-breaking)
  • Tests added
  • No new dependencies
  • Cross-platform: no OS-specific changes

…ills

The skills system already implements progressive disclosure across three
tiers (skills_tool.py docstring), but the system prompt (tier 0) was
duplicating tier 1 — injecting the full name+description list for every
installed skill on every turn.

With 71 bundled skills this costs ~2500 tokens/turn of system prompt
space, and the descriptions are truncated to 60 chars — worse than what
skills_list already returns (236 chars avg).  The cost scales linearly
with no upper bound as users install optional skills, plugins, and
external directories.

This commit makes each tier do its own job:

**Tier 0 (system prompt)** — compact awareness trigger (~500 tokens):
  - Total skill count and category summary (one line)
  - <pinned_skills> block: usage-driven top skills within a fixed token
    budget, falling back to one representative per category when no
    usage data exists (cold start)
  - <skill_categories> block: top-level categories with counts
  - Search guidance pointing to skills_list(query=...)

**Tier 1 (skills_list tool)** — gains a `query` parameter for keyword
search across name, description, and category.  This is the mechanism
that replaces the removed full index — the agent searches on demand
with richer matching information than the old truncated descriptions.

Token savings: ~2500 → ~500 tokens/turn (80% reduction).
Matching quality: improves — search returns full descriptions vs 60-char
truncations.

Implementation details:
- _get_pinned_skills(): queries state.db for skill_view call frequency
  (reusing the insights.py extraction pattern), with a char budget cap
  and category-representative fallback for cold start
- _get_usage_epoch(): coarse hourly timestamp in cache key so pinned
  skills refresh at most once/hour without cache thrashing
- skills_list(query=...): simple case-insensitive substring match on
  name, description, and category — no new dependencies
- All existing filtering (platform, disabled, conditional activation)
  unchanged
- build_skills_system_prompt() signature unchanged — callers unaffected
@alt-glitch alt-glitch added type/feature New feature or request P3 Low — cosmetic, nice to have comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint tool/skills Skills system (list, view, manage) labels Apr 23, 2026
@teknium1

Copy link
Copy Markdown
Contributor

Thanks for the work @sontianye — closing without merging.

The core token-saving idea is real (the skills block on a fully-loaded install is ~3,500 tokens), but a few things in the implementation don't fit how we want skills to work:

  1. Softens the "must load skills" framing. The current prompt is intentionally heavy-handed about scanning skills and erring on the side of loading. Models that don't tool-call aggressively rely on that to find the right skill. The new prompt's "search before assuming no skill exists" is gentler and pushes the discovery cost onto the model's judgement, which we've found regresses on smaller models.
  2. Duplicates an existing usage-tracking system. tools/skill_usage.py already maintains ~/.hermes/skills/.usage.json with per-skill view_count + last_used_at + pin state — that's the canonical source for "which skills are hot." The PR re-derives the same data by scanning state.db.messages.tool_calls JSON across a 30-day window, which is slower and ignores pin state.
  3. The hermes-agent skill auto-load instruction (added to the prompt after this PR was filed) would get dropped on rebase, and we'd see models guess at hermes config set commands again.

The query param on skills_list is independently nice but small enough we can do it ourselves without a salvage cycle.

If you want to take another swing at this, the path I'd suggest: read pinned candidates from tools/skill_usage.py (agent_created_report() + sidecar lookup) instead of state.db, keep the existing "MUST load" framing intact in the compact format, and preserve the hermes-agent instruction. Happy to review that version. Closing this one to keep the queue clean.

@teknium1 teknium1 closed this May 10, 2026
sontianye added a commit to sontianye/hermes-agent that referenced this pull request Jul 13, 2026
…erwise

The current index shows every skill with its full description, costing
~3,500 tokens on a fully-loaded install. This PR reduces that to a
fixed-budget format that still honours the demote-never-hide contract:
every skill name stays visible, but descriptions appear only for skills
explicitly pinned by the user or agent via the skill_usage sidecar.

How it works
- _get_pinned_candidates() reads ~/.hermes/skills/.usage.json via
  tools/skill_usage.agent_created_report(), filters pinned=True &&
  state != archived, orders by activity_count descending, and trims to
  _PINNED_SKILLS_CHAR_BUDGET (1200 chars) so the description budget stays
  bounded regardless of how many skills are pinned.
- The index loop shows "name: description" for pinned skills and "name"
  for everything else — no entries ever removed, no skills_list() needed
  for discovery.
- Posture-driven compact_categories demotion (names-only lines for
  non-coding categories in coding posture) is unchanged.
- _skill_usage_epoch() adds sidecar mtime_ns to the cache key so pin
  state changes take effect on the next prompt build automatically.

No new settings. build_skills_system_prompt() signature unchanged.

Closes NousResearch#14319
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint P3 Low — cosmetic, nice to have tool/skills Skills system (list, view, manage) type/feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants