Skip to content

feat(skills): compact system prompt index via sidecar-driven pinned skills - #23663

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

feat(skills): compact system prompt index via sidecar-driven pinned skills#23663
sontianye wants to merge 1 commit into
NousResearch:mainfrom
sontianye:feat/compact-skill-index-v2

Conversation

@sontianye

Copy link
Copy Markdown

Re-spin of #14319 along the path @teknium1 outlined when closing it.

Why

The skills section of the system prompt currently dumps every skill name + description on every turn — ~3,500 tokens on a fully-loaded install. The PR shrinks that to a fixed-budget compact format while keeping the existing framing the dispatcher relies on.

Mapping to the closing review on #14319

Reviewer point This PR
"read pinned candidates from tools/skill_usage.py (agent_created_report() + sidecar lookup) instead of state.db" _get_pinned_candidates() reads only via agent_created_report(), filters pinned=True && state != archived, orders by activity_count desc. No state.db access, no duplicate tracking.
"keep the existing 'MUST load' framing intact in the compact format" Entire MUST-load paragraph kept verbatim. Guarded by test_preserves_must_load_and_hermes_agent_framing.
"preserve the hermes-agent instruction" Entire hermes config set … paragraph kept verbatim. Same test.
"the query param on skills_list is independently nice but small enough we can do it ourselves" Dropped from this PR.

What it looks like

## Skills (mandatory)
You have access to 71 specialized skills across 19 categories.
[full MUST-load + hermes-agent framing — unchanged]

<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): …
  …
</skill_categories>

Load any pinned skill listed above directly with skill_view(name).
Otherwise, list a category with skills_list(category="<name>") to see
what's available, then load the chosen skill with skill_view(name).
Only proceed without loading a skill if genuinely none are relevant to the task.

When nothing is pinned, the <pinned_skills> block is omitted entirely — pinned means pinned, never an alphabetical placeholder.

Implementation notes

  • _PINNED_SKILLS_CHAR_BUDGET = 1200 keeps the pinned block bounded regardless of how many skills get pinned.
  • <skill_categories> reuses the existing category_descriptions mapping; counts are deduped per top-level category.
  • Cache key includes _skill_usage_epoch() (sidecar mtime_ns) so toggling pin state takes effect on the next build with no manual invalidation.
  • Bundled / hub-installed skills are intentionally not in the sidecar; the categories block (+ skills_list(category=...)) is how they stay reachable.
  • No new settings, no new dependencies, signature of build_skills_system_prompt() unchanged.

Tests

  • All existing TestBuildSkillsSystemPrompt and TestBuildSkillsSystemPromptConditional tests updated to assert the new contract (category presence + count) instead of raw name dumps.
  • New TestCompactSkillsPrompt:
    • test_preserves_must_load_and_hermes_agent_framing
    • test_pinned_candidates_from_sidecar
    • test_no_pinned_block_when_no_pins
    • test_no_pinned_block_when_skill_set_is_empty
    • test_skill_categories_summary_shows_counts
    • test_pin_change_invalidates_prompt_cache
  • tests/agent/test_prompt_builder.py: 127 passed, 1 skipped.

Test plan

  • uv run pytest tests/agent/test_prompt_builder.py — 127 passed
  • Full suite: 5021 passed (5 pre-existing failures in unrelated gateway_service / anthropic_adapter OAuth / kanban_cli / e2e/test_platform_commands tests, none touching skills code)

Closes #14319

@alt-glitch alt-glitch added type/feature New feature or request comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint tool/skills Skills system (list, view, manage) P3 Low — cosmetic, nice to have labels May 11, 2026
@swen-chan

Copy link
Copy Markdown

I hit the exact same problem — the skills block is eating ~4,500 tokens every turn on my install (90 skills across 20 categories), and there's no config workaround. This PR's approach (pinned from skill_usage.py + category summary, keeping the MUST-load and hermes-agent framing intact) is the right fix. Would love to see this land. Anything I can do to help move it forward?

@sontianye

Copy link
Copy Markdown
Author

Friendly ping — happy to rebase or adjust if you have feedback.

