本节摘要:外循环的中枢是
agent/curator.py(2034 行)——一个不靠 cron 守护的背景策展人:agent 空闲且距上次运行超过 interval(默认 7 天)时,maybe_run_curator()自动触发一次评审。curator 管理技能的完整生命周期 active→stale→archived(30 天未用标陈旧、90 天未用归档,归档可恢复、永不硬删),并可选地 fork 一个辅助模型评审 agent 做"伞形合并"——把一堆窄技能合并成类级大技能。全程有三套账本保驾护航:skill_ledger(.curator_ledger.jsonl+sha256 内容寻址备份,谁在何时改了什么,before/after 全量快照,单条回滚)、usage 遥测(.usage.json旁车文件,use_count/patch_generation/last_used_at)、provenance 溯源(ContextVar 区分技能是背景评审 fork 写的还是用户前台写的——用户自己的技能 curator 永不自治地动)。curator 与用户的关系是"建议而非擅权":默认只做确定性失活整理,LLM 合并大手术要显式 opt-in,dry-run 只出报告。
内容来源:原项目源码
agent/curator.py(调度/自动状态迁移/评审提示词)、tools/skill_ledger.py(记账与回滚)、tools/skill_usage.py(usage 记录/生命周期状态)、tools/skill_provenance.py(写源 ContextVar)、agent/curator_backup.py。
⚠️ 注意:curator 的严格不变量(源码 docstring 原文):只碰 agent 创建的技能(
is_agent_created);绝不自动硬删——归档(移入.archive/)是最大破坏动作且可恢复;被 pin 的技能绕过一切自动迁移;评审走辅助模型客户端,绝不触碰主会话的 prompt cache。内置技能默认也参与失活归档(prune_builtins默认 true),但plan等承重技能在保护名单里永不归档。
阅读完本节,你应当能够:
apply_automatic_transitions 的 active/stale/archived 状态机(含 cron 引用保护与 use=0 宽限)。curator 的第一巧思是触发方式(agent/curator.py:1-20 docstring):"runs inactivity-triggered (no cron daemon): when the agent is idle and the last curator run was longer than interval_hours ago"。没有后台常驻进程——gateway/CLI 的空闲心跳顺带问一句"该跑 curator 了吗"。默认参数(curator.py:70-78):
70 DEFAULT_INTERVAL_HOURS = 24 * 7 # 7 days 71 DEFAULT_MIN_IDLE_HOURS = 2 72 DEFAULT_STALE_AFTER_DAYS = 30 73 DEFAULT_ARCHIVE_AFTER_DAYS = 90 74 # Consolidation (the LLM umbrella-building fork) is OFF by default. ... 78 DEFAULT_CONSOLIDATE = False
门控逻辑 should_run_now(233 行起)依次检查:配置 curator.enabled(默认开)、未暂停、last_run_at 距今超过 interval。首跑行为特别谨慎:全新安装没有 last_run_at 时不立即跑,而是把 last_run_at 播种为"现在"、再等满一个完整周期——"Report-only; do not auto-mutate the library the very first time a gateway ticks after an update"。想立刻看效果只能显式 hermes curator run(可带 --dry-run)。空闲检查(min_idle_hours,默认 2 小时)在调用点执行——maybe_run_curator(2016 行):
2016 def maybe_run_curator(*, idle_for_seconds=None, on_summary=None): 2018 """Best-effort: run a curator pass if all gates pass. ... 2022 if not should_run_now(): 2023 return None 2025 if idle_for_seconds is not None: 2026 min_idle_s = get_min_idle_hours() * 3600.0 2027 if idle_for_seconds < min_idle_s: 2028 return None 2030 return run_curator_review(on_summary=on_summary)
一次 curator 运行分两层:确定性层(纯函数无 LLM)永远跑;LLM 层(fork 评审 agent)默认关闭、需 curator.consolidate: true 显式开启——"only the deterministic inactivity prune ... runs; ... no consolidation, no umbrella-building, no aux-model cost"。
curator.py:305 的 apply_automatic_transitions 是纯函数式的状态迁移器,遍历每个受管技能按"最新真实活动时间"推进状态:
321 stale_cutoff = now - timedelta(days=get_stale_after_days()) 322 archive_cutoff = now - timedelta(days=get_archive_after_days()) 324 cron_referenced = _cron_referenced_skills() 326 counts = {"marked_stale": 0, "archived": 0, "reactivated": 0, ...} 328 for row in _u.curated_report(): 330 name = row["name"] 331 if row.get("pinned"): 332 continue # ① pin 的技能永不自动迁移 340 if name in cron_referenced: 341 continue # ② 被 cron 任务引用=在用,永不老化 ... 350 last_activity = _parse_iso(row.get("last_activity_at")) 353 anchor = last_activity or _parse_iso(row.get("created_at")) or now ... 363 never_used = int(row.get("use_count", 0) or 0) == 0 364 if never_used and anchor > stale_cutoff: # ③ use=0 且未过陈旧期:留观(也把误标的 stale 撤回 active) 369 continue 371 if anchor <= archive_cutoff and current != _u.STATE_ARCHIVED: 375 from tools.skill_ledger import reset_ledger_actor, set_ledger_actor 376 _tok = set_ledger_actor("curator") # 记账标注执行者 381 ok, _msg = _u.archive_skill(name) 388 if ok: 389 counts["archived"] += 1 390 elif anchor <= stale_cutoff and current == _u.STATE_ACTIVE: 391 _u.set_state(name, _u.STATE_STALE) 392 counts["marked_stale"] += 1 393 elif anchor > stale_cutoff and current == _u.STATE_STALE: 395 _u.set_state(name, _u.STATE_ACTIVE) # ④ 复活
四处防御值得细读:①pinned 标志(hermes curator pin)是用户的一票否决;②cron 任务引用的技能即使任务暂停/低频也视为在用——调度器只在任务实际触发时 bump usage,低频任务会让技能"被老化拆台";③use=0 的宽限地板——"A use=0 skill is absence of evidence, not evidence of staleness",新建技能可能只是还没遇到触发场景;④陈旧技能再次被用会自动复活。归档动作前用 set_ledger_actor("curator") 给账本打执行者标签,把"自治归档"与"前台用户操作"区分开。首次纳入管理的内置技能还要 seed_record_if_missing 把时钟锚定到现在——否则一个从没被用过的老内置技能会在第一次 pass 时立即归档。整个生命周期画成状态机:

curator.py:432 的 CURATOR_REVIEW_PROMPT(约 150 行)是全书最"有态度"的提示词之一。开宗明义:"This is an UMBRELLA-BUILDING consolidation pass, not a passive audit and not a duplicate-finder"——目标形态是"类级指令与经验知识的图书馆",而"a collection of hundreds of narrow skills where each one captures one session's specific bug is a FAILURE of the library — not a feature"(几百个各记一次会话之坑的窄技能是图书馆的失败而非特色)。硬规则五条:不碰捆绑/hub/外部目录技能;不删任何技能,归档是上限;不碰 pinned 与保护名单(plan);不许拿 use=0 当剪枝理由("use=0 不是价值证据也不是无价值证据");不许以"触发词各不相同"拒绝合并——正确的问法是"人类维护者会写成 N 个独立技能,还是一个带 N 个小节的技能?"。合并三法:
# a. MERGE INTO EXISTING UMBRELLA — 簇中已有够宽的技能当伞,patch 加节,归档兄弟 # b. CREATE A NEW UMBRELLA SKILL.md — 无够宽者,skill_manage create 新类级技能 # c. DEMOTE TO REFERENCES/TEMPLATES/SCRIPTS — 窄而值钱的会话细节降级为伞的支撑文件 # • references/<topic>.md 会话细节/知识库 # • templates/<name>.<ext> 起步模板 # • scripts/<name>.<ext> 可重跑脚本
工作法要求先扫全表找前缀簇(hermes-config-、gateway-、pr-、salvage-……"Expect 10-25 clusters"),逐簇迭代,"If you end the pass with fewer than 10 archives, you stopped too early"。还有包完整性条款:降级前必须把技能当完整目录包检视,有支撑文件或相对链接时不许只拍扁 SKILL.md。结束时必须输出结构化 YAML(consolidations: from/into/reason 与 prunings: name/reason)供下游工具驱动 cron 引用改写。
--dry-run 时提示词前再贴一段 CURATOR_DRY_RUN_BANNER(405 行):双线横幅内三条禁令——不许调 skill_manage 的 patch/create/delete/write_file/remove_file、不许用 terminal 把技能目录 mv 进 .archive/、不许动 ~/.hermes/skills/ 下任何文件;skills_list 与 skill_view 随便读;"Your output IS the deliverable"——产出与真实运行完全相同的人类可读摘要加 YAML 块,只是把"做了"改成"将做"。甚至预案都写好了:若不慎真的动了写动作,须在摘要中明说以便评审者回滚。评审的执行侧 run_curator_review(1511 行)把候选列表(由 _render_candidate_list 从 curated_report() 渲染,内置技能仅在 prune_builtins 开启时入选)与提示词一起交给 _run_llm_review(1842 行):后者用辅助模型客户端跑完 fork,解析 YAML 块,再经 _classify_removed_skills/_reconcile_classification 把 fork 自述的动作与磁盘实际变化对账——不相信 LLM 的自我汇报,以文件系统为准。每次运行的报告落 ~/.hermes/logs/curator/{YYYYMMDD-HHMMSS}/run.json+REPORT.md,与技能数据分离存放。此外 agent/curator_backup.py(758 行)提供整运行级 tarball 快照与 hermes curator rollback,覆盖"一次运行搞砸一片"的场景——与 skill_ledger 的单条回滚构成两级撤销。
tools/skill_ledger.py 是变更审计账本:每次技能变更无论执行者是谁,都向 ~/.hermes/skills/.curator_ledger.jsonl 追加一条 JSONL:
176 entry = { 177 "id": uuid.uuid4().hex[:12], 178 "ts": datetime.now(timezone.utc).isoformat(), 179 "actor": actor if actor in _VALID_ACTORS else derive_actor(), 180 "action": action, # create/patch/delete/rollback/... 181 "skill": skill, 182 "evidence": evidence or {}, 183 "before": before or [], # [{path, sha256}, ...] 184 "after": after or [], 185 }
一条真实的账本行(示意,五个动作类型的语义):
{"id": "a1b2c3d4e5f6", "ts": "2026-08-14T03:12:55+00:00", "actor": "agent", "action": "patch", "skill": "deploy-checklist", "evidence": {"review": "background"}, "before": [{"path": ".../deploy-checklist/SKILL.md", "sha256": "9f2c..."}], "after": [{"path": ".../deploy-checklist/SKILL.md", "sha256": "77ad..."}]}
三个设计决策(docstring 标注 "Teknium-approved"):JSONL 而非状态库——账本要在 DB 重置后幸存、可 grep、可 rsync;覆盖所有执行者(curator/agent/user 三种 actor);按文件内容寻址的 blob 而非整树 tar——一次变更通常只动一个文件,相同内容跨条目去重到 ~/.hermes/.curator_backups/blobs/ 里的单个 sha256 文件。账本是遥测不是闸门:"a ledger failure must never block the mutation it describes"——所有写路径吞异常。唯一例外是 rollback_entry(300 行):fail-closed——先校验条目所有路径都在 HERMES_HOME 内(防手改账本变"任意写原语"),再预检全部 before blob 存在,然后先记一条 pre-rollback 安全条目(捕获当前状态,让回滚本身也可回滚),任何一步失败则"rollback aborted, nothing was changed"。
usage 存在旁车文件 ~/.hermes/skills/.usage.json("Sidecar, not frontmatter. Keeps operational telemetry out of user-authored SKILL.md content")。单条记录 13 字段(tools/skill_usage.py:644):created_by(agent/installed/None)、use_count/view_count、last_used_at/last_viewed_at、patch_count/patch_generation/last_reused_patch_generation/last_patched_at、created_at、state、pinned、archived_at。写入一律走 _mutate(743 行):锁内"读全量→就地变异→原子写回"(tempfile+fsync+os.replace),所有计数递增 best-effort——"A broken sidecar never breaks the underlying tool call"。bump_use(864 行)里最有意思的是补丁后复用判定:
875 def _apply(rec): 876 previous_use_count = _non_negative_int(rec.get("use_count")) 877 patch_generation = _non_negative_int(rec.get("patch_generation")) ... 881 reused = previous_use_count > 0 882 reuse_after_patch = reused and patch_generation > last_reused_generation 883 rec["use_count"] = previous_use_count + 1 884 rec["last_used_at"] = _now_iso()
reuse_after_patch 记录"技能被打过补丁后又再次被用"——这是自改进生效的直接证据(改了之后确实还有人/有会话用),会作为 lifecycle 事件发射出去。bump_view/bump_use 对一切来源的技能计数("usage tracking is pure observability and is orthogonal to whether a skill is ever curated"),而生命周期类变更(set_state/set_pinned/mark_agent_created)带 require_curation_eligible=True——不给 hub 技能写无意义的 archived 标志。provenance 则回答"这技能是谁写的":tools/skill_provenance.py 用 ContextVar 携带写源,run_agent.py 在 spawn 背景评审 fork 时设为 background_review,默认 foreground。只有 background_review 源下 skill_manage 创建的技能才会被 mark_agent_created 打上 created_by: agent 进入 curator 管辖;用户手写的、前台 agent 应用户要求创建的技能属于用户,"must never be auto-curated"——想纳管要走显式的 hermes curator adopt <name>(adopt_skill,596 行)。配套的 .bundled_manifest 与 hub 安装名单(is_bundled/is_hub_installed)构成第三层来源分类:捆绑/hub/外部目录技能对自治 actor 只读。
💡 循环要点:curator 把"图书馆整理"从一次性壮举变成低频自动化的卫生习惯:确定性层(失活/归档)零成本常开,LLM 层(合并)显式付费开启;每次写动作都有 ledger 兜底、可单条回滚;usage 与 provenance 双账本回答"这技能值不值、是谁的"。自治的边界被刻意压在"永不硬删、永不碰用户资产"之内——外循环的引擎再强,方向盘始终在用户手里。
hermes curator run 可绕过门控。reuse_after_patch 是自改进生效的信号;provenance 用 ContextVar 区分背景/前台写源,用户技能不被自治管理;计数观测一切技能、状态变更仅限受管技能。plan 等承重内置技能在保护名单;报告落 logs/curator/ 与技能数据分离。下一节补上闭环的最后一环:技能在使用中如何被发现问题、被即时修补,learning_graph 如何把整条学习轨迹沉淀成可见图谱——"用得越多越强"的完整证据链。