docs/design/hook-system-design.zh.md
refactor/agent)本设计围绕两个议题展开:
#1316:把 agent loop 重构为事件驱动、可中断、可追加、可观测#1796:在 EventBus 稳定后,把 hooks 设计为 EventBus 的 consumer,而不是重新发明一套事件模型当前分支已经完成了第一步里的“事件系统基础”,但还没有真正的 hook 挂载层。因此这里的目标不是重新设计 event,而是在已有实现上补出一层可扩展、可拦截、可外挂的 HookManager。
OpenClaw 的扩展能力分成三层:
值得借鉴的点:
不建议直接照搬的点:
pi-mono 的核心思路更接近当前分支:
transform / block / replace值得借鉴的点:
当前分支已经具备 hook 系统的地基:
pkg/agent/events.go 定义了稳定的 EventKind、EventMeta 和 payloadpkg/agent/eventbus.go 提供了非阻塞 fan-out 的 EventBuspkg/agent/loop.go 中的 runTurn() 已在 turn、llm、tool、interrupt、follow-up、summary 等节点发射事件pkg/agent/steering.go 已支持 steering、graceful interrupt、hard abortpkg/agent/turn.go 已维护 turn phase、恢复点、active turn、abort 状态当前分支还缺四件事:
pkg/tools/SubagentManager + RunToolLoop,没有接入 pkg/agent 的 turn tree 和事件流#1316 文案里提到“只读并行、写入串行”的工具执行策略,但当前 runTurn() 实现已经先收敛成“顺序执行 + 每个工具后检查 steering / interrupt”。因此 hook 设计不应依赖未来的并行模型,而应该先兼容当前顺序执行,再为以后增加 ReadOnlyIndicator 留口子。
pkg/agent 的 EventBus 和 turn 上下文之上EventKind.String()分成三层:
EventBus
负责广播只读事件,现有实现直接复用
HookManager
负责管理 hook、排序、超时、错误隔离,并在 runTurn() 的明确检查点执行同步拦截
HookMount
负责两种挂载方式:
换句话说:
不建议把所有 hook 都设计成 OnEvent(evt)。
建议拆成两类。
只消费事件,不修改流程:
type EventObserver interface {
OnEvent(ctx context.Context, evt agent.Event) error
}
这类 hook 直接订阅 EventBus 即可。
适用场景:
只在少数明确节点触发,允许返回动作:
type LLMInterceptor interface {
BeforeLLM(ctx context.Context, req *LLMRequest) HookDecision[*LLMRequest]
AfterLLM(ctx context.Context, resp *LLMResponse) HookDecision[*LLMResponse]
}
type ToolInterceptor interface {
BeforeTool(ctx context.Context, call *ToolCall) HookDecision[*ToolCall]
AfterTool(ctx context.Context, result *ToolResultView) HookDecision[*ToolResultView]
}
type ToolApprover interface {
ApproveTool(ctx context.Context, req *ToolApprovalRequest) ApprovalDecision
}
这里的 HookDecision 统一支持:
continuemodifydeny_toolabort_turnhard_abortV1 不需要把所有 EventKind 都变成可拦截点。
建议只开放这些同步 hook:
before_llmafter_llmbefore_toolafter_toolapprove_tool其余节点继续作为只读事件暴露:
turn_startturn_endllm_requestllm_responsetool_exec_starttool_exec_endtool_exec_skippedsteering_injectedfollow_up_queuedinterrupt_receivedcontext_compresssession_summarizeerrorsubturn_* 在 V1 中保留名字,但不承诺一定触发,直到子 turn 迁移完成。
内部挂载必须尽量低摩擦。
建议提供两种等价方式,底层都走 HookManager。
al.MountHook(hooks.Named("audit", &AuditHook{}))
适用于:
func init() {
hooks.RegisterBuiltin("audit", func() hooks.Hook {
return &AuditHook{}
})
}
启动时根据配置启用:
{
"hooks": {
"builtins": {
"audit": { "enabled": true }
}
}
}
这比 OpenClaw 的目录扫描更轻,也更贴合 Go 项目。
这是本设计的硬要求。
建议 V1 采用:
JSON-RPC over stdio原因:
PicoClaw 启动外部进程,并在其 stdin/stdout 上跑协议。
配置示例:
{
"hooks": {
"processes": {
"review-gate": {
"enabled": true,
"transport": "stdio",
"command": ["uvx", "picoclaw-hook-reviewer"],
"observe": ["turn_start", "turn_end", "tool_exec_end"],
"intercept": ["before_tool", "approve_tool"],
"timeout_ms": 5000
}
}
}
}
不要把内部 Go 结构体直接暴露给 IPC。
建议定义稳定的协议对象:
HookHandshakeHookEventNotificationBeforeLLMRequestAfterLLMRequestBeforeToolRequestAfterToolRequestApproveToolRequestHookDecision其中:
因为两者用途不同:
如果未来需要 OpenClaw 式 webhook,可以作为独立入口层,再把外部事件转成 inbound message 或 steering,而不是直接替代 hook IPC。
建议统一排序规则:
priority 从小到大执行原因:
500msbefore_llm / after_llm / before_tool / after_tool:默认 5sapprove_tool:默认 60s超时行为:
continuedeny这点应直接沿用 #1316 的安全倾向。
pkg/agent/events.gopkg/agent/eventbus.gopkg/agent/turn.gopkg/agent/loop.gopkg/agent/hooks.go
pkg/agent/hook_mount.go
pkg/agent/hook_ipc.go
pkg/agent/hook_types.go
pkg/agent/loop.go
pkg/tools/base.go
ReadOnlyIndicatorpkg/tools/spawn.go
pkg/tools/subagent.go
subturn_* hookrunTurn() -> emitEvent() -> EventBus -> observers
runTurn()
-> HookManager.BeforeLLM()
-> Provider.Chat()
-> HookManager.AfterLLM()
-> HookManager.BeforeTool()
-> HookManager.ApproveTool()
-> tool.Execute()
-> HookManager.AfterTool()
也就是说:
emitEvent()runTurn() 热路径建议新增:
{
"hooks": {
"enabled": true,
"builtins": {},
"processes": {},
"defaults": {
"observer_timeout_ms": 500,
"interceptor_timeout_ms": 5000,
"approval_timeout_ms": 60000
}
}
}
V1 不做复杂自动发现。
原因:
before_tool / after_tool / approve_toolbefore_llm / after_llmstdio 外部 hook 进程桥SubagentManager 迁移到 runTurn/sub-turnsubturn_spawn / subturn_end / subturn_result_deliveredReadOnlyIndicator最适合 PicoClaw 当前分支的方案,不是直接复制 OpenClaw 的 hooks,也不是完整照搬 pi-mono 的 extension system,而是:
EventBus 为只读观察面HookManager 为同步拦截面stdio JSON-RPC 进程通信挂载这样做有三个好处:
#1796 一致,hooks 只是 EventBus 之上的消费层refactor/agent 实现一致,不需要推翻已有事件系统