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”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: str | list[dict[str, Any]]
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 — bub.streaming
Section titled “Streaming — bub.streaming”from bub.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.
Errors — bub.errors
Section titled “Errors — bub.errors”from bub.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 (
AgentHooks,
LlmCallDecision,
LlmCallRequest,
LlmCallResult,
ToolCall,
ToolCallDecision,
ToolCallResult,
)
from bub.hooks.runtime import HookRuntime
Plugin authors normally need only hookimpl plus the payload types used by their hooks. 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.