feat: show system_prompt_preview when skill description exceeds prompt limit

When a skill is created or edited with a description longer than
SKILL_PROMPT_DESC_LIMIT (60 chars), the tool response now includes a
system_prompt_preview field showing exactly what the system prompt
skill index will display. This gives the agent immediate feedback to
self-correct truncated trigger phrases.

Also adds tool schema guidance about the 57-char window and fixes a
stale docstring in skill_commands.py that incorrectly claimed the
system prompt renders the full description.
This commit is contained in:
Alan Harman-Box 2026-05-12 09:59:31 +00:00 committed by Teknium
parent 5eb772111d
commit accbf4d912
3 changed files with 83 additions and 3 deletions

View file

@ -453,8 +453,8 @@ def reload_skills() -> Dict[str, Any]:
}
``description`` is the skill's full SKILL.md frontmatter
``description:`` field the same string the system prompt renders
as `` - name: description`` for pre-existing skills.
``description:`` field. Note: the system prompt skill index
truncates this to the first 57 chars; see ``extract_skill_description``.
"""
# Snapshot pre-reload state (name -> description) from the current
# slash-command cache. Using dicts lets the post-rescan diff carry

View file

@ -21,6 +21,11 @@ from tools.skill_manager_tool import (
skill_manage,
MAX_NAME_LENGTH,
)
from agent.skill_utils import (
extract_skill_description,
parse_frontmatter,
SKILL_PROMPT_DESC_LIMIT,
)
@contextmanager
@ -54,6 +59,17 @@ description: Updated description.
Step 1: Do the new thing.
"""
LONG_DESC_CONTENT = """\
---
name: long-desc
description: Use when deploying multi-region Kubernetes clusters with custom CNI plugins and service mesh.
---
# Long Desc Skill
Step 1.
"""
# ---------------------------------------------------------------------------
# _validate_name
@ -259,6 +275,37 @@ class TestCreateSkill:
assert f"Invalid category '{outside}'" in result["error"]
assert not (outside / "my-skill" / "SKILL.md").exists()
def test_create_long_desc_includes_prompt_preview(self, tmp_path):
with _skill_dir(tmp_path):
result = _create_skill("long-desc", LONG_DESC_CONTENT)
assert result["success"] is True
assert "system_prompt_preview" in result
assert "System prompt will show" in result["system_prompt_preview"]
fm, _ = parse_frontmatter(LONG_DESC_CONTENT)
assert extract_skill_description(fm) in result["system_prompt_preview"]
def test_create_short_desc_no_prompt_preview(self, tmp_path):
with _skill_dir(tmp_path):
result = _create_skill("my-skill", VALID_SKILL_CONTENT)
assert result["success"] is True
assert "system_prompt_preview" not in result
def test_create_boundary_at_limit_no_preview(self, tmp_path):
desc = "U" * SKILL_PROMPT_DESC_LIMIT
content = f"---\nname: boundary-at\ndescription: {desc}\n---\n\n# Boundary\n\nStep 1.\n"
with _skill_dir(tmp_path):
result = _create_skill("boundary-at", content)
assert result["success"] is True
assert "system_prompt_preview" not in result
def test_create_boundary_over_limit_has_preview(self, tmp_path):
desc = "U" * (SKILL_PROMPT_DESC_LIMIT + 1)
content = f"---\nname: boundary-over\ndescription: {desc}\n---\n\n# Boundary\n\nStep 1.\n"
with _skill_dir(tmp_path):
result = _create_skill("boundary-over", content)
assert result["success"] is True
assert "system_prompt_preview" in result
class TestEditSkill:
def test_edit_existing_skill(self, tmp_path):
@ -284,6 +331,14 @@ class TestEditSkill:
content = (tmp_path / "my-skill" / "SKILL.md").read_text()
assert "A test skill" in content
def test_edit_long_desc_includes_prompt_preview(self, tmp_path):
edit_content = LONG_DESC_CONTENT.replace("name: long-desc", "name: test-skill")
with _skill_dir(tmp_path):
_create_skill("test-skill", VALID_SKILL_CONTENT)
result = _edit_skill("test-skill", edit_content)
assert result["success"] is True
assert "system_prompt_preview" in result
class TestPatchSkill:
def test_patch_unique_match(self, tmp_path):

View file

@ -45,6 +45,12 @@ from typing import Any, Dict, List, Optional, Tuple
from hermes_constants import get_hermes_home, display_hermes_home
from utils import atomic_replace, is_truthy_value
from hermes_cli.config import cfg_get
from agent.skill_utils import (
extract_skill_description,
is_skill_description_truncated_for_prompt,
parse_frontmatter as _parse_frontmatter,
SKILL_PROMPT_DESC_LIMIT,
)
logger = logging.getLogger(__name__)
@ -810,6 +816,18 @@ def _atomic_write_text(file_path: Path, content: str, encoding: str = "utf-8") -
# Core actions
# =============================================================================
def _add_description_prompt_preview(result: Dict[str, Any], content: str) -> None:
"""Append a system_prompt_preview field when the description will be truncated."""
fm, _ = _parse_frontmatter(content)
if is_skill_description_truncated_for_prompt(fm):
result["system_prompt_preview"] = (
f"System prompt will show: \"{extract_skill_description(fm)}\""
f"keep the trigger self-contained in the first "
f"{SKILL_PROMPT_DESC_LIMIT - 3} chars."
)
def _create_skill(name: str, content: str, category: str = None) -> Dict[str, Any]:
"""Create a new user skill with SKILL.md content."""
# Validate name
@ -875,6 +893,7 @@ def _create_skill(name: str, content: str, category: str = None) -> Dict[str, An
"To add reference files, templates, or scripts, use "
"skill_manage(action='write_file', name='{}', file_path='references/example.md', file_content='...')".format(name)
)
_add_description_prompt_preview(result, content)
return result
@ -923,12 +942,14 @@ def _edit_skill(name: str, content: str) -> Dict[str, Any]:
except Exception:
pass
return {
result = {
"success": True,
"message": f"Skill '{name}' updated (full rewrite).",
"path": str(existing["path"]),
"_change": {"description": _desc},
}
_add_description_prompt_preview(result, content)
return result
def _patch_skill(
@ -1469,6 +1490,10 @@ SKILL_MANAGE_SCHEMA = {
"Skip for simple one-offs. Confirm with user before creating/deleting.\n\n"
"Good skills: trigger conditions, numbered steps with exact commands, "
"pitfalls section, verification steps. Use skill_view() to see format examples.\n\n"
"Description: long descriptions are truncated to the first 57 chars "
"plus '...' in the system prompt skill index; longer text is visible "
"via skills_list/skill_view. Keep the trigger self-contained in that "
"first 57-char window: 'Use when <trigger>. <one-line behavior>.'\n\n"
"Pinned skills are protected from deletion only — skill_manage(action='delete') "
"will refuse with a message pointing the user to `hermes curator unpin <name>`. "
"Patches and edits go through on pinned skills so you can still improve them as "