@teknium1 teknium1 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for preserving the MUST-load and hermes-agent guidance and for using the existing sidecar rather than state.db. The token-cost premise is still present on current main, but the implementation needs re-scoping against later skill-index decisions.

Problems

  • agent/prompt_builder.py:1246 removes every unpinned skill name from the default prompt. Current main deliberately preserves all names after a real capability-loss regression: commit ee1a744ac and agent/prompt_builder.py:1622-1667 state that models do not reliably rediscover hidden skills through skills_list. Commit 4d6a133a9 also makes even names-only demotion opt-in.
  • agent/prompt_builder.py:1270 aggregates nested categories, but the footer calls skills_list(category="<name>"). tools/skills_tool.py:827-830 filters categories by exact equality, so a top-level mlops listing omits mlops/evaluation, mlops/inference, and mlops/models skills included in its count.

Suggested changes

  • Rework this on the current demote-never-hide contract, retaining names and treating any broader default compaction as a deliberate design decision.
  • Align category summaries with exact filtering or add prefix-aware filtering, with nested-category coverage.

Automated hermes-sweeper review.

Comment thread agent/prompt_builder.py
@@ -1162,26 +1246,54 @@ def build_skills_system_prompt(
if not skills_by_category:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocking: this global replacement removes unpinned skill names from the default prompt. Current main intentionally preserves every name after a real capability-loss regression (ee1a744ac; see current agent/prompt_builder.py:1622-1667), and even names-only demotion is opt-in (4d6a133a9). Please re-scope onto that demote-never-hide contract.

Comment thread agent/prompt_builder.py Outdated
if top not in top_descs and category_descriptions.get(cat):
top_descs[top] = category_descriptions[cat]

total_skills = sum(top_counts.values())

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocking: these counts combine nested categories under top, but the footer later directs skills_list(category="<name>"). Current skills_list uses exact equality (tools/skills_tool.py:827-830), so mlops will not return skills in mlops/evaluation, mlops/inference, or mlops/models even though they are counted here.

@teknium1 teknium1 added sweeper:risk-caching Sweeper risk: may break/degrade prompt caching or cache-key stability (invariant) sweeper:blast-massive Sweeper blast radius: massive — everyone, every turn (invariant surface) labels Jul 13, 2026
@sontianye
sontianye force-pushed the feat/compact-skill-index-v2 branch from b55362c to 8c4eaff Compare July 13, 2026 03:36
…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
@sontianye
sontianye force-pushed the feat/compact-skill-index-v2 branch from 8c4eaff to f518658 Compare July 13, 2026 03:47
@sontianye

Copy link
Copy Markdown
Author

Reworked and force-pushed. The branch is now a single clean commit on top of current main — only agent/prompt_builder.py and tests/agent/test_prompt_builder.py are touched, nothing else.

What changed vs the previous push

The earlier push had unintended noise from a soft-reset accident. This is the correct minimal patch.

Demote-never-hide — Every skill name stays in <available_skills>. The index format is unchanged from main; only the inner loop changes: pinned skills (pinned=True && state != archived in the sidecar) show name: description, everything else shows name only. No skills removed, no skills_list() needed.

No category mismatch — The previous approach's <skill_categories> with top-level aggregation is gone entirely. Category headers now use the exact skills_by_category keys, so mlops/evaluation appears as mlops/evaluation: and its skills are directly reachable by name.

Cache_skill_usage_epoch() adds sidecar mtime_ns to the cache key so pin state changes take effect on the next build without manual invalidation.

Tests — Four existing assertions updated (Debug Python scripts etc. now correctly assert absent for non-pinned skills). New TestPinnedSkillsInIndex class: names-only for unpinned, description inline for pinned, cache invalidation on pin change, archived skill excluded from candidates.

Diff stat: +196 / -7 lines across two files.

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 sweeper:blast-massive Sweeper blast radius: massive — everyone, every turn (invariant surface) sweeper:risk-caching Sweeper risk: may break/degrade prompt caching or cache-key stability (invariant) 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.

4 participants