如何用 MCP 生命周期钩子(_Stop、_PostCompact)扩展 buzz-agent 的执行循环?
buzz-agent 是一个通过 ACP(JSON-RPC 2.0 over stdio)接收 session/prompt 的 LLM agent 子进程,它的主循环是:调用 LLM → 执行 MCP 工具调用 → 把结果喂回去 → 重复,直到 LLM 停止调用工具。有些场景下你希望在这个循环里插入自己的逻辑:比如 LLM 已经发出 end_turn,但测试还是红的,你希望 agent 继续干活;或者上下文被压缩(compaction/handoff)之后,希望把 todo 状态重新注入新的上下文。
buzz 用一套 MCP 生命周期钩子解决这件事:任何 MCP server 只要暴露以 _ 开头的工具,agent 就会在定义好的生命周期点调用它。这套约定不需要任何 MCP 协议改动——钩子就是普通的 MCP 工具,通过 tools/list 发现、tools/call 调用,对 LLM 不可见,且默认关闭,必须由操作者通过环境变量显式开启。本文按「实现钩子 server → 在 agent 侧开启 → 验证行为」的顺序走一遍完整路径。
两个钩子点的契约
目前 buzz-agent 实现了两个钩子点,命名与 Open Plugin Spec 的事件约定对齐(_Stop 对应 Stop 事件,_PostCompact 对应 PostCompact),详见 docs/MCP_DRIVEN_HOOKS.md:
| 钩子 | 触发时机 | 输入 | 输出语义 | 典型用途 |
|---|---|---|---|---|
_Stop |
LLM 发出 end_turn 之后、agent 真正接受它之前 |
{} |
非空文本 = 反对(objection),agent 继续循环;空 = 不反对,agent 停止 | 强制 todo 完成——只要还有未完成任务就反对停止 |
_PostCompact |
上下文压缩/交接之后、下一次 LLM 请求之前 | {} |
非空文本 = 注入到新的上下文;空 = 什么都不注入 | 历史被总结重置后,重新注入 todo 列表状态 |
信任模型上有几条硬约束,写钩子前需要知道:
- 钩子对 LLM 不可见:以
_开头的工具名会被从发给 LLM 的工具列表里过滤掉;LLM 如果直接调用钩子,调用会被拒绝(按未知工具处理)。 - 钩子是建议性(advisory)而非权威的:agent 用超时和预算限制防止一个有 bug 或恶意的钩子把 agent 拖死。
- 钩子的响应被作为 tool-result 消息注入历史(信任级别低于 system 消息),并且输出会做 JSON 编码,用于防 prompt 注入。
在 MCP server 中实现钩子
实现侧没有任何特殊 API:让你的 MCP server 在工具列表里返回一个裸名(bare name)以 _ 开头的工具即可。buzz-agent 文档给出的示例是一个测试 runner server,在测试失败时阻止 end_turn:
{
"name": "_Stop",
"description": "Returns failing test summary if suite is red.",
"inputSchema": { "type": "object" }
}
返回非空文本即表示反对,返回空字符串即允许 agent 停止。
需要注意 buzz-agent 对 MCP 的两条接入约束(见 crates/buzz-agent/README.md):
- 传输只有 stdio,没有 HTTP/SSE。agent 在
initialize响应中显式声明mcpCapabilities.http: false, sse: false。 - MCP server 由 ACP 客户端在
session/new里传入,agent 把每个 server 作为 stdio 子进程拉起。也就是说,钩子 server 的注册不经过 agent 的环境变量——你需要在你使用的 ACP 客户端(buzz-acp、Zed 等)的session/new参数里添加该 server:
{
"jsonrpc": "2.0",
"id": 2,
"method": "session/new",
"params": {
"cwd": "/work",
"mcpServers": [
{
"name": "testguard",
"command": "/usr/local/bin/testguard-mcp",
"args": [],
"env": []
}
]
}
}
上面的 testguard 和 /usr/local/bin/testguard-mcp 按你的实际 server 名与二进制路径替换;name 字段就是后续 agent 侧 allowlist 要填的 server 名。工具名在 agent 内部以 server__tool(双下划线分隔)做命名空间,所以 _Stop 钩子的全限定名形如 testguard___Stop。
在 agent 侧开启钩子并调参
钩子默认关闭。开启方式是给 buzz-agent 进程设置环境变量(agent 的全部配置都走环境变量,没有配置文件),相关项来自 docs/MCP_DRIVEN_HOOKS.md 的配置表:
| 环境变量 | 默认 | 说明 |
|---|---|---|
MCP_HOOK_SERVERS |
未设置 = 无钩子 | allowlist:* 表示所有 server,或逗号分隔的 server 名 |
BUZZ_AGENT_HOOK_TIMEOUT_MS |
2500 |
单次钩子调用超时(毫秒) |
BUZZ_AGENT_STOP_MAX_REJECTIONS |
3 |
每个 prompt 的 _Stop 反对预算;0 = 禁用 _Stop |
MCP_HOOK_SERVERS 被设计为标准环境变量名,意图是跨 agent 采用;allowlist 的解析逻辑(空值/未设置视为关闭、* 通配、逗号分隔名单)在 crates/buzz-agent/src/config.rs 的 HookServers 与 parse_hook_servers 中。
结合 crates/buzz-agent/README.md 的 Quick Start,一个开启了钩子的启动命令长这样(sk-ant-... 替换为你自己的 API key,testguard 替换为你在 session/new 中注册的名字):
cargo build --release -p buzz-agent
BUZZ_AGENT_PROVIDER=anthropic \
ANTHROPIC_API_KEY=sk-ant-... \
ANTHROPIC_MODEL=claude-sonnet-4-5 \
MCP_HOOK_SERVERS=testguard \
./target/release/buzz-agent
两个参数怎么取值,文档给出的约束是:
- 超时:单次钩子调用默认 2.5 秒,超时按“不反对”处理(fail-open);agent 只会在连续第二次超时时才杀掉该 server 的进程组,容忍一次性的偶发慢调用。相关逻辑在 crates/buzz-agent/src/mcp.rs 的
call_hooks。 - 反对预算:每个 prompt 内
_Stop最多被接受 3 次反对;耗尽后 agent 无论如何都停止,预算在下一个 prompt 时重置。call_hooks会并行调用 allowlist 内所有暴露了该钩子的 server,按注册顺序返回结果,丢弃空响应、错误和超时——钩子永不阻塞 agent。
验证钩子是否生效
仓库自带的集成测试就是现成的验证路径。buzz-agent 的测试策略是“真实子进程、无 mock”:tests/fake_llm.rs 起真实的 TCP listener 返回脚本化 JSON,tests/bin/fake_mcp.rs 是一个可用环境变量操控行为的假 MCP server 二进制。运行:
cargo test -p buzz-agent
其中 crates/buzz-agent/tests/permission_boundary.rs 直接驱动了钩子链路:它设置 MCP_HOOK_SERVERS=fake,让一个纯文本轮次触发 _Stop gate、钩子反对一次、agent 继续循环,然后断言 _Stop 恰好通过 call_hooks 到达 MCP server 一次,且该调用不产生权限询问。如果你的钩子 server 行为符合 _Stop 契约,这类断言在真实 server 上对应的可观察现象是:
- agent 在 LLM 发出
end_turn后没有结束轮次,而是继续执行(_Stop返回了非空文本,反对被接受); _Stop返回空或超时时,agent 正常结束轮次;- 连续超时会在 agent 日志(stderr)里出现警告,连续第二次超时后该 server 被终止并标记为 dead。
边界与限制
- 钩子不是插件系统。crates/buzz-agent/README.md 明确说它们是 advisory、fail-open、有预算上限的——不要把它们当成可以强制执行外部策略的机制。
- reply guard 不是钩子。buzz-agent 在同一个
_Stopgate 上还有一个进程内的反对:BUZZ_AGENT_REQUIRE_REPLY=1时,轮次即将以“没有任何发布到 Buzz 的尝试”结束时,会给模型一条提醒。它没有对应的 hook 工具、没有 server 可加进 allowlist,但它的提醒占用同一个BUZZ_AGENT_STOP_MAX_REJECTIONS预算——同一轮同时带钩子反对和提醒只计一次拒绝、两段文本都会送达;预算设为0会同时禁用两者。详见 crates/buzz-agent/README.md 的 Reply Guard 一节。 - 目前只有
_Stop和_PostCompact两个钩子点已实现;_PreToolUse、_PostToolUse因与 MCP Interceptors 工作组(SEP-2624)在协议层解决的问题重叠而推迟,SessionStart、SessionEnd等是后续候选——如果你的扩展方案依赖这些钩子点,文档明确说它们尚未提供。
完成 cargo test -p buzz-agent 通过后,剩下的调优空间只有两个旋钮:BUZZ_AGENT_HOOK_TIMEOUT_MS(你的钩子逻辑实际耗时多少)和 BUZZ_AGENT_STOP_MAX_REJECTIONS(允许模型被“打回”多少次),两者都按上面的约束取值即可。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust4.24 K638- DDeepSeek-V4.1-FlashDeepSeek-V4.1-Flash 是一个多模态混合专家(MoE)模型,拥有 5520 亿骨干参数,并支持最多一百万 token 的上下文长度。该模型原生支持图像和文本输入,并以自回归方式生成文本Python650
SlideSCIPPT插件,支持素材库、AI助手、一键添加图片标题,复制粘贴位置、一键图片对齐、一键插入Markdown(加粗、超链接等行内样式、代码块、LaTeX等块级样式)、便捷导出图片!C#180
hello-agents📚 《从零开始构建智能体》——从零开始的智能体原理与实践教程Python52774
new-apiAI模型聚合管理中转分发系统,一个应用管理您的所有AI模型,支持将多种大模型转为统一格式调用,支持OpenAI、Claude、Gemini等格式,可供个人或者企业内部管理与分发渠道使用。🍥 A Unified AI Model Management & Distribution System. Aggregate all your LLMs into one app and access them via an OpenAI-compatible API, with native support for Claude (Messages) and Gemini formats.Go22545
JeecgBoot🔥企业级低代码平台集成了AI应用平台,帮助企业快速实现低代码开发和构建AI应用!前后端分离架构 SpringBoot,SpringCloud、Mybatis,Ant Design4、 Vue3.0、TS+vite!强大的代码生成器让前后端代码一键生成,无需写任何代码! 引领AI低代码开发模式: AI生成->OnlineCoding-> 代码生成-> 手工MERGE,显著的提高效率,又不失灵活~Java36351