perf(skills): speed up snapshot prompt builds

Fixes #3356

Build the skills snapshot manifest in one directory walk, avoid importing gateway session context during CLI prompt startup, and reuse direct platform-list matching for snapshot entries.
This commit is contained in:
Dixing Xu 2026-07-07 16:41:32 +01:00
parent d33becd877
commit 1a64c2ed04
3 changed files with 82 additions and 38 deletions

View file

@ -7,6 +7,7 @@ assemble pieces, then combines them with memory and ephemeral prompts.
import json
import logging
import os
import sys
import threading
import contextvars
from collections import OrderedDict
@ -17,6 +18,8 @@ from typing import Optional
from agent.runtime_cwd import resolve_agent_cwd
from agent.skill_utils import (
EXCLUDED_SKILL_DIRS,
SKILL_SUPPORT_DIRS,
extract_skill_conditions,
extract_skill_description,
get_all_skills_dirs,
@ -25,6 +28,7 @@ from agent.skill_utils import (
parse_frontmatter,
skill_matches_environment,
skill_matches_platform,
skill_matches_platform_list,
)
from utils import atomic_json_write
@ -1276,13 +1280,26 @@ def clear_skills_system_prompt_cache(*, clear_snapshot: bool = False) -> None:
def _build_skills_manifest(skills_dir: Path) -> dict[str, list[int]]:
"""Build an mtime/size manifest of all SKILL.md and DESCRIPTION.md files."""
manifest: dict[str, list[int]] = {}
for filename in ("SKILL.md", "DESCRIPTION.md"):
for path in iter_skill_index_files(skills_dir, filename):
skills_dir_str = str(skills_dir)
base = os.path.join(skills_dir_str, "")
prefix_len = len(base)
for root, dirs, files in os.walk(skills_dir_str, followlinks=True):
has_skill_md = "SKILL.md" in files
dirs[:] = [
d
for d in dirs
if d not in EXCLUDED_SKILL_DIRS
and not (has_skill_md and d in SKILL_SUPPORT_DIRS)
]
for filename in ("SKILL.md", "DESCRIPTION.md"):
if filename not in files:
continue
path = os.path.join(root, filename)
try:
st = path.stat()
st = os.stat(path)
except OSError:
continue
manifest[str(path.relative_to(skills_dir))] = [st.st_mtime_ns, st.st_size]
manifest[path[prefix_len:]] = [st.st_mtime_ns, st.st_size]
return manifest
@ -1414,6 +1431,22 @@ def _skill_should_show(
return True
def _current_session_platform_hint() -> str:
"""Return the active platform without importing the gateway package on CLI startup."""
platform = os.environ.get("HERMES_PLATFORM") or os.environ.get("HERMES_SESSION_PLATFORM")
if platform:
return platform
session_context = sys.modules.get("gateway.session_context")
get_session_env = getattr(session_context, "get_session_env", None) if session_context else None
if get_session_env is None:
return ""
try:
return get_session_env("HERMES_SESSION_PLATFORM") or ""
except Exception:
return ""
def build_skills_system_prompt(
available_tools: "set[str] | None" = None,
available_toolsets: "set[str] | None" = None,
@ -1448,15 +1481,10 @@ def build_skills_system_prompt(
# ── Layer 1: in-process LRU cache ─────────────────────────────────
# Include the resolved platform so per-platform disabled-skill lists
# produce distinct cache entries (gateway serves multiple platforms).
from gateway.session_context import get_session_env
_platform_hint = (
os.environ.get("HERMES_PLATFORM")
or get_session_env("HERMES_SESSION_PLATFORM")
or ""
)
_platform_hint = _current_session_platform_hint()
disabled = get_disabled_skill_names(_platform_hint or None)
cache_key = (
str(skills_dir.resolve()),
str(skills_dir),
tuple(str(d) for d in external_dirs),
tuple(sorted(str(t) for t in (available_tools or set()))),
tuple(sorted(str(ts) for ts in (available_toolsets or set()))),
@ -1485,7 +1513,7 @@ def build_skills_system_prompt(
category = entry.get("category") or "general"
frontmatter_name = entry.get("frontmatter_name") or skill_name
platforms = entry.get("platforms") or []
if not skill_matches_platform({"platforms": platforms}):
if not skill_matches_platform_list(platforms):
continue
if frontmatter_name in disabled or skill_name in disabled:
continue

View file

@ -160,27 +160,8 @@ def parse_frontmatter(content: str) -> Tuple[Dict[str, Any], str]:
# ── Platform matching ─────────────────────────────────────────────────────
def skill_matches_platform(frontmatter: Dict[str, Any]) -> bool:
"""Return True when the skill is compatible with the current OS.
Skills declare platform requirements via a top-level ``platforms`` list
in their YAML frontmatter::
platforms: [macos] # macOS only
platforms: [macos, linux] # macOS and Linux
If the field is absent or empty the skill is compatible with **all**
platforms (backward-compatible default).
Termux note: on Termux/Android, ``sys.platform`` is ``"linux"`` on
older Pythons but became ``"android"`` on Python 3.13+. Termux is a
Linux userland riding on the Android kernel, so skills tagged
``linux`` are treated as compatible in Termux regardless of which
``sys.platform`` value Python reports. Individual Linux commands
inside a skill may still misbehave (no systemd, BusyBox utils, no
apt/dnf, etc.) but that is on the skill, not on platform gating.
"""
platforms = frontmatter.get("platforms")
def skill_matches_platform_list(platforms: Any) -> bool:
"""Return True when *platforms* is compatible with the current OS."""
if not platforms:
return True
if not isinstance(platforms, list):
@ -204,6 +185,29 @@ def skill_matches_platform(frontmatter: Dict[str, Any]) -> bool:
return False
def skill_matches_platform(frontmatter: Dict[str, Any]) -> bool:
"""Return True when the skill is compatible with the current OS.
Skills declare platform requirements via a top-level ``platforms`` list
in their YAML frontmatter::
platforms: [macos] # macOS only
platforms: [macos, linux] # macOS and Linux
If the field is absent or empty the skill is compatible with **all**
platforms (backward-compatible default).
Termux note: on Termux/Android, ``sys.platform`` is ``"linux"`` on
older Pythons but became ``"android"`` on Python 3.13+. Termux is a
Linux userland riding on the Android kernel, so skills tagged
``linux`` are treated as compatible in Termux regardless of which
``sys.platform`` value Python reports. Individual Linux commands
inside a skill may still misbehave (no systemd, BusyBox utils, no
apt/dnf, etc.) but that is on the skill, not on platform gating.
"""
return skill_matches_platform_list(frontmatter.get("platforms"))
# ── Environment matching ──────────────────────────────────────────────────
# Recognized environment tags and how each is detected. An environment tag is
@ -787,8 +791,9 @@ def iter_skill_index_files(skills_dir: Path, filename: str):
``SKILL.md`` files, but they are progressive-disclosure data loaded through
``skill_view(..., file_path=...)`` rather than active skill roots.
"""
matches = []
for root, dirs, files in os.walk(skills_dir, followlinks=True):
skills_dir_str = str(skills_dir)
matches: list[str] = []
for root, dirs, files in os.walk(skills_dir_str, followlinks=True):
has_skill_md = "SKILL.md" in files
dirs[:] = [
d
@ -797,9 +802,9 @@ def iter_skill_index_files(skills_dir: Path, filename: str):
and not (has_skill_md and d in SKILL_SUPPORT_DIRS)
]
if filename in files:
matches.append(Path(root) / filename)
for path in sorted(matches, key=lambda p: str(p.relative_to(skills_dir))):
yield path
matches.append(os.path.join(root, filename))
for path in sorted(matches):
yield Path(path)
# ── Namespace helpers for plugin-provided skills ───────────────────────────

View file

@ -12,6 +12,7 @@ from agent.skill_utils import (
iter_skill_index_files,
resolve_skill_config_values,
skill_matches_platform,
skill_matches_platform_list,
)
@ -266,6 +267,7 @@ class TestSkillMatchesPlatformTermux:
"agent.skill_utils.is_termux", return_value=True
):
assert skill_matches_platform(fm) is True
assert skill_matches_platform_list(fm["platforms"]) is True
def test_linux_macos_windows_skill_loads_on_termux(self):
# The common "[linux, macos, windows]" tag used by github-*,
@ -275,6 +277,7 @@ class TestSkillMatchesPlatformTermux:
"agent.skill_utils.is_termux", return_value=True
):
assert skill_matches_platform(fm) is True
assert skill_matches_platform_list(fm["platforms"]) is True
def test_linux_skill_loads_on_termux_linux_platform(self):
# Pre-3.13 Termux reports sys.platform == "linux" already — this
@ -284,6 +287,7 @@ class TestSkillMatchesPlatformTermux:
"agent.skill_utils.is_termux", return_value=True
):
assert skill_matches_platform(fm) is True
assert skill_matches_platform_list(fm["platforms"]) is True
def test_macos_only_skill_still_excluded_on_termux(self):
# macOS-only skills (apple-notes, imessage, ...) should NOT load
@ -293,6 +297,7 @@ class TestSkillMatchesPlatformTermux:
"agent.skill_utils.is_termux", return_value=True
):
assert skill_matches_platform(fm) is False
assert skill_matches_platform_list(fm["platforms"]) is False
def test_windows_only_skill_still_excluded_on_termux(self):
fm = {"platforms": ["windows"]}
@ -300,6 +305,7 @@ class TestSkillMatchesPlatformTermux:
"agent.skill_utils.is_termux", return_value=True
):
assert skill_matches_platform(fm) is False
assert skill_matches_platform_list(fm["platforms"]) is False
def test_explicit_termux_or_android_tag_matches(self):
# Skills can also opt in explicitly via platforms:[termux] or
@ -309,6 +315,8 @@ class TestSkillMatchesPlatformTermux:
):
assert skill_matches_platform({"platforms": ["termux"]}) is True
assert skill_matches_platform({"platforms": ["android"]}) is True
assert skill_matches_platform_list(["termux"]) is True
assert skill_matches_platform_list(["android"]) is True
def test_non_termux_android_does_not_widen(self):
# If we're somehow on a plain Android Python (not Termux), don't
@ -318,6 +326,7 @@ class TestSkillMatchesPlatformTermux:
"agent.skill_utils.is_termux", return_value=False
):
assert skill_matches_platform(fm) is False
assert skill_matches_platform_list(fm["platforms"]) is False
def test_linux_skill_on_real_linux_unaffected(self):
# The non-Termux Linux path must not change.
@ -326,6 +335,7 @@ class TestSkillMatchesPlatformTermux:
"agent.skill_utils.is_termux", return_value=False
):
assert skill_matches_platform(fm) is True
assert skill_matches_platform_list(fm["platforms"]) is True
def test_macos_skill_on_real_macos_unaffected(self):
fm = {"platforms": ["macos"]}
@ -333,6 +343,7 @@ class TestSkillMatchesPlatformTermux:
"agent.skill_utils.is_termux", return_value=False
):
assert skill_matches_platform(fm) is True
assert skill_matches_platform_list(fm["platforms"]) is True
class TestNormalizeSkillLookupName: