Skip to content

Hooks

This page lists every hook declared in bub/hookspecs.py (BubHookSpecs). Hook kind uses three labels:

  • firstresultpluggy firstresult=True; HookRuntime.call_first returns the first non-None value in priority order.
  • broadcast — every implementation runs; results are collected into a list (call_many).
  • sync-only consumer — invoked through call_first_sync or call_many_sync; awaitable returns are skipped with a warning.

For the why and how of each stage see Turn pipeline and Build › Hooks.

HookKindSignatureReturnsInvoked fromNotes
resolve_sessionfirstresult(message: Envelope) -> strsession idBubFramework.process_inboundFalls back to "{channel}:{chat_id}" when no impl returns a value.
load_statebroadcast(message: Envelope, session_id: str) -> Statedict to mergeBubFramework.process_inboundFramework reverses results, then merges in order so high-priority keys overwrite low-priority.
build_promptfirstresult(message: Envelope, session_id: str, state: State) -> str | list[dict]prompt or content partsBubFramework.process_inboundFalls back to content_of(message) when no impl returns a non-None value, or when the selected firstresult is falsy. A falsy selected value does not cause lower-priority impls to run.
run_modelfirstresult(prompt, session_id, state) -> strmodel textHookRuntime.run_modelLegacy text path. Implement either run_model or run_model_stream, not both.
run_model_streamfirstresult(prompt, session_id, state) -> AsyncStreamEventsasync streamHookRuntime.run_model_streamPreferred. Falls back to wrapping a run_model result in a one-chunk stream when no streaming impl exists.
save_statebroadcast(session_id, state, message, model_output) -> Nonenoneprocess_inbound model-stage finally blockRuns after prompt resolution for model-stage success or failure; failures before prompt/model execution skip it.
render_outboundbroadcast(message, session_id, state, model_output) -> list[Envelope]outbound batchBubFramework._collect_outboundsAll batches are concatenated via unpack_batch. Empty results trigger a default echo envelope.
dispatch_outboundbroadcast(message: Envelope) -> boolsent flagprocess_inbound per outboundEach outbound is fanned out to every impl.
register_cli_commandssync-only consumer(app: typer.Typer) -> NonenoneBubFramework.create_cli_app (call_many_sync)Bootstrap only; async impls log a warning and are skipped.
onboard_configsync-only consumer (custom merge)(current_config: dict) -> dict | Noneconfig fragmentBubFramework.collect_onboard_configIterated by priority; each fragment is merged via configure.merge. Non-dict returns raise TypeError.
on_errorobserver(stage: str, error: Exception, message: Envelope | None) -> NonenoneHookRuntime.notify_error / notify_error_syncFailures inside an on_error impl are caught and logged so other observers still run.
system_promptbroadcast (joined)(prompt, state) -> strprompt fragmentBubFramework.get_system_prompt (call_many_sync)Results are reversed and joined with \n\n; truthy fragments only.
provide_tape_storefirstresult() -> TapeStore | AsyncTapeStoretape storeBubFramework.running()Resolved once when the runtime scope opens; sync/async iterators are entered as context managers.
provide_channelssync-only consumer (deduped)(message_handler: MessageHandler) -> list[Channel]channelsBubFramework.get_channels (call_many_sync)Channels are deduplicated by Channel.name; the first channel seen in hook priority order wins.
build_tape_contextfirstresult() -> TapeContexttape contextBubFramework.build_tape_context (call_first_sync)Sync-only; awaitable returns are skipped.

HookRuntime (in src/bub/hook_runtime.py) wraps the pluggy.PluginManager with the following semantics.

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 returns implementations in registration order. HookRuntime reverses that list, so the most recently registered plugin runs first. Builtin is registered before entry-point plugins, so user plugins always win on firstresult hooks.

bub hooks is a discovery report. In the current implementation it prints the raw hook implementation order from pluggy, so use this section rather than the printed order alone when reasoning about runtime precedence.

MethodBehavior
call_first(name, **kwargs)Walk impls in reversed order, return the first non-None value.
call_many(name, **kwargs)Walk all impls, collect each non-skipped value into a list.
call_first_sync / call_many_syncSame iteration; awaitable returns are dropped with a warning.

Async calls (call_first, call_many) await any awaitable result. Sync calls (*_sync) check inspect.isawaitable(value) and emit hook.async_not_supported hook=<name> adapter=<plugin>, then skip the value.

Bootstrap hooks must be synchronous: register_cli_commands, onboard_config, provide_channels, provide_tape_store, build_tape_context, plus 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}

Each impl receives only the kwargs it declares. You can omit unused parameters from your function signature without breaking dispatch.

provide_tape_store is called once per BubFramework.running() scope, not per turn:

  • bub run opens the scope around a single inbound turn, then closes it.
  • bub chat and bub gateway keep the scope open until the listener exits.
  • Returning a sync or async iterator turns the impl into a contextmanager/asynccontextmanager; the yielded value is the active store until the scope exits.

BubFramework.get_tape_store() returns None outside the scope.

notify_error and notify_error_sync wrap each impl in a try/except; observer failures are logged (hook.on_error_failed stage=… adapter=…) but never propagate. This guarantees one broken observer does not block the others, and prevents an on_error from masking the original exception.

For the runtime contract behind every signature, read src/bub/hookspecs.py. To verify what is registered in your environment, run bub hooks (see CLI › hooks).