From cd65673a8fddfec8a0fa130197d49aaab1fefc77 Mon Sep 17 00:00:00 2001 From: nankingjing <1079826437@qq.com> Date: Mon, 6 Jul 2026 10:06:02 +0800 Subject: [PATCH] perf(skills): cache skill discovery results by directory mtime MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit _find_all_skills() re-reads every SKILL.md on every call, which is wasteful when nothing changed between turns. Cache results keyed by the max mtime across all scanned skill directories — a skill write touches the directory, bumping mtime past the cached value and triggering an automatic re-scan. skip_disabled True/False are cached separately. This commit is unstacked from #58984; it carries only the skill discovery cache change. --- tools/skills_tool.py | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/tools/skills_tool.py b/tools/skills_tool.py index cd699fae8f6..d126e7bf093 100644 --- a/tools/skills_tool.py +++ b/tools/skills_tool.py @@ -86,6 +86,15 @@ from agent.skill_utils import ( logger = logging.getLogger(__name__) +# Per-session skill discovery cache. _find_all_skills() re-reads every +# SKILL.md on every call; with hundreds of skills this is wasteful. +# We cache by the max mtime across all scanned skill directories so a +# write by skill_manage (which touches the directory) invalidates the +# cache automatically. skip_disabled True/False are cached separately. +_SKILLS_CACHE: dict = {} # {cache_key: (max_mtime, skills_list)} +_SKILLS_CACHE_KEY_DISABLED = "with_disabled" +_SKILLS_CACHE_KEY_FILTERED = "filtered" + # All skills live in ~/.hermes/skills/ (seeded from bundled skills/ on install). # This is the single source of truth -- agent edits, hub installs, and bundled @@ -611,9 +620,38 @@ def _find_all_skills(*, skip_disabled: bool = False) -> List[Dict[str, Any]]: Returns: List of skill metadata dicts (name, description, category). + + Results are cached per-session; the cache is invalidated when any + scanned skill directory's mtime changes (a skill write touches the + directory, triggering a re-scan on the next call). """ from agent.skill_utils import get_external_skills_dirs, iter_skill_index_files + cache_key = _SKILLS_CACHE_KEY_DISABLED if skip_disabled else _SKILLS_CACHE_KEY_FILTERED + + # Collect directories to scan (same set as the scan loop below). + dirs_to_scan: list = [] + if SKILLS_DIR.exists(): + dirs_to_scan.append(SKILLS_DIR) + dirs_to_scan.extend(get_external_skills_dirs()) + + # Compute the freshest mtime across all scanned directories. + max_mtime = 0.0 + for d in dirs_to_scan: + try: + st = d.stat() + if st.st_mtime > max_mtime: + max_mtime = st.st_mtime + except OSError: + continue + + # Serve from cache when nothing changed since last scan. + cached = _SKILLS_CACHE.get(cache_key) + if cached is not None: + cached_mtime, cached_skills = cached + if cached_mtime >= max_mtime: + return cached_skills + skills = [] seen_names: set = set() @@ -678,6 +716,10 @@ def _find_all_skills(*, skip_disabled: bool = False) -> List[Dict[str, Any]]: ) continue + # Store in cache keyed by the freshest directory mtime seen during + # this scan. Any subsequent skill write that touches a directory + # will bump the mtime past the cached value and trigger a re-scan. + _SKILLS_CACHE[cache_key] = (max_mtime, skills) return skills