Skip to content

Application hooks

Hooks connect plugins to the Bub application. This reference lists the 26 hooks declared by BubHookSpecs. For examples, see Change application behavior; for the order of application stages, see The application turn.

first below means the first non-None result in hook priority order. all means every implementation is called. An ordinary hook exception stops that dispatch and propagates; the agent interception hooks and on_error have separate exception handling.

Hook Signature Behavior
resolve_session (message: Envelope) -> str First result; a falsy result falls back to the framework’s channel/chat session ID.
load_state (message: Envelope, session_id: str) -> TurnState Collect all state fragments; merge lower-priority fragments first so higher-priority values win.
build_prompt (message: Envelope, session_id: str, state: TurnState) -> list[UserContent] | LegacyPrompt First result; a falsy selected value falls back to the inbound content rather than trying another implementation.
run_model (prompt, session_id, state) -> str Text result for the model stage. Implement this or run_model_stream.
run_model_stream (prompt, session_id, state) -> AsyncStreamEvents Stream result for the model stage. Implement this or run_model.
continue_prompt (prompt: LegacyPrompt | None, tape: Tape, state: StreamState) -> str | None First result, called through AgentPlugins on a StepFinished event whose status is continue. Adds a user message when a string is returned.
save_state (session_id, state, message, model_output) -> None All implementations, in the model stage’s finally block. Earlier session, state, or prompt failures skip it.
render_outbound (message, session_id, state, model_output) -> list[Envelope] Collect and concatenate batches. An empty combined result uses the framework’s default output envelope.
dispatch_outbound (message: Envelope) -> bool Call every implementation for each outbound message.

These hooks may be synchronous or asynchronous. continue_prompt receives a StreamState; the other state parameters in this table are mutable TurnState dictionaries.

The model adapter chooses between text and stream hooks by examining plugins in reverse registration order. It then dispatches the selected hook name by hook priority. It can collect a stream into text or wrap a text response in a stream. Choose one model hook per plugin.

Hook Signature Behavior
register_cli_commands (app: typer.Typer) -> None All implementations, synchronously during CLI creation.
onboard_config (current_config: dict) -> dict | None Synchronous; merge each returned fragment into the accumulated configuration. A non-dict result raises TypeError.
provide_model_options (session_id: str, workspace: Path | None) -> ModelOptions | None Collect choices in priority order. May be async; the caller owns selection and persistence.
provide_environment (session_id: str, workspace: Path) -> Environment | None First result; may be async. Cache one environment per session and close it when running() exits.
system_prompt (prompt, state: TurnState) -> str Synchronous; join truthy fragments in reverse hook priority order with blank lines.
provide_tape_store () -> TapeStore | AsyncTapeStore | None First result, acquired synchronously when running() opens; a generator may yield the active store.
provide_lifespan () -> Iterator[None] | AsyncIterator[None] | None Collect resource lifespans and enter each once per running() scope.
provide_tape_sidecar () -> TapeSidecar Synchronous; collect sidecars, keeping the first in priority order for each name.
provide_channels (message_handler: MessageHandler) -> list[Channel] Synchronous; collect channels, keeping the first in priority order for each Channel.name.
build_tape_context () -> TapeContext First synchronous result; selects the context strategy for the application agent.
admit_message (session_id, message, turn: TurnSnapshot) -> AdmitDecision | None First result before channel scheduling; may be async. None leaves the channel’s default concurrent scheduling in place.
provide_tools () -> list[Tool] Synchronous; collect tools, keeping the first in priority order for each name.
provide_steering_inbox () -> SteeringInbox | None First result, acquired synchronously when running() opens; a generator may yield the inbox.
on_error (stage: str, error: Exception, message: Envelope | None) -> None Notify every observer. Observer exceptions are logged and skipped. Async implementations work only on the async notification path.

Synchronous dispatch skips awaitable return values with a hook.async_not_supported warning. Resource providers can return sync or async generators, which Bub enters as context managers.

Hook Signature Behavior
before_llm_call (request: LLMCallRequest, state: TurnState) -> LLMCallRequest | LLMCallDecision | None Chain request updates; a finish decision skips the provider and stops the chain.
after_llm_call (request: LLMCallRequest, result: LLMCallResult, state: TurnState) -> None Observe a completed provider call or ordinary failure.
before_tool_call (call: ToolCall, state: TurnState) -> ToolCallDecision | None Chain argument updates; replace or deny stops the chain.
after_tool_call (call: ToolCall, result: ToolCallResult, state: TurnState) -> None Handle tool success, failure, replacement, or denial. Assign result.result to change the resulting value.

Bub registers builtin hooks first, then installed entry-point plugins. For ordinary implementations at the same priority, later registrations run first. @hookimpl(tryfirst=True) and @hookimpl(trylast=True) alter priority. These flags and registration order together determine which implementation runs first.

HookRuntime invokes ordinary implementations directly, using the reverse of pluggy’s get_hookimpls() list. Pluggy wrapper flags (wrapper=True and hookwrapper=True) are unsupported by this dispatch path.

call_first stops at the first non-None value. call_many collects values from all implementations. Their sync variants skip awaitables. Each implementation receives only the arguments it declares.

bub hooks confirms registration by displaying bindings in pluggy’s stored order. Execution follows the priority rules above.

build_prompt can return a list containing strings and Republic media: republic.Image, Audio, or Video. Build media with republic.image(...), republic.audio(...), and republic.video(...).

Legacy strings and content-block dictionaries remain accepted. Hooks receiving a prompt (run_model, run_model_stream, continue_prompt, system_prompt) receive the legacy form: a string without media, otherwise content blocks with media URLs. Inline media uses data URLs. bub.prompt.to_content(prompt) converts this to user content.

framework.running() enters lifespan providers, then acquires the tape store and steering inbox. It closes entered resources in reverse order, including after an error. The environment cache is also closed on scope exit.

A provider that needs cleanup can yield once and release its resource in finally. Use this for resource ownership; save_state only covers the model stage of a prepared turn.

bub run opens the scope for one message. bub chat and bub gateway keep it open while the application runs. A host embedding the application should do the same. get_tape_store() returns None outside the scope.

The builtin lifespan owns background shell processes started within that scope. On POSIX, shutdown targets their process groups; Windows currently targets direct shell processes. Processes outside those groups and forced process termination require host-level supervision.

Tape sidecar providers contribute named sibling tapes. Bub mounts them on the application’s root Tape and applies fork, merge, archive, and reset to mounted sidecars. Stored data remains after a provider is removed. See Tape and context for shared storage semantics.

AgentPlugins.attach() subscribes PluginAgentHooks to the core agent’s LLM and tool start/finish events; BubAgent attaches it to itself. This adapter gives application plugins access to those calls.

The before hooks run in hook priority order. Each sees updates made by earlier implementations. LLMCallDecision.finish(text), ToolCallDecision.replace(result), and ToolCallDecision.deny(message) stop the remaining before implementations. A finished LLM decision never calls the provider and has no matching after_llm_call.

The adapter catches ordinary exceptions from each interception hook, logs hook.agent_hook_failed, and continues. Return a decision to deny a call. Handlers registered directly on the core EventBus propagate their exceptions.

After hooks observe successful calls and ordinary failures. Cancellation and consumer close bypass them. after_tool_call also runs for denied or replaced calls; implementations share the same result object. Changing result.result changes the value passed onward, while result.error retains the original failure.

Payload types live in bub.core.events and are re-exported by bub.hooks.interception. Their run_id correlates calls with recorded entries.