mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-24 16:54:43 +00:00
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:
parent
5eb772111d
commit
accbf4d912
3 changed files with 83 additions and 3 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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 "
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue