AstroResearch/docs/architecture/agent/hooks.md
Asfmq f6df9d8136 feat: Agent 思考模式前端可控、子代理全链路持久化、权限系统、工具 ID 追踪体系、前端面板与文档架构重构
- 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 等变量说明
2026-06-18 01:21:02 +08:00

275 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

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

# 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&lt;HashSet&lt;String&gt;&gt;"]
Registry --> MH["MetricsHook<br/>Arc&lt;Mutex&lt;MetricsData&gt;&gt; (共享)"]
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 调用对应方法,不收集返回值。
## 内置 Hooks3 个)
| 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 拦截 |
---