Architecture
Core Components
Section titled “Core Components”BubFramework: creates the plugin manager, loads plugins, and runsprocess_inbound().BubHookSpecs: defines all hook contracts (src/bub/hookspecs.py).HookRuntime: executes hooks with sync/async compatibility helpers (src/bub/hook_runtime.py).Agent: builtin model-and-tools runtime (src/bub/builtin/agent.py).ChannelManager: starts channels, buffers inbound messages, and routes outbound messages (src/bub/channels/manager.py).
Turn Lifecycle
Section titled “Turn Lifecycle”BubFramework.process_inbound() currently executes in this order:
- Resolve session via
resolve_session(message)(fallback tochannel:chat_idif empty). - Initialize state with
_runtime_workspacefromBubFramework.workspace. - Merge all
load_state(message, session_id)dicts. - Build prompt via
build_prompt(message, session_id, state)(fallback to inboundcontentif empty). - Execute
run_model_stream(prompt, session_id, state). - For each stream event, call
OutboundChannelRouter.dispatch_event(...), which forwards tochannel.on_event(event, message)when the target channel exists. - Always execute
save_state(...)in afinallyblock. - Render outbound batches via
render_outbound(...), then flatten them. - If no outbound exists, emit one fallback outbound.
- Dispatch each outbound via
dispatch_outbound(message).
If no plugin implements run_model_stream, HookRuntime falls back to run_model(prompt, session_id, state) and adapts the returned text into a stream with a single text chunk.
Hook Priority Semantics
Section titled “Hook Priority Semantics”Registration order:
- Builtin plugin
builtin - External entry points (
group="bub")
Execution order:
HookRuntimereverses pluggy implementation order, so later-registered plugins run first.call_firstreturns the first non-Nonevalue.call_manycollects every implementation return value (includingNone).
Merge/override details:
load_stateis reversed again before merge so high-priority plugins win on key collisions.provide_channelsis collected byBubFramework.get_channels(), and the first channel name wins, so high-priority plugins can override builtin channel names.
Error Behavior
Section titled “Error Behavior”- For normal hooks,
HookRuntimedoes not swallow implementation errors. process_inbound()catches top-level exceptions, notifieson_error(stage="turn", ...), then re-raises.on_erroritself is observer-safe: one failing observer does not block the others.- In sync calls (
call_first_sync/call_many_sync), awaitable return values are skipped with a warning.
Builtin Runtime Notes
Section titled “Builtin Runtime Notes”Builtin BuiltinImpl behavior includes:
build_prompt: supports comma command mode; non-command text may includecontext_str.run_model_stream: delegates toAgent.run().system_prompt: combines a default prompt with workspaceAGENTS.md.register_cli_commands: installsrun,gateway,chat, plus hidden diagnostic commands.provide_channels: returnstelegramandclichannel adapters.provide_tape_store: returns a file-backed tape store under~/.bub/tapes.
Channel Event Streaming
Section titled “Channel Event Streaming”Channels have two different outbound surfaces:
send(message): handles the final rendered outbound message.on_event(event, message): handles raw stream events while the model is still running.
on_event is optional. Implement it when a channel can benefit from incremental rendering, typing indicators, progress updates, or partial text display. The message argument is the original inbound message, so channel implementations usually use it to recover routing metadata such as target channel, chat id, session id, or message kind.
If a channel does not implement any special event behavior, it can ignore on_event and rely entirely on send().
Boundaries
Section titled “Boundaries”Envelopestays intentionally weakly typed (Any+ accessor helpers).- There is no globally enforced schema for cross-plugin
state. - Runtime behavior in this document is aligned with current source code.