
Hook 与 Middleware
编写于 2026-08-16
Peri 的运行期行为由两条线组成,一条是中间件链(MiddlewareChain)——按固定顺序执行的 Rust 组件,负责上下文注入、工具提供、权限审批等能力;另一条是用户 hook 配置——JSON 规则,绑定到 Agent 生命周期的具体节点。两者通过 HookMiddleware 衔接,关系可以一句话概括,
中间件链是承载者,Hook 是链在生命周期节点上的触发面。
本文是概述,以 Middleware 为重点,先讲中间件链如何装配与执行,再讲 HookMiddleware 如何把生命周期方法映射为 hook 事件、HookDispatcher 如何执行用户规则。机制描述与当前 peri-middlewares / peri-acp 源码一致;事件集会随实现变化,具体清单应以当前代码为准。用户侧的配置方法见 Hooks 文档。
中间件承载运行期能力
每个会话创建时,装配器(peri-middlewares/src/assembly.rs)按 Agent 层 session 工厂(peri-agent/src/session/factory.rs)的 production_blueprint 蓝本构造 MiddlewareChain。链序是行为契约,源码注释明确禁止重排。当前蓝本分七组,
| 组 | 槽位 | 职责 |
|---|---|---|
| 一 | DefaultSystemPrompt、Lang、AgentsMd、AgentDefine、Plugin、Skills、SkillPreload、AtMention、Image | 上下文注入(system prompt 段落、agent 定义、插件、技能) |
| 二 | Filesystem、GitAttribution、Terminal、Web | 文件 / 终端 / Web 工具提供器 |
| 三 | Todo、Cron | 待办与定时工具 |
| 四 | Hook | 每个非空 hook 组展开一个 HookMiddleware 实例 |
| 五 | Permission、AskUser、SubAgent | 审批、提问通道、子 Agent |
| 六 | Mcp、Workflow、Ptc、ToolSearch、Artifact | 条件工具提供器 |
| 七 | Lsp、Goal | 诊断与目标引导 |
MiddlewareChain 执行时按注册顺序遍历,run_before_agent 依次调用每个中间件的 before_agent,任一返回错误即中断整条链;run_before_tool 让每个中间件依次改写 ToolCall,前一个的输出是后一个的输入。每个中间件还可以通过 collect_tools 向会话登记自己的工具。
Middleware 之所以是承载者,在于几乎所有能力都以中间件为承载单元,上下文是中间件注入的,工具是中间件提供的,审批是中间件执行的。Hook 能做什么、在什么时候做,都受链上节点约束——它只在这些节点上获得发言权,节点之间的推进由 Agent Loop 完成(见 Agent Loop)。
生命周期节点开放配置入口
三层分工可以沿《Hook 系统》时期的结论概括,但视角放在链上,
| 层 | 职责 | 何时扩展 |
|---|---|---|
| Agent Loop | 在固定生命周期节点调用链上中间件的方法,决定什么时候触发 | 改动需要 Rust 代码 |
HookMiddleware | 把生命周期方法映射为用户可见事件,决定触发什么事件 | 新增事件需要 Rust 代码 |
hook 规则 + HookDispatcher | 决定怎么响应事件,返回 Allow / Block / ModifyInput | 纯配置驱动,无需改代码 |
生命周期映射为事件
HookMiddleware(peri-middlewares/src/hooks/middleware.rs)实现 Middleware trait 的六类生命周期方法,每类触发一组事件,
| Middleware 方法 | 触发时机 | 触发的事件 |
|---|---|---|
before_agent | 每轮推理进入 | SessionStart(仅首次)→ UserPromptSubmit → InstructionsLoaded |
before_tool | 每个工具执行前 | PreToolUse → PermissionRequest(敏感工具 + 审批弹窗) |
after_tool | 每个工具执行后 | PostToolUse / PostToolUseFailure |
after_tools_batch | 一批并行工具全部完成 | PostToolBatch |
after_agent | 输出最终回答 | Stop → Notification |
on_error | LLM 调用失败 | StopFailure |
SessionStart 只在会话首次进入时触发,source 取 startup / resume / clear / compact 之一。
另有一组 Standalone 事件不经过 Middleware trait,由具体模块直接调用 fire_standalone_lifecycle_hooks() 触发,SubagentStart / SubagentStop(SubAgent 工具)、PreCompact / PostCompact(压缩阶段)、SessionEnd(TUI 关闭)。它们共享同一个分发引擎。
一个 turn 的事件时序大致如下,
sequenceDiagram
participant Loop as Agent Loop
participant Chain as MiddlewareChain
participant MW as HookMiddleware
participant Disp as HookDispatcher
participant Hook as 用户 hook
Loop->>Chain: before_agent()
Chain->>MW: before_agent()
MW->>Disp: SessionStart / UserPromptSubmit / InstructionsLoaded
Disp->>Hook: 执行规则(command 等)
Hook-->>Disp: Allow / Block / ModifyInput
Disp-->>MW: 归约后的动作
Loop->>Chain: before_tool() → after_tool()(每个工具)
Chain->>MW: PreToolUse / PostToolUse
Loop->>Chain: after_tools_batch()
Chain->>MW: PostToolBatch
Loop->>Chain: after_agent()
Chain->>MW: Stop → Notification规则按顺序归约
HookDispatcher(peri-middlewares/src/hooks/dispatcher.rs)是执行引擎。fire_event() 按事件名查表,对每条注册规则依次做四步判断,一次性标记(once)→ matcher 粗过滤(工具名匹配)→ if 细条件 → 执行。执行结果归约为三种动作,
- Allow,允许继续
- Block,短路——不再执行后续 hook
- ModifyInput,累积修改输入,继续执行后续 hook
支持四种执行类型,通过 JSON 的 type 字段区分,
Shell 命令执行,最常用的类型。stdin 接收完整 HookInput,退出码 0 解析 stdout 为结构化决策,1 为 Allow(warn),2 为 Block。
LLM 评估。$ARGUMENTS 替换为 HookInput,一次无工具调用。只在有 LLM factory 的主路径可用。
HTTP POST,HookInput 作为 JSON body。带 SSRF 防护,阻止私有网络(10.x / 172.16.x / 192.168.x),允许 loopback。
子 Agent 评估——Claude Code 没有此类,是 Peri 的扩展。同样只在有 LLM factory 的主路径可用。
几点可靠性设计,
- fail-open,hook 超时或进程异常按 Allow 处理,hook 不应成为 Agent 的单点故障。
- Stop 阻断,
Stop是唯一能阻止 Agent 继续执行的事件,支持continue: false彻底停止,或decision: block软阻断重试;连续 Block 超过 8 次强制放行,防止规则过严导致 Agent 永远无法结束。软阻断反馈以Human+<system-reminder>注入,不使用BaseMessage::system(),以免破坏冻结的 system prompt 前缀。 - 双重过滤,matcher 在锁外快速跳过无关工具,if 在锁内精确匹配。
配置进入中间件链
hook 规则从多个来源加载,由装配端(peri-acp/src/host/assemble.rs)聚成 hook 组,
- 插件 hooks,
hooks/hooks.json(优先)或plugin.json的hooks字段 - 全局
~/.claude/settings.json - 项目
.claude/settings.json - 项目
.claude/settings.local.json
每个非空组生成一个 HookMiddleware 实例,按组顺序挂到链的第四组槽位(Todo / Cron 之后、Permission 之前)。这意味着,同一事件下,配置靠后的组在链上执行得也更靠后;PreToolUse 类 hook 先于权限审批运行,可以在弹窗出现前改写或阻断工具调用。
配置结构沿用 Claude 风格——事件名 → matcher 组 → hook 列表,
{ "hooks": { "PreToolUse": [ { "matcher": "Bash|Write|Edit", "hooks": [ { "type": "command", "command": "python3 audit.py" } ] } ] }}限制与边界
- Agent Loop — RCRA 循环与中间件链的调用点
- Multi Agent 架构 — 子 Agent 的 standalone hook 触发路径
- 系统提示词 — 其他中间件如何以段落持有者身份注入上下文
- Hooks 文档 — hooks 的用户级配置与操作方法