Core contracts
Bub keeps contracts beside the subsystem that owns their behavior. Import a contract from its defining module; the top-level bub package intentionally exposes only the common framework entry points.
Kernel vocabulary
Section titled “Kernel vocabulary”Errors, prompt content, streaming, tracing and TurnState belong to the agent kernel, the bubling package, which depends on nothing in Bub. The former paths bub.errors, bub.streaming and bub.tracing remain as aliases of the bubling modules, and bub.turn re-exports TurnState.
Envelopes — bub.envelope
Section titled “Envelopes — bub.envelope”from bub.envelope import Envelope, content_of, field_of, normalize_envelope, unpack_batch
type Envelope = Any
An Envelope may be a mapping, dataclass, Pydantic model, or any attribute-based object. Use field_of and content_of to read values without coupling code to one transport schema. normalize_envelope produces a mutable dict; unpack_batch normalizes one render_outbound result into a list.
Turns — bub.turn
Section titled “Turns — bub.turn”from bub.turn import TurnResult, TurnState
type TurnState = dict[str, Any]
@dataclass(frozen=True)
class TurnResult:
session_id: str
prompt: list[UserContent]
model_output: str
outbounds: list[Envelope] = field(default_factory=list)
state: TurnState = field(default_factory=dict)
TurnState is the mutable state shared by every stage of one turn. process_inbound returns the final TurnResult, including the state that produced its outbound envelopes.
Model selection — bub.model_selection
Section titled “Model selection — bub.model_selection”from bub.model_selection import ModelChoice, ModelOptions
@dataclass(frozen=True)
class ModelChoice:
id: str
name: str | None = None
description: str | None = None
meta: dict[str, Any] | None = None
@dataclass(frozen=True)
class ModelOptions:
models: list[ModelChoice] = field(default_factory=list)
current_model: str | None = None
Channels and adapters may present these protocol-neutral choices to users. BubFramework.get_model_options merges values returned by provide_model_options; the caller owns selection UI and persistence.
Streaming — bubling.streaming
Section titled “Streaming — bubling.streaming”from bubling.streaming import AsyncStreamEvents, StreamEvent, StreamState
StreamEvent.kind is one of text, reasoning, tool_call, tool_result, usage, error, or final. AsyncStreamEvents wraps an async iterator and exposes terminal error and usage state.
Builtin tool_call events use Republic fields: id, name, arguments, and metadata. Usage dictionaries contain input_tokens, output_tokens, reasoning_tokens, cached_tokens, cache_write_tokens, and total_tokens.
Builtin multipart prompts contain text blocks (type: text, text) or media blocks (type: image|audio|video, media_type, url). The URL can be remote or a base64 data URL. Bub converts these persisted blocks into native Republic parts; Republic owns provider encoding. Hooks supply these text and media blocks.
Errors — bubling.errors
Section titled “Errors — bubling.errors”from bubling.errors import BubError, ErrorKind
BubError is the stable execution error object. Its kind is an ErrorKind, and as_dict() returns a transport-friendly payload.
Hook contracts
Section titled “Hook contracts”Hook declarations and markers live in bub.hooks; agent-loop interception payloads live with their execution semantics:
from bub.hooks import BUB_HOOK_NAMESPACE, BubHookSpecs, hookimpl, hookspec
from bub.hooks.interception import (
LlmCallDecision,
LlmCallRequest,
LlmCallResult,
PluginAgentHooks,
ToolCall,
ToolCallDecision,
ToolCallResult,
)
from bub.hooks.runtime import HookRuntime
from bub.sidecars import TapeSidecar, sidecar_tape_name
Plugin authors normally need only hookimpl plus the payload types used by their hooks. TapeSidecar is the minimal named contract returned by provide_tape_sidecar; sidecar_tape_name resolves its sibling tape name. See the Hook reference for dispatch and fault-isolation semantics.
Channel contracts
Section titled “Channel contracts”Handler and router — bub.channels.contracts
Section titled “Handler and router — bub.channels.contracts”from bub.channels.contracts import ChannelRouter, MessageHandler
type MessageHandler = Callable[[Envelope], Coroutine[Any, Any, None]]
class ChannelRouter(Protocol):
async def dispatch_output(self, message: Envelope) -> bool: ...
def wrap_stream(
self, message: Envelope, stream: AsyncIterable[StreamEvent]
) -> AsyncIterable[StreamEvent]: ...
async def quit(self, session_id: str) -> None: ...
async def admit_channel_message(
self, session_id: str, message: Envelope, turn: TurnSnapshot
) -> AdmitDecision | None: ...
ChannelManager implements this protocol and binds it through BubFramework.bind_channel_router.
Admission — bub.channels.admission
Section titled “Admission — bub.channels.admission”from bub.channels.admission import AdmitDecision, SteeringInbox, TurnSnapshot
@dataclass(frozen=True)
class AdmitDecision:
action: Literal["process", "drop", "follow_up", "steer"]
reason: str | None = None
@dataclass(frozen=True)
class TurnSnapshot:
session_id: str
is_running: bool
running_count: int
pending_count: int
steering_count: int = 0
admit_message implementations use these types to select immediate processing, dropping, follow-up queuing, or steering. SteeringInbox is the protocol for session steering queues.
Channel base — bub.channels.base
Section titled “Channel base — bub.channels.base”from bub.channels import Channel, Interface, Lifecycle
Channel requires start(stop_event) and stop(). Implement send, stream_events, or admit_message as needed. Interface marks user-facing I/O surfaces; Lifecycle marks background runtimes managed alongside channels.
BubFramework
Section titled “BubFramework”The main public methods that connect these contracts are:
from bub import BubFramework
async def process_inbound(
self, inbound: Envelope, stream_output: bool = False
) -> TurnResult: ...
async def get_model_options(
self, *, session_id: str, workspace: str | Path | None = None
) -> ModelOptions: ...
async def admit_message(
self, *, session_id: str, message: Envelope, turn: TurnSnapshot
) -> AdmitDecision | None: ...
def get_channels(self, message_handler: MessageHandler) -> dict[str, Channel]: ...
def bind_channel_router(self, router: ChannelRouter | None) -> None: ...
See Turn pipeline for the lifecycle of process_inbound and Channels for channel operation.
Top-level exports
Section titled “Top-level exports”bub.__init__ deliberately stays small: BubFramework, Settings, config, ensure_config, home, hookimpl, and tool. Import subsystem contracts from the modules above so ownership remains explicit.