Two-layer caching for build_skills_system_prompt(): 1. In-process LRU (OrderedDict, max 8) — same-process: 546ms → <1ms 2. Disk snapshot (.skills_prompt_snapshot.json) — cold start: 297ms → 103ms Key improvements over original PR #3366: - Extract shared logic into agent/skill_utils.py (parse_frontmatter, skill_matches_platform, get_disabled_skill_names, extract_skill_conditions, extract_skill_description, iter_skill_index_files) - tools/skills_tool.py delegates to shared module — zero code duplication - Proper LRU eviction via OrderedDict.move_to_end + popitem(last=False) - Cache invalidation on all skill mutation paths: - skill_manage tool (in-conversation writes) - hermes skills install (CLI hub) - hermes skills uninstall (CLI hub) - Automatic via mtime/size manifest on cold start prompt_builder.py no longer imports tools.skills_tool (avoids pulling in the entire tool registry chain at prompt build time). 6301 tests pass, 0 failures. Co-authored-by: kshitijk4poor <82637225+kshitijk4poor@users.noreply.github.com>
This commit is contained in:
@@ -547,6 +547,13 @@ def skill_manage(
|
||||
else:
|
||||
result = {"success": False, "error": f"Unknown action '{action}'. Use: create, edit, patch, delete, write_file, remove_file"}
|
||||
|
||||
if result.get("success"):
|
||||
try:
|
||||
from agent.prompt_builder import clear_skills_system_prompt_cache
|
||||
clear_skills_system_prompt_cache(clear_snapshot=True)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return json.dumps(result, ensure_ascii=False)
|
||||
|
||||
|
||||
|
||||
@@ -120,28 +120,11 @@ def set_secret_capture_callback(callback) -> None:
|
||||
def skill_matches_platform(frontmatter: Dict[str, Any]) -> bool:
|
||||
"""Check if a skill is compatible with the current OS platform.
|
||||
|
||||
Skills declare platform requirements via a top-level ``platforms`` list
|
||||
in their YAML frontmatter::
|
||||
|
||||
platforms: [macos] # macOS only
|
||||
platforms: [macos, linux] # macOS and Linux
|
||||
|
||||
Valid values: ``macos``, ``linux``, ``windows``.
|
||||
|
||||
If the field is absent or empty the skill is compatible with **all**
|
||||
platforms (backward-compatible default).
|
||||
Delegates to ``agent.skill_utils.skill_matches_platform`` — kept here
|
||||
as a public re-export so existing callers don't need updating.
|
||||
"""
|
||||
platforms = frontmatter.get("platforms")
|
||||
if not platforms:
|
||||
return True # No restriction → loads everywhere
|
||||
if not isinstance(platforms, list):
|
||||
platforms = [platforms]
|
||||
current = sys.platform
|
||||
for p in platforms:
|
||||
mapped = _PLATFORM_MAP.get(str(p).lower().strip(), str(p).lower().strip())
|
||||
if current.startswith(mapped):
|
||||
return True
|
||||
return False
|
||||
from agent.skill_utils import skill_matches_platform as _impl
|
||||
return _impl(frontmatter)
|
||||
|
||||
|
||||
def _normalize_prerequisite_values(value: Any) -> List[str]:
|
||||
@@ -419,40 +402,13 @@ def check_skills_requirements() -> bool:
|
||||
|
||||
|
||||
def _parse_frontmatter(content: str) -> Tuple[Dict[str, Any], str]:
|
||||
"""Parse YAML frontmatter from markdown content.
|
||||
|
||||
Delegates to ``agent.skill_utils.parse_frontmatter`` — kept here
|
||||
as a public re-export so existing callers don't need updating.
|
||||
"""
|
||||
Parse YAML frontmatter from markdown content.
|
||||
|
||||
Uses yaml.safe_load for full YAML support (nested metadata, lists, etc.)
|
||||
with a fallback to simple key:value splitting for robustness.
|
||||
|
||||
Args:
|
||||
content: Full markdown file content
|
||||
|
||||
Returns:
|
||||
Tuple of (frontmatter dict, remaining content)
|
||||
"""
|
||||
frontmatter = {}
|
||||
body = content
|
||||
|
||||
if content.startswith("---"):
|
||||
end_match = re.search(r"\n---\s*\n", content[3:])
|
||||
if end_match:
|
||||
yaml_content = content[3 : end_match.start() + 3]
|
||||
body = content[end_match.end() + 3 :]
|
||||
|
||||
try:
|
||||
parsed = yaml.safe_load(yaml_content)
|
||||
if isinstance(parsed, dict):
|
||||
frontmatter = parsed
|
||||
# yaml.safe_load returns None for empty frontmatter
|
||||
except yaml.YAMLError:
|
||||
# Fallback: simple key:value parsing for malformed YAML
|
||||
for line in yaml_content.strip().split("\n"):
|
||||
if ":" in line:
|
||||
key, value = line.split(":", 1)
|
||||
frontmatter[key.strip()] = value.strip()
|
||||
|
||||
return frontmatter, body
|
||||
from agent.skill_utils import parse_frontmatter
|
||||
return parse_frontmatter(content)
|
||||
|
||||
|
||||
def _get_category_from_path(skill_path: Path) -> Optional[str]:
|
||||
@@ -516,24 +472,13 @@ def _parse_tags(tags_value) -> List[str]:
|
||||
|
||||
|
||||
def _get_disabled_skill_names() -> Set[str]:
|
||||
"""Load disabled skill names from config (once per call).
|
||||
"""Load disabled skill names from config.
|
||||
|
||||
Resolves platform from ``HERMES_PLATFORM`` env var, falls back to
|
||||
the global disabled list.
|
||||
Delegates to ``agent.skill_utils.get_disabled_skill_names`` — kept here
|
||||
as a public re-export so existing callers don't need updating.
|
||||
"""
|
||||
import os
|
||||
try:
|
||||
from hermes_cli.config import load_config
|
||||
config = load_config()
|
||||
skills_cfg = config.get("skills", {})
|
||||
resolved_platform = os.getenv("HERMES_PLATFORM")
|
||||
if resolved_platform:
|
||||
platform_disabled = skills_cfg.get("platform_disabled", {}).get(resolved_platform)
|
||||
if platform_disabled is not None:
|
||||
return set(platform_disabled)
|
||||
return set(skills_cfg.get("disabled", []))
|
||||
except Exception:
|
||||
return set()
|
||||
from agent.skill_utils import get_disabled_skill_names
|
||||
return get_disabled_skill_names()
|
||||
|
||||
|
||||
def _is_skill_disabled(name: str, platform: str = None) -> bool:
|
||||
|
||||
Reference in New Issue
Block a user