fix(agent): demote non-coding skill categories to names-only — never hide skills (#44342)
Real-world failure with the original index pruning: under the default auto
posture, an agent-created ops skill in a demoted category vanished from the
prompt's skill index mid-project, and the agent silently fell back to a
stale sibling skill instead. The "discovery-only" premise didn't hold —
models do not reach for skills_list to rediscover what the index stops
showing them, and agent-created skills are the model's accumulated project
memory (runbooks, pitfalls, operating rules).
Gating pruning behind the opt-in focus mode was the wrong fix too: users
opening a worktree don't know the config exists, so the index-noise win
would effectively never ship.
Instead, the coding posture now DEMOTES non-coding categories rather than
hiding them: each demoted category renders as a single names-only line
("gaming [names only]: allthemons10-ops, mc-backup") with a footer note
explaining the omitted descriptions. Every skill name stays in the prompt,
so memory-anchored recall ("load <name>") keeps working in every mode,
while the description noise is still cut. Applies in auto/on/focus alike;
the general posture demotes nothing. Deny-list semantics unchanged —
unknown/custom categories and coding-adjacent ones keep full entries.
API renamed to match the honest semantics: hidden_skill_categories →
compact_skill_categories, build_skills_system_prompt(hidden_categories=) →
compact_categories=.
This commit is contained in:
+33
-27
@@ -1101,7 +1101,7 @@ def _skill_should_show(
|
||||
def build_skills_system_prompt(
|
||||
available_tools: "set[str] | None" = None,
|
||||
available_toolsets: "set[str] | None" = None,
|
||||
hidden_categories: "frozenset[str] | None" = None,
|
||||
compact_categories: "frozenset[str] | None" = None,
|
||||
) -> str:
|
||||
"""Build a compact skill index for the system prompt.
|
||||
|
||||
@@ -1117,11 +1117,11 @@ def build_skills_system_prompt(
|
||||
are read-only — they appear in the index but new skills are always created
|
||||
in the local dir. Local skills take precedence when names collide.
|
||||
|
||||
``hidden_categories`` (e.g. from the coding posture — see
|
||||
agent/coding_context.py) prunes whole categories from the rendered index.
|
||||
Discovery-only: the snapshot stores everything, ``skills_list`` /
|
||||
``skill_view`` still reach every skill, and a footer note tells the model
|
||||
the full catalog exists.
|
||||
``compact_categories`` (e.g. from the coding posture — see
|
||||
agent/coding_context.py) demotes whole categories to a names-only line in
|
||||
the rendered index. Nothing is ever hidden: every skill name stays
|
||||
visible and loadable via ``skill_view`` / ``skills_list``; only the
|
||||
descriptions are dropped, and a footer note explains the demotion.
|
||||
"""
|
||||
skills_dir = get_skills_dir()
|
||||
external_dirs = get_all_skills_dirs()[1:] # skip local (index 0)
|
||||
@@ -1146,7 +1146,7 @@ def build_skills_system_prompt(
|
||||
tuple(sorted(str(ts) for ts in (available_toolsets or set()))),
|
||||
_platform_hint,
|
||||
tuple(sorted(disabled)),
|
||||
tuple(sorted(hidden_categories or ())),
|
||||
tuple(sorted(compact_categories or ())),
|
||||
)
|
||||
with _SKILLS_PROMPT_CACHE_LOCK:
|
||||
cached = _SKILLS_PROMPT_CACHE.get(cache_key)
|
||||
@@ -1280,38 +1280,44 @@ def build_skills_system_prompt(
|
||||
except Exception as e:
|
||||
logger.debug("Could not read external skill description %s: %s", desc_file, e)
|
||||
|
||||
# Posture-driven category pruning (e.g. non-coding skills while pairing on
|
||||
# code). Match on the top-level category segment so nested categories
|
||||
# ("social-media/twitter") are pruned with their parent.
|
||||
# Posture-driven category demotion (e.g. non-coding skills while pairing
|
||||
# on code). Demoted categories stay in the index as a single names-only
|
||||
# line — descriptions are dropped to cut noise, but every skill name
|
||||
# remains visible so memory-anchored recall ("load <name>") keeps working.
|
||||
# NEVER remove entries entirely: agent-created skills are the model's
|
||||
# project memory, and models don't reach for skills_list to rediscover
|
||||
# what the index stops showing them. Match on the top-level category
|
||||
# segment so nested categories ("social-media/twitter") are demoted with
|
||||
# their parent.
|
||||
demoted = frozenset(
|
||||
cat for cat in skills_by_category
|
||||
if cat.split("/", 1)[0] in (compact_categories or frozenset())
|
||||
)
|
||||
|
||||
hidden_note = ""
|
||||
if hidden_categories:
|
||||
before = sum(len(v) for v in skills_by_category.values())
|
||||
skills_by_category = {
|
||||
cat: entries
|
||||
for cat, entries in skills_by_category.items()
|
||||
if cat.split("/", 1)[0] not in hidden_categories
|
||||
}
|
||||
pruned = before - sum(len(v) for v in skills_by_category.values())
|
||||
if pruned:
|
||||
hidden_note = (
|
||||
f"\n(Note: {pruned} skill(s) in categories unrelated to the "
|
||||
"current coding context are not listed here. The full catalog "
|
||||
"is available via skills_list if the user asks for something "
|
||||
"outside this list.)"
|
||||
)
|
||||
if demoted:
|
||||
hidden_note = (
|
||||
"\n(Categories marked [names only] are outside the current coding "
|
||||
"context, so their descriptions are omitted — the skills work "
|
||||
"normally and load with skill_view(name) as usual.)"
|
||||
)
|
||||
|
||||
if not skills_by_category:
|
||||
result = ""
|
||||
else:
|
||||
index_lines = []
|
||||
for category in sorted(skills_by_category.keys()):
|
||||
# Deduplicate and sort skills within each category
|
||||
seen = set()
|
||||
if category in demoted:
|
||||
names = sorted({name for name, _ in skills_by_category[category]})
|
||||
index_lines.append(f" {category} [names only]: {', '.join(names)}")
|
||||
continue
|
||||
cat_desc = category_descriptions.get(category, "")
|
||||
if cat_desc:
|
||||
index_lines.append(f" {category}: {cat_desc}")
|
||||
else:
|
||||
index_lines.append(f" {category}:")
|
||||
# Deduplicate and sort skills within each category
|
||||
seen = set()
|
||||
for name, desc in sorted(skills_by_category[category], key=lambda x: x[0]):
|
||||
if name in seen:
|
||||
continue
|
||||
|
||||
Reference in New Issue
Block a user