From 698d007f3953220845dc44ac2cbe82a59af02bfc Mon Sep 17 00:00:00 2001
From: Asfmq <2696428814@qq.com>
Date: Mon, 22 Jun 2026 20:29:37 +0800
Subject: [PATCH] =?UTF-8?q?feat:=20Agent=20=E5=AE=89=E5=85=A8=E7=BA=B5?=
=?UTF-8?q?=E6=B7=B1=E9=98=B2=E5=BE=A1=E3=80=81Checkpoint=20=E5=BF=AB?=
=?UTF-8?q?=E7=85=A7=E3=80=81=E4=BC=9A=E8=AF=9D=20Rewind/Branch=E3=80=81?=
=?UTF-8?q?=E8=87=AA=E8=BF=9B=E5=8C=96=20=20=20Skill=E3=80=81=E6=B5=81?=
=?UTF-8?q?=E5=BC=8F=E6=89=A7=E8=A1=8C=E4=BC=98=E5=8C=96=E4=B8=8E=E7=B3=BB?=
=?UTF-8?q?=E7=BB=9F=E6=9E=B6=E6=9E=84=E5=85=A8=E9=9D=A2=E5=8D=87=E7=BA=A7?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
本次提交对标 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 样式变量化
---
Cargo.lock | 75 +
Cargo.toml | 2 +
dashboard/src/App.tsx | 114 +-
.../src/features/agent/ResearchAgentPanel.tsx | 234 ++-
dashboard/src/features/reader/ReaderPanel.tsx | 48 +-
dashboard/src/index.css | 63 +-
dashboard/src/types.ts | 32 +
.../agent/claude-code-reference-analysis.md | 909 +++++++++++
docs/architecture/agent/hooks.md | 997 +++++++++---
docs/architecture/agent/permission.md | 278 +---
docs/architecture/agent/skills.md | 317 +++-
docs/architecture/agent/system-prompt.md | 463 +++---
docs/architecture/agent/tools.md | 68 +-
migrations/20260622000000_session_rewind.sql | 20 +
migrations/20260622000001_session_branch.sql | 13 +
src/agent/compact.rs | 198 ++-
src/agent/hooks.rs | 994 ------------
src/agent/hooks/builtins.rs | 265 ++++
src/agent/hooks/dispatch.rs | 790 ++++++++++
src/agent/hooks/matcher.rs | 302 ++++
src/agent/hooks/mod.rs | 549 +++++++
src/agent/hooks/registry.rs | 280 ++++
src/agent/hooks/traits.rs | 139 ++
src/agent/hooks/types.rs | 464 ++++++
src/agent/memory/mod.rs | 17 +-
src/agent/runtime/checkpoint.rs | 920 ++++++++++++
src/agent/runtime/error_recovery.rs | 1337 +++++++++++++++--
src/agent/runtime/executor.rs | 593 ++++++--
src/agent/runtime/finalize.rs | 2 +-
src/agent/runtime/hardline.rs | 534 +++++++
src/agent/runtime/mod.rs | 232 ++-
src/agent/runtime/partitioner.rs | 49 +
src/agent/runtime/permission.rs | 217 +++
src/agent/runtime/session.rs | 853 ++++++++++-
src/agent/runtime/streaming_executor.rs | 502 +++++--
src/agent/runtime/system_prompt.rs | 231 ++-
src/agent/runtime/untrusted.rs | 159 ++
src/agent/skills.rs | 274 ++++
src/agent/skills/curator.rs | 747 +++++++++
src/agent/skills/pattern_detector.rs | 448 ++++++
src/agent/subagent.rs | 24 +-
src/agent/tools/mod.rs | 70 +
src/agent/tools/subagent.rs | 55 +-
src/agent/trajectory.rs | 2 +-
src/api/agent.rs | 127 +-
src/api/mod.rs | 8 +-
src/clients/llm.rs | 2 +-
src/main.rs | 7 +
48 files changed, 12706 insertions(+), 2318 deletions(-)
create mode 100644 docs/architecture/agent/claude-code-reference-analysis.md
create mode 100644 migrations/20260622000000_session_rewind.sql
create mode 100644 migrations/20260622000001_session_branch.sql
delete mode 100644 src/agent/hooks.rs
create mode 100644 src/agent/hooks/builtins.rs
create mode 100644 src/agent/hooks/dispatch.rs
create mode 100644 src/agent/hooks/matcher.rs
create mode 100644 src/agent/hooks/mod.rs
create mode 100644 src/agent/hooks/registry.rs
create mode 100644 src/agent/hooks/traits.rs
create mode 100644 src/agent/hooks/types.rs
create mode 100644 src/agent/runtime/checkpoint.rs
create mode 100644 src/agent/runtime/hardline.rs
create mode 100644 src/agent/runtime/untrusted.rs
create mode 100644 src/agent/skills/curator.rs
create mode 100644 src/agent/skills/pattern_detector.rs
diff --git a/Cargo.lock b/Cargo.lock
index 34de065..c41c7bf 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -141,6 +141,7 @@ dependencies = [
"dotenvy",
"flate2",
"futures-util",
+ "git2",
"glob",
"hmac 0.12.1",
"html2md",
@@ -159,6 +160,7 @@ dependencies = [
"sha1 0.10.6",
"sqlite-vec",
"sqlx",
+ "tempfile",
"thiserror 1.0.69",
"tokio",
"tower-http 0.5.2",
@@ -1456,6 +1458,21 @@ dependencies = [
"wasm-bindgen",
]
+[[package]]
+name = "git2"
+version = "0.18.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "232e6a7bfe35766bf715e55a88b39a700596c0ccfd88cd3680b4cdb40d66ef70"
+dependencies = [
+ "bitflags 2.13.0",
+ "libc",
+ "libgit2-sys",
+ "log",
+ "openssl-probe",
+ "openssl-sys",
+ "url",
+]
+
[[package]]
name = "glob"
version = "0.3.3"
@@ -2066,6 +2083,20 @@ version = "0.2.186"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
+[[package]]
+name = "libgit2-sys"
+version = "0.16.2+1.7.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ee4126d8b4ee5c9d9ea891dd875cfdc1e9d0950437179104b183d7d8a74d24e8"
+dependencies = [
+ "cc",
+ "libc",
+ "libssh2-sys",
+ "libz-sys",
+ "openssl-sys",
+ "pkg-config",
+]
+
[[package]]
name = "libloading"
version = "0.8.9"
@@ -2105,6 +2136,32 @@ dependencies = [
"vcpkg",
]
+[[package]]
+name = "libssh2-sys"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "220e4f05ad4a218192533b300327f5150e809b54c4ec83b5a1d91833601811b9"
+dependencies = [
+ "cc",
+ "libc",
+ "libz-sys",
+ "openssl-sys",
+ "pkg-config",
+ "vcpkg",
+]
+
+[[package]]
+name = "libz-sys"
+version = "1.1.29"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85bc9657773828b90eeb625adff10eeac83cc21bbfd8e23a03eaa8a33c9e28d9"
+dependencies = [
+ "cc",
+ "libc",
+ "pkg-config",
+ "vcpkg",
+]
+
[[package]]
name = "linux-raw-sys"
version = "0.4.15"
@@ -2536,6 +2593,24 @@ dependencies = [
"syn 2.0.117",
]
+[[package]]
+name = "openssl-probe"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e"
+
+[[package]]
+name = "openssl-sys"
+version = "0.9.117"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b47e7e6bb2c38cd930d25a23b40fa52e068c10e85f3e03a7f5ba5aaca5713695"
+dependencies = [
+ "cc",
+ "libc",
+ "pkg-config",
+ "vcpkg",
+]
+
[[package]]
name = "outref"
version = "0.5.2"
diff --git a/Cargo.toml b/Cargo.toml
index bd74e80..075617a 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -55,9 +55,11 @@ async-trait = "0.1"
async-stream = "0.3"
serde_yaml = "0.9"
notify = { version = "6", default-features = false, features = ["macos_kqueue"] }
+tempfile = "3"
glob = "0.3"
walkdir = "2"
lru = "0.12"
+git2 = "0.18"
[features]
default = []
diff --git a/dashboard/src/App.tsx b/dashboard/src/App.tsx
index cd9ba6b..2cf9506 100644
--- a/dashboard/src/App.tsx
+++ b/dashboard/src/App.tsx
@@ -337,7 +337,7 @@ export default function App() {
}
};
- // 5.5. 对文献进行向量化分块入库 (独立任务)
+ // 5.5. 对文献进行知识入库 (独立任务)
const handleVectorize = async (bibcode: string) => {
setVectorizing(true);
try {
@@ -347,10 +347,10 @@ export default function App() {
if (selectedPaper?.bibcode === bibcode) {
setSelectedPaper(prev => prev ? { ...prev, has_vector: true } : null);
}
- showAlert(`文献向量化分块入库成功,共切片并录入 ${res.data.chunk_count} 个向量块。`, '向量化成功');
+ showAlert(`文献已成功录入馆藏智能库,共提炼并录入 ${res.data.chunk_count} 个核心知识节点。`, '知识入库成功');
} catch (e) {
- console.error('文献向量化失败', e);
- showAlert('向量化失败,请检查 .env 中的 Embedding API 配置。', '向量化失败');
+ console.error('知识入库失败', e);
+ showAlert('知识入库失败,请检查 .env 中的 Embedding API 配置。', '知识入库失败');
} finally {
setVectorizing(false);
}
@@ -698,11 +698,11 @@ export default function App() {
>
e.stopPropagation()}
- className="bg-white rounded-xl border border-slate-200 shadow-xl max-w-sm w-full p-6 space-y-4"
+ className="bg-[var(--bg-card)] rounded-xl border border-[var(--border-precision)] shadow-md max-w-sm w-full p-6 space-y-4"
>
-
-
+
+
{dialog.title}
-
{dialog.message}
+
{dialog.message}
@@ -755,11 +755,11 @@ export default function App() {
>
e.stopPropagation()}
- className="bg-white rounded-xl border border-slate-200 shadow-xl max-w-sm w-full p-6 space-y-4"
+ className="bg-[var(--bg-card)] rounded-xl border border-[var(--border-precision)] shadow-md max-w-sm w-full p-6 space-y-4"
>
-
-
+
+
文献尚未入库
-
- 文献 {uncachedBibcode} 尚未收录在本地数据库中。
+
+ 文献 {uncachedBibcode} 尚未收录在本地数据库中。
-
+
您可以选择在线拉取该文献元数据并入库,或是直接跳转至 NASA ADS 平台查看其原始页面。
@@ -795,13 +795,13 @@ export default function App() {
axios.post('/api/active_bibcode', { bibcode: uncachedBibcode }).catch(() => {});
window.open(`https://ui.adsabs.harvard.edu/abs/${uncachedBibcode}/abstract`, '_blank');
}}
- className="flex-1 bg-white hover:bg-slate-50 text-slate-700 border border-slate-250 py-2 rounded-lg text-[11px] font-bold text-center transition-all shadow-sm cursor-pointer"
+ className="flex-1 btn-console btn-console-secondary py-2 rounded-lg text-[11px] font-bold text-center cursor-pointer"
>
跳转到 ADS
@@ -818,13 +818,13 @@ export default function App() {
>
e.stopPropagation()}
- className="bg-white rounded-xl border border-slate-200 shadow-xl max-w-lg w-full p-6 space-y-4 cursor-default animate-fade-in"
+ className="bg-[var(--bg-card)] rounded-xl border border-[var(--border-precision)] shadow-md max-w-lg w-full p-6 space-y-4 cursor-default animate-fade-in"
>
{/* 标题 & 关闭 */}
-
文献详情元数据
-
+ 文献详情元数据
+
{getDoctypeBadge(detailPaper.doctype)}
{detailPaper.title}
@@ -844,48 +844,48 @@ export default function App() {
{/* 作者 */}
-
作者列表
-
{detailPaper.authors.join(', ')}
+
作者列表
+
{detailPaper.authors.join(', ')}
{/* 期刊 & 年份 */}
- 发表期刊
+ 发表期刊
{detailPaper.pub_journal || '未标注'}
- 发表年份
- {detailPaper.year}
+ 发表年份
+ {detailPaper.year}
{/* 摘要 */}
-
摘要
-
+ 摘要
+
{detailPaper.abstract_text || '该文献暂无摘要数据。'}
{/* 关键字 */}
-
关键词
+
关键词
{detailPaper.keywords && detailPaper.keywords.length > 0 ? (
{detailPaper.keywords.map(kw => (
-
+
{kw}
))}
) : (
-
+
暂无关键词
)}
@@ -893,48 +893,48 @@ export default function App() {
{/* 标识符 */}
-
-
BIBCODE
-
+
-
-
DOI
-
+
-
-
ARXIV ID
-
+
+
ARXIV ID
+
{detailPaper.arxiv_id ? (
axios.post('/api/active_bibcode', { bibcode: detailPaper.bibcode }).catch(() => {})}
- className="hover:underline text-sky-600"
+ className="text-[var(--accent-blueprint)] hover:underline"
>
{detailPaper.arxiv_id}
@@ -946,14 +946,14 @@ export default function App() {
{/* 手动上传文件(应对防爬阻断) */}
- 手动离线上传文献
+ 手动离线上传文献
防爬/人机验证备用
-
+
若自动下载受阻,可在浏览器中打开上方链接,手动保存 PDF 或 HTML 后在此处上传覆盖。
-
+
)}
-
+
{uploadingBibcode === detailPaper.bibcode ? '上传中...' : '上传 PDF 文献'}
- 支持 .pdf 格式
+ 支持 .pdf 格式
-
@@ -1004,7 +1004,7 @@ export default function App() {
@@ -1012,9 +1012,9 @@ export default function App() {
)}
@@ -1080,7 +1080,7 @@ export default function App() {
setActiveTab('citation');
loadCitations(detailPaper.bibcode);
}}
- className="flex-1 bg-white hover:bg-slate-50 text-slate-700 border border-slate-250 py-2.5 rounded-lg text-xs font-bold text-center transition-all cursor-pointer shadow-sm"
+ className="flex-1 btn-console btn-console-secondary py-2.5 rounded-lg text-xs font-bold text-center cursor-pointer"
>
查看引用图谱
@@ -1091,7 +1091,7 @@ export default function App() {
}, '确认重新下载');
}}
disabled={downloadingBibcodes[detailPaper.bibcode]}
- className="px-4 py-2.5 bg-slate-100 hover:bg-slate-200 text-amber-700 rounded-lg text-xs font-bold transition-all cursor-pointer disabled:opacity-50"
+ className="px-4 btn-console btn-console-secondary text-[var(--accent-star)] py-2.5 rounded-lg text-xs font-bold transition-all cursor-pointer disabled:opacity-50"
>
{downloadingBibcodes[detailPaper.bibcode] ? '重下中...' : '重新下载'}
@@ -1124,7 +1124,7 @@ export default function App() {
setActiveTab('citation');
loadCitations(detailPaper.bibcode);
}}
- className="px-6 bg-white hover:bg-slate-50 text-slate-700 border border-slate-250 py-2.5 rounded-lg text-xs font-bold text-center transition-all cursor-pointer shadow-sm"
+ className="px-6 btn-console btn-console-secondary py-2.5 rounded-lg text-xs font-bold text-center cursor-pointer"
>
引用图谱
diff --git a/dashboard/src/features/agent/ResearchAgentPanel.tsx b/dashboard/src/features/agent/ResearchAgentPanel.tsx
index b896ef7..cfacb19 100644
--- a/dashboard/src/features/agent/ResearchAgentPanel.tsx
+++ b/dashboard/src/features/agent/ResearchAgentPanel.tsx
@@ -11,7 +11,8 @@ import 'katex/dist/katex.min.css';
import {
Brain, Settings, Eye, CheckCircle2, AlertTriangle,
Send, Loader, Plus, Trash2, Compass, Clock, Square,
- BarChart3, ScrollText, Network
+ BarChart3, ScrollText, Network, Rewind, RotateCcw,
+ GitBranch, RefreshCw
} from 'lucide-react';
import { AskUserQuestionCard } from './AskUserQuestionCard';
import { PermissionRequestCard } from './PermissionRequestCard';
@@ -76,6 +77,7 @@ interface ActiveTurn {
interface ProcessedTurn {
turn_index: number;
question: string;
+ questionMessageId?: number;
timeline: TimelineItem[];
usage?: {
prompt_tokens: number;
@@ -85,6 +87,26 @@ interface ProcessedTurn {
createdAt: string;
}
+interface RewindResult {
+ rewound_count: number;
+ target_preview: string;
+ new_turn_index: number;
+ session_id: string;
+}
+
+interface BranchResult {
+ branch_session_id: string;
+ forked_at_message_id: number;
+ copied_count: number;
+}
+
+interface RetryResult {
+ retried_message: string;
+ new_turn_index: number;
+ deleted_count: number;
+ session_id: string;
+}
+
const safeSchema = {
...defaultSchema,
attributes: {
@@ -119,9 +141,9 @@ function getToolDisplayName(name: string): string {
case 'download_paper': return '下载文献全文资源';
case 'parse_paper': return '结构化解析文献内容';
case 'get_paper_content': return '获取文献全文内容';
- case 'rag_search': return '语义库检索 (RAG)';
- case 'query_target': return '查询 CDS Sesame 天体物理参数';
- case 'save_note': return '保存研究笔记';
+ case 'rag_search': return '检索馆藏知识库';
+ case 'query_target': return '查询天体物理参数 (CDS)';
+ case 'save_note': return '保存文献手札';
// Agent 控制工具
case 'todo_write': return '管理任务列表';
case 'compress_context': return '压缩上下文窗口';
@@ -325,6 +347,156 @@ export function ResearchAgentPanel({ showConfirm, showAlert }: ResearchAgentPane
}
};
+ // 回退会话到指定消息
+ const handleRewind = async (messageId?: number, n?: number) => {
+ if (!currentSessionId) return;
+
+ const msg = messageId
+ ? '确定要回退到此消息之前吗?此后的对话将被移除(可通过“恢复”按钮撤销)。'
+ : `确定要回退最近 ${n || 1} 个对话轮次吗?`;
+
+ const performRewind = async () => {
+ try {
+ const body: { n?: number; message_id?: number } = {};
+ if (messageId) body.message_id = messageId;
+ else body.n = n || 1;
+
+ const res = await axios.post
(
+ `/api/chat/sessions/${currentSessionId}/rewind`,
+ body
+ );
+
+ if (showAlert) {
+ showAlert(`已回退 ${res.data.rewound_count} 条消息`, '成功');
+ } else {
+ alert(`已回退 ${res.data.rewound_count} 条消息`);
+ }
+
+ // 重新加载会话(静默刷新,避免闪烁)
+ loadSessionHistory(currentSessionId, true);
+ fetchSessions(); // 更新侧栏 turn_count
+ } catch (e: any) {
+ const errMsg = `回退失败: ${e.response?.data || e.message}`;
+ if (showAlert) {
+ showAlert(errMsg, '错误');
+ } else {
+ alert(errMsg);
+ }
+ }
+ };
+
+ if (showConfirm) {
+ showConfirm(msg, performRewind, '确认回退');
+ } else {
+ if (window.confirm(msg)) {
+ performRewind();
+ }
+ }
+ };
+
+ // 恢复上次回退(undo-of-undo)
+ const handleRestoreRewind = async () => {
+ if (!currentSessionId) return;
+
+ try {
+ const res = await axios.post<{ restored_count: number }>(
+ `/api/chat/sessions/${currentSessionId}/rewind/restore`
+ );
+ if (res.data.restored_count > 0) {
+ const msg = `已恢复 ${res.data.restored_count} 条被回退的消息`;
+ if (showAlert) {
+ showAlert(msg, '成功');
+ } else {
+ alert(msg);
+ }
+ loadSessionHistory(currentSessionId, true);
+ fetchSessions();
+ } else {
+ const msg = '没有可恢复的回退操作';
+ if (showAlert) {
+ showAlert(msg, '提示');
+ } else {
+ alert(msg);
+ }
+ }
+ } catch (e: any) {
+ const errMsg = `恢复失败: ${e.response?.data || e.message}`;
+ if (showAlert) {
+ showAlert(errMsg, '错误');
+ } else {
+ alert(errMsg);
+ }
+ }
+ };
+
+ // 分叉当前会话
+ const handleBranch = async () => {
+ if (!currentSessionId) return;
+
+ const confirmed = window.confirm('确定要将当前活跃的消息分叉到一个新会话吗?');
+ if (!confirmed) return;
+
+ try {
+ const res = await axios.post(
+ `/api/chat/sessions/${currentSessionId}/branch`
+ );
+ if (showAlert) {
+ showAlert(`成功分叉会话!已复制 ${res.data.copied_count} 条消息`, '成功');
+ } else {
+ alert(`成功分叉会话!已复制 ${res.data.copied_count} 条消息`);
+ }
+
+ // 刷新列表并选中新的分叉会话
+ await fetchSessions();
+ setCurrentSessionId(res.data.branch_session_id);
+ } catch (e: any) {
+ const errMsg = `分叉失败: ${e.response?.data || e.message}`;
+ if (showAlert) {
+ showAlert(errMsg, '错误');
+ } else {
+ alert(errMsg);
+ }
+ }
+ };
+
+ // 重试最后一轮
+ const handleRetry = async () => {
+ if (!currentSessionId || streaming) return;
+
+ const confirmed = window.confirm('确定要重试最后一轮对话吗?原回答将被删除。');
+ if (!confirmed) return;
+
+ try {
+ const res = await axios.post(
+ `/api/chat/sessions/${currentSessionId}/retry`
+ );
+
+ const questionText = res.data.retried_message;
+ if (!questionText) {
+ if (showAlert) {
+ showAlert('没有找到可重试的上一轮问题', '提示');
+ } else {
+ alert('没有找到可重试的上一轮问题');
+ }
+ return;
+ }
+
+ // 重新加载会话以移除已被硬删除的消息,然后自动触发发送
+ await loadSessionHistory(currentSessionId, true);
+ await fetchSessions();
+
+ // 触发自动重发
+ handleSend(questionText);
+ } catch (e: any) {
+ const errMsg = `重试失败: ${e.response?.data || e.message}`;
+ if (showAlert) {
+ showAlert(errMsg, '错误');
+ } else {
+ alert(errMsg);
+ }
+ }
+ };
+
// 手动停止智能体执行
const handleStop = async () => {
if (!currentSessionId) return;
@@ -1082,10 +1254,21 @@ export function ResearchAgentPanel({ showConfirm, showAlert }: ResearchAgentPane
{turns.map((turn) => (
{/* 用户提问 */}
-
+
我
-
- {turn.question}
+
+ {turn.questionMessageId && (
+
+ )}
+
+ {turn.question}
+
@@ -1151,7 +1334,7 @@ export function ResearchAgentPanel({ showConfirm, showAlert }: ResearchAgentPane
{/* Error block */}
{activeTurn.error && (
-
+
查询发生错误
@@ -1161,6 +1344,40 @@ export function ResearchAgentPanel({ showConfirm, showAlert }: ResearchAgentPane
)}
)}
+
+ {/* 会话操作小图标栏 */}
+ {currentSessionId && turns.length > 0 && !streaming && (
+
+
+
+
+
+
+ )}
)}
@@ -1269,6 +1486,7 @@ function groupMessagesIntoTurns(messages: MessageRecord[]): ProcessedTurn[] {
if (msg.role === 'user') {
turn.question = msg.content;
+ turn.questionMessageId = msg.id;
} else if (msg.role === 'assistant') {
const stepNum = msg.step_index;
const hasToolCalls = msg.tool_calls && msg.tool_calls.length > 0;
diff --git a/dashboard/src/features/reader/ReaderPanel.tsx b/dashboard/src/features/reader/ReaderPanel.tsx
index 1842df4..597c3d1 100644
--- a/dashboard/src/features/reader/ReaderPanel.tsx
+++ b/dashboard/src/features/reader/ReaderPanel.tsx
@@ -547,26 +547,26 @@ export function ReaderPanel({
onClick={() => handleVectorize(selectedPaper.bibcode)}
disabled={vectorizing}
className="btn-console btn-console-primary px-4 py-2 rounded-lg text-xs font-bold flex items-center gap-2"
- title="对文献进行向量化分块入库,以开启学术 AI 问答"
+ title="对文献进行知识入库,以开启学术 AI 研讨"
>
{vectorizing ?
:
}
- {vectorizing ? '向量化入库中...' : '向量化入库'}
+ {vectorizing ? '正在进行知识入库...' : '知识入库'}
)}
{selectedPaper.has_markdown && selectedPaper.has_vector && (
)}
@@ -941,56 +941,56 @@ export function ReaderPanel({
top: `${hoverCardPos.y}px`,
zIndex: 9999,
}}
- className="console-panel rounded-xl p-4 bg-white border border-slate-200 shadow-xl w-72 text-xs space-y-2 pointer-events-auto animate-in fade-in duration-200"
+ className="console-panel rounded-xl p-4 bg-[var(--bg-card)] border border-[var(--border-precision)] shadow-md w-72 text-xs space-y-2 pointer-events-auto animate-in fade-in duration-200"
>
-
{hoveredTarget.target_name}
+
{hoveredTarget.target_name}
-
+
-
RA (J2000):
-
{hoveredTarget.ra || '未知'}
+
RA (J2000):
+
{hoveredTarget.ra || '未知'}
-
Dec (J2000):
-
{hoveredTarget.dec || '未知'}
+
Dec (J2000):
+
{hoveredTarget.dec || '未知'}
-
光谱型:
-
{hoveredTarget.spectral_type || '未知'}
+
光谱型:
+
{hoveredTarget.spectral_type || '未知'}
-
视星等 (V):
-
+
视星等 (V):
+
{hoveredTarget.v_magnitude !== null && hoveredTarget.v_magnitude !== undefined
? `${hoveredTarget.v_magnitude.toFixed(2)}`
: '未知'}
-
视差 / 估算距离:
-
+
视差 / 估算距离:
+
{hoveredTarget.parallax !== null && hoveredTarget.parallax !== undefined
? `${hoveredTarget.parallax.toFixed(2)} mas (~${(1000.0 / hoveredTarget.parallax).toFixed(1)} pc)`
: '未知'}
@@ -999,8 +999,8 @@ export function ReaderPanel({
{hoveredTarget.aliases && hoveredTarget.aliases.length > 0 && (
-
常用别名:
-
+
常用别名:
+
{hoveredTarget.aliases.slice(0, 8).join(', ')}
{hoveredTarget.aliases.length > 8 && ' ...'}
diff --git a/dashboard/src/index.css b/dashboard/src/index.css
index a0a6525..7d02d44 100644
--- a/dashboard/src/index.css
+++ b/dashboard/src/index.css
@@ -1,4 +1,4 @@
-@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
+@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500;700&display=swap');
@import "tailwindcss";
@plugin "@tailwindcss/typography";
@@ -6,15 +6,18 @@
font-family: 'Inter', system-ui, -apple-system, sans-serif;
color-scheme: light;
- --bg-main: #f1f5f9;
+ /* 极简白蓝学术科技风配色 */
+ --bg-main: #f4f6f9;
--bg-card: #ffffff;
- --bg-sidebar: #f8fafc;
- --text-main: #0f172a;
- --text-muted: #475569;
+ --bg-sidebar: #0f2540; /* 保持深色侧边栏作为界面骨架结构 */
+ --text-main: #0a2540;
+ --text-muted: #5c6b84;
- --accent-blue: #0284c7;
- --accent-navy: #1e3a8a;
- --border-clean: #e2e8f0;
+ --accent-blueprint: #106ba3;
+ --accent-star: #d97706;
+ --border-precision: #d2d8e2;
+
+ --font-mono: 'JetBrains Mono', monospace;
}
body {
@@ -32,7 +35,7 @@ body {
height: 6px;
}
::-webkit-scrollbar-track {
- background: #f1f5f9;
+ background: #f4f6f9;
}
::-webkit-scrollbar-thumb {
background: #cbd5e1;
@@ -45,20 +48,20 @@ body {
/* Premium clean panel cards */
.console-panel {
background: var(--bg-card);
- border: 1px solid var(--border-clean);
- box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.05), 0 2px 4px -1px rgba(0, 0, 0, 0.03);
+ border: 1px solid var(--border-precision);
+ box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.05); /* 弱化阴影,更显扁平学术感 */
}
.console-panel-active {
- border-color: var(--accent-blue);
- box-shadow: 0 0 0 1px var(--accent-blue), 0 4px 6px -1px rgba(0, 0, 0, 0.05);
+ border-color: var(--accent-blueprint);
+ box-shadow: 0 0 0 1px var(--accent-blueprint), 0 1px 3px 0 rgba(0, 0, 0, 0.05);
}
/* High contrast clean console button */
.btn-console {
background: #ffffff;
- border: 1px solid #cbd5e1;
- color: #334155;
+ border: 1px solid var(--border-precision);
+ color: var(--text-main);
font-weight: 500;
transition: all 0.2s ease;
}
@@ -66,42 +69,42 @@ body {
.btn-console:hover:not(:disabled) {
background: #f8fafc;
border-color: #94a3b8;
- color: #0f172a;
+ color: var(--text-main);
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
}
.btn-console-primary {
- background: var(--accent-blue);
- border: 1px solid var(--accent-blue);
+ background: var(--accent-blueprint);
+ border: 1px solid var(--accent-blueprint);
color: #ffffff;
}
.btn-console-primary:hover:not(:disabled) {
- background: #0369a1;
- border-color: #0369a1;
+ background: #0d5988;
+ border-color: #0d5988;
color: #ffffff;
- box-shadow: 0 2px 4px 0 rgba(2, 132, 199, 0.2);
+ box-shadow: 0 2px 4px 0 rgba(16, 107, 163, 0.2);
}
.btn-console-secondary {
background: #f1f5f9;
- border: 1px solid #e2e8f0;
- color: #334155;
+ border: 1px solid var(--border-precision);
+ color: var(--text-muted);
}
.btn-console-secondary:hover:not(:disabled) {
background: #e2e8f0;
- color: #0f172a;
+ color: var(--text-main);
}
/* Premium clean console select dropdown styling */
.select-console {
display: inline-block;
background-color: #ffffff;
- border: 1px solid #cbd5e1;
+ border: 1px solid var(--border-precision);
border-radius: 0.5rem; /* 8px */
padding: 0.5rem 1.75rem 0.5rem 0.625rem; /* padding-right leaves space for custom arrow */
- color: #334155;
+ color: var(--text-main);
font-size: 0.75rem; /* text-xs */
font-weight: 500;
cursor: pointer;
@@ -116,13 +119,13 @@ body {
.select-console:hover:not(:disabled) {
border-color: #94a3b8;
- color: #0f172a;
+ color: var(--text-main);
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
}
.select-console:focus {
- border-color: #0284c7;
- box-shadow: 0 0 0 1px #0284c7;
+ border-color: var(--accent-blueprint);
+ box-shadow: 0 0 0 1px var(--accent-blueprint);
}
.select-console:disabled {
@@ -130,5 +133,3 @@ body {
color: #94a3b8;
cursor: not-allowed;
}
-
-
diff --git a/dashboard/src/types.ts b/dashboard/src/types.ts
index bb3e400..e3c7403 100644
--- a/dashboard/src/types.ts
+++ b/dashboard/src/types.ts
@@ -172,3 +172,35 @@ export interface AgentTask {
created_at: string;
updated_at: string;
}
+
+// ── 会话回退 (Rewind) ──
+
+export interface RewindRequest {
+ n?: number; // 回退 N 个轮次
+ message_id?: number; // 或指定消息 ID
+}
+
+export interface RewindResponse {
+ rewound_count: number;
+ target_preview: string;
+ new_turn_index: number;
+ session_id: string;
+}
+
+export interface RestoreResponse {
+ restored_count: number;
+ session_id: string;
+}
+
+export interface BranchResponse {
+ branch_session_id: string;
+ forked_at_message_id: number;
+ copied_count: number;
+}
+
+export interface RetryResponse {
+ retried_message: string;
+ new_turn_index: number;
+ deleted_count: number;
+ session_id: string;
+}
diff --git a/docs/architecture/agent/claude-code-reference-analysis.md b/docs/architecture/agent/claude-code-reference-analysis.md
new file mode 100644
index 0000000..0539821
--- /dev/null
+++ b/docs/architecture/agent/claude-code-reference-analysis.md
@@ -0,0 +1,909 @@
+# Claude Code / Hermes-Agent 参考分析
+
+对 Claude Code (`/home/fmq/program/claudecode/src/`) 和 Hermes-Agent (`libs/hermes-agent/`)
+源码的全面架构分析,记录对 AstroResearch Agent 系统的参考价值与改进方向。
+
+> 分析日期: 2026-06-22 | 最后更新: 2026-06-22
+
+## 实施状态
+
+| 优先级 | 改进项 | 状态 | 涉及文件 |
+|--------|--------|------|---------|
+| P0 | Context Overflow 自动修复 | ✅ 已完成 | `error_recovery.rs` (+150 行) |
+| P0 | StreamingExecutor 真正流式调度 | ✅ 已完成 | `streaming_executor.rs` (重写 ~400 行) |
+| P0 | Executor 集成分区器(批次串行/并行) | ✅ 已完成 | `executor.rs` (Phase 3 重写 + 2 个提取函数) |
+| P1 | 工具并发分区 `partition_tool_calls` | ✅ 已完成 | `partitioner.rs` (+2 测试) |
+| P1 | PermissionRequest / PermissionDenied Hooks | ✅ 已完成 | `hooks/types.rs`, `traits.rs`, `dispatch.rs`, `mod.rs` |
+| P1 | Auto-mode Classifier | ⏳ 待定 | — |
+| P2 | Self-improving Skills(模式检测 + 自动创建 + Curator) | ✅ 已完成 | `skills/pattern_detector.rs` + `curator.rs` + `SkillCreator` |
+| P2 | Coordinator Mode | ⏳ 待定 | — |
+| P2 | UserPromptSubmit / PreCompact / PostCompact Hook | ⏳ 待定 | — |
+| P3 | FTS5 跨 session 搜索 | ⏳ 待定 | — |
+| P3 | Tool `defer_loading` / `classifier_summary` | ⏳ 待定 | — |
+| P3 | 模型回退策略 | ⏳ 待定 | — |
+| P3 | Session Memory Compaction | ⏳ 待定 | — |
+
+---
+
+## 目录
+
+1. [总体评估](#1-总体评估)
+2. [工具并发执行模型](#2-工具并发执行模型)
+3. [Permission 系统](#3-permission-系统)
+4. [Hooks 系统](#4-hooks-系统)
+5. [Error Recovery / 重试系统](#5-error-recovery--重试系统)
+6. [Tool 定义系统](#7-tool-定义系统)
+8. [Memory 持久化](#8-memory-持久化)
+9. [Coordinator / Multi-Agent](#9-coordinator--multi-agent)
+10. [Hermes-Agent 的独特贡献](#10-hermes-agent-的独特贡献)
+11. [优先级排序 —— 建议实施路线](#11-优先级排序--建议实施路线)
+
+---
+
+## 1. 总体评估
+
+### 1.1 参考项目概览
+
+| 维度 | Claude Code | Hermes-Agent | AstroResearch |
+|------|-------------|-------------|---------------|
+| 语言 | TypeScript (Node.js) | Python (3.11+) | Rust (Axum) |
+| 定位 | 终端 IDE 编程助手 | 通用 AI 个人助手 | 天文科研 Agent |
+| Agent 循环 | 流式 `query()` generator | 同步 `while` 循环 | 流式 ReAct 循环 |
+| 工具注册 | 手动 import + `getAllBaseTools()` | 文件系统自动发现 | 手动 `ToolRegistry::new()` |
+| 工具接口 | `Tool
` — ~70 个方法 | `handler(args) -> JSON string` | `AgentTool` trait — ~10 个方法 |
+| 权限系统 | 6 层优先级 + Classifier + 沙箱 | 无内置 | 3 层规则 + PermissionChecker |
+| Hooks | 27 种事件,6 种 hook 类型 | PluginManager 生命周期 | 15 种事件,2 种 hook 类型 |
+| 子代理 | `AgentTool` + Fork + Worktree 隔离 | `delegate_task` + 子 AIAgent | `SubAgentTool` + 独立 ReAct |
+| 多 Agent | Coordinator 模式 + Swarm/Team | Kanban 工作队列 | Team 系统 (lead/teammate) |
+| 持久化 | 文件系统 Markdown + cost tracker | SQLite (FTS5) + SessionDB | SQLite + MEMORY.md |
+| 上下文压缩 | 微压缩 + 自动压缩 + 手动压缩 | ContextCompressor | 4 层压缩 (微/snip/auto/aggro) |
+
+### 1.2 核心结论
+
+AstroResearch 的 Agent 系统架构本身就是**对标 Claude Code 设计的**——`StreamingToolExecutor`、
+`PermissionChecker`、`HookRegistry` 都明确标注了参考来源。当前差距主要是**实现深度**而非**设计方向**。
+
+Claude Code 的参考价值在**工程细节**:流式调度的时机选择、Overflow 的自动修复、Classifier
+的并行化设计。Hermes-Agent 的独特价值在**自我进化**(Self-improving Skills)和**多 Profile 隔离**。
+
+---
+
+## 2. 工具并发执行模型
+
+### 2.1 对比
+
+| 特性 | Claude Code | AstroResearch (当前) |
+|------|-------------|---------------------|
+| 流式调度 | tool_use 到达**立即**开始执行 | tool_use 全部收集,`flush()` 批量执行 |
+| 并发分区 | `partitionToolCalls()` 自动分组连续只读工具并行 | `ToolPartitioner` 存在但基本未使用 |
+| Sibling Abort | Bash 错误 → 级联取消兄弟工具,有专用 `siblingAbortController` | `AbortReason::SiblingError` + 广播通道存在,取消逻辑不完整 |
+| Progress 流式 | `pendingProgress` 即时 yield,`progressAvailableResolve` 唤醒等待 | `execute_with_progress` 有通道,`getCompletedResults` 未检查 |
+| 中断行为 | `interruptBehavior()` 区分 `cancel` vs `block` | `InterruptBehavior` 枚举存在但未在 Executor 中使用 |
+
+### 2.2 Claude Code 的分区逻辑
+
+```typescript
+// src/services/tools/toolOrchestration.ts
+// 自动将连续只读工具分组并行,写工具独立串行
+function partitionToolCalls(toolUseMessages, toolUseContext): Batch[] {
+ return toolUseMessages.reduce((acc, toolUse) => {
+ const tool = findToolByName(toolUseContext.options.tools, toolUse.name)
+ const parsedInput = tool?.inputSchema.safeParse(toolUse.input)
+ const isConcurrencySafe = parsedInput?.success
+ ? (() => { try { return Boolean(tool.isConcurrencySafe(parsedInput.data)) } catch { return false } })()
+ : false
+
+ if (isConcurrencySafe && acc[acc.length - 1]?.isConcurrencySafe) {
+ acc[acc.length - 1].blocks.push(toolUse) // 合并到当前批次
+ } else {
+ acc.push({ isConcurrencySafe, blocks: [toolUse] }) // 新批次
+ }
+ return acc
+ }, [])
+}
+```
+
+关键点:
+- **输入感知**的并发安全判断:同一工具可能因参数不同而安全属性不同
+- 并发安全的工具**连续分组**——不打断写入顺序
+- 非并发安全的工具**独占执行**——等上一个完成后才启动下一个
+
+### 2.3 Claude Code 的 StreamingToolExecutor 核心逻辑
+
+```
+文件: src/services/tools/StreamingToolExecutor.ts (531 行)
+
+状态机: Queued → Executing → Completed → Yielded
+
+生命周期:
+1. addTool(block) — LLM 流产生 tool_use 时立即调用
+2. processQueue() — 检查 concurrency 条件,启动可执行工具
+3. executeTool(tool) — 创建子 AbortController,调用 runToolUse generator
+4. getCompletedResults() — 按序 yield 结果(非阻塞)
+5. getRemainingResults() — 等待未完成工具(async generator)
+
+关键设计:
+- siblingAbortController: 父 AbortController 的子节点
+ Bash 错误 → siblingAbortController.abort('sibling_error') → 取消所有兄弟
+ 但 toolAbortController 的 abort 会向上冒泡到父 AbortController
+- progressAvailableResolve: Promise resolver 用于唤醒等待 progress 的 getRemainingResults
+- 中断行为: 'cancel' 工具被用户中断时生成 REJECT_MESSAGE; 'block' 工具不受影响
+```
+
+### 2.4 AstroResearch 的现状(2026-06-22 更新)
+
+```
+文件: src/agent/runtime/streaming_executor.rs (~400 行,已重写)
+
+已实现:
+✅ TrackedTool 状态机 (Queued/Executing/Completed/Yielded)
+✅ Sibling Abort 广播通道(broadcast::channel + tokio::select! 竞速)
+✅ on_tool_use / flush / next_result 接口
+✅ 输出截断
+✅ 真正的流式调度 — on_tool_use 中对并发安全工具立即 spawn tokio task
+✅ 非并发安全工具独占执行 — executing_non_concurrent 标志阻塞后续启动
+✅ 并发取消 — tokio::select! 在工具执行和 Sibling Abort 之间竞速
+✅ 输入感知的并发安全判断 — 通过 tool_registry.get().is_concurrency_safe(&args)
+✅ ToolContext 实现 Clone(支持 per-task 复制上下文)
+
+与 Claude Code 的对齐:
+- 核心理念一致:addTool → 立即 processQueue
+- Sibling Abort 机制等效:broadcast::Sender + subscribe
+- collectCompletedTasks 使用 JoinHandle::is_finished() 做非阻塞检查
+```
+
+
+### 2.5 已实施改进(2026-06-22)
+
+**✅ P0: 真正的流式调度** — 已完成
+
+`on_tool_use` 中对并发安全工具立即 `tokio::spawn`,非并发安全工具标记 `executing_non_concurrent`
+并阻塞后续启动,直到独占工具完成。
+
+**✅ P0: Executor 集成分区器** — 已完成
+
+`src/agent/runtime/executor.rs` Phase 3 从「所有非拒绝工具单一 `FuturesUnordered` 无差别并发」
+改为「`ToolPartitioner` 分区 → 逐批次执行」:
+- 并行批次内 `FuturesUnordered` 并发
+- 串行批次内逐个执行(非并发安全工具独占)
+- 提取 `execute_single_tool()` 和 `process_single_result()` 两个辅助函数
+
+**✅ P1: 并发分区器** — 已完成
+
+`src/agent/runtime/partitioner.rs` 新增 2 个测试(`run_bash`、`file_write` 打断并发批)。
+
+### 2.6 原始建议(已过时)
+
+```rust
+// 建议在 on_tool_use 中对并发安全的工具立即 spawn
+pub fn on_tool_use(&mut self, call_id: String, name: String, args: Value) -> bool {
+ let is_concurrency_safe = /* 判断 */
+ let idx = self.tracked.len();
+ self.tracked.push(tool);
+ if is_concurrency_safe && self.can_execute_now() {
+ let handle = tokio::spawn(/* 执行 */);
+ self.tracked[idx].handle = Some(handle);
+ true
+ } else {
+ false
+ }
+}
+```
+
+**P1: 并发分区**
+
+```rust
+/// 将 tool_use 列表分区为 (并发安全批次, 非并发安全单例)
+fn partition_tool_calls(calls: &[PreparedCall], registry: &ToolRegistry) -> Vec {
+ calls.iter().fold(Vec::new(), |mut acc, call| {
+ let is_safe = registry.get(&call.tool_name)
+ .map(|t| t.is_concurrency_safe(&call.args))
+ .unwrap_or(false);
+ if is_safe && acc.last().map_or(false, |b: &Batch| b.concurrent) {
+ acc.last_mut().unwrap().calls.push(call.clone());
+ } else {
+ acc.push(Batch { concurrent: is_safe, calls: vec![call.clone()] });
+ }
+ acc
+ })
+}
+```
+
+---
+
+## 3. Permission 系统
+
+### 3.1 对比
+
+| 特性 | Claude Code | AstroResearch (当前) |
+|------|-------------|---------------------|
+| 规则来源分层 | 6 层优先级:policy > project > user > plugin > flag > command | 单一规则列表 |
+| 规则行为 | Allow / Deny / Ask | Allow / Deny / Ask ✅ |
+| Classifier 自动模式 | 两阶段(快速 + 思考),并行于 hooks 启动 | 无 |
+| 拒绝追踪 | 带时间窗口的限流回退 (DenialTracker) | 简单计数 |
+| 沙箱集成 | `shouldUseSandbox()` + `sandbox-adapter` | 无沙箱概念 |
+| 决策溯源 | 每条 PermissionDecisionReason 记录完整来源链 | 只返回 Allow/Deny/Ask |
+| 权限模式 | 5 种:default, acceptEdits, bypassPermissions, dontAsk, plan | 4 种 ✅ |
+
+### 3.2 Claude Code 的 Permission 决策管道
+
+```
+1. validateInput() — Zod schema 验证
+2. runPreToolUseHooks() — Session hooks(用户配置的)
+3. canUseTool — 检查 deny 规则
+4. resolveHookPermissionDecision() — Allow 规则
+5. [auto mode] Classifier — 两阶段分类器(Haiku)
+6. [default mode] 用户弹窗 — 交互式确认
+7. PermissionDecisionReason — 记录决策来源
+```
+
+每个决策都烙印 `PermissionDecisionReason`:
+```
+rule | mode | subcommandResults | permissionPromptTool |
+hook | asyncAgent | sandboxOverride | classifier |
+workingDir | safetyCheck | other
+```
+
+### 3.3 Classifier 系统(最值得借鉴)
+
+Claude Code 的 auto-mode classifier 是一个**独立的小模型调用**(Haiku),在后台并行运行:
+
+```
+Auto Mode 决策流程:
+┌──────────────────────────────────────────────┐
+│ 1. startSpeculativeClassifierCheck() │
+│ └─ 并行于 PreToolUse hooks 启动 │
+│ 2. 两阶段分类: │
+│ ├─ Fast: 简单模式匹配(秒级) │
+│ └─ Thinking: 深度分析(复杂命令时) │
+│ 3. 结果: Allow / Deny / Ask + confidence │
+│ 4. DenialTracker: 连续 Deny 后 fallback 用户 │
+└──────────────────────────────────────────────┘
+```
+
+AstroResearch 目前没有 auto-mode——所有非白名单工具都需要用户交互确认。
+
+### 3.4 建议改进
+
+**P1: Auto-mode Classifier**
+
+```rust
+/// Auto-mode 分类器 — 使用廉价模型在后台预分类工具调用
+pub struct AutoClassifier {
+ llm: LlmClient, // 使用廉价模型(如 Haiku 级别 provider)
+ cache: LruCache,
+}
+
+#[derive(Debug)]
+pub struct ClassificationResult {
+ pub decision: PermissionResult,
+ pub confidence: f64,
+ pub reason: String,
+}
+
+impl AutoClassifier {
+ /// 在工具执行前异步预分类(不阻塞用户)
+ pub async fn preclassify(
+ &self,
+ tool_name: &str,
+ args: &Value,
+ context: &str, // 从 CLAUDE.md 和当前对话提取
+ ) -> ClassificationResult {
+ // 构建精简 prompt:
+ // "You are a security classifier. Evaluate this tool call:
+ // Tool: {tool_name}
+ // Args: {args}
+ // Context: {context}
+ // Respond: ALLOW|DENY|ASK "
+ todo!()
+ }
+}
+```
+
+**P1: PermissionRequest / PermissionDenied Hook 事件**
+
+这两个事件对科研场景的审计至关重要:
+
+```rust
+// 在 hooks/types.rs 中添加
+pub enum HookEvent {
+ // ... 现有事件 ...
+ /// 权限请求前触发(可阻止或修改)
+ PermissionRequest,
+ /// 权限被拒绝后触发(审计日志)
+ PermissionDenied,
+}
+```
+
+---
+
+## 4. Hooks 系统
+
+### 4.1 对比
+
+| 特性 | Claude Code | AstroResearch (当前) |
+|------|-------------|---------------------|
+| Hook 类型 | 6 种:command, prompt, agent, http, callback, function | 2 种:sync AgentHook + async AsyncAgentHook |
+| 匹配器 | simple / pipe-separated / regex | glob + 精确匹配 |
+| if 条件 | `preparePermissionMatcher()` — Bash 上有 tree-sitter | 无 |
+| 输出协议 | JSON `{continue, decision, reason, suppressOutput, hookSpecificOutput}` | 直接返回值 |
+| 超时 | 每个 hook 独立超时(默认 10min) | 统一 `DEFAULT_HOOK_TIMEOUT` |
+| 事件数量 | 27 种 | ~15 种 |
+| 来源 | config + plugin + SDK + session + function | registry + session |
+
+### 4.2 Claude Code 的 27 种 Hook 事件
+
+```
+生命周期类:
+ SessionStart, Setup, SubagentStart, SubagentStop, SessionEnd, Stop, StopFailure
+
+用户交互类:
+ UserPromptSubmit, Elicitation, ElicitationResult, PermissionRequest, PermissionDenied
+
+工具执行类:
+ PreToolUse, PostToolUse, PostToolUseFailure
+
+上下文类:
+ PreCompact, PostCompact, InstructionsLoaded
+
+环境监控类:
+ FileChanged, CwdChanged, ConfigChange
+
+Swarm/Team 类:
+ TeammateIdle, TaskCreated, TaskCompleted
+
+UI 类:
+ Notification, StatusLine, FileSuggestion
+```
+
+### 4.3 AstroResearch 缺失的关键事件
+
+| 缺失事件 | 用途 | 优先级 | 状态 |
+|---------|------|--------|------|
+| `UserPromptSubmit` | 用户提交 prompt 前拦截(自动上下文注入) | P2 | ⏳ |
+| `Notification` | 长时间操作完成通知 | P1 | ⏳ |
+| `PermissionRequest` | 权限弹窗前触发 | P1 | ✅ 已实现 |
+| `PermissionDenied` | 权限被拒绝后记录审计 | P1 | ✅ 已实现 |
+| `PreCompact` | 上下文压缩前机会(保存重要信息) | P2 | ⏳ |
+| `PostCompact` | 上下文压缩后通知(更新外部状态) | P2 | ⏳ |
+
+### 4.4 已实施改进(2026-06-22)
+
+**✅ P1: PermissionRequest / PermissionDenied 事件** — 已完成
+
+新增类型(`src/agent/hooks/types.rs`):
+- `HookEvent::PermissionRequest` / `HookEvent::PermissionDenied`
+- `PermissionRequestContext` — 携带 `current_decision`、`permission_mode`、`is_subagent`
+- `PermissionDeniedContext` — 携带 `reason`、`source` (Rule/Classifier/User/Timeout)
+- `PermissionRequestAction` — Continue / Override / InjectContext
+- `PermissionDecision` / `PermissionDenialSource` 枚举
+
+新增 trait 方法(`src/agent/hooks/traits.rs`):
+- `AgentHook::on_permission_request()` → `PermissionRequestAction`
+- `AgentHook::on_permission_denied()` → void (审计日志)
+
+新增调度方法(`src/agent/hooks/dispatch.rs`):
+- `HookRegistry::run_on_permission_request()` — 并行调用,第一个 Override 生效
+- `HookRegistry::run_on_permission_denied()` — fire-and-forget 审计
+
+### 4.5 原始建议(部分已过时)
+
+原建议的 context 设计已被更完善的实现替代:
+}
+```
+
+**P2: 支持 command 类型 hook**
+
+```rust
+/// Command 类型 hook — 执行外部 shell 命令处理事件
+pub struct CommandHook {
+ command: String, // 如 "python3 audit.py"
+ timeout: Duration, // 默认 10min
+ shell: HookShell, // Bash | PowerShell
+}
+```
+
+---
+
+## 5. Error Recovery / 重试系统
+
+### 5.1 对比
+
+| 特性 | Claude Code | AstroResearch (当前) |
+|------|-------------|---------------------|
+| 重试结构 | Generator 模式,yield 系统消息直到不可重试 | ErrorRecovery 枚举 + 简单决策 |
+| 退避算法 | 指数 + 25% jitter,可配置上限(32s 默认,5min 持久) | 固定退避 |
+| 错误分类 | `shouldRetry()` 检查 15+ 种错误,每种不同策略 | 8 步分类管线 |
+| 529 Overloaded | 3 次重试 → 模型回退(Opus→Sonnet)→ 持久化重试 | 无模型回退 |
+| Context Overflow | 解析 "X + Y > Z",自动调整 max_tokens + 1000 安全缓冲 | 只分类不修复 |
+| 持久化重试 | 无限重试 + 30s 心跳 | 无 |
+| Fast Cooldown | 429/529 在 fast mode → retry-after <20s → 10min cooldown | N/A |
+
+### 5.2 Context Overflow 自动修复(最值得借鉴)
+
+Claude Code 的做法:
+
+```typescript
+// src/services/api/withRetry.ts
+// 解析 Anthropic API 的错误消息:
+// "input length and max_tokens exceed context limit: 180000 + 32000 > 200000"
+// → 计算安全的 max_tokens = 200000 - 180000 - 1000(safety) = 19000
+
+function parseContextOverflowError(errorMessage: string) {
+ const match = errorMessage.match(
+ /input length and max_tokens exceed context limit: (\d+) \+ (\d+) > (\d+)/
+ );
+ if (match) {
+ const [, inputLen, maxTokens, contextLimit] = match.map(Number);
+ const newMaxTokens = contextLimit - inputLen - SAFETY_MARGIN;
+ if (newMaxTokens > MIN_TOKENS) {
+ return { shouldRetry: true, adjustedMaxTokens: newMaxTokens };
+ }
+ }
+ return { shouldRetry: false };
+}
+```
+
+### 5.3 已实施改进(2026-06-22)
+
+**✅ P0: Context Overflow 自动修复** — 已完成
+
+新增公共 API(`src/agent/runtime/error_recovery.rs`):
+- `ContextOverflowInfo` — 从错误消息解析的数值结构体
+- `parse_context_overflow()` — 支持 Anthropic/OpenAI/通用三种格式
+- `calculate_safe_max_tokens()` — `context_limit - input_length - SAFETY_MARGIN(1000)`
+- `RecoveryStep::AdjustMaxTokens { new_max_tokens }` — 恢复管线第 0 步
+- `extract_three_numbers()` — 正则匹配 "A + B > C" 模式(使用已有 `regex` crate)
+
+`AgentRuntime` 集成(`src/agent/runtime/mod.rs`):
+```rust
+let overflow_info = error_recovery::parse_context_overflow(&e_str);
+while let Some(recovery_step) = recovery.try_recover(&error_kind, overflow_info.as_ref()) {
+ // AdjustMaxTokens 优先于 AggressiveCompact,仅无空间时才回退到压缩
+}
+```
+
+12 个新增测试覆盖 Anthropic/OpenAI/Generic 格式、边界条件、恢复优先级。
+
+### 5.4 原始建议(部分已过时)
+
+**P1: 模型回退策略**
+
+```rust
+/// 模型回退链 — 529/Overloaded 时自动降级
+pub struct ModelFallback {
+ chain: Vec,
+}
+
+#[derive(Debug, Clone)]
+pub enum ModelTier {
+ Primary(String), // 如 "claude-opus-4-8"
+ Fallback(String), // 如 "claude-sonnet-4-6"
+ Emergency(String), // 如 "claude-haiku-4-5"
+}
+```
+
+---
+
+## 6. Tool 定义系统
+
+### 7.1 对比
+
+| 特性 | Claude Code `Tool` | AstroResearch `AgentTool` trait |
+|------|----------------------|-------------------------------|
+| 并发安全 | `isConcurrencySafe(input)` — 输入感知 ✅ | `is_concurrency_safe(args)` — ✅ |
+| 语义标记 | `isReadOnly()` / `isDestructive()` / `isConcurrencySafe()` | `causes_sibling_abort()` / `interrupt_behavior()` |
+| 权限逻辑 | `checkPermissions()` — 工具自己决定权限 | 集中在 PermissionChecker |
+| 分类器输入 | `toAutoClassifierInput()` — 精简信息 | 无 |
+| 延迟加载 | `shouldDefer` / `alwaysLoad` — 减小 prompt | 无 |
+| 搜索提示 | `searchHint` — 帮助 ToolSearch 匹配 | 无 |
+| UI 渲染 | `renderToolUseMessage/Result/Progress/Error` (6 种) | 前端独立处理 |
+| 中断行为 | `interruptBehavior()` — cancel vs block | `interrupt_behavior()` ✅ |
+| 输出大小 | `maxResultSizeChars` — 超限存磁盘 | env var 全局配置 |
+| 权限匹配器 | `preparePermissionMatcher()` — 工具级模式匹配 | HookMatcher |
+
+### 7.2 Claude Code 的 `buildTool()` Factory
+
+```typescript
+// 每个工具通过 buildTool 创建,自动填充安全默认值
+const myTool: Tool = buildTool({
+ name: 'MyTool',
+ inputSchema: z.object({ ... }),
+ async call(input, context, toolUseId) { ... },
+ // 以下都有安全默认值,按需覆盖:
+ // isEnabled: true (可根据 permission mode 禁用)
+ // isConcurrencySafe: false
+ // isReadOnly: false
+ // isDestructive: false
+ // checkPermissions: {behavior: 'allow'} (最宽松)
+ // toAutoClassifierInput: '' (不参与分类)
+ // shouldDefer: false (立即加载)
+ // interruptBehavior: 'block' (不可中断)
+})
+```
+
+### 7.3 建议改进
+
+**P2: 为 AgentTool trait 添加方法**
+
+```rust
+pub trait AgentTool: Send + Sync {
+ // ... 现有方法 ...
+
+ /// 返回用于 auto-mode 分类器的精简摘要
+ fn classifier_summary(&self, args: &Value) -> String {
+ format!("{}", self.name())
+ }
+
+ /// 是否是只读操作(与并发安全不同——glob 是 readonly 但不能和 bash 并发)
+ fn is_readonly(&self) -> bool { false }
+
+ /// 是否需要延迟加载工具描述(大工具可延迟以减小 prompt)
+ fn defer_loading(&self) -> bool { false }
+}
+```
+
+**P3: 工具 allow/deny 列表**
+
+参考 Claude Code 的做法,AstroResearch 已有 `subagent_allowed_tools`,可扩展:
+
+```rust
+// 为子代理/异步代理定义工具过滤策略
+pub struct ToolFilterPolicy {
+ /// 全局禁止(不计代理类型)
+ pub all_agent_disallowed: Vec,
+ /// 自定义代理禁用(不能 spawn 子代理 + 编辑文件的代理)
+ pub custom_agent_disallowed: Vec,
+ /// 异步代理白名单(只读子集)
+ pub async_agent_allowed: Vec,
+}
+```
+
+---
+
+## 7. Memory 持久化
+
+### 8.1 对比
+
+AstroResearch 与 Claude Code 在 Memory 设计上高度相似(都是文件系统 frontmatter markdown +
+`MEMORY.md` 索引),差距很小。
+
+Claude Code 多了:
+- **Team Memory**: 共享给团队的记忆(AstroResearch 有 team 系统但无 team memory)
+- **Session Memory compaction**: 压缩时自动通过 LLM 提取记忆到文件
+
+### 8.2 建议改进
+
+**P3: Session Memory Compaction**
+
+压缩时自动提取记忆:
+
+```rust
+/// 在 compact.rs 的 auto_compact 过程中提取 session memory
+pub async fn extract_session_memory(
+ llm: &LlmClient,
+ messages: &[ChatMessage],
+) -> Vec {
+ // 用专门的小 prompt 让 LLM 从对话中提取可持久化的记忆
+ // 返回候选记忆列表,由 MemoryManager 做 dedup + 衰减
+}
+```
+
+---
+
+## 8. Coordinator / Multi-Agent
+
+### 9.1 Claude Code 的 Coordinator Mode
+
+```
+Coordinator Mode 架构:
+┌─────────────────────────────────────────────────┐
+│ Coordinator (Coordinator System Prompt) │
+│ Tools: Agent, SendMessage, TaskStop, │
+│ SyntheticOutput (仅 4 个) │
+│ │
+│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
+│ │ Worker 1 │ │ Worker 2 │ │ Worker 3 │ │
+│ │ (async) │ │ (async) │ │ (async) │ │
+│ │ standard │ │ standard │ │ standard │ │
+│ │ tools │ │ tools │ │ tools │ │
+│ └──────────┘ └──────────┘ └──────────┘ │
+│ │
+│ Worker 结果以 返回 │
+│ Coordinator 做 Synthesis → 下一轮 Workers │
+└─────────────────────────────────────────────────┘
+```
+
+### 9.2 关键设计点
+
+1. **Coordinator 只看到 4 个工具**——它不能直接读文件或执行 bash,只能编排 Workers
+2. **Workers 全异步**——Coordinator 不等待,结果以 `` XML 注入
+3. **Continue-vs-Spawn 决策矩阵**:
+ - 新任务与已有 Worker 上下文重叠 <30% → 创建新 Worker
+ - 新任务是对已有 Worker 的跟进 → `SendMessage` 继续
+4. **Worker prompt 写法规范**:自包含(self-contained),包含完整 spec,明确交付物
+
+### 9.3 AstroResearch 的现状
+
+AstroResearch 有 `SubAgentTool` + `TeamManager`,但没有 Coordinator 的概念。Team
+是平级的(lead ↔ teammates),不是层级编排。
+
+### 9.4 建议改进
+
+**P2: Coordinator Mode 原型**
+
+```
+当 Agent 检测到复杂多步骤任务时,自动切换为 Coordinator 模式:
+
+用户请求
+ → Coordinator 做任务分解
+ → 并行子代理执行 (SubAgentTool, async)
+ → 结果综合
+ → 减少单 Agent 的步骤数和 token 消耗
+
+关键实现:
+1. Coordinator system prompt: 类似 Claude Code coordinatorMode.ts
+2. 仅暴露 SubAgentTool + 少量管理工具
+3. 子代理结果以结构化格式注入
+4. 综合阶段由 Coordinator 处理
+```
+
+---
+
+## 9. Hermes-Agent 的独特贡献
+
+### 10.1 文件位置
+
+`/home/fmq/program/AstroResearch/libs/hermes-agent/`
+
+### 10.2 Hermes-Agent vs Claude Code 架构对比
+
+| 关注点 | Hermes-Agent | Claude Code |
+|--------|-------------|-------------|
+| 语言 | Python 3.11+ | TypeScript (Node.js 20+) |
+| Agent 循环 | 同步 `while` 循环 | 异步 `query()` generator |
+| 工具注册 | 文件系统自动发现 (`tools/*.py`) | 手动 import + `getAllBaseTools()` |
+| 工具接口 | `handler(args) -> JSON string` | `Tool` — ~70 方法 |
+| 状态管理 | SQLite (SessionDB + FTS5) | 内存 `AppState` React store |
+| Plugin 系统 | PluginManager + 生命周期 hooks | Plugin loader + MCP 集成 |
+| Profile 隔离 | 多 profile + 独立 `HERMES_HOME` | 单 profile |
+| 子代理 | 子 `AIAgent` 实例 | `LocalAgentTask` + `runAgent()` |
+| Swarm/Team | Kanban 工作队列 | `InProcessTeammateTask` + coordinator |
+| 上下文压缩 | `ContextCompressor` | `compact/` 服务 |
+| MCP 支持 | `mcp_tool.py` + catalog | 完整的 `services/mcp/` |
+| 定位 | 个人 AI 助手(自我改进、记忆、跨平台) | 编程 AI 助手(终端集成、文件操作) |
+
+### 10.3 Hermes-Agent 的核心架构
+
+```
+hermes-agent/
+├── run_agent.py # AIAgent 类 (~11k LOC) — 核心入口
+├── model_tools.py # 工具编排层 (~2.7k LOC)
+├── toolsets.py # 工具集定义
+├── hermes_state.py # SessionDB — SQLite + FTS5
+├── cli.py # HermesCLI 类 (~11k LOC)
+│
+├── agent/ # Agent 内部
+│ ├── conversation_loop.py # 主循环
+│ ├── turn_context.py # 每轮上下文 dataclass
+│ ├── system_prompt.py # 三层 System Prompt
+│ ├── prompt_builder.py # Prompt 片段构建
+│ ├── memory_manager.py # 记忆编排
+│ ├── context_compressor.py # 上下文压缩
+│ ├── tool_executor.py # 工具执行分发
+│ ├── tool_guardrails.py # 安全 guardrails
+│ └── curator.py # 后台 Skill 生命周期管理
+│
+├── tools/ # 工具实现(自动发现)
+│ ├── registry.py # ToolRegistry(discover_builtin_tools)
+│ ├── delegate_tool.py # 子代理 spawn
+│ ├── skills_tool.py # Skill 管理
+│ └── cronjob_tools.py # Cron 调度
+│
+└── hermes_cli/ # CLI 子系统
+ ├── plugins.py # PluginManager + 生命周期 hooks
+ └── profiles.py # 多 Profile 隔离
+```
+
+### 10.4 最值得借鉴的 Hermes 特性
+
+**Self-improving Skills**
+
+```
+工作流:
+1. Agent 在科研中反复使用某流程
+ (如: 搜索某类天体 → 下载论文 → RAG → 总结)
+2. Agent 自动检测重复模式
+3. Agent 创建 Skill 保存该流程
+4. Curator 管理 Skill 生命周期
+5. 下次相似查询直接加载 Skill,无需重新探索
+
+AstroResearch 已有 Skills 系统 (skills.rs),缺少:
+- 自动检测重复模式
+- Agent 自主创建 Skill
+- Curator 管理 Skill 质量
+```
+
+**Session FTS5 搜索**
+
+```python
+# Hermes 的 SessionDB 使用 SQLite FTS5 实现跨 session 搜索
+class SessionDB:
+ def search_sessions(self, query: str) -> List[Session]:
+ """全文本搜索所有历史会话"""
+ return self.db.execute(
+ "SELECT * FROM sessions WHERE sessions MATCH ?", (query,)
+ )
+```
+
+AstroResearch 的 `agent_sessions` 表有基本的 title/status 字段,但没有全文搜索。
+
+### 10.5 建议改进
+
+**P2: Self-improving Skills 原型**
+
+```
+1. Pattern Detector (自动检测)
+ - 监控 N 个 session 中的工具调用序列
+ - 使用简单的子序列匹配识别重复模式
+ - 阈值: 3 次相似序列 → 候选 Skill
+
+2. Skill Creator (Agent 自主创建)
+ - 将候选 Skill 展示给用户确认
+ - 生成 SKILL.md 文件(含 when_to_use, steps)
+ - 注册到 SkillRegistry
+
+3. Curator (管理生命周期)
+ - 跟踪 Skill 使用频率
+ - 长时间未用的 Skill 标记为 stale
+ - 提示用户审查或删除
+```
+
+---
+
+## 10. 优先级排序 —— 建议实施路线
+
+按价值/投入比排序:
+
+| 优先级 | 改进项 | 来源 | 状态 | 价值 | 预估投入 | 依赖 |
+|--------|--------|------|------|------|---------|------|
+| **P0** | Context Overflow 自动修复 | Claude Code | ✅ | 减少 ~50% LLM 调用失败 | ~30 行 | 无 |
+| **P0** | StreamingExecutor 真正的流式调度 | Claude Code | ✅ | 减少 30-50% 工具执行延迟 | ~200 行 | 无 |
+| **P0** | Executor 集成分区器(批次串行/并行) | Claude Code | ✅ | 修复非安全工具错误并发 | ~300 行 | ToolPartitioner |
+| **P1** | 工具并发分区 `partition_tool_calls` | Claude Code | ✅ | 批量只读操作 3-5x 加速 | ~100 行 | 无 |
+| **P1** | PermissionRequest / PermissionDenied Hook 事件 | Claude Code | ✅ | 安全审计能力 | ~100 行 | 无 |
+| **P1** | Auto-mode Classifier(廉价模型预分类) | Claude Code | ⏳ | 消除 80%+ 权限弹窗 | ~300 行 | LLM client 支持 |
+| **P2** | Self-improving Skills(Agent 保存成功流程) | Hermes | ✅ | 科研场景独特价值 | ~500 行 | Skills 系统 |
+| **P2** | Coordinator Mode(层级多 Agent 编排) | Claude Code | ⏳ | 复杂任务效果提升 | ~800 行 | SubAgent + Team |
+| **P2** | UserPromptSubmit / PreCompact / PostCompact Hook | Claude Code | ⏳ | Hook 系统完善 | ~200 行 | 无 |
+| **P3** | FTS5 跨 session 搜索 | Hermes | ⏳ | 历史研究可复用 | 中 | SQLite 迁移 |
+| **P3** | Tool `defer_loading` / `classifier_summary` | Claude Code | ⏳ | 减小 tool schema prompt | ~50 行 | 无 |
+| **P3** | 模型回退策略 (Model Fallback) | Claude Code | ⏳ | 提高可用性 | ~200 行 | ErrorRecovery |
+| **P3** | Session Memory Compaction | Claude Code | ⏳ | 自动化记忆提取 | ~300 行 | MemoryManager + Compact |
+
+### 实施进度(2026-06-22)
+
+1. **P0 项 — 全部完成** ✅
+ - Context Overflow 自动修复(`error_recovery.rs`)
+ - StreamingExecutor 真正流式调度(`streaming_executor.rs` 重写)
+ - Executor 集成分区器(`executor.rs` Phase 3 重写)
+2. **P1 项 — 部分完成**
+ - ✅ 工具并发分区
+ - ✅ PermissionRequest / PermissionDenied Hook 事件
+ - ⏳ Auto-mode Classifier — 需要设计讨论
+3. **P2 项在下一个大版本规划**:需要设计讨论和更多测试
+4. **P3 项作为 backlog**:长期优化方向
+
+---
+
+## 附录 A: Claude Code 关键源码索引
+
+| 文件 | 用途 | 与 AstroResearch 对应 |
+|------|------|----------------------|
+| `src/Tool.ts` | Tool 类型 + buildTool factory | `src/agent/tools/mod.rs` |
+| `src/tools.ts` | 工具注册 + assembleToolPool | `ToolRegistry::new()` |
+| `src/services/tools/toolExecution.ts` | 工具执行管道 | `executor.rs` |
+| `src/services/tools/toolOrchestration.ts` | 并发分区 + 批量执行 | `partitioner.rs` + `executor.rs` |
+| `src/services/tools/StreamingToolExecutor.ts` | 流式工具执行 | `streaming_executor.rs` |
+| `src/services/api/withRetry.ts` | 错误重试 + 退避 | `error_recovery.rs` |
+| `src/services/api/claude.ts` | API 流式调用 | `streaming.rs` |
+| `src/constants/prompts.ts` | System Prompt 构建 | `system_prompt.rs` |
+| `src/utils/hooks.ts` | Hook 执行引擎 (5022 行) | `hooks/dispatch.rs` |
+| `src/utils/permissions/permissions.ts` | 权限逻辑 | `permission.rs` |
+| `src/utils/permissions/yoloClassifier.ts` | Auto-mode 分类器 | 无 (建议新增) |
+| `src/tools/AgentTool/runAgent.ts` | 子代理执行引擎 | `subagent.rs` |
+| `src/tools/AgentTool/AgentTool.tsx` | 子代理编排 | `tools/subagent.rs` |
+| `src/coordinator/coordinatorMode.ts` | Coordinator 模式 | 无 (建议参考) |
+| `src/memdir/memdir.ts` | Memory 文件系统 | `memory/mod.rs` |
+| `src/skills/loadSkillsDir.ts` | Skill 加载 | `skills.rs` |
+
+## 附录 B: Hermes-Agent 关键源码索引
+
+| 文件 | 用途 | 与 AstroResearch 对应 |
+|------|------|----------------------|
+| `run_agent.py` | AIAgent 核心类 | `runtime/mod.rs` |
+| `agent/conversation_loop.py` | Agent 主循环 | `runtime/mod.rs` (ReAct loop) |
+| `agent/system_prompt.py` | 三层 System Prompt | `system_prompt.rs` |
+| `agent/context_compressor.py` | 上下文压缩 | `compact.rs` |
+| `agent/memory_manager.py` | 记忆编排 | `memory/mod.rs` |
+| `agent/curator.py` | Skill 生命周期管理 | 无 (建议参考) |
+| `tools/registry.py` | 工具自动发现 | `tools/mod.rs` |
+| `tools/delegate_tool.py` | 子代理 | `subagent.rs` |
+| `hermes_state.py` | SessionDB + FTS5 | `api/agent.rs` (sessions) |
+| `hermes_cli/plugins.py` | PluginManager | 无 |
+| `hermes_cli/profiles.py` | 多 Profile 隔离 | 无 |
+| `model_tools.py` | 工具编排 | `executor.rs` |
+
+## 附录 C: 变更日志
+
+### 2026-06-22 — 首轮实施
+
+**P0: Context Overflow 自动修复**
+- `src/agent/runtime/error_recovery.rs`: +150 行
+ - 新增 `ContextOverflowInfo`、`parse_context_overflow()`、`calculate_safe_max_tokens()`
+ - 新增 `RecoveryStep::AdjustMaxTokens`、`extract_three_numbers()`
+ - 12 个新增测试(Anthropic/OpenAI/Generic 格式 + 边界 + 恢复优先级)
+- `src/agent/runtime/mod.rs`: AgentRuntime 集成调用点
+- `Cargo.lock`: 无新增依赖(使用已有 `regex` crate)
+
+**P0: StreamingExecutor 真正流式调度**
+- `src/agent/runtime/streaming_executor.rs`: 重写 ~400 行(原 316 行)
+ - `on_tool_use` 中对并发安全工具立即 `tokio::spawn`
+ - `tokio::select!` 在工具执行和 Sibling Abort 之间竞速
+ - `collect_completed_tasks()` 使用 `JoinHandle::is_finished()` 非阻塞检查
+ - `executing_non_concurrent` 标志实现独占执行
+- `src/agent/tools/mod.rs`: `ToolContext` 添加 `Clone` derive
+
+**P0: Executor 集成分区器**
+- `src/agent/runtime/executor.rs`: Phase 3 重写 + 2 个提取函数
+ - Phase 3a: 构建非拒绝工具的 (原索引, PreparedCall) 映射
+ - Phase 3b: `ToolPartitioner::partition()` 分区
+ - Phase 3c: 逐批次执行(并行批次 `FuturesUnordered`,串行批次逐个执行)
+ - 提取 `execute_single_tool()` 和 `process_single_result()` 辅助函数
+- `src/agent/runtime/partitioner.rs`: +2 测试(`run_bash`、`file_write` 打断并发批)
+
+**P1: PermissionRequest / PermissionDenied Hook 事件**
+- `src/agent/hooks/types.rs`: +80 行
+ - 新增 `HookEvent::PermissionRequest`、`HookEvent::PermissionDenied`
+ - 新增 `PermissionRequestContext`、`PermissionDeniedContext`
+ - 新增 `PermissionRequestAction`、`PermissionDecision`、`PermissionDenialSource`
+- `src/agent/hooks/traits.rs`: +20 行
+ - `AgentHook::on_permission_request()`、`AgentHook::on_permission_denied()`
+- `src/agent/hooks/dispatch.rs`: +120 行
+ - `run_on_permission_request()` (并行调用,第一个 Override 生效)
+ - `run_on_permission_denied()` (fire-and-forget 审计)
+- `src/agent/hooks/mod.rs`: 更新 re-exports
+
+### 2026-06-22 — Self-improving Skills
+
+**P2: Self-improving Skills(模式检测 + 自动创建 + Curator)**
+- `src/agent/skills/pattern_detector.rs`: +420 行
+ - `PatternDetector` — 从 `agent_messages` DB 表扫描工具调用序列
+ - `DetectedPattern` — 跨 session 重复模式(携带出现次数、置信度、指纹)
+ - 滑动窗口子序列提取 + Jaccard 相似度去重 + 超序列包含检测
+ - 8 个单元测试(子序列提取、指纹、去重、Jaccard 计算)
+- `src/agent/skills/curator.rs`: +700 行
+ - `Curator` — Skill 生命周期管理(Active → Inactive → Stale → Deprecated)
+ - `SkillQuality` — 基于调用次数和新鲜度的质量评分(对数 + 指数衰减)
+ - `CuratorReport` — 分析报告 + 清理建议列表
+ - `archive_stale_skills()` — 将过期 skill 移动到归档目录
+ - `CuratorRunner` — 后台空闲触发审查(`should_run_now()` + `record_activity()` 心跳)
+ - 12 个单元测试(5 种生命周期状态 + 质量评分 + 清理候选 + pinned + runner)
+- `src/agent/skills.rs`: +230 行
+ - `SkillCreator` — 将 `DetectedPattern` 转为 SKILL.md 文件(kebab-case 命名 + YAML frontmatter)
+ - `SelfImprovePipeline` — 一站式管道(检测 → 创建 → 质量审查)
+ - `SelfImproveResult` — 管道结果(patterns_found + skills_created + curator_report)
+ - `SkillFrontmatter` + `SkillMeta` 增加 `pinned` 字段
+
+### 2026-06-22 — Hermes Curator 特性补齐
+
+**Pinned Skills(不可清理)**
+- `src/agent/skills.rs`: `SkillFrontmatter.pinned` + `SkillMeta.pinned` — YAML `pinned: true`
+- `src/agent/skills/curator.rs`: `evaluate_quality(pinned)` — 强制 Active + min_score 0.8;`analyze` 过滤 pinned 不进入 cleanup_candidates
+
+**Seed Record(新 Skill 锚定时钟)**
+- `src/agent/skills/curator.rs`: `evaluate_quality` 对无统计记录 skill 设置 `days_since_last_use=Some(0)`(等效刚创建),`NEW_SKILL_GRACE_PERIOD_DAYS=7` 防止立即 stale
+
+**CuratorRunner(后台空闲触发审查)**
+- `src/agent/skills/curator.rs`: `CuratorRunner` 结构体 + `should_run_now()`(paused/idle/interval 三重检查)+ `record_activity()` 心跳 + `run_once()` + `spawn()` tokio 后台任务 + `pause()`/`resume()`
+- 5 个新增测试(pinned_always_active、pinned_not_in_cleanup、seed_record、runner_paused、runner_idle)
diff --git a/docs/architecture/agent/hooks.md b/docs/architecture/agent/hooks.md
index 40d9fd0..d166101 100644
--- a/docs/architecture/agent/hooks.md
+++ b/docs/architecture/agent/hooks.md
@@ -1,274 +1,921 @@
-# Hooks 生命周期系统 (`hooks.rs`)
+# Agent Hooks — 生命周期事件系统
-参考 Claude Code hooks 协议,提供 **9 种生命周期事件回调**,基于 **观察者模式 + 责任链模式** 实现。
+参考 Claude Code hooks 协议,提供 **12 种生命周期事件回调**,基于 **观察者模式 + 责任链模式** 实现。核心目标:在 Agent ReAct 循环的各个关键节点插入横切关注点(取消检查、指标采集、审计日志、权限增强等),**不污染主循环代码**。
-核心思路:允许在 Agent 运行的各个关键节点插入自定义逻辑(取消检查、指标采集、审计日志等),而不污染核心 ReAct 循环代码。
+---
+
+## 源码结构
+
+```
+src/agent/hooks/
+├── mod.rs — 模块入口(声明 + 重导出 + 测试,~540 行)
+├── types.rs — 数据类型定义(事件、上下文、动作、结果,~370 行)
+├── traits.rs — AgentHook + AsyncAgentHook trait(~120 行)
+├── matcher.rs — ToolNamePattern / ToolMatchFilter 工具匹配器(~300 行)
+├── registry.rs — HookRegistry 结构体 + 基础方法(~280 行)
+├── dispatch.rs — HookRegistry 调度方法(所有 run_*,~700 行)
+└── builtins.rs — 内置 Hooks(CancellationHook、MetricsHook、AuditLogHook、ContextDeduplicator,~260 行)
+```
+
+---
+
+## 设计哲学
+
+```mermaid
+mindmap
+ root((Hook 系统
设计原则))
+ 接口隔离
+ 10 个生命周期方法
+ +2 个权限专用方法 (on_permission_request/on_permission_denied)
+ 全部有默认空实现
+ 只覆写关心的 hook 点
+ 事件索引
+ subscribed_events 声明
+ 预计算 event_index
+ 避免无关 hook 调用
+ 工具级过滤
+ match_filter 声明
+ ToolNamePattern 匹配
+ content_patterns 过滤
+ 并行优先
+ 无依赖事件 join_all
+ 有副作用事件顺序执行
+ OnSessionStop 例外
+ 超时隔离
+ per-hook timeout 可配置
+ 默认 30 秒
+ 超时跳过不拖垮全部
+ 聚合不丢
+ additional_contexts 全保留
+ tagged_contexts 有来源
+ blocking_errors 全收集
+ warnings/metadata 不丢失
+ 双模式共存
+ 全局 hooks 构造时注册
+ session hooks 运行时动态
+ AsyncAgentHook 异步 fire-and-forget
+ 析构自动清理
+```
+
+---
## 架构总览
```mermaid
graph TB
- subgraph Registry["HookRegistry"]
+ subgraph RT["AgentRuntime::run_turn()"]
direction TB
- Methods["聚合方法(遍历所有 hook 依次调用)
run_on_session_start() | run_pre_tool_use()
run_post_tool_use() | run_on_step_complete()
run_on_session_stop() | run_on_subagent_start()
run_on_subagent_stop() | run_on_pre_compact()
run_on_post_compact()"]
+ Start["OnSessionStart"] --> Loop["ReAct Loop"]
+ Loop --> Comp["PreCompact / PostCompact"]
+ Loop --> Stop["OnSessionStop"]
end
- Registry --> CH["CancellationHook
Arc<HashSet<String>>"]
- Registry --> MH["MetricsHook
Arc<Mutex<MetricsData>> (共享)"]
- Registry --> AH["AuditLogHook
SqlitePool (fire-and-forget 写入)"]
+ subgraph Registry["HookRegistry"]
+ direction LR
+ Index["event_index
HashMap<HookEvent, Vec<usize>>
预计算索引"]
+ AIndex["async_event_index
异步 hook 索引"]
+ SH["session_hooks
HashMap<String, Vec<Box<dyn AgentHook>>
运行时动态管理"]
+ end
+
+ subgraph Executor["executor::execute_parallel()"]
+ direction LR
+ Pre["PreToolUse
并行 join_all + timeout + match_filter"]
+ Exec["工具执行"]
+ Post["PostToolUse
并行 join_all + timeout + match_filter"]
+ Fail["PostToolUseFailure
is_error 时触发"]
+ Async["AsyncAgentHook
fire-and-forget, 5s 超时"]
+ end
+
+ subgraph Builtins["内置 Hooks(3 个 sync + 0 async)"]
+ CH["CancellationHook
订阅: PreToolUse, OnSessionStop"]
+ MH["MetricsHook
订阅: OnSessionStart, PostToolUse,
OnStepComplete, OnSessionStop"]
+ AH["AuditLogHook
订阅: PostToolUse, OnSessionStop"]
+ end
+
+ RT --> Registry
+ Loop --> Executor
+ Registry --> Builtins
+ Executor --> Registry
+ Executor --> Async
```
-## 生命周期事件(9 个)
+---
-| # | 事件 | 触发时机 | 返回值 | 调用位置 |
-|---|------|---------|--------|---------|
-| 1 | `OnSessionStart` | 会话创建/恢复 | 无 (fire-and-forget) | `AgentRuntime::run_turn()` |
-| 2 | `PreToolUse` | 每个工具执行前 | `PreToolUseAction` (可拦截/修改参数) | `executor::execute_parallel()` |
-| 3 | `PostToolUse` | 每个工具执行后 | `PostToolUseAction` (可修改输出) | `executor::execute_parallel()` |
-| 4 | `OnStepComplete` | 每步 ReAct 结束 | 无 | `AgentRuntime::run_react_loop()` |
-| 5 | `OnSessionStop` | 会话终止(任何原因) | 无 | `finalize::finalize_turn()` |
-| 6 | `OnSubagentStart` | 子代理启动 | 无 | `SubAgentRunner::run()` |
-| 7 | `OnSubagentStop` | 子代理停止 | 无 | `SubAgentRunner::run()` |
-| 8 | `OnPreCompact` | 上下文压缩前 | 无 | `compact::compress_context_with_hooks()` |
-| 9 | `OnPostCompact` | 上下文压缩后 | 无 | `compact::compress_context_with_hooks()` |
+## 类型系统
-## 核心类型
+```mermaid
+classDiagram
+ class AgentHook {
+ <>
+ +name() &str
+ +subscribed_events() &[HookEvent]
+ +timeout() Option~Duration~
+ +match_filter() ToolMatchFilter
+ +on_session_start(ctx)
+ +pre_tool_use(ctx) PreToolUseAction
+ +post_tool_use(ctx) PostToolUseAction
+ +on_post_tool_use_failure(ctx) PostToolUseAction
+ +on_step_complete(ctx)
+ +on_session_stop(ctx)
+ +on_subagent_start(ctx)
+ +on_subagent_stop(ctx)
+ +on_pre_compact(ctx)
+ +on_post_compact(ctx)
+ +on_permission_request(ctx) PermissionRequestAction
+ +on_permission_denied(ctx)
+ }
-### AgentHook trait
+ class AsyncAgentHook {
+ <>
+ +name() &str
+ +subscribed_events() &[HookEvent]
+ +on_post_tool_use_async(ctx)
+ +on_post_tool_use_failure_async(ctx)
+ +on_session_stop_async(ctx)
+ }
+
+ class HookEvent {
+ <>
+ OnSessionStart
+ PreToolUse
+ PostToolUse
+ PostToolUseFailure
+ OnStepComplete
+ OnSessionStop
+ OnSubagentStart
+ OnSubagentStop
+ OnPreCompact
+ OnPostCompact
+ PermissionRequest
+ PermissionDenied
+ }
+
+ class ToolMatchFilter {
+ +name_patterns: Vec~ToolNamePattern~
+ +content_patterns: Vec~String~
+ +matches(tool_name, tool_args) bool
+ +exact(name) Self
+ +prefix(name) Self
+ +is_match_all() bool
+ }
+
+ class ToolNamePattern {
+ <>
+ Exact(String)
+ Prefix(String)
+ Wildcard
+ +parse(pattern) Self
+ +matches(tool_name) bool
+ }
+
+ class PreToolUseAction {
+ <>
+ Continue
+ Block ~reason: String~
+ MutateInput ~updated_args, additional_context~
+ PermissionRequired ~permission, tool_name~
+ }
+
+ class PermissionRequestAction {
+ <>
+ Continue
+ Override ~decision: PermissionDecision, reason~
+ InjectContext ~context: String~
+ }
+
+ class PermissionDecision {
+ <>
+ Allow
+ Ask
+ Deny
+ }
+
+ class PermissionDenialSource {
+ <>
+ Rule
+ Classifier
+ User
+ Timeout
+ }
+
+ class PostToolUseAction {
+ <>
+ Continue
+ MutateOutput ~updated_content, additional_context~
+ Warning ~message, truncate_output~
+ PermissionRequired ~permission, tool_name~
+ Metadata ~key, value~
+ }
+
+ class PreToolUseResult {
+ +action: PreToolUseAction
+ +additional_contexts: Vec~String~
+ +additional_context: Option~String~
+ +blocking_errors: Vec~BlockingError~
+ +final_args: Value
+ +tagged_contexts: Vec~TaggedContext~
+ }
+
+ class PostToolUseResult {
+ +final_content: String
+ +additional_contexts: Vec~String~
+ +tagged_contexts: Vec~TaggedContext~
+ +warnings: Vec~String~
+ +post_permission_requests: Vec~(String,String)~
+ +metadata: HashMap~String, Value~
+ }
+
+ class BlockingError {
+ +hook_name: String
+ +reason: String
+ }
+
+ class TaggedContext {
+ +hook_name: String
+ +source_event: HookEvent
+ +content: String
+ +timestamp: DateTime~Utc~
+ }
+
+ class HookRegistry {
+ -hooks: Vec~Box~dyn AgentHook~~
+ -event_index: HashMap
+ -session_hooks: HashMap
+ -async_hooks: Vec~Box~dyn AsyncAgentHook~~
+ -async_event_index: HashMap
+ +with_builtins(db, cancelled, metrics) Self
+ +add(hook)
+ +add_async(hook)
+ +add_session_hook(sid, hook)
+ +remove_session_hook(sid, name) bool
+ +clear_session_hooks(sid)
+ +run_pre_tool_use(ctx) PreToolUseResult
+ +run_post_tool_use(ctx) PostToolUseResult
+ +run_on_session_start(ctx)
+ +run_on_step_complete(ctx)
+ +run_on_session_stop(ctx)
+ +run_on_subagent_start(ctx)
+ +run_on_subagent_stop(ctx)
+ +run_on_pre_compact(ctx)
+ +run_on_post_compact(ctx)
+ +run_on_post_tool_use_failure(ctx) PostToolUseResult
+ +run_on_permission_request(ctx) PermissionRequestAction
+ +run_on_permission_denied(ctx)
+ }
+
+ AgentHook --> HookEvent : subscribes to
+ AgentHook --> ToolMatchFilter : declares
+ AgentHook --> PreToolUseAction : returns
+ AgentHook --> PostToolUseAction : returns
+ AsyncAgentHook --> HookEvent : subscribes to
+ HookRegistry --> AgentHook : manages
+ HookRegistry --> AsyncAgentHook : manages
+ HookRegistry --> PreToolUseResult : aggregates
+ HookRegistry --> PostToolUseResult : aggregates
+ PreToolUseResult --> PreToolUseAction
+ PreToolUseResult --> BlockingError
+ PreToolUseResult --> TaggedContext
+ PostToolUseResult --> TaggedContext
+```
+
+### 上下文类型(Context Objects)
+
+每个生命周期事件携带专用的不可变上下文:
+
+| 事件 | 上下文类型 | 关键字段 |
+|------|-----------|---------|
+| OnSessionStart | `SessionStartContext` | session_id, turn_index, is_resume |
+| PreToolUse | `PreToolUseContext` | session_id, tool_name, tool_args, step |
+| PostToolUse | `PostToolUseContext` | session_id, agent_name, tool_name, tool_args, output_content, is_error, step, elapsed_ms |
+| PostToolUseFailure | `PostToolUseFailureContext` | session_id, agent_name, tool_name, error_message, is_interrupt, step, elapsed_ms |
+| OnStepComplete | `StepCompleteContext` | session_id, step, max_steps, messages_count, estimated_tokens, token_limit |
+| OnSessionStop | `SessionStopContext` | session_id, terminal (TurnTerminal), total_steps |
+| OnSubagentStart | `SubagentStartContext` | parent_session_id, subagent_name, prompt |
+| OnSubagentStop | `SubagentStopContext` | parent_session_id, subagent_name, result_summary, steps, is_error |
+| OnPreCompact | `PreCompactContext` | session_id, message_count, estimated_tokens |
+| OnPostCompact | `PostCompactContext` | session_id, new_message_count, compression_method |
+| PermissionRequest | `PermissionRequestContext` | session_id, tool_name, current_decision, permission_mode, is_subagent |
+| PermissionDenied | `PermissionDeniedContext` | session_id, tool_name, reason, source (Rule/Classifier/User/Timeout) |
+
+---
+
+## ToolMatchFilter — 工具级过滤
+
+比 Claude Code 的 `if` 条件更结构化。Hook 通过 `match_filter()` 返回过滤器,HookRegistry 在 dispatch 时仅对匹配的工具名和参数调用该 hook。
+
+### ToolNamePattern
+
+| 模式 | 示例输入 | 匹配 |
+|------|---------|------|
+| `Exact("search_papers")` | `"search_papers"` | ✅ |
+| `Exact("search_papers")` | `"download_paper"` | ❌ |
+| `Prefix("file_")` | `"file_write"` | ✅ |
+| `Prefix("file_")` | `"search_papers"` | ❌ |
+| `Wildcard` | 任意 | ✅ |
+
+### ToolMatchFilter
+
+```rust
+// 仅匹配文件相关工具且参数路径包含 .env
+let filter = ToolMatchFilter {
+ name_patterns: vec![ToolNamePattern::parse("file_*")],
+ content_patterns: vec!["path:.env".to_string()],
+};
+```
+
+- **快捷构造**:`ToolMatchFilter::exact("search_papers")`、`ToolMatchFilter::prefix("file_")`
+- **默认行为**:`ToolMatchFilter::default()` 匹配所有工具
+- **仅对** `PreToolUse`、`PostToolUse`、`PostToolUseFailure` 生效
+
+---
+
+## PostToolUseAction — 5 种变体
+
+| 变体 | 用途 | 对执行的影响 |
+|------|------|------------|
+| `Continue` | 默认,保持输出不变 | 无 |
+| `MutateOutput { updated_content, additional_context }` | 修改工具输出内容 | 最终输出替换为 updated_content |
+| `Warning { message, truncate_output }` | 标记警告(不阻止) | 注入 `` |
+| `PermissionRequired { permission, tool_name }` | 事后权限请求(advisory) | 记录审计日志 |
+| `Metadata { key, value }` | 附加结构化元数据 | 保留在 PostToolUseResult.metadata 中 |
+
+---
+
+## 异步 Hook (AsyncAgentHook)
+
+与 `AgentHook` 分离设计,适合后台操作(远程上报、大文件处理等):
```rust
#[async_trait]
-pub trait AgentHook: Send + Sync {
+pub trait AsyncAgentHook: Send + Sync {
fn name(&self) -> &str;
- async fn on_session_start(&self, _ctx: &SessionStartContext) {}
- async fn pre_tool_use(&self, _ctx: &PreToolUseContext) -> PreToolUseAction { ... }
- async fn post_tool_use(&self, _ctx: &PostToolUseContext) -> PostToolUseAction { ... }
- async fn on_step_complete(&self, _ctx: &StepCompleteContext) {}
- async fn on_session_stop(&self, _ctx: &SessionStopContext<'_>) {}
- async fn on_subagent_start(&self, _ctx: &SubagentStartContext) {}
- async fn on_subagent_stop(&self, _ctx: &SubagentStopContext) {}
- async fn on_pre_compact(&self, _ctx: &PreCompactContext) {}
- async fn on_post_compact(&self, _ctx: &PostCompactContext) {}
+ fn subscribed_events(&self) -> &[HookEvent] { &[] }
+
+ async fn on_post_tool_use_async(&self, _ctx: PostToolUseContext) {}
+ async fn on_post_tool_use_failure_async(&self, _ctx: PostToolUseFailureContext) {}
+ async fn on_session_stop_async(&self, _ctx: SessionStopContext<'_>) {}
}
```
-所有 9 个方法都有默认空实现——hook 实现者只需覆写关心的 hook 点,遵循**接口隔离原则**。
+- **注册**:`registry.add_async(Box::new(my_async_hook))`
+- **调度**:与 sync hook 并行执行,默认 5s 超时
+- **返回值**:无——fire-and-forget 语义
-### PreToolUseAction(工具执行前返回值)
+---
+
+## TaggedContext — Hook 来源追踪
+
+注入 LLM 的 system-reminder 携带 hook 来源标记:
+
+```
+[Hook: AuditLogHook | PostToolUse] 内容...
+[Hook: CancellationHook | PreToolUse] 内容...
+```
+
+替代原来的匿名 `[Hook 注入上下文]`。
+
+---
+
+## ContextDeduplicator — 内容去重
+
+单 dispatch cycle 内防止多个 hook 注入相同上下文消息:
```rust
-pub enum PreToolUseAction {
- Continue, // 允许执行(默认)
- Block { reason: String }, // 阻止执行
- MutateInput { // 修改参数 + 注入上下文
- updated_args: serde_json::Value,
- additional_context: Option,
- },
- PermissionRequired { permission: String, tool_name: String }, // 需要权限决策 (Phase 2)
+let mut dedup = ContextDeduplicator::new();
+for ctx in &result.hook_contexts {
+ if !dedup.is_duplicate(ctx) {
+ messages.push(ChatMessage::user(ctx));
+ }
}
```
-向后兼容:`pub type HookAction = PreToolUseAction;`
+---
-### PostToolUseAction(工具执行后返回值)
+## 权限决策优先级
-```rust
-pub enum PostToolUseAction {
- Continue, // 保持输出不变
- MutateOutput { updated_content: String }, // 修改输出内容
-}
-```
+当多个权限来源冲突时,`resolve_permission_precedence()` 按以下优先级裁决:
-### 聚合结果类型
+| Priority | Source | Overridable By |
+|----------|--------|----------------|
+| **P0** | `PermissionChecker::Deny` | 不可覆盖 |
+| **P1** | Tool-level `PermissionRule::Deny` | 不可覆盖 |
+| **P2** | Session-level Checker::Deny | 不可覆盖 |
+| **P3** | Hook `PreToolUseAction::Block` | P0-P2 |
+| **P4** | Hook `PermissionRequired` | P0-P3 |
+| **P5** | Session-level Checker::Ask | 仅 Allow → Ask 升级 |
+| **P6** | Tool-level `PermissionRule::Ask` | 仅 Allow → Ask 升级 |
+| **P7** | `PermissionChecker::Allow` | — |
+| **P8** | Implicit Allow (default) | — |
-```rust
-pub struct PreToolUseResult {
- pub action: PreToolUseAction, // 最终动作(第一个 Block 获胜)
- pub additional_context: Option, // 累积的附加上下文(所有 MutateInput 拼接)
- pub final_args: serde_json::Value, // 最终参数(最后一个 MutateInput 获胜)
-}
+详见 [permission.md](./permission.md)。
-pub struct PostToolUseResult {
- pub final_content: String, // 最终输出(最后一个 MutateOutput 获胜)
-}
-```
+---
-## HookRegistry 聚合逻辑
+## PermissionRequest / PermissionDenied — 权限决策钩子
-**PreToolUse 聚合(责任链 + 短路)**:
+这两个专用事件在权限决策管线中提供 Hook 级别的可编程控制点,参考 Claude Code 的 `PermissionRequest` / `PermissionDenied` 事件设计。
+
+### PermissionRequest — 权限请求前
+
+**触发时机**:`resolve_permission_precedence()` 之后、最终权限决策生效之前。
+
+**调度策略**:并行调用所有匹配的 hook,**第一个返回 `Override` 的生效**(后续 hook 仍执行但结果忽略),其他 hook 返回 `Continue` 表示不干预。
```mermaid
-flowchart TD
- Start["run_pre_tool_use(ctx)"] --> Loop["遍历 hooks,依次调用 pre_tool_use()"]
- Loop --> Check{"结果类型?"}
- Check -->|"Block"| Short["立即短路返回
不询问后续 hooks"]
- Check -->|"MutateInput"| Mut["更新 final_args
累积 additional_context (\\n 拼接)"]
- Check -->|"PermissionRequired"| Log["记录日志但不阻止执行
(Phase 2 预留)"]
- Check -->|"Continue"| Next["继续下一个 hook"]
- Mut --> Next
- Log --> Next
- Next --> Loop
- Short --> Return["返回 PreToolUseResult"]
- Next -->|"遍历完毕"| Return
+flowchart LR
+ subgraph Request["run_on_permission_request()"]
+ direction LR
+ H1["Hook 1"] -->|"Override(Allow)"| Win["✅ 首个 Override 生效"]
+ H2["Hook 2"] -->|"Continue"| Ignore["被忽略"]
+ H3["Hook 3"] -->|"InjectContext"| Accumulate["累积上下文"]
+ end
```
-关键设计:
-- **第一个 Block 获胜** — 短路保护
-- **最后一个 MutateInput 获胜** — 后覆盖前
-- **additional_context 累积** — 多个 hook 的上下文用 `\n` 连接
+**返回值语义**:
-**PostToolUse 聚合(全部执行,无短路)**:
+| 动作 | 效果 |
+|------|------|
+| `PermissionRequestAction::Continue` | 不干预,沿用 `resolve_permission_precedence()` 的结果 |
+| `PermissionRequestAction::Override { decision, reason }` | 覆盖决策:`Allow` 强制放行、`Ask` 升级确认、`Deny` 强制拒绝 |
+| `PermissionRequestAction::InjectContext { context }` | 向 LLM 注入额外上下文(如安全策略解释) |
-```mermaid
-flowchart TD
- Start2["run_post_tool_use(ctx)"] --> Loop2["遍历所有 hooks,依次调用 post_tool_use()"]
- Loop2 --> MutOut{"MutateOutput ?"}
- MutOut -->|"是"| Update["更新 final_content
(最后的 MutateOutput 获胜)"]
- MutOut -->|"Continue"| Next2["继续下一个 hook"]
- Update --> Next2
- Next2 --> Loop2
- Next2 -->|"遍历完毕"| Return2["返回 PostToolUseResult { final_content }"]
+**上下文字段**:
+
+| 字段 | 类型 | 说明 |
+|------|------|------|
+| `session_id` | `String` | 当前会话 ID |
+| `tool_name` | `String` | 被请求的工具名 |
+| `tool_args` | `Value` | 工具参数(可检查路径、命令等) |
+| `current_decision` | `PermissionDecision` | `resolve_permission_precedence()` 的裁决结果 |
+| `permission_mode` | `String` | 当前权限模式(default/acceptEdits/bypassPermissions/plan) |
+| `is_subagent` | `bool` | 是否在子代理上下文中 |
+
+### PermissionDenied — 权限拒绝后(审计)
+
+**触发时机**:权限决策结果为 `Deny`(无论来源)。
+
+**调度策略**:fire-and-forget 并行调用,不阻塞主循环,返回值忽略。设计用于审计日志、安全告警、拒绝统计。
+
+**来源标记(PermissionDenialSource)**:
+
+| 来源 | 说明 |
+|------|------|
+| `Rule` | 被 `PermissionChecker` 或工具级规则拒绝 |
+| `Classifier` | 被 Auto-mode Classifier 拒绝(待实现) |
+| `User` | 用户在交互弹窗中手动拒绝 |
+| `Timeout` | 权限请求超时自动拒绝 |
+
+**使用示例**:
+
+```rust
+struct DenyRateLimiter {
+ deny_counts: Arc>>,
+}
+
+#[async_trait]
+impl AgentHook for DenyRateLimiter {
+ fn name(&self) -> &str { "DenyRateLimiter" }
+ fn subscribed_events(&self) -> &[HookEvent] {
+ &[HookEvent::PermissionRequest, HookEvent::PermissionDenied]
+ }
+
+ async fn on_permission_request(
+ &self,
+ ctx: &PermissionRequestContext,
+ ) -> PermissionRequestAction {
+ let count = self.get_deny_count(&ctx.tool_name);
+ if count >= 3 {
+ // 连续拒绝 3 次后强制降级为 Ask(不允许自动 Deny)
+ return PermissionRequestAction::Override {
+ decision: PermissionDecision::Ask,
+ reason: format!("工具 {} 已被拒绝 {} 次,需要用户确认", ctx.tool_name, count),
+ };
+ }
+ PermissionRequestAction::Continue
+ }
+
+ async fn on_permission_denied(&self, ctx: &PermissionDeniedContext) {
+ // 记录拒绝统计
+ *self.deny_counts.lock().unwrap()
+ .entry(ctx.tool_name.clone()).or_default() += 1;
+ // 触发安全告警
+ eprintln!("[SECURITY] 工具 {} 被拒绝: {:?} (来源: {:?})",
+ ctx.tool_name, ctx.reason, ctx.source);
+ }
+}
```
-**其余 7 个事件** 均为 fire-and-forget:遍历所有 hooks 调用对应方法,不收集返回值。
+---
-## 内置 Hooks(3 个)
+---
-| Hook | 覆写的事件 | 职责 | 关键依赖 |
-|------|-----------|------|---------|
-| `CancellationHook` | `pre_tool_use`, `on_session_stop` | 每次工具执行前检查用户是否中止会话;会话停止时清理取消令牌 | `Arc>>` (与 AppState 共享) |
-| `MetricsHook` | `on_session_start`, `post_tool_use`, `on_step_complete`, `on_session_stop` | 采集运行指标:工具调用次数、步数、错误数、token 消耗;每 3 步输出摘要日志 | `Arc>` (**共享引用**,API 通过 `AgentRuntime::get_metrics()` 实时查询) |
-| `AuditLogHook` | `post_tool_use`, `on_session_stop` | 所有工具调用写入 `agent_audit_log` 表(工具名、状态、耗时、输出预览);会话终止写入 SESSION_STOP 标记 | `SqlitePool` (**fire-and-forget** 写入,不阻塞主循环) |
-
-> **注意**:代码中**不存在** PermissionHook。权限检查由独立的 `PermissionChecker` (`src/agent/runtime/permission.rs`) 负责,该组件在工具执行前与 hooks 并行调用,不属于 hooks 体系。`PreToolUseAction::PermissionRequired` 变体预留于 Phase 2 完善。
-
-## 数据流
+## 生命周期事件全景
```mermaid
sequenceDiagram
participant API as API Handler
participant RT as AgentRuntime
participant HR as HookRegistry
- participant CH as CancellationHook
- participant MH as MetricsHook
- participant AH as AuditLogHook
participant EX as Executor
+ participant SA as SubAgentRunner
+ participant CMP as Compact
- Note over API,EX: Phase 1 — 会话启动
+ Note over API,CMP: ═══ Phase 1: 会话启动 ═══
API->>RT: run_turn(question)
- RT->>HR: HookRegistry::with_builtins(db, cancelled_runs, metrics_data)
- RT->>HR: run_on_session_start(ctx)
- HR->>MH: 记录 session_id
+ RT->>HR: with_builtins(db, cancelled, metrics)
+ RT->>HR: run_on_session_start(ctx) ⚡并行
+ Note over HR: ① OnSessionStart
- Note over API,EX: Phase 2 — ReAct 循环
- loop 每步 (最多 max_steps)
- RT->>RT: LLM 流式调用
- RT->>EX: execute_parallel(tool_calls, hook_registry)
+ Note over API,CMP: ═══ Phase 2: ReAct 循环 ═══
+ loop 每步 (1..max_steps)
+ RT->>RT: LLM 流式调用 → tool_calls
+ RT->>EX: execute_parallel(calls, registry)
par 每个工具调用
- EX->>HR: run_pre_tool_use(pre_ctx)
- HR->>CH: 检查取消状态
- alt 已取消
- CH-->>HR: Block { reason }
- HR-->>EX: PreToolUseResult { action: Block }
- EX-->>EX: 跳过该工具
- else 未取消
- CH-->>HR: Continue
- HR-->>EX: PreToolUseResult { final_args, additional_context }
- EX->>EX: 执行工具
- EX->>HR: run_post_tool_use(post_ctx)
- HR->>MH: 更新工具调用计数/错误数
- HR->>AH: fire-and-forget INSERT agent_audit_log
- HR-->>EX: PostToolUseResult { final_content }
+ EX->>HR: run_pre_tool_use(ctx) ⚡并行 + match_filter
+ Note over HR: ② PreToolUse
+ HR-->>EX: PreToolUseResult {final_args, tagged_contexts, blocks}
+ EX->>EX: 权限检查 (Checker + 工具级 + 会话级 + Hook)
+ EX->>EX: resolve_permission_precedence()
+ EX->>HR: run_on_permission_request(ctx) ⚡并行 (首个 Override 生效)
+ Note over HR: ⑪ PermissionRequest — Hook 可覆盖权限决策
+ alt 权限被拒绝
+ EX->>HR: run_on_permission_denied(ctx) ⚡并行 (fire-and-forget 审计)
+ Note over HR: ⑫ PermissionDenied
+ end
+ EX->>EX: 工具执行
+ EX->>HR: run_post_tool_use(ctx) ⚡并行 + match_filter
+ Note over HR: ③ PostToolUse + AsyncHook dispatch
+ HR-->>EX: PostToolUseResult {final_content, tagged_contexts, warnings, metadata}
+ alt is_error
+ EX->>HR: run_on_post_tool_use_failure(ctx) ⚡并行
+ Note over HR: ④ PostToolUseFailure + AsyncHook dispatch
end
end
- EX-->>RT: ToolExecutionResult { tool_messages }
- RT->>HR: run_on_step_complete(ctx)
- HR->>MH: 每 3 步输出摘要日志
+ EX-->>RT: ToolExecutionResult {messages, hook_contexts, blocking_errors}
+ RT->>RT: "hook_contexts → [system-reminder] 注入 (去重)"
+ RT->>HR: run_on_step_complete(ctx) ⚡并行
+ Note over HR: ⑤ OnStepComplete
opt 上下文超限
- RT->>RT: snapshot_compress_restore()
- Note over RT: PreCompact / PostCompact hooks 触发
+ RT->>CMP: compress_context_with_hooks()
+ CMP->>HR: run_on_pre_compact(ctx) ⚡并行
+ Note over HR: ⑨ OnPreCompact
+ CMP->>CMP: compress_with_fallback()
+ CMP->>HR: run_on_post_compact(ctx) ⚡并行
+ Note over HR: ⑩ OnPostCompact
+ end
+
+ opt 子代理调用
+ RT->>SA: SubAgentRunner::run()
+ SA->>HR: run_on_subagent_start(ctx) ⚡并行
+ Note over HR: ⑦ OnSubagentStart
+ SA->>SA: 独立 ReAct 循环
+ Note over SA: PreToolUse / PostToolUse 同样触发
+ SA->>HR: run_on_subagent_stop(ctx) ⚡并行
+ Note over HR: ⑧ OnSubagentStop
+ SA-->>RT: ToolOutput (summary)
end
end
- Note over API,EX: Phase 3 — 会话收尾
+ Note over API,CMP: ═══ Phase 3: 会话收尾 ═══
RT->>RT: finalize_turn()
- RT->>HR: run_on_session_stop(ctx)
- HR->>CH: 清理 cancelled_runs
- HR->>MH: 输出会话结束摘要
- HR->>AH: fire-and-forget SESSION_STOP 记录
- RT-->>API: Done (SSE)
+ RT->>HR: run_on_session_stop(ctx) 🔒顺序
+ Note over HR: ⑥ OnSessionStop
+ RT-->>API: SSE Done
```
-## HookRegistry 构建
+**执行模式图例**:⚡ 并行 `join_all` | 🔒 顺序执行 | ⚡+⏱ 并行 + per-hook timeout
-`AgentRuntime::run_turn()` 在每次 turn 开始时构建 `HookRegistry`:
+---
-```rust
-let hook_registry = HookRegistry::with_builtins(
- db.clone(), // → AuditLogHook
- self.app_state.cancelled_runs.clone(), // → CancellationHook
- Some(self.metrics_data.clone()), // → MetricsHook (共享引用)
-);
+## HookRegistry 调度机制
+
+### 事件索引 + 工具过滤
+
+```mermaid
+flowchart LR
+ subgraph Register["add(hook)"]
+ H["hook: Box<dyn AgentHook>"] --> Subs{"subscribed_events()"}
+ Subs -->|"空 = 全部"| All["遍历所有 12 个 HookEvent
event_index[event].push(idx)"]
+ Subs -->|"指定"| Spec["仅注册声明的事件
event_index[event].push(idx)"]
+ end
+
+ subgraph Dispatch["run_*_tool_use(ctx, session_id)"]
+ Collect["collect_tool_hooks_for(event, sid, tool_name, tool_args)"]
+ Collect --> GI["event_index[event] → 全局 hooks"]
+ Collect --> FI["match_filter().matches(tool_name, tool_args) → 过滤"]
+ Collect --> SI["session_hooks[sid] → session hooks (不过滤)"]
+ GI --> Merge["统一 Vec<&dyn AgentHook>"]
+ FI --> Merge
+ SI --> Merge
+ end
+
+ Register --> Index["event_index: HashMap<HookEvent, Vec<usize>>"]
+ Dispatch --> Index
```
-`MetricsHook` 使用 `from_arc()` 复用 `AgentRuntime` 自身的 `metrics_data: Arc>`,确保 hook 内部采集的指标与 `AgentRuntime::get_metrics()` API 查询返回的是同一份数据。
+**关键**:`collect_tool_hooks_for()` 在 `collect_hooks_for()` 基础上增加 `match_filter()` 过滤层。全局 hooks 按事件索引 + 工具匹配双重过滤,session hooks 不过滤(保持全量响应)。
-## 子代理中的 Hooks
+---
-`SubAgentRunner` 拥有独立的 hook 管道,共享同一个 `HookRegistry` 实例:
+## 数据流:Hook 结果如何影响 Agent 行为
-- `on_subagent_start` / `on_subagent_stop` 在子代理生命周期的首尾触发
-- 子代理的工具执行也经过 `run_pre_tool_use` / `run_post_tool_use`(通过 `subagent.rs:447-518`)
-- **已知不足**:子代理内部的上下文压缩 (`subagent.rs:270`) 直接调用 `compress_context` 而非 `compress_context_with_hooks`,导致 PreCompact/PostCompact 事件**不会**在子代理压缩时触发
+```mermaid
+flowchart TD
+ subgraph Executor["executor::execute_parallel()"]
+ PreHooks["PreToolUse hooks → PreToolUseResult"]
+ Perms["权限检查 4 层叠加
Checker → 工具级规则 → 会话级规则 → resolve_permission_precedence"]
+ PermHooks["PermissionRequest hooks → PermissionRequestAction
首个 Override 生效,可覆盖 Allow→Ask 升级或 Deny→Ask 降级"]
+ PermDenied["PermissionDenied hooks → fire-and-forget 审计
Rule/Classifier/User/Timeout 四种来源标记"]
+ Tools["工具执行"]
+ PostHooks["PostToolUse hooks → PostToolUseResult"]
+ FailHooks["PostToolUseFailure hooks
(仅 is_error)"]
+ AsyncHooks["AsyncAgentHook dispatch
(fire-and-forget, 5s 超时)"]
+ end
-## 扩展方式
+ subgraph ReactLoop["AgentRuntime::run_react_loop()"]
+ Push["tool_messages → messages"]
+ Inject["hook_contexts 包装为 [system-reminder]
(带 TaggedContext 来源 + ContextDeduplicator 去重)"]
+ LogBlock["blocking_errors → warn! 日志"]
+ LogWarn["warnings → warn! 日志"]
+ LogPerm["post_permission_requests → info! 审计"]
+ StepHook["run_on_step_complete"]
+ end
-添加自定义 hook 只需两步:
+ PreHooks -->|"final_args 覆盖参数"| Perms
+ PreHooks -->|"PermissionRequired → Allow→Ask 升级"| Perms
+ Perms -->|"权限决策"| PermHooks
+ PermHooks -->|"Override 可覆盖 Allow/Deny/Ask"| Perms
+ Perms -->|"Deny: 触发"| PermDenied
+ PermDenied -->|"审计日志"| LogPerm
+ Perms -->|"Deny: 注入错误 result"| Push
+ Perms -->|"Allow/Ask/Approve: 进入执行"| Tools
+ Tools -->|"output"| PostHooks
+ PostHooks -->|"final_content 覆盖输出"| Push
+ PostHooks -->|"warnings, metadata, tagged_contexts"| Inject
+ Tools -->|"is_error"| FailHooks
+ FailHooks -->|"final_content 覆盖错误输出"| Push
+ PostHooks -.->|"并行 fire-and-forget"| AsyncHooks
+ FailHooks -.->|"并行 fire-and-forget"| AsyncHooks
+ Push --> Inject
+ Inject --> StepHook
+ LogBlock --> StepHook
+ LogWarn --> StepHook
+ LogPerm --> StepHook
+```
+
+### hook_contexts 注入格式
+
+```rust
+// 带来源标记(TaggedContext)
+// runtime/mod.rs
+let mut dedup = ContextDeduplicator::new();
+for ctx in &exec_result.hook_contexts {
+ if dedup.is_duplicate(ctx) {
+ continue;
+ }
+ let reminder = format!(
+ "\n[Hook 注入上下文]\n{}\n",
+ ctx
+ );
+ messages.push(ChatMessage::user(&reminder));
+}
+```
+
+内容格式:
+```
+[Hook: AuditLogHook | PostToolUse] 内容...
+[Hook: CancellationHook | PreToolUse] 内容...
+```
+
+---
+
+## 内置 Hooks 详解
+
+### CancellationHook
+
+```mermaid
+stateDiagram-v2
+ [*] --> Active: 会话开始
+ Active --> CheckPreTool: 每次 PreToolUse
+ CheckPreTool --> Blocked: cancelled_runs 含 session_id
+ CheckPreTool --> Continue: cancelled_runs 不含 session_id
+ Blocked --> [*]: Block{reason: "用户已手动中止"}
+ Continue --> Active: 继续执行
+ Active --> Cleanup: OnSessionStop
+ Cleanup --> [*]: cancelled_runs.remove(session_id)
+```
+
+依赖 `Arc>>`(与 `AppState.cancelled_runs` 共享引用)。
+
+### MetricsHook
+
+通过 `from_arc()` 复用 `AgentRuntime` 自身的 `metrics_data: Arc>`,确保 hook 内部采集的数据与 `AgentRuntime::get_metrics()` API 返回的是同一份数据。
+
+订阅事件:`OnSessionStart`(设置 session_id)、`PostToolUse`(累加计数)、`OnStepComplete`(每 3 步输出摘要)、`OnSessionStop`(输出终止原因)。
+
+### AuditLogHook
+
+```mermaid
+sequenceDiagram
+ participant EX as Executor
+ participant AH as AuditLogHook
+ participant DB as SQLite
+
+ EX->>AH: post_tool_use(ctx)
+ AH->>AH: 构造 status = "OK" | "FAIL"
+ AH->>AH: output_preview = content[..200]
+ AH--)DB: "tokio::spawn INSERT agent_audit_log"
+ EX->>AH: on_session_stop(ctx)
+ AH--)DB: "tokio::spawn INSERT SESSION_STOP"
+```
+
+Fire-and-forget 写入,不阻塞主循环。未来可改为实现 `AsyncAgentHook`。
+
+---
+
+## 扩展指南
+
+### 自定义全局 Sync Hook
```rust
-// 1. 实现 AgentHook trait
struct MyCustomHook;
#[async_trait]
impl AgentHook for MyCustomHook {
fn name(&self) -> &str { "MyCustomHook" }
+ // 声明只关心 PreToolUse 和 PostToolUseFailure
+ fn subscribed_events(&self) -> &[HookEvent] {
+ &[HookEvent::PreToolUse, HookEvent::PostToolUseFailure]
+ }
+
+ // 仅对文件工具生效
+ fn match_filter(&self) -> ToolMatchFilter {
+ ToolMatchFilter::prefix("file_")
+ }
+
+ fn timeout(&self) -> Option {
+ Some(Duration::from_secs(10))
+ }
+
async fn pre_tool_use(&self, ctx: &PreToolUseContext) -> PreToolUseAction {
- // 自定义逻辑
+ if ctx.tool_name == "run_bash" {
+ return PreToolUseAction::PermissionRequired {
+ permission: "执行系统命令需要二次确认".into(),
+ tool_name: ctx.tool_name.clone(),
+ };
+ }
PreToolUseAction::Continue
}
+
+ async fn post_tool_use(&self, ctx: &PostToolUseContext) -> PostToolUseAction {
+ if ctx.is_error {
+ PostToolUseAction::Warning {
+ message: format!("工具 {} 执行失败", ctx.tool_name),
+ truncate_output: false,
+ }
+ } else {
+ PostToolUseAction::Continue
+ }
+ }
+
+ async fn on_post_tool_use_failure(
+ &self,
+ ctx: &PostToolUseFailureContext,
+ ) -> PostToolUseAction {
+ if ctx.is_interrupt {
+ return PostToolUseAction::Continue;
+ }
+ PostToolUseAction::MutateOutput {
+ updated_content: format!(
+ "[已由 MyCustomHook 处理] 工具 {} 执行失败: {}。建议尝试替代方案。",
+ ctx.tool_name, ctx.error_message
+ ),
+ additional_context: None,
+ }
+ }
}
-// 2. 注册到 HookRegistry
+// 注册
registry.add(Box::new(MyCustomHook));
```
-## 测试覆盖
+### 自定义 Async Hook
-`hooks.rs` 包含 8 个单元测试(`#[cfg(test)] mod tests`),覆盖:
+```rust
+struct RemoteLogger;
-| 测试 | 验证点 |
-|------|-------|
-| `test_hook_registry_runs_all_hooks` | 注册表遍历调用所有 hook |
-| `test_blocking_hook_stops_chain` | Block 短路机制 |
-| `test_mutate_input_accumulates_context` | 参数修改 + 上下文累积 |
-| `test_post_tool_use_mutate_output` | 输出修改 |
-| `test_cancellation_hook_blocks_when_cancelled` | CancellationHook 阻止逻辑 |
-| `test_cancellation_hook_allows_when_not_cancelled` | CancellationHook 放行逻辑 |
-| `test_metrics_hook_accumulates_counts` | MetricsHook 累加正确性 |
-| `test_session_start_hook_called` | OnSessionStart 调用 |
-| `test_new_lifecycle_events_called` | OnSubagentStart/Stop, PreCompact/PostCompact 调用 |
+#[async_trait]
+impl AsyncAgentHook for RemoteLogger {
+ fn name(&self) -> &str { "RemoteLogger" }
-## 已知改进项
+ async fn on_post_tool_use_async(&self, ctx: PostToolUseContext) {
+ // 异步上报工具调用日志到远程服务
+ tokio::spawn(async move {
+ upload_to_remote(ctx).await;
+ });
+ }
+}
-| 问题 | 说明 |
-|------|------|
-| `PermissionRequired` 未实现 | 代码中存在此变体但被当作 `Continue` 处理,注释标明 "Permission 系统在 Phase 2 中完善" |
-| 取消检查重复 | `CancellationHook::pre_tool_use` 与 `AgentRuntime::run_react_loop` 中的显式检查存在功能重叠 |
-| 子代理压缩未走 hooks | `subagent.rs:270` 直接调用 `compress_context` 而非 `compress_context_with_hooks` |
-| 缺少 `on_error` 事件 | `AgentHook` trait 没有错误生命周期事件,错误场景无法通过 hook 拦截 |
+registry.add_async(Box::new(RemoteLogger));
+```
+
+### 动态 Session Hook
+
+```rust
+// 为当前会话临时添加
+registry.add_session_hook(&session_id, Box::new(MyTempHook));
+
+// 移除
+registry.remove_session_hook(&session_id, "MyTempHook");
+
+// 会话结束时自动清理(HookRegistry 析构)
+```
---
+
+## 集成点地图
+
+| 文件 | 集成点 | 方法 |
+|------|--------|------|
+| `runtime/mod.rs` | 会话启动 | `run_on_session_start()` |
+| `runtime/mod.rs` | ReAct 循环 | `run_on_step_complete()` |
+| `runtime/mod.rs` | Hook 上下文注入 | `hook_contexts → [system-reminder]` |
+| `runtime/mod.rs` | 会话终止 | `run_on_session_stop()` |
+| `executor.rs` | 工具执行前 | `run_pre_tool_use()` |
+| `executor.rs` | 工具执行后 | `run_post_tool_use()` |
+| `executor.rs` | 工具执行失败 | `run_on_post_tool_use_failure()` |
+| `executor.rs` | 权限决策前 | `run_on_permission_request()` |
+| `executor.rs` | 权限拒绝后 | `run_on_permission_denied()` (fire-and-forget 审计) |
+| `executor.rs` | 权限决策 | `resolve_permission_precedence()` |
+| `subagent.rs` | 子代理启动 | `run_on_subagent_start()` |
+| `subagent.rs` | 子代理停止 | `run_on_subagent_stop()` |
+| `compact.rs` | 压缩前后 | `run_on_pre_compact()` / `run_on_post_compact()` |
+
+---
+
+## 配置
+
+| 环境变量 | 默认值 | 说明 |
+|---------|--------|------|
+| `HOOK_TIMEOUT_SECS` | 30 | 全局 per-hook 超时(秒)。单个 hook 可通过 `timeout()` trait 方法覆盖 |
+
+---
+
+## 设计权衡
+
+| 考量点 | 说明 |
+|--------|------|
+| **工具级过滤** | `match_filter()` 提供 name + content 双重过滤,session hooks 不过滤 |
+| **并行 vs 顺序** | 单 hook 自动走顺序路径(无 timeout 开销),多 hook 并行 + timeout |
+| **聚合策略** | 最后覆盖 final_args/content,全部保留 contexts/blocks/warnings/metadata |
+| **OnSessionStop** | 顺序执行(关键清理),`&self` 限制需调用方手动 clear session hooks |
+| **AsyncAgentHook** | 独立 trait,fire-and-forget 语义,5s dispatch 超时,与 sync hook 并行 |
+| **TaggedContext** | 带 hook 名称 + 事件来源,格式化注入为 `[Hook: 名称 | 事件]` |
+| **ContextDeduplicator** | 内容哈希去重,单 cycle 内有效,不跨 step 去重 |
+| **Block 非全局中断** | `PreToolUseAction::Block` 只阻止当前工具的单个调用,全局中断由 `CancellationHook` 实现 |
+| **Warning/Post-hoc Permission** | PostToolUse 的 Warning 和 PermissionRequired 均为 advisory,不阻塞主循环 |
+| **PermissionRequest 调度** | 并行调用所有 hook,首个 `Override` 生效,后续 Continue 被忽略;`InjectContext` 全部累积 |
+| **PermissionDenied 调度** | fire-and-forget 并行调用,不阻塞主循环,返回值忽略(纯审计用途) |
+
+---
+
+## 变更记录
+
+| 日期 | 变更 |
+|------|------|
+| 2026-06-22 | Phase 1: 新增 ToolMatchFilter / TaggedContext / 工具级过滤 |
+| 2026-06-22 | Phase 2: 新增 resolve_permission_precedence + PermissionChecker::explain |
+| 2026-06-22 | Phase 3: PostToolUseAction 扩展 (Warning/PermissionRequired/Metadata) + ContextDeduplicator |
+| 2026-06-22 | Phase 4: AsyncAgentHook trait + async hook 调度 |
+| 2026-06-22 | 模块拆分: mod.rs(537) ← types/traits/matcher/registry/dispatch/builtins |
+| 2026-06-22 | Phase 5: PermissionRequest/PermissionDenied 事件 — 权限决策钩子 + fire-and-forget 审计 |
+
+---
+
+## 测试覆盖
+
+| 测试 | 覆盖路径 |
+|------|---------|
+| `test_hook_registry_runs_all_hooks` | 注册表遍历调用 |
+| `test_blocking_hook_stops_chain` | Block 收集到 blocking_errors |
+| `test_mutate_input_accumulates_context` | 参数修改 + 上下文累积 |
+| `test_post_tool_use_mutate_output` | 输出修改(含 additional_context) |
+| `test_cancellation_hook_blocks_when_cancelled` | CancellationHook Block 路径 |
+| `test_cancellation_hook_allows_when_not_cancelled` | CancellationHook 放行路径 |
+| `test_metrics_hook_accumulates_counts` | MetricsHook 累加正确性 |
+| `test_session_start_hook_called` | OnSessionStart 触发 |
+| `test_new_lifecycle_events_called` | OnSubagentStart/Stop, Pre/PostCompact 触发 |
+| `test_match_filter_skips_irrelevant` | ToolMatchFilter 正确过滤无关工具 |
+| `test_full_hook_pipeline_integration` | 全链路:match_filter + TaggedContext + Warning + Metadata |
+| `test_matcher_*` (12 个) | ToolNamePattern / ToolMatchFilter 匹配逻辑 |
+| `test_precedence_*` (5 个) | 多源权限优先级裁决 |
+| `test_context_deduplication` | ContextDeduplicator 去重 |
+| `test_async_hook_dispatch` | AsyncAgentHook 并行调度 |
+| `test_permission_request_override` | PermissionRequest — Override 覆盖 Allow→Ask 升级 |
+| `test_permission_request_continue` | PermissionRequest — Continue 不干预默认决策 |
+| `test_permission_denied_fire_and_forget` | PermissionDenied — fire-and-forget 审计调度 |
+| `test_permission_denied_sources` | PermissionDenied — Rule/Classifier/User/Timeout 四种来源标记 |
diff --git a/docs/architecture/agent/permission.md b/docs/architecture/agent/permission.md
index 2055e9b..bbc7de8 100644
--- a/docs/architecture/agent/permission.md
+++ b/docs/architecture/agent/permission.md
@@ -133,6 +133,29 @@ graph TD
└────────────────────────────────────────────────────────────┘
```
+### 多源权限优先级 (Multi-Source Permission Precedence)
+
+当多个来源(Checker 规则、Hook、工具级规则、会话规则)同时做出权限决策时,
+`resolve_permission_precedence()` 按以下优先级裁决:
+
+| Priority | Source | Description |
+|----------|--------|-------------|
+| **P0** (highest) | `PermissionChecker::Deny` | 环境变量/配置文件配置的 Deny 规则,不可覆盖 |
+| **P1** | Tool-level `PermissionRule::Deny` | 工具自身拒绝执行(如 Bash 危险命令) |
+| **P2** | Session-level `PermissionChecker::Deny` | API 动态添加的会话级 Deny |
+| **P3** | Hook `PreToolUseAction::Block` | Hook 主动阻止工具执行 |
+| **P4** | Hook `PermissionRequired` | 仅当 Checker 返回 Allowed 时升级为 AskUser |
+| **P5** | Session-level `PermissionChecker::Ask` | 仅当当前为 Allowed 时升级 |
+| **P6** | Tool-level `PermissionRule::Ask` | 仅当当前为 Allowed 时升级 |
+| **P7** | `PermissionChecker::Allow` | 显式 Allow 规则 |
+| **P8** (lowest) | Implicit Allow (default) | 无任何规则匹配 → 允许 |
+
+**关键规则:**
+- **Deny 不可覆盖**: P0-P2 的 Deny 规则在任何情况下生效
+- **Ask 可升级**: P4-P6 在 Allow 状态下升级为 Ask;在 Deny 状态下被忽略
+- **Block = Deny**: Hook Block 等同于 Deny,由 P0-P2 可覆盖
+- **冲突日志**: `conflict_log` 记录所有被覆盖的决策,用于审计```
+
### PermissionChecker API
```rust
@@ -433,7 +456,7 @@ flowchart TD
| `grep "ssh_config" *.rs` | ❌ 误拦(含子串 `ssh `) | ✅ 允许(首词 `grep` 在白名单) |
| `echo "use sudo carefully"` | ❌ 误拦(含子串 `sudo `) | ✅ 允许(首词 `echo` 在白名单) |
| `cat /usr/share/vim/vimrc` | ❌ 误拦(含子串 `vim `) | ✅ 允许(首词 `cat` 在白名单) |
-| `python script.py` | ✅ 允许 | ✅ 允许(不在黑名单,默认允许) |
+| `python script.py` | ✅ 允许 | ⚠️ 通过校验,但触发 `Ask` 用户确认(不在白名单) |
| `vim file.txt` | ✅ 拒绝 | ✅ 拒绝(首词 `vim` 在黑名单) |
| `$(echo sud; echo o) /etc/passwd` | ✅ 允许(绕过!) | ❌ 拒绝(检测到命令替换绕过) |
@@ -449,7 +472,7 @@ const SAFE_COMMANDS: &[&str] = &[
];
```
-白名单内的命令**优先放行**,不经过黑名单检查。非白名单命令经过黑名单精确匹配和危险参数二次检查后默认允许。
+白名单内的命令**优先放行**,不经过黑名单检查。非白名单命令经黑名单精确匹配和危险参数检查后,通过 `bash_needs_permission()` → `RunBashTool::check_permissions()` 返回 `Ask` 规则,触发用户确认弹窗(PermissionRequestCard)。
### 其他约束
@@ -462,7 +485,7 @@ const SAFE_COMMANDS: &[&str] = &[
### 已知局限
1. ~~**黑名单子串匹配**~~ — ✅ 已修复:改用首词精确匹配,`grep "ssh_config"` 不再误拦
-2. **未限制网络访问** — `curl`、`wget` 不在黑名单中
+2. ~~**未限制网络访问**~~ — ✅ 已修复:`NETWORK_COMMANDS` 名单(curl/wget/nc/socat 等)默认阻止,`AGENT_BLOCK_NETWORK=false` 可放行
3. **未限制进程数** — fork bomb(如 `:(){ :\|:& };:`)未被检测
4. **管道/重定向完整放行** — `<`、`>`、`|` 不做限制
5. ~~**`$()` 命令替换**~~ — ✅ 已修复:检测首词位置 `$()` 和反引号绕过
@@ -684,258 +707,9 @@ Diminishing Returns 检测
---
-## Claude Code 权限系统对比分析
-
-> 对比基准:Claude Code (`/home/fmq/program/claudecode/src/utils/permissions/`)
-> 分析日期:2026-06-17
-
-### 架构差异总览
-
-| 维度 | AstroResearch (当前) | Claude Code (参考) | 差距 |
-|------|---------------------|-------------------|------|
-| 规则引擎 | ✅ PermissionChecker (完成) | ✅ hasPermissionsToUseTool 多步流水线 | 相当 |
-| 规则匹配粒度 | ✅ 内容级 `Tool(content*)` 前缀/后缀/包含 | ✅ 前缀/通配/内容级 / 正则 | 小 |
-| AskUser 交互流 | ✅ SSE → PermissionRequestCard → Allow/Deny/Always Allow | ✅ 完整 SSE → Dialog → 决策 | 相当 |
-| 权限模式 | ✅ Default/AcceptEdits/Bypass/DontAsk | ✅ 6种模式 (含 plan/auto) | 小 |
-| 规则持久化 | ✅ 环境变量加载 + `PermissionChecker::from_config()` | ✅ settings.json 多层加载 (8级来源优先级) | 小 |
-| 规则来源追踪 | ✅ `PermissionRuleSource` 枚举 (Env/Session) | ✅ cliArg > command > session > userSettings > ... | 小 |
-| Bash 权限分类器 | ✅ SAFE_COMMANDS 白名单 + `check_permissions()` 集成 | ✅ AST解析 + AI分类器 + 异步推测 | 中等 |
-| 拒绝追踪/熔断 | ✅ `DenialTracker` 连续/累计计数 + ReAct 循环熔断 | ✅ 连续/总计拒绝计数 + 自动终止 | 相当 |
-| 权限 Hook 集成 | ✅ PreToolUseAction::PermissionRequired 完整流程 | ✅ 完整 PermissionRequest hook + 多路径决议 | 相当 |
-| 规则遮蔽检测 | ✅ `detect_shadowed_rules()` deny/ask 双重检查 | ✅ `shadowedRuleDetection` deny/ask 遮蔽检测 | 相当 |
-| Auto Mode (AI 分类) | ❌ 无 | ✅ YOLO classifier + 快速路径 + 安全工具白名单 | **远期** |
-| 权限解释器 | ✅ 启发式 `explain_permission()` (Bash 风险等级 + 路径检测) | ✅ Haiku 生成风险解释 | 中等 |
-| 会话内规则更新 | ✅ `POST/PUT /api/chat/sessions/:id/permissions/*` | ✅ `/permissions` 命令 + API | 小 |
-| 子代理权限继承 | ✅ 完整 `check()` 三态检查 | ✅ 完整继承父级权限上下文 | 相当 |
-| 附加目录沙箱 | ✅ `AGENT_ADDITIONAL_DIRS` + `is_path_allowed()` 扩展 | ✅ `additionalDirectories` 可配置 | 相当 |
-
-### Claude Code 权限流水线 (参考架构)
-
-```
-hasPermissionsToUseTool(toolName, input, context):
- Step 1a: 工具级 deny 规则检查 → deny → 返回 deny
- Step 1b: 工具级 ask 规则检查 → ask → 返回 ask (sandbox 例外)
- Step 1c: 工具自定义 checkPermissions() → 内容级规则匹配
- Step 1d: 工具实现返回 deny → deny → 返回 deny
- Step 1e: requiresUserInteraction? → ask → 强制 ask (bypass 免疫)
- Step 1f: 内容级 ask 规则 → ask → 强制 ask (bypass 免疫)
- Step 1g: 安全检查 (敏感路径等) → ask → 强制 ask (bypass 免疫)
- Step 2a: bypassPermissions 模式? → allow → 返回 allow
- Step 2b: 工具级 allow 规则 → allow → 返回 allow
- Step 3: 剩余 passthrough → ask → 返回 ask
-
-外层模式变换:
- dontAsk 模式: ask → deny
- auto 模式: acceptEdits 快速路径 → 安全工具白名单 → AI分类器
- headless: hooks 先运行 → 无 hook 决定 → auto-deny
-```
-
-### 关键设计决策对比
-
-**1. 规则格式**
-
-Claude Code 使用 `ToolName(content)` 格式支持内容级规则:
-```
-Bash → 匹配所有 bash 命令
-Bash(npm install) → 匹配精确命令
-Bash(npm *) → 前缀通配
-Bash(rm:*) → 旧版前缀(已废弃)
-Read(.env) → 文件模式
-mcp__server__tool → MCP 工具级
-mcp__server → MCP 服务级
-Agent(Explore) → 代理类型级
-```
-
-AstroResearch 已实现相同格式:
-```
-"*" → 通配所有工具
-"tool_name" → 精确工具名匹配
-"tool_name(content*)" → 前缀通配(如 "run_bash(rm *)" 匹配 "rm -rf /")
-"tool_name(*suffix)" → 后缀通配(如 "read_file(*.env)" 匹配 ".env")
-"tool_name(exact)" → 包含匹配(子串命中)
-"*(content)" → 工具通配 + 内容匹配(如 "*(sudo)" 匹配任意工具的 sudo 命令)
-```
-从 args 中自动提取 `command`/`file_path`/`path`/`pattern`/`url` 字段进行内容匹配。
-
-**2. 权限模式**
-
-Claude Code 的 6 种模式通过 Shift+Tab 循环切换:
-- `default` — 标准逐项确认
-- `acceptEdits` — 工作目录内文件编辑自动通过
-- `bypassPermissions` — 跳过所有 Ask(deny/ask 规则仍生效;安全检查 bypass 免疫)
-- `dontAsk` — 所有 Ask 转 Deny
-- `plan` — 计划模式
-- `auto` — AI 自动分类(内部使用)
-
-AstroResearch 已实现 4 种模式(通过 `AGENT_PERMISSION_MODE` 环境变量或 API 切换):
-- `default` — 标准规则链,Ask 触发用户交互
-- `acceptEdits` — 工作目录内文件编辑自动通过(路径检查由 executor 完成)
-- `bypassPermissions` — 跳过所有 Ask(Deny 规则仍生效)
-- `dontAsk` — 所有 Ask 转为 Deny
-
-`PermissionChecker::from_config()` 从 `AgentConfig` 加载环境变量规则并构造完整检查器。
-
-**3. 多路径权限决议**
-
-Claude Code 的 AskUser 决议支持多个并行路径,任一先返回即生效(`claim()` 模式):
-- 本地 UI 对话框
-- Bridge 响应(CCR 远程)
-- Channel 响应(Telegram 等)
-- PermissionRequest hooks(后台异步运行)
-- Bash 分类器(后台推测性异步分类)
-
-AstroResearch 已实现完整的 AskUser 交互流:
-- `PermissionChecker::check()` 返回 `AskUser` 时,executor 发送 `AgentStreamEvent::PermissionRequest` SSE 事件
-- 通过 `oneshot` 通道创建 `PendingPermission`,存入 `AppState::pending_permissions`
-- 等待用户通过前端 `PermissionRequestCard` 组件响应(Allow / Deny / Always Allow),120s 超时自动拒绝
-- 单一路径决议(oneshot),不支持多路径 claim 模式
-
----
-
-## 优化路线图
-
-### ✅ P0 — 已全部完成
-
-#### P0-1: 规则加载与持久化 ✅
-
-`AgentConfig::from_env_optional()` 从环境变量加载规则(`AGENT_PERMISSIONS_DENY`/`ALLOW`/`ASK`),`PermissionChecker::from_config()` 按 Deny → Ask → Allow 优先级顺序构造规则链。
-
-**实现位置**: `src/agent/runtime/mod.rs:113-117`, `src/agent/runtime/permission.rs:268-290`
-
-#### P0-2: 完成 AskUser 权限交互流 ✅
-
-executor Phase 2.5 中完整的 AskUser 处理:
-- `AgentStreamEvent::PermissionRequest` SSE 事件 → 前端 `PermissionRequestCard` 组件
-- `oneshot` 通道 + `AppState::pending_permissions` 存储
-- 120s 超时自动拒绝,支持 Allow / Deny / Always Allow 决策
-
-**实现位置**: `src/agent/runtime/executor.rs:282-422`, `src/api/agent.rs`, `dashboard/src/features/agent/PermissionRequestCard.tsx`
-
-#### P0-3: 内容级权限匹配 ✅
-
-`PermissionChecker::matches()` 支持 `"tool_name(content_pattern)"` 格式,前缀通配(`prefix*`)、后缀通配(`*suffix`)、包含匹配,自动从 args 提取 `command`/`file_path`/`path`/`pattern`/`url` 字段。
-
-**实现位置**: `src/agent/runtime/permission.rs:183-255`
-
-### ✅ P1 — 已全部完成
-
-#### P1-1: 权限模式系统 ✅
-
-`PermissionMode` 枚举实现 4 种模式(Default/AcceptEdits/Bypass/DontAsk),通过 `AGENT_PERMISSION_MODE` 环境变量或 API 切换。`PermissionChecker::apply_mode()` 在 executor Phase 2.5 中对检查结果进行模式变换(Bypass 将 Ask→Allowed,DontAsk 将 Ask→Denied)。
-
-**实现位置**: `src/agent/runtime/permission.rs:39-60, 139-177`
-
-#### P1-2: Bash 权限接入 PermissionChecker ✅
-
-`RunBashTool::check_permissions()` 调用 `bash_needs_permission()` —— 安全白名单中的命令返回空规则(自动允许),非白名单命令返回 `Ask` 规则。executor Phase 2.5 中与 PermissionChecker 结果叠加。
-
-**实现位置**: `src/agent/tools/filesystem/bash.rs:58-73, 362-365`
-
-#### P1-3: 权限 Hook 集成 ✅
-
-`PreToolUseAction::PermissionRequired` 在 executor 中被检测:若 hook 返回 `PermissionRequired` 且 PermissionChecker 返回 `Allowed`,则升级为 `AskUser` 触发用户交互。已修复 Continue 覆盖 meaningful action 的 bug。
-
-**实现位置**: `src/agent/hooks.rs:378-384`, `src/agent/runtime/executor.rs:221-230`
-
-#### P1-4: 子代理完整权限继承 ✅
-
-`SubAgentRunner` 使用 `check(tool_name, Some(&final_args))` 进行三态检查:Deny → 注入错误跳过执行,AskUser → 自动拒绝(子代理不应打断用户),Allowed → 正常执行。
-
-**实现位置**: `src/agent/subagent.rs`
-
-### ✅ P2-1、P2-2 — 已实现
-
-#### P2-1: 拒绝追踪与熔断 ✅
-
-`DenialTracker` 追踪连续拒绝和总拒绝数,阈值触发 ReAct 循环终止。
-配置:`AGENT_DENIAL_MAX_CONSECUTIVE` (默认 3) / `AGENT_DENIAL_MAX_TOTAL` (默认 20)。
-
-**实现位置**: `src/agent/runtime/denial_tracker.rs`, `src/agent/runtime/mod.rs`
-
-#### P2-2: 会话内规则更新 API ✅
-
-`POST /api/chat/sessions/:id/permissions/rules` — add/remove 规则
-`PUT /api/chat/sessions/:id/permissions/mode` — 切换权限模式
-通过 `AppState::session_permission_checker` (`Arc>`) 实现跨 turn 共享。
-
-**实现位置**: `src/api/permissions.rs`, `src/agent/runtime/executor.rs` Phase 2.5
-
-### 🟡 P2-3~P2-5 — 远期增强(按需实现)
-
-#### P2-3: 规则遮蔽检测 ✅
-
-`PermissionChecker::detect_shadowed_rules()` 检测 Deny/Ask 遮蔽 Allow 的情况,输出 `ShadowedRule` 列表(含 reason + fix 建议),在 AgentRuntime 初始化时通过 `warn!` 日志输出。
-
-**实现位置**: `src/agent/runtime/permission.rs`
-
-#### P2-4: 权限解释器 ✅
-
-启发式 `explain_permission()` 函数,根据工具名和参数生成 `{risk_level, explanation, reasoning, risk}` 结构。Bash 命令通过关键词检测风险等级(HIGH/MEDIUM/LOW),文件操作检测系统路径。结果随 `PermissionRequest` SSE 事件推送到前端。
-
-**实现位置**: `src/agent/runtime/permission_explainer.rs`, `src/agent/runtime/mod.rs` `AgentStreamEvent::PermissionRequest.explanation`
-
-#### P2-5: Auto Mode (AI 权限分类器) ❌
-
-使用 LLM 自动评估工具调用的风险:
-- 快速路径:`AcceptEdits` 模式自动允许工作目录内的文件编辑
-- 安全工具白名单:`read_file`、`grep_files`、`search_papers` 等只读操作自动允许
-- AI 分类:对不确定的操作调用快速模型判断安全性
-- 失败封闭:分类器不可用时拒绝所有非白名单操作(安全优先)
-
-**工作量**: 3-5天
-
-#### P2-4: 权限解释器
-
-在执行前用 LLM 生成人类可读的风险描述:
-```
-"该命令将执行 npm install,可能修改 node_modules/ 目录并下载外部依赖包。"
-```
-
-**工作量**: 1天
-
-#### P2-5: 子代理最小权限 (ToolRegistry::restrict)
-
-```rust
-impl ToolRegistry {
- pub fn restrict(&self, allowed_tools: &[&str]) -> Self {
- // 创建仅包含指定工具的受限注册表
- }
-}
-```
-
-**工作量**: 0.5天
-
----
-
-## 实现路线图
-
-```
-已完成 (Phase 1): P0-1 规则加载 + P0-2 AskUser 交互流 + P0-3 内容级匹配
-已完成 (Phase 2): P1-1 权限模式 + P1-2 Bash 集成 + P1-3 Hook 集成 + P1-4 子代理继承
-已完成 (Phase 3): P2-1 拒绝追踪熔断 + P2-2 会话内规则更新 + P2-3 规则遮蔽检测 + P2-4 权限解释器 + 附加目录沙箱 + 规则来源追踪
-远期规划 (按需): P2-5 Auto Mode (AI 分类器) + P2-6 权限解释器 LLM 升级 + 子代理最小权限 + 文件写入大小限制
-```
-
----
-
## 待完善项
| 优先级 | 项目 | 当前状态 | 建议 |
|---|---|---|---|
-| ~~**HIGH**~~ | ~~PermissionChecker 集成到主执行路径~~ | ✅ **已完成** | — |
-| ~~**HIGH**~~ | ~~Bash 黑名单改为命令解析~~ | ✅ **已完成** | — |
-| ~~**P0**~~ | ~~规则加载与持久化~~ | ✅ **已完成**:`AgentConfig` 新增 `permission_deny_rules` / `permission_allow_rules` / `permission_ask_rules` / `permission_mode` 字段,通过 `AGENT_PERMISSIONS_*` 环境变量加载 | — |
-| ~~**P0**~~ | ~~AskUser 权限交互流~~ | ✅ **已完成**:executor Phase 2.5 AskUser 分支重写为完整 oneshot → SSE → 120s 超时流程。前端 `PermissionRequestCard` 组件提供 Allow / Deny / Always Allow | — |
-| ~~**P0**~~ | ~~内容级权限匹配~~ | ✅ **已完成**:`check(tool_name, tool_args)` 签名,`matches()` 支持 `"tool(content*)"` 格式(前缀/后缀/包含),自动提取 args 字段 | — |
-| ~~**P1**~~ | ~~权限模式系统~~ | ✅ **已完成**:`PermissionMode` (Default/AcceptEdits/Bypass/DontAsk),`apply_mode()` 方法,`AGENT_PERMISSION_MODE` 配置 | — |
-| ~~**P1**~~ | ~~Bash 权限集成~~ | ✅ **已完成**:`RunBashTool::check_permissions()` 调用 `bash_needs_permission()`(安全命令自动允许),executor 合并工具级检查 | — |
-| ~~**P1**~~ | ~~权限 Hook 集成~~ | ✅ **已完成**:`PreToolUseResult::is_permission_required()`,修复 Continue 覆盖 bug,executor 触发 AskUser | — |
-| ~~**P1**~~ | ~~子代理完整权限继承~~ | ✅ **已完成**:`is_denied()` → `check(tool_name, Some(&final_args))`,子代理中 AskUser 自动拒绝 | — |
-| ~~**MEDIUM**~~ | ~~拒绝追踪与熔断~~ | ✅ **已完成** | `DenialTracker`:连续/总计拒绝计数,阈值触发 ReAct 循环终止 |
-| ~~**MEDIUM**~~ | ~~会话内规则更新 API~~ | ✅ **已完成** | `POST/PUT /api/chat/sessions/:id/permissions/*` 动态 add/remove/mode |
-| ~~**MEDIUM**~~ | ~~权限解释器~~ | ✅ **已完成** | 启发式 `explain_permission()`,Bash 风险等级 + 路径检测,随 SSE PermissionRequest 推送前端 |
| **MEDIUM** | Auto Mode (AI 分类器) | 未实现 | LLM 评估风险,快速路径 + 安全工具白名单 |
-| **LOW** | 子代理最小权限 | 继承全部父工具 | `ToolRegistry::restrict()` |
| **LOW** | 文件写入大小限制 | 无上限 | 添加 `max_file_size` 参数 |
-| **LOW** | 网络访问控制 | `curl`/`wget` 未限制 | Bash 黑名单扩展 |
-| **LOW** | 用户权限 profiles | 不支持 | YAML/TOML 权限配置 |
diff --git a/docs/architecture/agent/skills.md b/docs/architecture/agent/skills.md
index 25d5c79..254a17b 100644
--- a/docs/architecture/agent/skills.md
+++ b/docs/architecture/agent/skills.md
@@ -42,7 +42,9 @@ sequenceDiagram
| 文件 | 行数 | 职责 |
|---|---|---|
-| `src/agent/skills.rs` | 847 | SkillRegistry 缓存、文件解析、热更新、条件激活、系统提示构建 |
+| `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 |
@@ -95,6 +97,7 @@ effort: high
| `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 清单
@@ -112,7 +115,8 @@ effort: high
SkillFrontmatter — serde_yaml 解析的 YAML frontmatter,含 validate() 校验方法
│
├──▶ SkillMeta — Layer 1 摘要(name, description, context, allowed_tools,
- │ when_to_use, disable_model_invocation, user_invocable, paths)
+ │ when_to_use, disable_model_invocation, user_invocable, paths,
+ │ pinned: bool)
│
└──▶ Skill — Layer 2 完整对象(meta + body + skill_dir)
│
@@ -120,6 +124,12 @@ SkillFrontmatter — serde_yaml 解析的 YAML frontmatter,含 valida
├── skills: Vec
├── last_scan_mtime: Option
└── usage_stats: HashMap
+
+SelfImprovePipeline — 一站式管道
+ ├── PatternDetector — 扫描 agent_messages 检测重复工具调用序列
+ ├── SkillCreator — 将 DetectedPattern 转换为 SKILL.md 文件
+ └── Curator — Skill 生命周期管理(Active→Inactive→Stale→Deprecated)
+ └── CuratorRunner — 后台空闲触发审查 + 心跳记录
```
### 关键方法
@@ -319,12 +329,304 @@ Box::new(LoadSkillTool::new(skill_registry)),
- 团队成员(`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()
扫描 agent_messages 表"]
+ SC["SkillCreator::create_from_patterns()
生成 SKILL.md 文件"]
+ CR["Curator::analyze()
质量评分 + 生命周期评估"]
+ end
+
+ subgraph Background["后台定期维护"]
+ direction LR
+ Runner["CuratorRunner::spawn()
空闲触发 + 间隔检查"]
+ Archive["archive_stale_skills()
30d stale / 90d deprecated"]
+ end
+
+ PD -->|"Vec<DetectedPattern>"| SC
+ SC -->|"Vec<Skill>"| 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, // 如 ["search_papers", "download_paper", "rag_search"]
+ pub session_ids: Vec, // 出现的 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, 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` 自动从工具名推断
+- 每个工具调用作为 `` 写入正文
+- `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,
+ last_run: RwLock