Docker 容器化部署
- 提供 Mode A (Alpine musl, ~23MB) 和 Mode B (Distroless glibc, ~87MB)
两种镜像,Docker Compose 一键启动
- build.rs 支持 SKIP_DASHBOARD_BUILD 跳过前端构建
- 国内镜像加速 (npm/apt/apk) 通过 USE_MIRRORS build-arg 控制
安全:Cookie-Based 鉴权系统
- HttpOnly/SameSite=Strict Cookie 会话管理(24h 过期自动清理)
- 登录/登出/验证接口 + 中间件注入
- 前端登录页面 + 退出按钮
- 三层 CORS:localhost 鉴权 / 全放通 bookmarklet / 受保护路由
- 书签脚本 fetch 添加 credentials:'include'
Coordinator 模式 (P2)
- 4 个 meta-tool (delegate_task/check_task/task_stop/synthesize)
- WorkerPool + Semaphore 并发控制 + 超时保护
- 前端协调者模式开关
Hook 系统:UserPromptSubmit 事件 (P2)
- 第 13 个生命周期事件,fire-and-forget 审计
FTS5 全文搜索 (P3)
- agent_sessions_fts + agent_messages_fts 虚拟表
- search_history Agent 工具 + /api/search/history HTTP 接口
- 前端防抖搜索框 + 仅当前会话筛选
工具加载优化 (P3)
- defer_loading 延迟加载 (7 个重型工具)
- is_readonly 只读标记 (9 个查询工具)
- classifier_summary 工具目录供 LLM 按需判断
模型回退策略 (P3)
- LLM_FALLBACK_MODEL 优先回退 + LLM_FALLBACK_CHAIN 链式轮换
- LlmClient model 改为 Arc<RwLock> 支持运行时切换
- 连续 3 次过载后自动切换
压缩记忆桥接 (P3)
- 压缩丢弃消息 → 子代理提取持久记忆 (extract_memories_from_compaction)
git2 依赖修复
- 切换到 vendored-libgit2,消除 OpenSSL 系统依赖
33 KiB
Agent Hooks — 生命周期事件系统
参考 Claude Code hooks 协议,提供 13 种生命周期事件回调,基于 观察者模式 + 责任链模式 实现。核心目标:在 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 行)
设计哲学
mindmap
root((Hook 系统<br/>设计原则))
接口隔离
11 个生命周期方法
+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
析构自动清理
架构总览
graph TB
subgraph RT["AgentRuntime::run_turn()"]
direction TB
Start["OnSessionStart"] --> Loop["ReAct Loop"]
Loop --> Comp["PreCompact / PostCompact"]
Loop --> Stop["OnSessionStop"]
end
subgraph Registry["HookRegistry"]
direction LR
Index["event_index<br/>HashMap<HookEvent, Vec<usize>><br/>预计算索引"]
AIndex["async_event_index<br/>异步 hook 索引"]
SH["session_hooks<br/>HashMap<String, Vec<Box<dyn AgentHook>><br/>运行时动态管理"]
end
subgraph Executor["executor::execute_parallel()"]
direction LR
Pre["PreToolUse<br/>并行 join_all + timeout + match_filter"]
Exec["工具执行"]
Post["PostToolUse<br/>并行 join_all + timeout + match_filter"]
Fail["PostToolUseFailure<br/>is_error 时触发"]
Async["AsyncAgentHook<br/>fire-and-forget, 5s 超时"]
end
subgraph Builtins["内置 Hooks(3 个 sync + 0 async)"]
CH["CancellationHook<br/>订阅: PreToolUse, OnSessionStop"]
MH["MetricsHook<br/>订阅: OnSessionStart, PostToolUse,<br/>OnStepComplete, OnSessionStop"]
AH["AuditLogHook<br/>订阅: PostToolUse, OnSessionStop"]
end
RT --> Registry
Loop --> Executor
Registry --> Builtins
Executor --> Registry
Executor --> Async
类型系统
classDiagram
class AgentHook {
<<trait>>
+name() &str
+subscribed_events() &[HookEvent]
+timeout() Option~Duration~
+match_filter() ToolMatchFilter
+on_session_start(ctx)
+on_user_prompt_submit(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)
}
class AsyncAgentHook {
<<trait>>
+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 {
<<enumeration>>
OnSessionStart
UserPromptSubmit
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 {
<<enumeration>>
Exact(String)
Prefix(String)
Wildcard
+parse(pattern) Self
+matches(tool_name) bool
}
class PreToolUseAction {
<<enumeration>>
Continue
Block ~reason: String~
MutateInput ~updated_args, additional_context~
PermissionRequired ~permission, tool_name~
}
class PermissionRequestAction {
<<enumeration>>
Continue
Override ~decision: PermissionDecision, reason~
InjectContext ~context: String~
}
class PermissionDecision {
<<enumeration>>
Allow
Ask
Deny
}
class PermissionDenialSource {
<<enumeration>>
Rule
Classifier
User
Timeout
}
class PostToolUseAction {
<<enumeration>>
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_user_prompt_submit(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 |
| UserPromptSubmit | UserPromptSubmitContext |
session_id, prompt, turn_index (fire-and-forget) |
| 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
// 仅匹配文件相关工具且参数路径包含 .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 } |
标记警告(不阻止) | 注入 <system-reminder> |
PermissionRequired { permission, tool_name } |
事后权限请求(advisory) | 记录审计日志 |
Metadata { key, value } |
附加结构化元数据 | 保留在 PostToolUseResult.metadata 中 |
异步 Hook (AsyncAgentHook)
与 AgentHook 分离设计,适合后台操作(远程上报、大文件处理等):
#[async_trait]
pub trait AsyncAgentHook: Send + Sync {
fn name(&self) -> &str;
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<'_>) {}
}
- 注册:
registry.add_async(Box::new(my_async_hook)) - 调度:与 sync hook 并行执行,默认 5s 超时
- 返回值:无——fire-and-forget 语义
TaggedContext — Hook 来源追踪
注入 LLM 的 system-reminder 携带 hook 来源标记:
[Hook: AuditLogHook | PostToolUse] 内容...
[Hook: CancellationHook | PreToolUse] 内容...
替代原来的匿名 [Hook 注入上下文]。
ContextDeduplicator — 内容去重
单 dispatch cycle 内防止多个 hook 注入相同上下文消息:
let mut dedup = ContextDeduplicator::new();
for ctx in &result.hook_contexts {
if !dedup.is_duplicate(ctx) {
messages.push(ChatMessage::user(ctx));
}
}
权限决策优先级
当多个权限来源冲突时,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) | — |
详见 permission.md。
PermissionRequest / PermissionDenied — 权限决策钩子
这两个专用事件在权限决策管线中提供 Hook 级别的可编程控制点,参考 Claude Code 的 PermissionRequest / PermissionDenied 事件设计。
PermissionRequest — 权限请求前
触发时机:resolve_permission_precedence() 之后、最终权限决策生效之前。
调度策略:并行调用所有匹配的 hook,第一个返回 Override 的生效(后续 hook 仍执行但结果忽略),其他 hook 返回 Continue 表示不干预。
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
返回值语义:
| 动作 | 效果 |
|---|---|
PermissionRequestAction::Continue |
不干预,沿用 resolve_permission_precedence() 的结果 |
PermissionRequestAction::Override { decision, reason } |
覆盖决策:Allow 强制放行、Ask 升级确认、Deny 强制拒绝 |
PermissionRequestAction::InjectContext { context } |
向 LLM 注入额外上下文(如安全策略解释) |
上下文字段:
| 字段 | 类型 | 说明 |
|---|---|---|
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 |
权限请求超时自动拒绝 |
使用示例:
struct DenyRateLimiter {
deny_counts: Arc<Mutex<HashMap<String, u32>>>,
}
#[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);
}
}
生命周期事件全景
sequenceDiagram
participant API as API Handler
participant RT as AgentRuntime
participant HR as HookRegistry
participant EX as Executor
participant SA as SubAgentRunner
participant CMP as Compact
Note over API,CMP: ═══ Phase 1: 会话启动 ═══
API->>RT: run_turn(question)
RT->>HR: with_builtins(db, cancelled, metrics)
RT->>HR: run_on_session_start(ctx) ⚡并行
Note over HR: ① OnSessionStart
RT->>HR: run_on_user_prompt_submit(ctx) 🔥fire-and-forget
Note over HR: ⑬ UserPromptSubmit
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(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 {messages, hook_contexts, blocking_errors}
RT->>RT: "hook_contexts → [system-reminder] 注入 (去重)"
RT->>HR: run_on_step_complete(ctx) ⚡并行
Note over HR: ⑤ OnStepComplete
opt 上下文超限
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,CMP: ═══ Phase 3: 会话收尾 ═══
RT->>RT: finalize_turn()
RT->>HR: run_on_session_stop(ctx) 🔒顺序
Note over HR: ⑥ OnSessionStop
RT-->>API: SSE Done
执行模式图例:⚡ 并行 join_all | 🔒 顺序执行 | ⚡+⏱ 并行 + per-hook timeout
HookRegistry 调度机制
事件索引 + 工具过滤
flowchart LR
subgraph Register["add(hook)"]
H["hook: Box<dyn AgentHook>"] --> Subs{"subscribed_events()"}
Subs -->|"空 = 全部"| All["遍历所有 13 个 HookEvent<br/>event_index[event].push(idx)"]
Subs -->|"指定"| Spec["仅注册声明的事件<br/>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
关键:collect_tool_hooks_for() 在 collect_hooks_for() 基础上增加 match_filter() 过滤层。全局 hooks 按事件索引 + 工具匹配双重过滤,session hooks 不过滤(保持全量响应)。
数据流:Hook 结果如何影响 Agent 行为
flowchart TD
subgraph Executor["executor::execute_parallel()"]
PreHooks["PreToolUse hooks → PreToolUseResult"]
Perms["权限检查 4 层叠加<br/>Checker → 工具级规则 → 会话级规则 → resolve_permission_precedence"]
PermHooks["PermissionRequest hooks → PermissionRequestAction<br/>首个 Override 生效,可覆盖 Allow→Ask 升级或 Deny→Ask 降级"]
PermDenied["PermissionDenied hooks → fire-and-forget 审计<br/>Rule/Classifier/User/Timeout 四种来源标记"]
Tools["工具执行"]
PostHooks["PostToolUse hooks → PostToolUseResult"]
FailHooks["PostToolUseFailure hooks<br/>(仅 is_error)"]
AsyncHooks["AsyncAgentHook dispatch<br/>(fire-and-forget, 5s 超时)"]
end
subgraph ReactLoop["AgentRuntime::run_react_loop()"]
Push["tool_messages → messages"]
Inject["hook_contexts 包装为 [system-reminder]<br/>(带 TaggedContext 来源 + ContextDeduplicator 去重)"]
LogBlock["blocking_errors → warn! 日志"]
LogWarn["warnings → warn! 日志"]
LogPerm["post_permission_requests → info! 审计"]
StepHook["run_on_step_complete"]
end
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 注入格式
// 带来源标记(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!(
"<system-reminder>\n[Hook 注入上下文]\n{}\n</system-reminder>",
ctx
);
messages.push(ChatMessage::user(&reminder));
}
内容格式:
[Hook: AuditLogHook | PostToolUse] 内容...
[Hook: CancellationHook | PreToolUse] 内容...
内置 Hooks 详解
CancellationHook
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<Mutex<HashSet<String>>>(与 AppState.cancelled_runs 共享引用)。
MetricsHook
通过 from_arc() 复用 AgentRuntime 自身的 metrics_data: Arc<Mutex<MetricsData>>,确保 hook 内部采集的数据与 AgentRuntime::get_metrics() API 返回的是同一份数据。
订阅事件:OnSessionStart(设置 session_id)、PostToolUse(累加计数)、OnStepComplete(每 3 步输出摘要)、OnSessionStop(输出终止原因)。
AuditLogHook
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
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<Duration> {
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,
}
}
}
// 注册
registry.add(Box::new(MyCustomHook));
自定义 Async Hook
struct RemoteLogger;
#[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;
});
}
}
registry.add_async(Box::new(RemoteLogger));
动态 Session Hook
// 为当前会话临时添加
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 |
用户输入提交后 | run_on_user_prompt_submit() (fire-and-forget) |
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 审计 |
| 2026-06-23 | Phase 6: UserPromptSubmit hook — 第 13 个生命周期事件,在用户提交提示词后、context building 前触发(fire-and-forget),hooks 可以检查/记录用户输入 |
测试覆盖
| 测试 | 覆盖路径 |
|---|---|
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 四种来源标记 |