- AgentConfig/LlmClient 新增 enable_thinking 参数,前端 SSE 请求传递 thinking 开关,仅千问/DashScope 时启用 - 完善权限系统,支持细粒度的权限控制和用户权限申请 - delegate_research 工具重命名为 subagent,SubAgentTool/SubAgentRunner 重构 - 子代理消息(system/user/assistant/tool)持久化到 agent_messages 表,带 agent_name 标识 - 子代理活动日志(工具调用列表+思考摘要)注入返回结果,Hooks 获得正确 session_id 和 subagent_name - LLM 工具调用 ID 回退生成 UUID(llm.rs),ToolCall/ToolResult SSE 事件增加 id/tool_call_id 双字段 - ToolContext 扩展 session_id/sse_tx/enable_thinking 字段,executor 统一注入而非构造函数传参 - agent_messages 新增 metadata+raw_json 列,agent_sessions 暴露 summary 字段 - 删除文件级 transcript 快照(compact.rs),改为依赖 DB 持久化 - ResearchAgentPanel 重写:TimelineItem 类型替代 StreamStep,支持会话历史回放 - 新增 AgentMetricsPanel/AskUserQuestionCard/AuditLogViewer 三个前端组件,types.ts 完整类型定义 - docs/architecture/ 分层重组:概览/核心模块/核心工作流 + agent/ 子目录 11 篇专题文档 - docs/api.md 补充 RAG/Target/Agent 接口,docs/development.md 新建开发指南 - .env.example 完全重写,补充 FALLBACK_MODEL 等变量说明
275 lines
12 KiB
Markdown
275 lines
12 KiB
Markdown
# Hooks 生命周期系统 (`hooks.rs`)
|
||
|
||
参考 Claude Code hooks 协议,提供 **9 种生命周期事件回调**,基于 **观察者模式 + 责任链模式** 实现。
|
||
|
||
核心思路:允许在 Agent 运行的各个关键节点插入自定义逻辑(取消检查、指标采集、审计日志等),而不污染核心 ReAct 循环代码。
|
||
|
||
## 架构总览
|
||
|
||
```mermaid
|
||
graph TB
|
||
subgraph Registry["HookRegistry"]
|
||
direction TB
|
||
Methods["聚合方法(遍历所有 hook 依次调用)<br/>run_on_session_start() | run_pre_tool_use()<br/>run_post_tool_use() | run_on_step_complete()<br/>run_on_session_stop() | run_on_subagent_start()<br/>run_on_subagent_stop() | run_on_pre_compact()<br/>run_on_post_compact()"]
|
||
end
|
||
|
||
Registry --> CH["CancellationHook<br/>Arc<HashSet<String>>"]
|
||
Registry --> MH["MetricsHook<br/>Arc<Mutex<MetricsData>> (共享)"]
|
||
Registry --> AH["AuditLogHook<br/>SqlitePool (fire-and-forget 写入)"]
|
||
```
|
||
|
||
## 生命周期事件(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()` |
|
||
|
||
## 核心类型
|
||
|
||
### AgentHook trait
|
||
|
||
```rust
|
||
#[async_trait]
|
||
pub trait AgentHook: 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) {}
|
||
}
|
||
```
|
||
|
||
所有 9 个方法都有默认空实现——hook 实现者只需覆写关心的 hook 点,遵循**接口隔离原则**。
|
||
|
||
### PreToolUseAction(工具执行前返回值)
|
||
|
||
```rust
|
||
pub enum PreToolUseAction {
|
||
Continue, // 允许执行(默认)
|
||
Block { reason: String }, // 阻止执行
|
||
MutateInput { // 修改参数 + 注入上下文
|
||
updated_args: serde_json::Value,
|
||
additional_context: Option<String>,
|
||
},
|
||
PermissionRequired { permission: String, tool_name: String }, // 需要权限决策 (Phase 2)
|
||
}
|
||
```
|
||
|
||
向后兼容:`pub type HookAction = PreToolUseAction;`
|
||
|
||
### PostToolUseAction(工具执行后返回值)
|
||
|
||
```rust
|
||
pub enum PostToolUseAction {
|
||
Continue, // 保持输出不变
|
||
MutateOutput { updated_content: String }, // 修改输出内容
|
||
}
|
||
```
|
||
|
||
### 聚合结果类型
|
||
|
||
```rust
|
||
pub struct PreToolUseResult {
|
||
pub action: PreToolUseAction, // 最终动作(第一个 Block 获胜)
|
||
pub additional_context: Option<String>, // 累积的附加上下文(所有 MutateInput 拼接)
|
||
pub final_args: serde_json::Value, // 最终参数(最后一个 MutateInput 获胜)
|
||
}
|
||
|
||
pub struct PostToolUseResult {
|
||
pub final_content: String, // 最终输出(最后一个 MutateOutput 获胜)
|
||
}
|
||
```
|
||
|
||
## HookRegistry 聚合逻辑
|
||
|
||
**PreToolUse 聚合(责任链 + 短路)**:
|
||
|
||
```mermaid
|
||
flowchart TD
|
||
Start["run_pre_tool_use(ctx)"] --> Loop["遍历 hooks,依次调用 pre_tool_use()"]
|
||
Loop --> Check{"结果类型?"}
|
||
Check -->|"Block"| Short["立即短路返回<br/>不询问后续 hooks"]
|
||
Check -->|"MutateInput"| Mut["更新 final_args<br/>累积 additional_context (\\n 拼接)"]
|
||
Check -->|"PermissionRequired"| Log["记录日志但不阻止执行<br/>(Phase 2 预留)"]
|
||
Check -->|"Continue"| Next["继续下一个 hook"]
|
||
Mut --> Next
|
||
Log --> Next
|
||
Next --> Loop
|
||
Short --> Return["返回 PreToolUseResult"]
|
||
Next -->|"遍历完毕"| Return
|
||
```
|
||
|
||
关键设计:
|
||
- **第一个 Block 获胜** — 短路保护
|
||
- **最后一个 MutateInput 获胜** — 后覆盖前
|
||
- **additional_context 累积** — 多个 hook 的上下文用 `\n` 连接
|
||
|
||
**PostToolUse 聚合(全部执行,无短路)**:
|
||
|
||
```mermaid
|
||
flowchart TD
|
||
Start2["run_post_tool_use(ctx)"] --> Loop2["遍历所有 hooks,依次调用 post_tool_use()"]
|
||
Loop2 --> MutOut{"MutateOutput ?"}
|
||
MutOut -->|"是"| Update["更新 final_content<br/>(最后的 MutateOutput 获胜)"]
|
||
MutOut -->|"Continue"| Next2["继续下一个 hook"]
|
||
Update --> Next2
|
||
Next2 --> Loop2
|
||
Next2 -->|"遍历完毕"| Return2["返回 PostToolUseResult { final_content }"]
|
||
```
|
||
|
||
**其余 7 个事件** 均为 fire-and-forget:遍历所有 hooks 调用对应方法,不收集返回值。
|
||
|
||
## 内置 Hooks(3 个)
|
||
|
||
| Hook | 覆写的事件 | 职责 | 关键依赖 |
|
||
|------|-----------|------|---------|
|
||
| `CancellationHook` | `pre_tool_use`, `on_session_stop` | 每次工具执行前检查用户是否中止会话;会话停止时清理取消令牌 | `Arc<Mutex<HashSet<String>>>` (与 AppState 共享) |
|
||
| `MetricsHook` | `on_session_start`, `post_tool_use`, `on_step_complete`, `on_session_stop` | 采集运行指标:工具调用次数、步数、错误数、token 消耗;每 3 步输出摘要日志 | `Arc<Mutex<MetricsData>>` (**共享引用**,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
|
||
|
||
Note over API,EX: 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
|
||
|
||
Note over API,EX: Phase 2 — ReAct 循环
|
||
loop 每步 (最多 max_steps)
|
||
RT->>RT: LLM 流式调用
|
||
RT->>EX: execute_parallel(tool_calls, hook_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 }
|
||
end
|
||
end
|
||
|
||
EX-->>RT: ToolExecutionResult { tool_messages }
|
||
RT->>HR: run_on_step_complete(ctx)
|
||
HR->>MH: 每 3 步输出摘要日志
|
||
|
||
opt 上下文超限
|
||
RT->>RT: snapshot_compress_restore()
|
||
Note over RT: PreCompact / PostCompact hooks 触发
|
||
end
|
||
end
|
||
|
||
Note over API,EX: 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)
|
||
```
|
||
|
||
## HookRegistry 构建
|
||
|
||
`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 (共享引用)
|
||
);
|
||
```
|
||
|
||
`MetricsHook` 使用 `from_arc()` 复用 `AgentRuntime` 自身的 `metrics_data: Arc<Mutex<MetricsData>>`,确保 hook 内部采集的指标与 `AgentRuntime::get_metrics()` API 查询返回的是同一份数据。
|
||
|
||
## 子代理中的 Hooks
|
||
|
||
`SubAgentRunner` 拥有独立的 hook 管道,共享同一个 `HookRegistry` 实例:
|
||
|
||
- `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 事件**不会**在子代理压缩时触发
|
||
|
||
## 扩展方式
|
||
|
||
添加自定义 hook 只需两步:
|
||
|
||
```rust
|
||
// 1. 实现 AgentHook trait
|
||
struct MyCustomHook;
|
||
|
||
#[async_trait]
|
||
impl AgentHook for MyCustomHook {
|
||
fn name(&self) -> &str { "MyCustomHook" }
|
||
|
||
async fn pre_tool_use(&self, ctx: &PreToolUseContext) -> PreToolUseAction {
|
||
// 自定义逻辑
|
||
PreToolUseAction::Continue
|
||
}
|
||
}
|
||
|
||
// 2. 注册到 HookRegistry
|
||
registry.add(Box::new(MyCustomHook));
|
||
```
|
||
|
||
## 测试覆盖
|
||
|
||
`hooks.rs` 包含 8 个单元测试(`#[cfg(test)] mod tests`),覆盖:
|
||
|
||
| 测试 | 验证点 |
|
||
|------|-------|
|
||
| `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 调用 |
|
||
|
||
## 已知改进项
|
||
|
||
| 问题 | 说明 |
|
||
|------|------|
|
||
| `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 拦截 |
|
||
|
||
---
|