AstroResearch/docs/architecture/agent/skills.md
Asfmq 698d007f39 feat: Agent 安全纵深防御、Checkpoint 快照、会话 Rewind/Branch、自进化
Skill、流式执行优化与系统架构全面升级

  本次提交对标 Claude Code 与 Hermes-Agent 的工程细节,在安全、可靠性、
  会话管理、自我进化四个维度进行了系统性加固,变更总量 48 文件 / +12680 -2292 行。

  ═══════ 安全纵深防御 ═══════

  1. Hardline 硬阻止层 (src/agent/runtime/hardline.rs, +534 行)
     - 不可绕过的危险命令拦截(关重启、磁盘擦除、Fork 炸弹、rm -rf /、kill -1)
     - 反规避标准化管线: ANSI 序列剥离 → Unicode NFKC → shell 反斜杠还原 → 空字面量清理
     - 在 PermissionChecker 之前执行,YOLO/Bypass 模式下同样生效
     - 集成到 executor Phase 2,被拒绝工具直接注入错误结果

  2. Permission 优先级裁决器 (src/agent/runtime/permission.rs, +200 行)
     - 7 层正式优先级规则 (P0 Deny → P7 Allow),带冲突日志
     - explain() 方法支持审计追溯
     - Hook PermissionRequired 与 Checker 结果的正确叠加逻辑

  ═══════ Checkpoint 文件快照系统 ═══════

  3. git2 原生快照 (src/agent/runtime/checkpoint.rs, +920 行)
     - 基于 git2 bare repo,内容寻址自动去重
     - 文件变更操作前自动触发 (file_write/file_edit/run_bash)
     - 每目录每 turn 最多一次快照,防止同一轮重复
     - 支持 list/diff/restore API + pre-rollback 安全快照
     - 旧快照自动 prune(保留最近 N 个)+ 按目录隔离 ref
     - 排除规则自动过滤 node_modules/target/.git/*.pdf 等
     - 集成到 executor: 文件操作前 ckpt.ensure_checkpoint()

  ═══════ 错误恢复系统大升级 ═══════

  4. 21 种 FailoverReason 分类 (src/agent/runtime/error_recovery.rs, +1200 行)
     - 参考 Hermes-Agent error_classifier.py
     - 8 步分类管线: provider-specific → HTTP status → text pattern → error body → fallback
     - is_retryable / should_compress / should_failover / is_permanent 方法
     - Context Overflow 自动修复: 从错误消息提取 token 限制,自动下调预算
     - RecoveryStep::AdjustMaxTokens 实现 (参考 Claude Code 自动修复)
     - 向后兼容 ErrorKind 别名

  ═══════ 会话 Rewind / Branch / Retry 体系 ═══════

  5. 完整 undo 栈 (src/agent/runtime/session.rs, +800 行 + 2 迁移脚本)
     - Rewind (软删除): active=0 标记,审计 trail 保留,LLM 不可见
     - Restore (撤销回退): 冲突检测——回退后有新消息则拒绝,引导使用 Branch
     - Branch: 分叉会话,复制所有 active=1 消息到新会话
     - Retry: 硬删除最后一轮对话,返回原消息文本供前端重提交
     - 数据库: agent_messages.active 列 + agent_sessions.rewind_count + parent_session_id
     - API: 4 个新端点 (/branch, /retry, /rewind, /rewind/restore)
     - load_history_for_agent 全面使用 active=1 过滤

  ═══════ Hooks 系统模块化重构 ═══════

  6. 单文件 → 7 模块体系 (src/agent/hooks/)
     hooks.rs (994 行) 拆分为:
     - mod.rs    — 入口 + HookRegistry + SessionHookManager
     - types.rs  — 类型定义 (Context, TaggedContext, PermissionRequestAction 等)
     - traits.rs — AgentHook + AsyncAgentHook + 15 种生命周期事件
     - matcher.rs — 工具名/参数匹配 + session 作用域过滤
     - dispatch.rs — 并行调度引擎 (run_pre/post_tool_use 等)
     - registry.rs — 注册/注销/查询
     - builtins.rs — CancellationHook + MetricsHook + AuditLogHook + ContextDeduplicator

     关键改进:
     - run_pre_tool_use 并行执行所有匹配 hooks,聚合 Block/MutateInput/Continue
     - TaggedContext 带完整来源标记的上下文注入 (hook_name + event)
     - ContextDeduplicator 单 dispatch cycle 内内容哈希去重
     - AsyncAgentHook 支持 fire-and-forget 异步 hooks

  ═══════ Executor 并发执行升级 ═══════

  7. 三阶段管道重写 (src/agent/runtime/executor.rs, +600 行)
     - Phase 1: 死循环检测 + 参数解析 (不变)
     - Phase 2: Hardline 预检查 (新增) → PermissionChecker (改进)
     - Phase 3: ToolPartitioner 分区 → 逐批次执行 (重写)
       - 并行批次内 FuturesUnordered 并发
       - 串行批次确保非并发安全工具独占执行
       - Checkpoint 预触发集成
     - Hook 上下文注入: system-reminder 格式 + ContextDeduplicator 去重
     - Hook 阻塞错误详细记录

  ═══════ 流式执行真正的流式调度 ═══════

  8. StreamingExecutor 重写 (src/agent/runtime/streaming_executor.rs, ~400 行变更)
     - on_tool_use 中对并发安全工具立即 tokio::spawn,不等待 flush
     - executing_non_concurrent 标志阻塞后继工具直到独占工具完成
     - JoinHandle 管理替代自定义 cancel channel
     - completed_queue 按流顺序 yield
     - Sibling Abort 通过 broadcast channel + tokio::select! 竞速
     - ToolContext 实现 Clone (支持 per-task 上下文复制)

  ═══════ 自改进 Skill 系统 ═══════

  9. PatternDetector + SkillCreator + Curator (src/agent/skills/, +1500 行)
     - PatternDetector: 扫描 agent_messages 表,检测跨 session 重复工具调用模式
     - SkillCreator: 将高置信度模式自动生成 SKILL.md (YAML frontmatter + 工作流步骤)
     - SelfImprovePipeline: 一站式 模式检测 → 创建 → 质量审查
     - Curator: 分析 skill 使用统计,标记 stale/deprecated,建议清理
     - Skill frontmatter 新增 pinned 字段 (禁止 Curator 自动清理)

  ═══════ 基础设施优化 ═══════

  10. 系统提示词缓存 (src/agent/runtime/system_prompt.rs + mod.rs)
      - SystemPromptCache: 首次计算后永久复用,/clear 时失效
      - 新增 SAFETY / SYSTEM_CONTEXT / TOOL_USAGE 静态 section
      - 环境/tools/skills/memory 动态 section 通过 get_or_compute 缓存

  11. ToolRegistry schema 缓存 (src/agent/tools/mod.rs)
      - schema_cache + schema_generation 版本号
      - 工具变更/过滤器变更时自动失效
      - precompute_definitions() 预计算 (AgentRuntime 初始化时调用)

  12. 迭代摘要融合 (src/agent/compact.rs, +100 行)
      - 参考 Hermes context_compressor.py
      - CollapseLog 追踪压缩历史,支持溢出合并
      - extract_prior_summary: 提取已有摘要融入新压缩

  13. SubAgent 系统提示词模块化 (src/agent/tools/subagent.rs)
      - 复用 5 个标准 section + 子代理专有上下文 section
      - 独立 ToolRegistry 构建工具列表

  ═══════ 前端 — CSS 变量主题系统 ═══════

  14. 全新主题变量体系 (dashboard/src/index.css + App.tsx + 各面板)
      - CSS 自定义属性: --bg-card, --text-main, --text-muted, --border-precision
      - 语义化颜色: --accent-blueprint, --accent-star
      - 全面替换硬编码 Tailwind 颜色 (slate-xxx → var(--xxx))
      - 文献入库提示优化 ("核心知识节点" 替代 "向量块")
      - ReaderPanel 样式变量化
2026-06-22 20:29:37 +08:00

647 lines
25 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Skills 系统 (`skills.rs` + `tools/skill.rs`)
参考 Claude Code 的两层 Skill 加载架构 — **Layer 1** 在每轮系统提示词中列出可用 skill 名称(~20 tokens/skill**Layer 2** 在 LLM 调用 `load_skill` 工具时注入完整内容(~2000 tokens/skill
## 整体数据流
```mermaid
sequenceDiagram
participant FS as skills/*/SKILL.md
participant SR as SkillRegistry<br/>(Arc&lt;RwLock&lt;&gt;&gt;)
participant SP as SystemPrompt
participant LLM as LLM
participant Tool as LoadSkillTool
participant Sub as SubAgentRunner
Note over SR: 启动阶段 (main.rs)
SR->>FS: discover_skill_dirs() 扫描目录
FS-->>SR: parse_skill_file() 解析 YAML + Markdown
SR->>SR: refresh() 完整重载,按 usage_score 排序
SR->>SR: start_watcher() 启动 notify 文件监听
Note over SR: Layer 1 — System Reminder (每轮请求)
SP->>SR: build_reminder()
SR-->>SP: &lt;system-reminder&gt; XML 块
SP->>LLM: 注入 system prompt "skills" section
Note over SR: Layer 2 — 按需加载
LLM->>Tool: load_skill(skill_name)
Tool->>SR: get_skill(name)
SR-->>Tool: Skill { meta + body }
alt context = "fork"
Tool->>Sub: 启动子代理 (max_steps ≤ 10)
Sub-->>Tool: 子代理执行结果
else context = "inline" (默认)
Tool->>Tool: substitute_variables(body)
end
Tool-->>LLM: 格式化技能内容
Tool->>SR: record_usage(name)
```
## 代码结构
| 文件 | 行数 | 职责 |
|---|---|---|
| `src/agent/skills.rs` | ~1100 | SkillRegistry 缓存、文件解析、热更新、条件激活、系统提示构建、SkillCreator / SelfImprovePipeline |
| `src/agent/skills/pattern_detector.rs` | ~420 | PatternDetector — 从 agent_messages 扫描工具调用序列、子序列匹配、Jaccard 去重 |
| `src/agent/skills/curator.rs` | ~700 | Curator — Skill 生命周期管理 + CuratorRunner — 后台空闲触发审查 |
| `src/agent/tools/skill.rs` | 222 | LoadSkillTool — Layer 2 按需加载的 AgentTool 实现 |
| `src/agent/runtime/mod.rs` | ~1182 | 将 `build_reminder()` 注入 SystemPrompt section 3 |
| `src/agent/runtime/system_prompt.rs` | ~64 | 静态 system prompt 中引导 LLM 使用 load_skill |
| `src/main.rs` | ~151-163 | 启动时初始化 SkillRegistry、refresh、启动文件监听器 |
| `skills/{name}/SKILL.md` | — | 实际 skill 定义文件(当前 3 个) |
## SKILL.md 格式
每个 skill 为 `skills/{name}/SKILL.md`,包含 YAML frontmatter + Markdown 正文:
```markdown
---
name: methodology
description: 系统性文献综述方法论——如何高效地完成学术文献调研
context: inline
allowed-tools:
- read_file
- search_papers
model: sonnet
argument-hint: "<research question>"
when_to_use: 当用户请求文献综述或调研时
disable-model-invocation: false
user-invocable: true
version: "1.0"
paths:
- "*.rs"
- "*.toml"
agent: code-reviewer
effort: high
---
# Skill 正文 (Markdown)
详细指引...
```
### Frontmatter 字段全量说明
| 字段 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| `name` | `string` | 目录名 | Skill 唯一标识 |
| `description` | `string` | "(无描述)" | 一句话描述,出现在 Layer 1 列表;**缺少时产生 warning** |
| `context` | `"inline"` / `"fork"` | `inline` | 执行模式;**非法值时产生 warning** |
| `allowed-tools` | `string[]` | `[]`(无限制) | 工具白名单fork 模式下建议仅使用这些工具 |
| `model` | `"haiku"` / `"sonnet"` / `"opus"` / `"inherit"` | — | 推荐的执行模型 |
| `argument-hint` | `string` | — | 参数提示(如 `<research question>`),已定义但 LoadSkillTool 尚未使用 |
| `when_to_use` | `string` | — | 触发场景描述,**Layer 1 reminder 中直接拼接到 description 后** |
| `disable-model-invocation` | `bool` | `false` | `true` 时 LLM 不能通过 load_skill 工具自动调用;同时控制条件激活的初始状态 |
| `user-invocable` | `bool` | `true` | `false` 时不出现在 Layer 1 reminder 中,用户无法手动调用 |
| `version` | `string` | — | 版本号 |
| `paths` | `string[]` | `[]`(始终激活) | 条件激活的 glob 模式,非空时 skill 仅在匹配文件路径后激活 |
| `agent` | `string` | — | fork 模式下游的 agent 类型(如 `code-reviewer`),已定义但 LoadSkillTool 尚未使用 |
| `effort` | `string` | — | fork 模式下的 effort 级别,已定义但 LoadSkillTool 尚未使用 |
| `pinned` | `bool` | `false` | `true` 时 Curator 强制保持 Active 生命周期,最低质量评分 0.8,不被自动清理 |
### 当前项目 Skill 清单
| Skill | Context | 状态 | 说明 |
|---|---|---|---|
| `methodology` | inline | ✅ 完整 | 系统性文献综述 6 步流程:范围界定 → 按引用筛选 → 逐篇深读 → 补充检索 → 交叉验证 → 输出综述 |
| `plotting` | fork | 🚧 占位 | 科研绘图规范matplotlib/seaborn/plotly白名单 bash+save_note |
| `presentation` | fork | 🚧 占位 | 学术 PPT 生成Python-pptx/Beamer/Marp白名单 bash+save_note |
## SkillRegistry 核心实现 (`skills.rs`)
### 数据结构
```
SkillFrontmatter — serde_yaml 解析的 YAML frontmatter含 validate() 校验方法
├──▶ SkillMeta — Layer 1 摘要name, description, context, allowed_tools,
│ when_to_use, disable_model_invocation, user_invocable, paths,
│ pinned: bool
└──▶ Skill — Layer 2 完整对象meta + body + skill_dir
└──▶ SkillRegistry — 缓存容器 + 生命周期管理
├── skills: Vec<Skill>
├── last_scan_mtime: Option<SystemTime>
└── usage_stats: HashMap<String, SkillUsageStat>
SelfImprovePipeline — 一站式管道
├── PatternDetector — 扫描 agent_messages 检测重复工具调用序列
├── SkillCreator — 将 DetectedPattern 转换为 SKILL.md 文件
└── Curator — Skill 生命周期管理Active→Inactive→Stale→Deprecated
└── CuratorRunner — 后台空闲触发审查 + 心跳记录
```
### 关键方法
| 方法 | 返回值 | 说明 |
|---|---|---|
| `new(skills_dir)` | `Self` | 创建空注册表,调用 `refresh()` 触发初始扫描 |
| `refresh()` | `()` | **完整重载**(非增量):扫描目录 → 解析 → 按 `usage_score` 降序排序 |
| `needs_refresh()` | `bool` | 检查目录 mtime 是否变化(首次或 mtime > last_scan_mtime
| `build_reminder()` | `Option<String>` | 构建 Layer 1 XML `<system-reminder>` 块,仅包含 `user_invocable=true && disable_model_invocation=false` 的 skill |
| `build_tool_description()` | `String` | 动态生成 load_skill 工具的 description列出所有 `disable_model_invocation=false` 的 skill |
| `get_skill(name)` | `Option<&Skill>` | O(n) 按名称查找完整 Skill |
| `list_skills()` | `Vec<SkillMeta>` | 获取所有 skill 的元信息列表 |
| `record_usage(name)` | `()` | 记录调用:`invoke_count += 1``last_used_at = now` |
| `usage_score(name)` | `f64` | **指数衰减评分**`ln(1 + count) × 0.5^(age_hours / 168)`7 天半衰期) |
| `start_watcher(arc)` | `JoinHandle<()>` | 启动文件监听线程(见 4.6.5 |
| `activate_conditional_for_paths(paths)` | `Vec<String>` | 激活匹配指定文件路径的条件 skill见 4.6.6 |
| `matching_skills_for_paths(paths)` | `Vec<SkillMeta>` | 查询匹配指定文件路径的所有 skill只读不改变状态 |
### usage_score 算法
```
usage_score(name) = ln(1 + invoke_count) × 0.5^(age_hours / 168)
其中:
invoke_count — 从 record_usage() 累积
age_hours — 自 last_used_at 起的小时数(无记录时为 0.1
168 — 7 天(半衰期),即每过 7 天权重衰减 50%
```
这是一个**指数衰减 + 对数压缩**的评分:频率越高、越近使用,评分越高。`refresh()` 按此评分**降序**排列 skills使热技能优先出现在 remind 列表中。
## LoadSkillTool (`tools/skill.rs`)
实现 `AgentTool` traittool name = `"load_skill"`,参数:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `skill_name` | `string` | ✅ | 要加载的技能名称 |
| `max_steps` | `integer` | 否 | fork 模式下子代理最大步数,默认 5上限 10 |
### 执行流程
```
execute(args, ctx)
├─ 1. 从 SkillRegistry 缓存读取 SkillRwLock::read
│ ├─ 命中 → 2
│ └─ 未命中 → 返回错误(含可用 skill 列表提示)
├─ 2. record_usage() 更新调用统计RwLock::write
├─ 3. substitute_variables(body, skill_dir, session_id)
│ 替换 ${SKILL_DIR} → skill 目录绝对路径
│ 替换 ${SESSION_ID} → 当前会话 ID无则清空
├─ 4. 判断 context 模式
│ ┌─ context = "fork" ──────────────────────────────────────
│ │ • 构建子代理 system_prompt含 skill 正文 + base_dir
│ │ • 调用 SubAgentRunner::run(system_prompt, task, max_steps)
│ │ • 返回格式:[子代理执行结果 - 技能: xxx]
│ │ • metadata: { context: "fork", execution_mode: "subagent", ... }
│ │
│ └─ context = "inline" (默认) ─────────────────────────────
│ • 格式化输出:# 技能xxx (描述)\n\nBase directory: ...\n\nbody
│ • 附加 allowed_tools 白名单提示
│ • metadata: { context: "inline", execution_mode: "inline", ... }
└─ 5. 返回 ToolOutput
```
### 变量替换 (`substitute_variables`)
| 变量 | 替换目标 | 无值行为 |
|---|---|---|
| `${SKILL_DIR}` | skill 所在目录的绝对路径(如 `/app/skills/methodology` | 保持原样(`to_str()` 返回 None 时) |
| `${SESSION_ID}` | 当前 Agent 会话 ID | 清空为空字符串 |
> **已知问题**:当前 session_id 获取逻辑通过检查 `config.database_url.contains("session")` 来决定是否为 "current",这是一个脆弱的 hack应改为从 `ToolContext` 直接读取 `ctx.session_id`。
## 热重载 (Hot Reload)
`SkillRegistry::start_watcher()` 使用 **`notify` crate** 实现事件驱动的文件监听:
```
┌──────────────────────────────────────────────────────────┐
│ notify 文件监听线程 │
│ │
│ watcher.watch(skills_dir, RecursiveMode::Recursive) │
│ │ │
│ ▼ │
│ 仅过滤 SKILL.md 文件变更事件 │
│ │ │
│ ▼ 发送 () 到 mpsc channel │
│ ┌─────────────────┐ │
│ │ 300ms debounce │ ← rx.recv_timeout(300ms) │
│ │ 合并连续变更 │ 第一个事件后等待 300ms │
│ └────────┬────────┘ 期间有新事件则重置计时器 │
│ │ │
│ ▼ │
│ registry.write().refresh() 完整重载 │
└──────────────────────────────────────────────────────────┘
```
| 属性 | 说明 |
|---|---|
| 实现方式 | `notify::recommended_watcher` 事件驱动(非定时轮询) |
| 监听范围 | `RecursiveMode::Recursive`(递归监听子目录) |
| 过滤条件 | 仅处理文件名 == `SKILL.md` 的事件 |
| 防抖窗口 | 300ms — 快速连续的变更合并为一次刷新 |
| 刷新策略 | **完整重载**(始终重新扫描整个目录),非增量 |
| 测试模式 | `#[cfg(test)]` 下为空实现(不启动线程) |
## 条件 Skill 激活 (Paths-based Activation)
部分 skill 通过 `paths` frontmatter 声明 glob 模式,初始状态 `disable_model_invocation = true`,仅在 Agent 访问匹配文件时激活。
**激活流程(`activate_conditional_for_paths`**
```
当 Agent 通过 Read/Grep/Glob 访问文件时:
for each skill where paths is not empty AND disable_model_invocation == true:
if any(file_path matches any(pattern in skill.paths)):
skill.disable_model_invocation = false // 激活
log: "条件 skill '{name}' 已激活"
return newly_activated_skill_names
```
**匹配查询(`matching_skills_for_paths`** 只读方法,返回匹配的 skill 列表而不改变激活状态,可用于向 LLM 提示当前上下文相关的 skill。
**Glob 匹配(`glob_match_simple`**
| 通配符 | 匹配 | 示例 |
|---|---|---|
| `*` | 任意非 `/` 字符序列 | `"*.rs"``main.rs` |
| `**` | 任意字符(含 `/` | `"**/test/*"``src/test/foo` |
| `?` | 单个非 `/` 字符 | `"file_?.rs"``file_a.rs` |
使用 `glob::Pattern` crate降级方案为简单字符串包含匹配。
## 系统集成
### 启动初始化 (`main.rs:151-163`)
```rust
// 1. 创建注册表
let skill_registry = Arc::new(RwLock::new(SkillRegistry::new(config.skills_dir.clone())));
// 2. 初始刷新
if let Ok(mut reg) = skill_registry.write() {
reg.refresh();
}
// 3. 启动文件监听(热更新)
let _watcher_handle = SkillRegistry::start_watcher(skill_registry.clone());
// 4. 注入 AppState
let app_state = Arc::new(AppState {
skill_registry, // Arc<RwLock<SkillRegistry>>
// ... 其他字段
});
```
### System Prompt 注入 (`runtime/mod.rs:1178-1187`)
每轮 LLM 请求构建 system prompt 时,从 SkillRegistry 读取 reminder 并注入为 "skills" section
```rust
if let Some(skills) = self.app_state.skill_registry
.read().ok()
.and_then(|r| r.build_reminder())
{
sp.add_section("skills", skills);
}
```
### System Prompt 静态指引 (`system_prompt.rs:64`)
```
7. 对于复杂任务(如文献综述),调用 load_skill 获取方法论指引,再用 todo_write 制定计划。
```
### 工具注册 (`tools/mod.rs:254`)
LoadSkillTool 在所有工具注册表中作为第 18 个工具注册(紧跟 CompressTool 之后):
```rust
Box::new(LoadSkillTool::new(skill_registry)),
```
### 共享范围
所有 Agent 组件共享同一个 `Arc<RwLock<SkillRegistry>>` 实例:
- 主 Agent`AgentRuntime`
- 子代理(`SubAgentRunner`
- 团队成员(`teammate.rs`
- 后台任务 Agent`background.rs`
## Self-improving Skills — 自我进化管道
参考 Hermes-Agent 的 Self-improving Skills 模式AstroResearch 实现了从**模式检测 → 自动创建 → 生命周期管理**的完整自我进化管道。
### 架构总览
```mermaid
graph TB
subgraph Pipeline["SelfImprovePipeline::run()"]
direction TB
PD["PatternDetector::scan()<br/>扫描 agent_messages 表"]
SC["SkillCreator::create_from_patterns()<br/>生成 SKILL.md 文件"]
CR["Curator::analyze()<br/>质量评分 + 生命周期评估"]
end
subgraph Background["后台定期维护"]
direction LR
Runner["CuratorRunner::spawn()<br/>空闲触发 + 间隔检查"]
Archive["archive_stale_skills()<br/>30d stale / 90d deprecated"]
end
PD -->|"Vec&lt;DetectedPattern&gt;"| SC
SC -->|"Vec&lt;Skill&gt;"| CR
CR -->|"CuratorReport"| Runner
```
### PatternDetector — 模式检测器
`agent_messages` 表中自动发现跨 session 重复的工具调用序列:
```
检测算法:
1. 查询每个 session 的 tool 消息(按时间排序)
2. 滑动窗口 (2-8 长度) 提取所有子序列
3. 跨 session 频率计数≥3 次为候选)
4. Jaccard 相似度去重 + 超序列包含检测
5. 计算 confidence = frequency_score × similarity_penalty
```
**数据结构**
```rust
pub struct DetectedPattern {
pub tool_sequence: Vec<String>, // 如 ["search_papers", "download_paper", "rag_search"]
pub session_ids: Vec<String>, // 出现的 session
pub frequency: usize, // 跨 session 出现次数
pub confidence: f64, // 0.0-1.0 置信度
pub fingerprint: String, // 去重指纹
}
```
**置信度计算**
```
confidence = ln(frequency) / ln(3) × (1 - max_jaccard_similarity_with_other_patterns)
```
即:频率越高越好,与已有模式越不相似越好。
### SkillCreator — 自动 Skill 生成
`DetectedPattern` 转换为完整的 `SKILL.md` 文件:
```rust
pub struct SkillCreator {
skills_dir: PathBuf,
}
impl SkillCreator {
/// 检测到的模式 → 写入 skills/{kebab-case-name}/SKILL.md
pub fn create_from_patterns(
&self,
patterns: &[DetectedPattern],
dry_run: bool, // dry_run=true 时只预览不移交
) -> Result<Vec<Skill>, Error>;
/// 工具序列名 → kebab-case skill 名称
fn pattern_to_skill_name(tools: &[String]) -> String;
// 例: ["search_papers", "download_paper", "rag_search"] → "search-download-rag"
/// 生成 SKILL.md 正文(含 YAML frontmatter + step-by-step 指引)
fn generate_skill_md(pattern: &DetectedPattern) -> String;
}
```
生成的 SKILL.md 自动包含:
- `pinned: false`(初始不固定)
- `when_to_use` 自动从工具名推断
- 每个工具调用作为 `<step>` 写入正文
- `version: "0.1.0"`(自动生成版本)
### Curator — Skill 生命周期管理
基于 Hermes-Agent `curator.py` 的设计,实现确定性的时间戳驱动生命周期:
```mermaid
stateDiagram-v2
[*] --> Active: 创建 / 使用
Active --> Active: seed_record (0d) / 有调用记录
Active --> Inactive: 7d 无使用
Inactive --> Active: 再次使用
Inactive --> Stale: 30d 无使用
Stale --> Deprecated: 90d 无使用
Deprecated --> [*]: 手动删除
state Active {
[*] --> Pinned: pinned=true
Pinned --> Pinned: 强制保持 (min_score=0.8)
}
```
**生命周期状态**
| 状态 | 条件 | 行为 |
|------|------|------|
| `Active` | 最近使用 ≤ 7 天 | 正常在 remind 列表中出现 |
| `Inactive` | 7-30 天未使用 | 不出现在 remind 列表,可被重新激活 |
| `Stale` | 30-90 天未使用 | 标记为 stale出现在清理候选列表 |
| `Deprecated` | > 90 天未使用 | 建议归档或删除 |
**质量评分**
```
quality_score = ln(1 + invoke_count) × 0.5^(days_since_last_use / 7)
```
**Pinned 保护**`pinned=true` 的 skill 强制 `Active` 状态,最低评分 0.8,不会出现在清理候选列表中。
### Seed Record — 新 Skill 锚定时钟
新创建或自动生成的 skill 可能没有使用统计,`seed_record` 机制防止它们被立即标记为 stale
```rust
fn evaluate_quality(&self, skill: &Skill, stats: Option<&SkillUsageStat>) -> SkillQuality {
let (invoke_count, days_since_last_use) = match stats {
Some(s) => (s.invoke_count, s.days_since_last_use()),
None => (
0,
// seed_record: 无统计 → days_since_last_use = 0视为刚创建
Some(0),
),
};
// 如果 days_since_last_use <= 7 (NEW_SKILL_GRACE_PERIOD_DAYS) → Active
// ...
}
```
关键常量:
- `NEW_SKILL_GRACE_PERIOD_DAYS = 7`:新 skill 在 7 天内即使零调用也保持 Active
- `STALE_THRESHOLD_DAYS = 30`30 天未用标记为 stale
- `DEPRECATED_THRESHOLD_DAYS = 90`90 天未用标记为 deprecated
### CuratorRunner — 后台空闲触发审查
参考 Hermes-Agent 的 inactivity-triggered curator
```mermaid
sequenceDiagram
participant User as 用户交互
participant App as AgentRuntime
participant CR as CuratorRunner
participant Curator as Curator
Note over CR: 后台 tokio task
loop 每 check_interval
CR->>CR: should_run_now()
alt 未暂停 AND 空闲 > min_idle AND 距上次 > interval
CR->>Curator: run_once()
Curator->>Curator: evaluate_quality() / archive_stale_skills()
Curator-->>CR: CuratorReport
else 不满足条件
CR->>CR: skip
end
end
User->>App: 发送查询
App->>CR: record_activity() 更新心跳
```
**CuratorRunner API**
```rust
pub struct CuratorRunner {
curator: Curator,
db: SqlitePool,
interval: Duration, // 最小审查间隔(默认 7 天)
min_idle: Duration, // 最小空闲时间(默认 2 小时)
check_interval: Duration, // 检查间隔(默认 1 小时)
paused: AtomicBool,
last_activity: RwLock<Instant>,
last_run: RwLock<Option<Instant>>,
}
impl CuratorRunner {
pub fn new(curator: Curator, db: SqlitePool) -> Self;
pub fn with_interval(mut self, interval: Duration) -> Self;
pub fn with_min_idle(mut self, min_idle: Duration) -> Self;
/// 判断是否应运行:未暂停 + 空闲超时 + 距上次超间隔
pub fn should_run_now(&self) -> bool;
/// 记录用户活动心跳
pub async fn record_activity(&self);
/// 执行一次审查(仅在 should_run_now 时)
pub async fn run_once(
&self,
usage_stats: &HashMap<String, SkillUsageStat>,
skill_metas: &[SkillMeta],
) -> Option<CuratorReport>;
/// 启动后台任务
pub fn spawn(
self: Arc<Self>,
usage_stats: Arc<RwLock<HashMap<String, SkillUsageStat>>>,
skill_metas: Arc<RwLock<Vec<SkillMeta>>>,
check_interval: Duration,
) -> JoinHandle<()>;
pub fn pause(&self);
pub fn resume(&self);
}
```
**使用示例**
```rust
let curator = Curator::new(skills_dir.clone());
let runner = Arc::new(
CuratorRunner::new(curator, db_pool.clone())
.with_interval(Duration::from_secs(7 * 24 * 3600)) // 最少间隔 7 天
.with_min_idle(Duration::from_secs(2 * 3600)), // 空闲 2 小时后
);
// 每次用户交互时更新心跳
runner.record_activity().await;
// 启动后台任务
let _handle = runner.spawn(usage_stats, skill_metas, Duration::from_secs(3600));
```
### SelfImprovePipeline — 一站式管道
```rust
pub struct SelfImprovePipeline {
detector: PatternDetector,
creator: SkillCreator,
curator: Curator,
}
impl SelfImprovePipeline {
pub fn new(db: SqlitePool, skills_dir: PathBuf) -> Self;
/// 完整管道:检测 → 创建 → 审查
pub async fn run(&self, dry_run: bool) -> Result<SelfImproveResult>;
/// 仅检测模式(不创建)
pub async fn detect_only(&self) -> Result<Vec<DetectedPattern>>;
/// 仅分析已有 skills不检测新模式
pub async fn analyze_only(
&self,
usage_stats: &HashMap<String, SkillUsageStat>,
skill_metas: &[SkillMeta],
) -> Result<CuratorReport>;
}
pub struct SelfImproveResult {
pub patterns_found: usize,
pub skills_created: usize,
pub skills_created_names: Vec<String>,
pub curator_report: CuratorReport,
}
```
**管道流程**
```
SelfImprovePipeline::run(dry_run=true)
├─ 1. PatternDetector::scan()
│ └─ 从 agent_messages 中检测 ≥3 次跨 session 重复序列
│ └─ 结果: Vec<DetectedPattern>(按 confidence 降序)
├─ 2. SkillCreator::create_from_patterns(patterns, dry_run)
│ ├─ dry_run=true → 只记录日志,不写入文件
│ └─ dry_run=false → 写入 skills/ 目录 + 触发 SkillRegistry::refresh()
└─ 3. Curator::analyze(usage_stats, skill_metas)
└─ 质量评分 + 生命周期状态 + 清理建议
```
---
## 与 Claude Code 参考设计的对应关系
| Claude Code / Hermes 概念 | AstroResearch 实现 |
|---|---|---|
| `src/skills/` 目录 + `SKILL.md` | 完全相同 |
| YAML frontmattername, description, context, allowed-tools... | 相同,增加 `version`、`agent`、`effort`、`paths`、`pinned` 字段 |
| `<system-reminder>` Layer 1 注入 | `build_reminder()` → 结构化 XML 块 |
| `SkillTool` Layer 2 按需加载 | `LoadSkillTool`AgentTool trait 实现) |
| inline 模式(注入指令内容) | ✅ 实现 |
| fork 模式(子代理隔离执行) | ✅ 实现SubAgentRunner |
| 热重载(目录监控) | ✅ `notify` crate + 300ms debounce |
| 调用统计 | ✅ 指数衰减评分 |
| 条件 skillpaths glob | ✅ `activate_conditional_for_paths()` |
| 变量替换 | ✅ `${SKILL_DIR}`, `${SESSION_ID}` |
| `Skill` 工具接口 + `skill` slash command | LoadSkillTooltool 形式),前端的 `/skill-name` 通过 tool 调用实现 |
| Hermes: Self-improving Skills模式检测 + 自动创建) | ✅ `PatternDetector` + `SkillCreator` + `SelfImprovePipeline` |
| Hermes: Curator 生命周期管理 | ✅ `Curator`Active/Inactive/Stale/Deprecated |
| Hermes: Pinned Skills不可清理 | ✅ `pinned: true` frontmatter + Curator 保护 |
| Hermes: Seed Record新 skill 锚定时钟) | ✅ `days_since_last_use=Some(0)` + 7 天 grace period |
| Hermes: Inactivity-triggered Runner | ✅ `CuratorRunner`(后台 tokio task + 心跳记录) |
---