mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-18 04:41:56 +00:00
feat(curator): hint at hermes curator pin in the rename block (#23212)
Surfaces the pin command at the moment users care about it: when a
consolidation just landed against their skill library and they're
looking at the umbrella name in the curator output. Previously `hermes
curator pin` existed but had no discovery surface — users only learned
it existed by reading docs or stumbling onto `hermes curator --help`.
The hint:
archived 3 skill(s):
• docx-extraction → document-tools
• pdf-extraction → document-tools
• old-stale — pruned (stale)
full report: hermes curator status
keep an umbrella stable: hermes curator pin document-tools
Gated on having at least one consolidation that produced an umbrella.
Pruned-only runs (nothing surviving to pin) skip the hint. When
multiple umbrellas were produced, picks alphabetically first as a
concrete example rather than listing them all.
3 new tests in tests/agent/test_curator_classification.py covering:
consolidation produces hint with real umbrella name, pruned-only run
omits it, multi-umbrella picks one example.
This commit is contained in:
parent
50f9fee988
commit
7312f7f849
2 changed files with 118 additions and 1 deletions
|
|
@ -899,9 +899,12 @@ def _build_rename_summary(
|
||||||
• flaky-thing — pruned (stale)
|
• flaky-thing — pruned (stale)
|
||||||
• old-utility → spreadsheet-ops
|
• old-utility → spreadsheet-ops
|
||||||
full report: hermes curator status
|
full report: hermes curator status
|
||||||
|
keep an umbrella stable: hermes curator pin document-tools
|
||||||
|
|
||||||
Cap is 10 entries so a 50-skill consolidation doesn't blow up
|
Cap is 10 entries so a 50-skill consolidation doesn't blow up
|
||||||
agent.log; the full list is always in REPORT.md.
|
agent.log; the full list is always in REPORT.md. The pin hint only
|
||||||
|
appears when at least one consolidation produced an umbrella worth
|
||||||
|
pinning (pruned-only runs skip it).
|
||||||
"""
|
"""
|
||||||
after_by_name = {r.get("name"): r for r in after_report if isinstance(r, dict)}
|
after_by_name = {r.get("name"): r for r in after_report if isinstance(r, dict)}
|
||||||
after_names = set(after_by_name.keys())
|
after_names = set(after_by_name.keys())
|
||||||
|
|
@ -950,6 +953,17 @@ def _build_rename_summary(
|
||||||
if total > SHOW:
|
if total > SHOW:
|
||||||
lines.append(f" … and {total - SHOW} more")
|
lines.append(f" … and {total - SHOW} more")
|
||||||
lines.append("full report: hermes curator status")
|
lines.append("full report: hermes curator status")
|
||||||
|
# Pin hint — only surface it when there's actually a destination skill
|
||||||
|
# worth pinning. The umbrella skills that absorbed content are the natural
|
||||||
|
# candidates: pinning one tells future curator runs to leave it alone.
|
||||||
|
# Pruned-only runs don't get this hint (nothing surviving to pin).
|
||||||
|
if consolidated:
|
||||||
|
umbrellas = sorted({e.get("into") for e in consolidated if e.get("into")})
|
||||||
|
if umbrellas:
|
||||||
|
example = umbrellas[0]
|
||||||
|
lines.append(
|
||||||
|
f"keep an umbrella stable: hermes curator pin {example}"
|
||||||
|
)
|
||||||
return "\n".join(lines)
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1020,3 +1020,106 @@ def test_rename_summary_mixed_consolidation_and_pruning(curator_env):
|
||||||
assert merge_idx < drop_idx, "consolidated should render before pruned"
|
assert merge_idx < drop_idx, "consolidated should render before pruned"
|
||||||
assert "merge-me → umbrella" in lines[merge_idx]
|
assert "merge-me → umbrella" in lines[merge_idx]
|
||||||
assert "drop-me — pruned (stale)" in lines[drop_idx]
|
assert "drop-me — pruned (stale)" in lines[drop_idx]
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Pin hint — surfaces `hermes curator pin <umbrella>` in the rename block so
|
||||||
|
# users learn the command exists at the moment they care (a consolidation
|
||||||
|
# just landed against their library). The hint is gated on having at least
|
||||||
|
# one umbrella destination — pruned-only runs skip it.
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def test_rename_summary_pin_hint_appears_when_consolidation_produced_umbrella(curator_env):
|
||||||
|
"""When at least one skill was absorbed into an umbrella, hint at pinning it."""
|
||||||
|
result = curator_env._build_rename_summary(
|
||||||
|
before_names={"pdf-extraction", "docx-extraction", "document-tools"},
|
||||||
|
after_report=[{"name": "document-tools", "state": "active"}],
|
||||||
|
tool_calls=[
|
||||||
|
{
|
||||||
|
"name": "skill_manage",
|
||||||
|
"arguments": json.dumps({
|
||||||
|
"action": "delete",
|
||||||
|
"name": "pdf-extraction",
|
||||||
|
"absorbed_into": "document-tools",
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "skill_manage",
|
||||||
|
"arguments": json.dumps({
|
||||||
|
"action": "delete",
|
||||||
|
"name": "docx-extraction",
|
||||||
|
"absorbed_into": "document-tools",
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
model_final="",
|
||||||
|
)
|
||||||
|
assert "hermes curator pin document-tools" in result
|
||||||
|
assert "keep an umbrella stable" in result
|
||||||
|
|
||||||
|
|
||||||
|
def test_rename_summary_pin_hint_skipped_for_pruned_only_runs(curator_env):
|
||||||
|
"""Pruned-only runs have nothing surviving to pin — hint should not appear."""
|
||||||
|
result = curator_env._build_rename_summary(
|
||||||
|
before_names={"old-flaky-thing", "another-stale", "keeper"},
|
||||||
|
after_report=[{"name": "keeper", "state": "active"}],
|
||||||
|
tool_calls=[
|
||||||
|
{
|
||||||
|
"name": "skill_manage",
|
||||||
|
"arguments": json.dumps({
|
||||||
|
"action": "delete",
|
||||||
|
"name": "old-flaky-thing",
|
||||||
|
"absorbed_into": "",
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "skill_manage",
|
||||||
|
"arguments": json.dumps({
|
||||||
|
"action": "delete",
|
||||||
|
"name": "another-stale",
|
||||||
|
"absorbed_into": "",
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
model_final="",
|
||||||
|
)
|
||||||
|
# Block still renders (skills were archived) but no pin hint.
|
||||||
|
assert "archived 2 skill(s):" in result
|
||||||
|
assert "hermes curator pin" not in result
|
||||||
|
assert "keep an umbrella stable" not in result
|
||||||
|
|
||||||
|
|
||||||
|
def test_rename_summary_pin_hint_picks_one_umbrella_when_multiple_absorbed(curator_env):
|
||||||
|
"""Multiple umbrellas → hint shows one example (alphabetically first), not a list."""
|
||||||
|
result = curator_env._build_rename_summary(
|
||||||
|
before_names={"a-skill", "b-skill", "umbrella-zeta", "umbrella-alpha"},
|
||||||
|
after_report=[
|
||||||
|
{"name": "umbrella-zeta", "state": "active"},
|
||||||
|
{"name": "umbrella-alpha", "state": "active"},
|
||||||
|
],
|
||||||
|
tool_calls=[
|
||||||
|
{
|
||||||
|
"name": "skill_manage",
|
||||||
|
"arguments": json.dumps({
|
||||||
|
"action": "delete",
|
||||||
|
"name": "a-skill",
|
||||||
|
"absorbed_into": "umbrella-zeta",
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "skill_manage",
|
||||||
|
"arguments": json.dumps({
|
||||||
|
"action": "delete",
|
||||||
|
"name": "b-skill",
|
||||||
|
"absorbed_into": "umbrella-alpha",
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
model_final="",
|
||||||
|
)
|
||||||
|
# Sorted picks alphabetically first.
|
||||||
|
assert "hermes curator pin umbrella-alpha" in result
|
||||||
|
# Exactly one hint line, not one per umbrella.
|
||||||
|
pin_lines = [ln for ln in result.splitlines() if "hermes curator pin" in ln]
|
||||||
|
assert len(pin_lines) == 1
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue