跳转到内容

钩子

本页列出 bub/hookspecs.py (BubHookSpecs) 中声明的每一个钩子。类型 列使用三种标签:

  • firstresult — 对应 pluggyfirstresult=True;HookRuntime.call_first 按优先级返回第一个非 None 的结果。
  • broadcast — 所有实现都会执行,结果聚合到列表(call_many)。
  • sync-only consumer — 通过 call_first_synccall_many_sync 调用;返回 awaitable 的实现会被跳过并写入告警。

每个阶段的语义与典型用法见 Turn 流水线构建 › 钩子配方

钩子类型签名返回调用方备注
resolve_sessionfirstresult(message: Envelope) -> strsession idBubFramework.process_inbound没有任何实现返回值时,框架回退为 "{channel}:{chat_id}"
load_statebroadcast(message: Envelope, session_id: str) -> Statedict to mergeBubFramework.process_inbound框架先反转结果列表再依次合并,使高优先级的键覆盖低优先级。
build_promptfirstresult(message: Envelope, session_id: str, state: State) -> str | list[dict]prompt or content partsBubFramework.process_inbound没有实现返回非 None 值,或选中的 firstresult 为 falsy 时,回退到 content_of(message)。已选中的 falsy 值不会触发继续尝试低优先级实现。
run_modelfirstresult(prompt, session_id, state) -> strmodel textHookRuntime.run_model旧版纯文本路径。run_modelrun_model_stream 选其一实现,不要同时实现。
run_model_streamfirstresult(prompt, session_id, state) -> AsyncStreamEventsasync streamHookRuntime.run_model_stream推荐路径。无流式实现时框架将 run_model 结果包装为单个 chunk 的流。
save_statebroadcast(session_id, state, message, model_output) -> Nonenoneprocess_inbound model-stage finally blockprompt 解析完成后,在模型阶段成功或失败时执行;若失败发生在 prompt/model 执行前则跳过。
render_outboundbroadcast(message, session_id, state, model_output) -> list[Envelope]outbound batchBubFramework._collect_outbounds所有批次通过 unpack_batch 拼接;结果为空时使用默认回声 envelope。
dispatch_outboundbroadcast(message: Envelope) -> boolsent flagprocess_inbound per outbound每个 outbound 都会广播给所有实现。
register_cli_commandssync-only consumer(app: typer.Typer) -> NonenoneBubFramework.create_cli_app (call_many_sync)仅用于启动期;async 实现会被跳过并产生告警。
onboard_configsync-only consumer (custom merge)(current_config: dict) -> dict | Noneconfig fragmentBubFramework.collect_onboard_config按优先级遍历;每个返回值通过 configure.merge 合并;非 dict 返回会抛出 TypeError
provide_runtime_optionsbroadcast(session_id: str, workspace: Path | None) -> RuntimeOptions | Noneruntime choicesBubFramework.get_runtime_options模型选项会按 hook 优先级顺序追加。选择状态由调用方或 adapter 自己持有。
on_errorobserver(stage: str, error: Exception, message: Envelope | None) -> NonenoneHookRuntime.notify_error / notify_error_syncon_error 实现内部抛出的异常会被吞掉并写日志,确保其他观察者继续运行。
system_promptbroadcast (joined)(prompt, state) -> strprompt fragmentBubFramework.get_system_prompt (call_many_sync)结果先反转再用 \n\n 拼接,只保留真值片段。
provide_tape_storefirstresult() -> TapeStore | AsyncTapeStoretape storeBubFramework.running()仅在 runtime 作用域开启时解析一次;返回同步或异步迭代器时会被作为 context manager 进入。
provide_channelssync-only consumer (deduped)(message_handler: MessageHandler) -> list[Channel]channelsBubFramework.get_channels (call_many_sync)Channel.name 去重;在钩子优先级顺序中最先出现的 channel 胜出。
build_tape_contextfirstresult() -> TapeContexttape contextBubFramework.build_tape_context (call_first_sync)仅同步;awaitable 返回会被跳过。
admit_messagefirstresult(session_id, message, turn) -> AdmitDecision | Noneturn admission decisionChannelManager调度 channel message 前调用。返回 None 保持默认并发调度;decision 类型见 类型
before_llm_callchained(request: LlmCallRequest, state: State) -> LlmCallRequest | LlmCallDecision | None修改后的 request 或 finish 决定ModelRunner.runAgentHooks实现按 LIFO 链式执行,每个实现看到的是前一个实现修改后的 request。返回 LlmCallDecision.finish(text) 跳过 provider 调用。实现抛异常仅记日志并跳过。
after_llm_callobserver(request: LlmCallRequest, result: LlmCallResult, state: State) -> NoneModelRunner.runAgentHooks每次完成的调用恰好触发一次:成功或 Exception 失败。取消 / 消费方 aclose() 不观察。result.error 是原始异常。
before_tool_callchained(call: ToolCall, state: State) -> ToolCallDecision | NonedecisionToolExecutorAgentHooks逐次工具调用。proceed(arguments=…) 折叠参数修改;replace(result) / deny(message) 短路。否决只能通过 decision 对象——异常仅记日志并跳过。
after_tool_callobserver(call: ToolCall, result: ToolCallResult, state: State) -> NoneToolExecutorAgentHooks成功、失败、deny/replace 会触发。取消不观察。result.error 是原始 BubError

HookRuntime(位于 src/bub/hook_runtime.py)在 pluggy.PluginManager 之上提供以下语义。

def _iter_hookimpls(self, hook_name: str) -> list[Any]:
    hook = getattr(self._plugin_manager.hook, hook_name, None)
    if hook is None or not hasattr(hook, "get_hookimpls"):
        return []
    return list(reversed(hook.get_hookimpls()))

pluggy 按注册顺序返回实现。HookRuntime 将列表反转,因此 最近注册的插件最先运行。Builtin 早于所有 entry-point 插件注册,所以用户插件在 firstresult 钩子上始终优先。

bub hooks 是发现报告。当前实现打印的是 pluggy 的原始 hook implementation 顺序,因此判断运行时优先级时应以本节说明为准,而不是只看输出顺序。

方法行为
call_first(name, **kwargs)按反转后的顺序遍历实现,返回第一个非 None 的值。
call_many(name, **kwargs)遍历全部实现,把每个未跳过的返回值收集到列表里。
call_first_sync / call_many_sync同样的遍历;awaitable 返回值会被丢弃并写入告警。

异步调用 (call_firstcall_many) 会 await 任何 awaitable 返回值。同步调用 (*_sync) 通过 inspect.isawaitable(value) 判断,若为真则发出 hook.async_not_supported hook=<name> adapter=<plugin> 告警并跳过该值。

启动期钩子 必须 同步:register_cli_commandsonboard_configprovide_channelsprovide_tape_storebuild_tape_context,以及 system_prompt

@staticmethod
def _kwargs_for_impl(impl: Any, kwargs: dict[str, Any]) -> dict[str, Any]:
    return {name: kwargs[name] for name in impl.argnames if name in kwargs}

每个实现只会收到自己声明的关键字参数。函数签名中可以省略不需要的参数,不会影响分发。

provide_tape_store 在每个 BubFramework.running() 作用域内 只解析一次,而不是每个 turn 一次:

  • bub run 在单次 inbound turn 周围打开作用域,然后关闭。
  • bub chatbub gateway 让作用域持续到 listener 退出。
  • 实现返回同步或异步迭代器时,Bub 会把它当作 contextmanager / asynccontextmanager;yield 出的值是作用域结束前生效的 store。

BubFramework.get_tape_store() 在作用域之外返回 None

notify_errornotify_error_sync 把每个实现包在 try/except 中;观察者失败会写入日志 (hook.on_error_failed stage=… adapter=…) 但不会向上传递。这样可以保证某个观察者出错不会阻塞其他观察者,也不会让 on_error 掩盖原始异常。

四个 *_llm_call / *_tool_call 钩子经由 AgentHooks 门面(src/bub/hook_runtime.py)分发,而不是 call_first/call_many,有三处刻意差异:

  • 链式before_llm_callbefore_tool_call 按 LIFO 顺序执行全部实现并折叠修改:每个实现收到的是前序实现修改后的 request/call。第一个短路决定(LlmCallDecision.finishToolCallDecision.replace/deny)终止链。
  • 错误隔离 — 实现抛异常只记日志(hook.agent_hook_failed)并跳过,绝不炸掉回合。阻断只能通过 decision 对象表达,坏掉的策略插件无法靠 crash 否决。
  • 终态恰好一次after_llm_callafter_tool_call 对真实完成的调用恰好触发一次:成功或 Exception 失败。取消与消费方关闭(BaseException)刻意不进入 after 钩子。result.error 携带原始异常对象(工具失败为完整 BubError,kind/details 保留)。

载荷数据类(LlmCallRequestLlmCallResultToolCallToolCallDecisionToolCallResultLlmCallDecision)位于 src/bub/agent_hooks.py。所有载荷携带 run_id,与 tape 条目 meta 中的 run_id 对应,观察者可将钩子事件与 tape 记录对齐。before_llm_call 改写的 model/max_tokens 全链路生效:provider 收到改写值,tape 记录 effective model。

continuation 风格的 wrap_tool_call(重试 / human-in-the-loop / 缓存)曾在原 PR 中实现,按 review 方向移除;若相关用例成熟,将作为独立提案回归。

每个签名的运行时契约见 src/bub/hookspecs.py。要核对当前环境注册的实现,运行 bub hooks(详见 CLI › hooks)。