feat(skills): 接入技能使用埋点,修复活跃技能被 curator 误归档
技能正文不进 system prompt,Agent 按 Progressive Loading 用 read_file 加载 SKILL.md 才算真正用到该技能。此前 read_file 与 skill_manage 的 patch/edit 都不更新 .usage.json,curator 的年龄流转据 latest_activity_at 判断闲置,导致每天都在用的 agent 创建技能在 30/90 天后被误标陈旧并归档。 - sandbox/tools.py: 新增 _custom_skill_name_from_path / _record_skill_read, read_file 读取 custom 技能目录下文件时 bump_use(best-effort,失败不影响主流程) - skill_manage_tool.py: patch/edit 经 _bump_patch_best_effort 记 patch_count - tests/test_skill_usage_signals.py: 12 项,覆盖路径解析、埋点与异常兜底 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
39def1c20b
commit
0386f54449
2 files changed
+57
No files matched your search
@@ -153,6 +153,43 @@ def _resolve_skills_path(path: str) -> str:
|
|||||||
return _join_path_preserving_style(skills_host, relative)
|
return _join_path_preserving_style(skills_host, relative)
|
||||||
|
|
||||||
|
|
||||||
|
def _custom_skill_name_from_path(path: str) -> str | None:
|
||||||
|
"""从虚拟技能路径里解析出 custom 技能名,用于"被使用"埋点。
|
||||||
|
|
||||||
|
只识别 ``{skills_container}/custom/{name}/...`` 形式 —— public 技能不参与
|
||||||
|
策展,给它们埋点只会往 ``.usage.json`` 写无用条目。读技能目录下任意文件
|
||||||
|
(SKILL.md 或 references/templates/scripts 支持文件)都算一次该技能的使用。
|
||||||
|
解析不出技能名时返回 None。
|
||||||
|
"""
|
||||||
|
skills_container = _get_skills_container_path()
|
||||||
|
if not (path == skills_container or path.startswith(f"{skills_container}/")):
|
||||||
|
return None
|
||||||
|
relative = path[len(skills_container) :].lstrip("/")
|
||||||
|
parts = [segment for segment in relative.split("/") if segment]
|
||||||
|
if len(parts) >= 2 and parts[0] == "custom":
|
||||||
|
return parts[1]
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _record_skill_read(virtual_path: str) -> None:
|
||||||
|
"""技能文件被 ``read_file`` 读取时记一次 use —— 技能"被使用"的真实信号。
|
||||||
|
|
||||||
|
技能正文不进 system prompt(prompt 里只有 name/description/location),Agent
|
||||||
|
按 Progressive Loading 用 ``read_file`` 加载 SKILL.md 才算真正用到该技能。
|
||||||
|
curator 的年龄流转据此判断闲置,没有这个埋点会把每天都在用的技能误判为陈旧。
|
||||||
|
best-effort:埋点失败绝不影响 ``read_file`` 本身。
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
name = _custom_skill_name_from_path(virtual_path)
|
||||||
|
if name is None:
|
||||||
|
return
|
||||||
|
from deerflow.skills.usage import bump_use
|
||||||
|
|
||||||
|
bump_use(name)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
def _is_acp_workspace_path(path: str) -> bool:
|
def _is_acp_workspace_path(path: str) -> bool:
|
||||||
"""Check if a path is under the ACP workspace virtual path."""
|
"""Check if a path is under the ACP workspace virtual path."""
|
||||||
return path == _ACP_WORKSPACE_VIRTUAL_PATH or path.startswith(f"{_ACP_WORKSPACE_VIRTUAL_PATH}/")
|
return path == _ACP_WORKSPACE_VIRTUAL_PATH or path.startswith(f"{_ACP_WORKSPACE_VIRTUAL_PATH}/")
|
||||||
@@ -1467,6 +1504,9 @@ def read_file_tool(
|
|||||||
path = _resolve_and_validate_user_data_path(path, thread_data)
|
path = _resolve_and_validate_user_data_path(path, thread_data)
|
||||||
# Custom mount paths are resolved by LocalSandbox._resolve_path()
|
# Custom mount paths are resolved by LocalSandbox._resolve_path()
|
||||||
content = sandbox.read_file(path)
|
content = sandbox.read_file(path)
|
||||||
|
# 读成功即记一次技能使用 —— requested_path 仍是虚拟路径(本地/容器沙箱都一样),
|
||||||
|
# 非技能路径会被 _record_skill_read 自行忽略。
|
||||||
|
_record_skill_read(requested_path)
|
||||||
if not content:
|
if not content:
|
||||||
return "(empty)"
|
return "(empty)"
|
||||||
if start_line is not None and end_line is not None:
|
if start_line is not None and end_line is not None:
|
||||||
|
|||||||
@@ -66,6 +66,21 @@ async def _to_thread(func, /, *args, **kwargs):
|
|||||||
return await asyncio.to_thread(func, *args, **kwargs)
|
return await asyncio.to_thread(func, *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
async def _bump_patch_best_effort(name: str) -> None:
|
||||||
|
"""记一次技能维护活动(patch / edit)。
|
||||||
|
|
||||||
|
刷新 ``.usage.json`` 的 ``patch_count`` 与 ``last_patched_at``,让 curator 的
|
||||||
|
年龄流转把"Agent 正在主动维护的技能"算作活跃,而不是误判为闲置后归档。
|
||||||
|
best-effort:埋点失败只记 DEBUG 日志,绝不影响技能写入主流程。
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
from deerflow.skills.usage import bump_patch
|
||||||
|
|
||||||
|
await _to_thread(bump_patch, name)
|
||||||
|
except Exception:
|
||||||
|
logger.debug("Failed to bump patch count for %r", name, exc_info=True)
|
||||||
|
|
||||||
|
|
||||||
async def _ensure_skill_db_registered(store: SkillStore, name: str, user_id: str) -> None:
|
async def _ensure_skill_db_registered(store: SkillStore, name: str, user_id: str) -> None:
|
||||||
"""Idempotently record an agent-created skill's ownership in the DB.
|
"""Idempotently record an agent-created skill's ownership in the DB.
|
||||||
|
|
||||||
@@ -229,6 +244,7 @@ async def _skill_manage_impl(
|
|||||||
name,
|
name,
|
||||||
_history_record(action="edit", file_path=SKILL_MD_FILE, prev_content=prev_content, new_content=content, thread_id=thread_id, scanner=scan),
|
_history_record(action="edit", file_path=SKILL_MD_FILE, prev_content=prev_content, new_content=content, thread_id=thread_id, scanner=scan),
|
||||||
)
|
)
|
||||||
|
await _bump_patch_best_effort(name)
|
||||||
await refresh_skills_system_prompt_cache_async()
|
await refresh_skills_system_prompt_cache_async()
|
||||||
return f"Updated custom skill '{name}'."
|
return f"Updated custom skill '{name}'."
|
||||||
|
|
||||||
@@ -253,6 +269,7 @@ async def _skill_manage_impl(
|
|||||||
name,
|
name,
|
||||||
_history_record(action="patch", file_path=SKILL_MD_FILE, prev_content=prev_content, new_content=new_content, thread_id=thread_id, scanner=scan),
|
_history_record(action="patch", file_path=SKILL_MD_FILE, prev_content=prev_content, new_content=new_content, thread_id=thread_id, scanner=scan),
|
||||||
)
|
)
|
||||||
|
await _bump_patch_best_effort(name)
|
||||||
await refresh_skills_system_prompt_cache_async()
|
await refresh_skills_system_prompt_cache_async()
|
||||||
return f"Patched custom skill '{name}' ({replacement_count} replacement(s) applied, {occurrences} match(es) found)."
|
return f"Patched custom skill '{name}' ({replacement_count} replacement(s) applied, {occurrences} match(es) found)."
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user