feat(skills): curator 面板加 pinned 计数 + 运行时输出技能名日志

- get_curator_status 的 agent_created_skills 增加 pinned 计数,与
  active/stale/archived 正交;前端技能进化面板同步展示「被钉住」一项
  (curator/types.ts、skill-evolution-panel.tsx、zh-CN/en-US/types i18n)。
- run_curator_review 每次运行时把涉及的技能名输出到日志:新增
  _log_scanned_skills(扫描到的技能名)与 _candidate_skill_names
  (候选技能名),logger 与 curator_log 双写。

新增 test_curator_status_counts.py、test_curator_skill_logging.py。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
xuhdandClaude Opus 4.7 committed 2026-05-18 15:25:13 +08:00
1 parent 4aee413680
commit f80a00e211
8 files changed
+179 -2

No files matched your search

@@ -177,6 +177,9 @@ function StatusCard() {
<span>
{tt.archived}: {status.agent_created_skills.archived}
</span>
<span>
{tt.pinned}: {status.agent_created_skills.pinned}
</span>
</span>
</StatRow>
<StatRow label={tt.lastActions} className="sm:col-span-2">
+2
View File
@@ -47,6 +47,8 @@ export interface CuratorStatus {
active: number;
stale: number;
archived: number;
/** 被 pin 的数量;与 active/stale/archived 正交。 */
pinned: number;
};
/** 上次运行 LLM 自述的建议(它"说"要做什么)。 */
last_consolidation_suggestions: CuratorSuggestions;
@@ -546,6 +546,7 @@ export const enUS: Translations = {
stale: "Stale",
archived: "Archived",
total: "Total",
pinned: "Pinned",
runNow: "Run now",
runStarted: "Background run started",
runBlockedEvolutionDisabled:
@@ -474,6 +474,7 @@ export interface Translations {
stale: string;
archived: string;
total: string;
pinned: string;
runNow: string;
runStarted: string;
runBlockedEvolutionDisabled: string;
@@ -522,6 +522,7 @@ export const zhCN: Translations = {
stale: "陈旧",
archived: "已归档",
total: "合计",
pinned: "被钉住",
runNow: "立即执行",
runStarted: "已触发后台执行",
runBlockedEvolutionDisabled: "自我进化未开启 —— 请先在下方打开开关。",
@@ -556,6 +556,41 @@ def _render_candidate_list(user_id: str) -> str:
return "\n".join(lines)
def _candidate_skill_names(candidate_list: str) -> list[str]:
"""从 :func:`_render_candidate_list` 的文本里抽出技能名。
候选行形如 ``- {name} [state=..., ...]``,技能名是 ``- `` 之后的第一个 token。
"""
return [
line[2:].split(" ", 1)[0]
for line in candidate_list.splitlines()
if line.startswith("- ")
]
def _log_scanned_skills(user_id: str) -> list[str]:
"""记录并返回本次策展扫描到的技能名(Agent 创建、属于该用户)。
每次技能进化运行时调用 —— 把涉及的技能名打到日志,便于排查"哪些技能
被策展程序处理了"。返回排序后的技能名列表。
"""
from deerflow.skills import curator_log
from deerflow.skills.usage import agent_created_entries
owned = _list_user_owned_skill_names(user_id)
scanned = agent_created_entries()
if owned is not None:
scanned = {n: e for n, e in scanned.items() if n in owned}
names = sorted(scanned)
logger.info("Curator[%s]: 本次扫描到 %d 个技能 — %s", user_id, len(names), names)
curator_log.record(
"info",
f"本次扫描到 {len(names)} 个 Agent 创建的技能:{', '.join(names) or '(无)'}",
user_id=user_id,
)
return names
_CURATOR_REVIEW_PROMPT = (
"You are running as DeerFlow's background skill CURATOR. This is an "
"UMBRELLA-BUILDING consolidation pass, not a passive audit and not a "
@@ -959,6 +994,9 @@ def run_curator_review(user_id: str) -> dict:
logger.info("Curator[%s]: starting run at %s", user_id, now.isoformat())
curator_log.record("info", f"本次策展开始,启动时间 {now.isoformat()}", user_id=user_id)
# 输出本次技能进化涉及的技能名,便于排查。
_log_scanned_skills(user_id)
transition_counters = apply_automatic_transitions(user_id, now)
logger.info("Curator[%s]: transitions=%s", user_id, transition_counters)
curator_log.record(
@@ -972,9 +1010,19 @@ def run_curator_review(user_id: str) -> dict:
)
candidate_list = _render_candidate_list(user_id)
candidate_line_count = 0 if not candidate_list.strip() else len([line for line in candidate_list.splitlines() if line.startswith("- ")])
candidate_names = _candidate_skill_names(candidate_list)
candidate_line_count = len(candidate_names)
logger.info(
"Curator[%s]: %d 个候选技能进入 LLM 评审 — %s",
user_id,
candidate_line_count,
candidate_names,
)
curator_log.record(
"info", f"进入 LLM 评审的候选技能:共 {candidate_line_count} 个", user_id=user_id
"info",
f"进入 LLM 评审的候选技能:共 {candidate_line_count} 个"
+ (f" —— {', '.join(candidate_names)}" if candidate_names else ""),
user_id=user_id,
)
if candidate_line_count == 0:
@@ -1168,9 +1216,13 @@ def get_curator_status(user_id: str) -> dict:
if owned is not None:
entries = {name: entry for name, entry in entries.items() if name in owned}
counts: dict[str, int] = {STATE_ACTIVE: 0, STATE_STALE: 0, STATE_ARCHIVED: 0}
pinned_count = 0
for entry in entries.values():
s = entry.get("state", STATE_ACTIVE)
counts[s] = counts.get(s, 0) + 1
# pinned 与生命周期状态正交 —— 单独计数。
if entry.get("pinned"):
pinned_count += 1
cfg = _get_curator_config(user_id)
return {
@@ -1184,6 +1236,8 @@ def get_curator_status(user_id: str) -> dict:
"active": counts[STATE_ACTIVE],
"stale": counts[STATE_STALE],
"archived": counts[STATE_ARCHIVED],
# 被 pin 的数量;与 active/stale/archived 正交,不计入它们的和。
"pinned": pinned_count,
},
# 上次运行的两份记录:LLM 自述的建议 + 实际发生的工具调用,供前端对照展示。
"last_consolidation_suggestions": state.get("last_consolidation_suggestions")
@@ -0,0 +1,56 @@
"""技能进化运行时把涉及的技能名输出到日志。"""
from __future__ import annotations
import logging
from deerflow.skills import curator
_CURATOR_LOGGER = "deerflow.skills.curator"
class TestCandidateSkillNames:
def test_extracts_names_from_candidate_lines(self):
text = "- alpha [state=active, use=1]\n- beta-skill [state=stale, use=0]"
assert curator._candidate_skill_names(text) == ["alpha", "beta-skill"]
def test_ignores_non_candidate_lines(self):
# _render_candidate_list 无候选时返回的提示文本不应被误识别为技能名
assert curator._candidate_skill_names("No agent-created active skills found.") == []
def test_empty(self):
assert curator._candidate_skill_names("") == []
class TestLogScannedSkills:
def _patch_common(self, monkeypatch, entries: dict, owned=None) -> None:
monkeypatch.setattr(curator, "_list_user_owned_skill_names", lambda uid: owned)
monkeypatch.setattr("deerflow.skills.usage.agent_created_entries", lambda: entries)
monkeypatch.setattr("deerflow.skills.curator_log.record", lambda *a, **k: None)
def test_logs_and_returns_sorted_skill_names(self, monkeypatch, caplog):
self._patch_common(
monkeypatch,
{"zeta": {"source": "agent"}, "alpha": {"source": "agent"}},
)
with caplog.at_level(logging.INFO, logger=_CURATOR_LOGGER):
names = curator._log_scanned_skills("u1")
assert names == ["alpha", "zeta"]
assert "alpha" in caplog.text
assert "zeta" in caplog.text
def test_filters_to_owned_skills(self, monkeypatch, caplog):
self._patch_common(
monkeypatch,
{"alpha": {"source": "agent"}, "beta": {"source": "agent"}},
owned={"alpha"},
)
with caplog.at_level(logging.INFO, logger=_CURATOR_LOGGER):
names = curator._log_scanned_skills("u1")
assert names == ["alpha"]
def test_empty_skills(self, monkeypatch, caplog):
self._patch_common(monkeypatch, {})
with caplog.at_level(logging.INFO, logger=_CURATOR_LOGGER):
names = curator._log_scanned_skills("u1")
assert names == []
@@ -0,0 +1,59 @@
"""get_curator_status 的技能计数 —— 含与生命周期正交的 pinned 计数。"""
from __future__ import annotations
from deerflow.config.skill_evolution_config import CuratorConfig
from deerflow.skills import curator
def _patch(monkeypatch, entries: dict) -> None:
"""注入受控依赖:固定 curator 状态/配置,owned 用 legacy 全量,entries 由测试给定。"""
monkeypatch.setattr(curator, "_read_curator_state", lambda uid: {})
monkeypatch.setattr(curator, "_list_user_owned_skill_names", lambda uid: None)
monkeypatch.setattr(curator, "_get_curator_config", lambda uid: CuratorConfig(enabled=True))
monkeypatch.setattr(
"deerflow.skills.usage.agent_created_entries",
lambda: entries,
)
def test_counts_by_state(monkeypatch):
entries = {
"a": {"source": "agent", "state": "active"},
"b": {"source": "agent", "state": "active"},
"c": {"source": "agent", "state": "stale"},
"d": {"source": "agent", "state": "archived"},
}
_patch(monkeypatch, entries)
counts = curator.get_curator_status("u1")["agent_created_skills"]
assert counts == {"total": 4, "active": 2, "stale": 1, "archived": 1, "pinned": 0}
def test_pinned_count_is_orthogonal_to_state(monkeypatch):
# pinned 与 active/stale/archived 正交:一个 active+pinned、一个 archived+pinned
entries = {
"a": {"source": "agent", "state": "active", "pinned": True},
"b": {"source": "agent", "state": "active"},
"c": {"source": "agent", "state": "archived", "pinned": True},
"d": {"source": "agent", "state": "stale"},
}
_patch(monkeypatch, entries)
counts = curator.get_curator_status("u1")["agent_created_skills"]
assert counts["total"] == 4
assert counts["active"] == 2
assert counts["stale"] == 1
assert counts["archived"] == 1
# pinned 单独统计,不受状态影响,也不计入上面三者之和
assert counts["pinned"] == 2
def test_pinned_zero_when_none_pinned(monkeypatch):
_patch(monkeypatch, {"a": {"source": "agent", "state": "active"}})
counts = curator.get_curator_status("u1")["agent_created_skills"]
assert counts["pinned"] == 0
def test_empty_skills(monkeypatch):
_patch(monkeypatch, {})
counts = curator.get_curator_status("u1")["agent_created_skills"]
assert counts == {"total": 0, "active": 0, "stale": 0, "archived": 0, "pinned": 0}