# Agent Hooks — 生命周期事件系统 参考 Claude Code hooks 协议,提供 **12 种生命周期事件回调**,基于 **观察者模式 + 责任链模式** 实现。核心目标:在 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 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
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 ``` --- ## 类型系统 ```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) } 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 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 注入相同上下文消息: ```rust 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](./permission.md)。 --- ## PermissionRequest / PermissionDenied — 权限决策钩子 这两个专用事件在权限决策管线中提供 Hook 级别的可编程控制点,参考 Claude Code 的 `PermissionRequest` / `PermissionDenied` 事件设计。 ### PermissionRequest — 权限请求前 **触发时机**:`resolve_permission_precedence()` 之后、最终权限决策生效之前。 **调度策略**:并行调用所有匹配的 hook,**第一个返回 `Override` 的生效**(后续 hook 仍执行但结果忽略),其他 hook 返回 `Continue` 表示不干预。 ```mermaid 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` | 权限请求超时自动拒绝 | **使用示例**: ```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); } } ``` --- --- ## 生命周期事件全景 ```mermaid 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 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 调度机制 ### 事件索引 + 工具过滤 ```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 ``` **关键**:`collect_tool_hooks_for()` 在 `collect_hooks_for()` 基础上增加 `match_filter()` 过滤层。全局 hooks 按事件索引 + 工具匹配双重过滤,session hooks 不过滤(保持全量响应)。 --- ## 数据流:Hook 结果如何影响 Agent 行为 ```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 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 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, } } } // 注册 registry.add(Box::new(MyCustomHook)); ``` ### 自定义 Async Hook ```rust 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 ```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 四种来源标记 |