Python API
bub.core
Section titled “bub.core”class Agent: def __init__( self, *, model: Model | None = None, store: TapeStore | AsyncTapeStore | None = None, tools: Iterable[Tool | BuiltinTool] = (), system_prompt: SystemPrompt | PromptSection | Iterable[PromptSection] = (), context: TapeContext | None = None, sidecars: Sequence[TapeSidecar] = (), max_steps: int = sys.maxsize, max_tool_concurrency: int | None = None, name: str = "agent", command_prefix: str | None = None, ) -> None: ...
def tape(self, name: str, *, state: TurnState | None = None) -> Tape: ...
@asynccontextmanager async def session( self, name: str, *, state: TurnState | None = None, merge_back: bool = True, ) -> AsyncGenerator[Tape]: ...
@asynccontextmanager async def run( self, prompt: str | list[UserContent], *, model: Model | None = None, options: Mapping[str, Any] | None = None, tape: str, state: TurnState | None = None, merge_back: bool = True, metadata: dict[str, Any] | None = None, ) -> AsyncGenerator[AsyncStreamEvents]: ...
async def start( self, prompt: str | list[UserContent], *, model: Model | None = None, options: Mapping[str, Any] | None = None, tape: str, state: TurnState | None = None, merge_back: bool = True, metadata: dict[str, Any] | None = None, ) -> AsyncStreamEvents: ...
async def run_on( self, tape: Tape, prompt: list[UserContent], *, model: Model | None = None, options: Mapping[str, Any] | None = None, metadata: dict[str, Any] | None = None, span: Span | None = None, on_close: Callable[[], Awaitable[None]] | None = None, ) -> AsyncStreamEvents: ...
async def run_once( self, prompt: list[UserContent] | None, *, model: Model | None = None, options: Mapping[str, Any] | None = None, tape: Tape, state: StreamState | None = None, pending: Sequence[list[UserContent]] = (), ) -> AsyncGenerator[StreamEvent, None]: ...
async def run_command(self, tape: Tape, line: str) -> str: ...
async def prepare_tools( self, tape: Tape, prompt: list[UserContent] | None, ) -> list[Tool]: ...Model = republic.ChatModel | ModelClientModelClient
Section titled “ModelClient”class ModelClient(Protocol): max_tokens: int | None
@property def spec(self) -> str: ...
def open_stream( self, messages: list[republic.Message], *, spec: str, options: Mapping[str, Any], ) -> AbstractAsyncContextManager[tuple[str, republic.Stream[Any]]]: ...ChatModelClient
Section titled “ChatModelClient”class ChatModelClient: max_tokens: int | None = None
def __init__(self, model: republic.ChatModel) -> None: ...
@property def spec(self) -> str: ...
@asynccontextmanager async def open_stream( self, messages: list[republic.Message], *, spec: str, options: Mapping[str, Any], ) -> AsyncIterator[tuple[str, republic.Stream[Any]]]: ...ToolExposure
Section titled “ToolExposure”ToolExposure = Literal["auto", "direct", "code", "command"]ToolContext
Section titled “ToolContext”@dataclass(frozen=True)class ToolContext: tape: Tape run_id: str | None = None state: dict[str, Any] = field(default_factory=dict) code_mode: bool = False command: bool = False events: EventBus | None = field(default=None, repr=False, compare=False)@dataclass(frozen=True)class Tool: name: str handler: Callable[..., Any] description: str = "" parameters: dict[str, Any] = field(default_factory=dict) context: bool = False exposure: ToolExposure = "auto" renderer: Callable[[Any], str] | None = None output_schema: dict[str, Any] | None = None deferred: bool = False
async def run(self, *args: Any, **kwargs: Any) -> Any: ... def render(self, result: Any) -> str: ... def to_schema(self) -> dict[str, Any]: ...
@classmethod def from_callable( cls, func: Callable[..., Any], *, name: str | None = None, description: str | None = None, context: bool = False, exposure: ToolExposure = "auto", renderer: Callable[[Any], str] | None = None, deferred: bool = False, ) -> Tool: ...@overloaddef tool( func: Callable, *, name: str | None = ..., model: type[BaseModel] | None = ..., description: str | None = ..., context: bool = ..., exposure: ToolExposure = ..., renderer: Callable[[Any], str] | None = ..., deferred: bool = ...,) -> Tool: ...
@overloaddef tool( func: None = ..., *, name: str | None = ..., model: type[BaseModel] | None = ..., description: str | None = ..., context: bool = ..., exposure: ToolExposure = ..., renderer: Callable[[Any], str] | None = ..., deferred: bool = ...,) -> Callable[[Callable], Tool]: ...
def tool( func: Callable | None = None, *, name: str | None = None, model: type[BaseModel] | None = None, description: str | None = None, context: bool = False, exposure: ToolExposure = "auto", renderer: Callable[[Any], str] | None = None, deferred: bool = False,) -> Tool | Callable[[Callable], Tool]: ...content_result
Section titled “content_result”def content_result(content: Sequence[UserContent]) -> dict[str, Any]: ...result_content
Section titled “result_content”def result_content(result: Any) -> list[UserContent]: ...PromptContext
Section titled “PromptContext”@dataclass(frozen=True)class PromptContext: agent: Agent tape: Tape prompt: list[UserContent] tools: list[Tool] model: str
@property def state(self) -> TurnState: ...
@property def available_tools(self) -> list[Tool]: ...PromptSection
Section titled “PromptSection”PromptSection = str | Callable[[PromptContext], str | Awaitable[str | None] | None]SystemPrompt
Section titled “SystemPrompt”class SystemPrompt: def __init__(self, sections: Iterable[PromptSection] = ()) -> None: ...
@classmethod def of( cls, value: SystemPrompt | PromptSection | Iterable[PromptSection], ) -> SystemPrompt: ...
@property def sections(self) -> tuple[PromptSection, ...]: ...
def add(self, section: S) -> S: ... def remove(self, section: PromptSection) -> None: ... async def render(self, context: PromptContext) -> str: ...TapeEntry
Section titled “TapeEntry”@dataclass(frozen=True)class TapeEntry: id: int kind: str payload: dict[str, Any] meta: dict[str, Any] = field(default_factory=dict) date: str = field(default_factory=utc_now)
def copy(self) -> TapeEntry: ...
@classmethod def message(cls, message: dict[str, Any], **meta: Any) -> TapeEntry: ...
@classmethod def system(cls, content: str, **meta: Any) -> TapeEntry: ...
@classmethod def anchor( cls, name: str, state: dict[str, Any] | None = None, **meta: Any, ) -> TapeEntry: ...
@classmethod def tool_call( cls, calls: list[dict[str, Any]], *, content: str = "", **meta: Any, ) -> TapeEntry: ...
@classmethod def tool_result(cls, results: list[Any], **meta: Any) -> TapeEntry: ...
@classmethod def error(cls, error: BubError, **meta: Any) -> TapeEntry: ...
@classmethod def event( cls, name: str, data: dict[str, Any] | None = None, **meta: Any, ) -> TapeEntry: ...TapeContext
Section titled “TapeContext”@dataclass(frozen=True)class TapeContext: anchor: AnchorSelector = LAST_ANCHOR select: ContextSelector | None = None state: dict[str, Any] = field(default_factory=dict)
def build_query(self, query: TapeQuery) -> TapeQuery: ...default_tape_context
Section titled “default_tape_context”def default_tape_context() -> TapeContext: ...TapeInfo
Section titled “TapeInfo”@dataclass(frozen=True)class TapeInfo: name: str entries: int anchors: int last_anchor: str | None entries_since_last_anchor: int last_token_usage: int | None last_token_cache_hit_rate: float | NoneAnchorSummary
Section titled “AnchorSummary”@dataclass(frozen=True)class AnchorSummary: name: str state: dict[str, object]@dataclass(frozen=True)class Tape: store: AsyncTapeStore context: TapeContext sidecars: tuple[TapeSidecar, ...] = field(default=(), repr=False)
@property def name(self) -> str: ...
def with_context(self, context: TapeContext) -> Tape: ... def scoped(self, name: str, context: TapeContext | None = None) -> Tape: ... def query(self) -> TapeQuery[AsyncTapeStore]: ... async def info(self) -> TapeInfo: ... async def anchors(self, limit: int = 20) -> list[AnchorSummary]: ... async def search(self, query: TapeQuery[AsyncTapeStore]) -> list[TapeEntry]: ... async def append_event(self, name: str, payload: dict[str, Any], **meta: Any) -> None: ... async def read_messages(self) -> list[republic.Message]: ...
async def handoff( self, *, name: str, state: dict[str, Any] | None = None, **meta: Any, ) -> list[TapeEntry]: ...
async def reset( self, *, state: dict[str, Any] | None = None, keep_sidecars: Iterable[str] = (), ) -> None: ...
@asynccontextmanager async def fork_tape(self, merge_back: bool = True) -> AsyncGenerator[Tape, None]: ...session_tape_name
Section titled “session_tape_name”def session_tape_name(session_id: str, workspace: Path) -> str: ...TapeSidecar
Section titled “TapeSidecar”class TapeSidecar(Protocol): @property def name(self) -> str: ...
def sidecar_tape_name(owner: str, sidecar: str) -> str: ...Sidecar 由配套 Tape <owner>__sidecar__<name> 承载:随主 Tape 分支、合并和重置,条目不进入主上下文。挂载方式见 Sidecar,应用层的提供方式见 provide_tape_sidecar。
TapeStore
Section titled “TapeStore”class TapeStore(Protocol): def list_tapes(self) -> list[str]: ... def reset(self, tape: str) -> None: ... def fetch_all(self, query: TapeQuery) -> Iterable[TapeEntry]: ... def append(self, tape: str, entry: TapeEntry) -> None: ...AsyncTapeStore
Section titled “AsyncTapeStore”class AsyncTapeStore(Protocol): async def list_tapes(self) -> list[str]: ... async def reset(self, tape: str) -> None: ... async def fetch_all(self, query: TapeQuery) -> Iterable[TapeEntry]: ... async def append(self, tape: str, entry: TapeEntry) -> None: ...TapeQuery
Section titled “TapeQuery”@dataclass(frozen=True)class TapeQuery[T: TapeStore | AsyncTapeStore]: tape: str store: T
def query(self, value: str) -> Self: ... def after_anchor(self, name: str) -> Self: ... def last_anchor(self) -> Self: ... def between_anchors(self, start: str, end: str) -> Self: ... def between_dates(self, start: str | date_type, end: str | date_type) -> Self: ... def kinds(self, *kinds: str) -> Self: ... def limit(self, value: int) -> Self: ...
@overload def all(self: TapeQuery[TapeStore]) -> Iterable[TapeEntry]: ...
@overload async def all(self: TapeQuery[AsyncTapeStore]) -> Iterable[TapeEntry]: ...| 过滤条件 | 语义 |
|---|---|
after_anchor(name) / last_anchor() |
指定 anchor 或最近一个 anchor 之后;Tape 中没有 anchor 时抛 BubError(kind=NOT_FOUND) |
between_anchors(start, end) |
两个 anchor 之间,不含边界;任一 anchor 不存在时抛 NOT_FOUND |
between_dates(start, end) |
ISO 日期或时间范围,含边界;只有日期的上界覆盖整天 |
kinds(*kinds) |
条目类型 |
query(value) |
在 kind、日期、payload 和 meta 的 JSON 文本中做不区分大小写的包含匹配 |
limit(value) |
最多返回的条目数,在其他过滤之后应用 |
after_anchor / last_anchor 返回指定或最近一个 anchor 之后的条目,不包含边界 anchor 本身;anchor 不存在时抛出 BubError(kind=NOT_FOUND)。因此默认投影不会收到边界 anchor。使用 anchor=None 等不过滤 anchor 的读取方式时,选择器可能收到 anchor 条目,默认选择器会把它转换为 [Anchor created: …] 助手消息。直接使用 TapeQuery 时不叠加 TapeContext 的 context=False 过滤。
InMemoryQueryMixin
Section titled “InMemoryQueryMixin”class InMemoryQueryMixin: def read(self, tape: str) -> list[TapeEntry] | None: ... def fetch_all(self, query: TapeQuery) -> Iterable[TapeEntry]: ...InMemoryTapeStore
Section titled “InMemoryTapeStore”class InMemoryTapeStore(InMemoryQueryMixin): def __init__(self) -> None: ... def list_tapes(self) -> list[str]: ... def reset(self, tape: str) -> None: ... def read(self, tape: str) -> list[TapeEntry] | None: ... def append(self, tape: str, entry: TapeEntry) -> None: ...AsyncTapeStoreAdapter
Section titled “AsyncTapeStoreAdapter”class AsyncTapeStoreAdapter: def __init__(self, store: TapeStore) -> None: ... async def list_tapes(self) -> list[str]: ... async def reset(self, tape: str) -> None: ... async def fetch_all(self, query: TapeQuery) -> Iterable[TapeEntry]: ... async def append(self, tape: str, entry: TapeEntry) -> None: ...FileTapeStore
Section titled “FileTapeStore”class FileTapeStore(InMemoryQueryMixin): def __init__(self, directory: Path) -> None: ... def list_tapes(self) -> list[str]: ... def reset(self, tape: str) -> None: ... def append(self, tape: str, entry: TapeEntry) -> None: ... def read(self, tape: str) -> list[TapeEntry] | None: ...is_async_tape_store
Section titled “is_async_tape_store”def is_async_tape_store( store: TapeStore | AsyncTapeStore,) -> TypeIs[AsyncTapeStore]: ...EventBus
Section titled “EventBus”class EventBus: @overload def on(self, event_type: type[E]) -> Callable[[EventHandler[E]], EventHandler[E]]: ...
@overload def on(self, event_type: type[E], handler: EventHandler[E]) -> EventHandler[E]: ...
def off(self, event_type: type[E], handler: EventHandler[E]) -> None: ... async def emit(self, event: E) -> E: ...class Event: @property def stopped(self) -> bool: ...| 事件 | 触发时机 | 处理器可读取或修改的内容 |
|---|---|---|
RunStarted |
模型 Run 开始 | model、prompt、metadata |
RunFinished |
Run 的流关闭 | status(ok / error / cancelled)、steps、耗时和错误 |
StepStarted |
Step 开始 | step、可选的 prompt |
StepFinished |
Step 结束 | 进度、用量;继续时设置 next_prompt |
MessagesPending |
构建请求时,或特定 Step 结束时 | 向 messages 追加用户内容 |
LLMCallStarted |
调用模型提供商之前 | 替换 request;设置 LLMCallDecision.finish() |
LLMCallFinished |
提供商调用成功或因普通异常失败 | request 和汇总后的 result |
ToolCallStarted |
工具执行之前 | 替换 call;设置 deny 或 replace |
ToolCallFinished |
工具成功、普通失败、被拒绝或被替换 | 给 event.result.result 赋值,替换面向模型的结果 |
ChatFinished |
当前 Step 的对话和工具往返可以记录时 | 消息、工具往返、用量和错误 |
CommandFinished |
直接命令成功或失败 | 命令、状态和输出 |
RunFinished 在流关闭时发出。尚未迭代就关闭的 Run 以 cancelled 结束,Step 数为零。模型和工具的 Finished 事件不保证覆盖取消路径,因此计费和清理不能只依赖它们。
StepFinished.next_prompt 提供下一个 Step 的用户消息,MessagesPending.messages 接收 Run 期间到达的消息。达到 max_steps(应用层由 BUB_MAX_STEPS 设置)时会抛出 max_steps_reached 错误。
直接命令发出工具事件和 CommandFinished,但不发出 RunStarted 或 RunFinished。所有 Agent 事件都有 state,TapeEvent 子类还带有当前 tape。分发规则与决策语义见 Agent 事件;应用 hooks 与这些事件的映射见 Hooks 参考。
AgentEvent
Section titled “AgentEvent”@dataclass(kw_only=True)class AgentEvent(Event): state: TurnStateTapeEvent
Section titled “TapeEvent”@dataclass(kw_only=True)class TapeEvent(AgentEvent): tape: TapeRunStarted
Section titled “RunStarted”@dataclass(kw_only=True)class RunStarted(TapeEvent): model: str prompt: list[UserContent] metadata: dict[str, Any] = field(default_factory=dict)RunFinished
Section titled “RunFinished”@dataclass(kw_only=True)class RunFinished(TapeEvent): status: str steps: int elapsed_ms: int error: str | None = NoneStepStarted
Section titled “StepStarted”@dataclass(kw_only=True)class StepStarted(TapeEvent): step: int prompt: list[UserContent] | NoneStepFinished
Section titled “StepFinished”@dataclass(kw_only=True)class StepFinished(TapeEvent): step: int status: str elapsed_ms: int error: str | None = None prompt: list[UserContent] | None = None stream: StreamState = field(default_factory=StreamState) next_prompt: list[UserContent] | None = NoneMessagesPending
Section titled “MessagesPending”@dataclass(kw_only=True)class MessagesPending(TapeEvent): prompt: list[UserContent] | None messages: list[list[UserContent]] = field(default_factory=list)CommandFinished
Section titled “CommandFinished”@dataclass(kw_only=True)class CommandFinished(TapeEvent): line: str name: str status: str elapsed_ms: int output: strLLMCallStarted
Section titled “LLMCallStarted”@dataclass(kw_only=True)class LLMCallStarted(TapeEvent): request: LLMCallRequest decision: LLMCallDecision | None = None
@property def stopped(self) -> bool: ...LLMCallFinished
Section titled “LLMCallFinished”@dataclass(kw_only=True)class LLMCallFinished(TapeEvent): request: LLMCallRequest result: LLMCallResultToolCallStarted
Section titled “ToolCallStarted”@dataclass(kw_only=True)class ToolCallStarted(AgentEvent): call: ToolCall decision: ToolCallDecision = field(default_factory=lambda: ToolCallDecision.proceed())
@property def stopped(self) -> bool: ...ToolCallFinished
Section titled “ToolCallFinished”@dataclass(kw_only=True)class ToolCallFinished(AgentEvent): call: ToolCall result: ToolCallResultChatFinished
Section titled “ChatFinished”@dataclass(kw_only=True)class ChatFinished(TapeEvent): run_id: str model: str system_prompt: str | None new_messages: list[republic.Message] response_text: str | None tool_calls: list[dict[str, Any]] = field(default_factory=list) tool_results: list[Any] | None = None usage: dict[str, Any] | None = None provider: str | None = None assistant_fields: dict[str, Any] = field(default_factory=dict) error: BubError | None = None context_error: BubError | None = NoneCodeTools
Section titled “CodeTools”CodeTools = Iterable[Tool] | Callable[[TurnState], Iterable[Tool] | None]ModelResolver
Section titled “ModelResolver”ModelResolver = Callable[[str | None, ToolContext], tuple[str, Mapping[str, Any]]]CodeMode
Section titled “CodeMode”class CodeMode: def __init__( self, tools: CodeTools = (), *, environment: Environment | Callable[[TurnState], Environment] | None = None, models: str | ModelResolver | None = None, stub_dir: Path | Callable[[TurnState], Path] | None = None, media_tools: bool = True, name: str = RUN_CODE_TOOL_NAME, ) -> None: ...
def code_tools(self, state: TurnState) -> list[Tool] | None: ... def environment(self, state: TurnState) -> Environment: ... def prompt(self, context: PromptContext) -> str | None: ...bub.prompt
Section titled “bub.prompt”UserContent
Section titled “UserContent”type UserContent = str | republic.Image | republic.Audio | republic.VideoLegacyPrompt
Section titled “LegacyPrompt”type LegacyPrompt = str | list[dict[str, Any]]to_content
Section titled “to_content”def to_content(prompt: list[UserContent] | LegacyPrompt) -> list[UserContent]: ...to_legacy_prompt
Section titled “to_legacy_prompt”def to_legacy_prompt(content: list[UserContent]) -> LegacyPrompt: ...prompt_text
Section titled “prompt_text”def prompt_text(content: list[UserContent]) -> str: ...bub.streaming
Section titled “bub.streaming”StreamState
Section titled “StreamState”@dataclassclass StreamState: error: BubError | None = None usage: dict[str, Any] | None = None system_prompt: str | None = None tools: list[Tool] = field(default_factory=list)StreamEvent
Section titled “StreamEvent”@dataclass(frozen=True)class StreamEvent: kind: Literal["text", "reasoning", "tool_call", "tool_result", "usage", "error", "final"] data: dict[str, Any]AsyncStreamEvents
Section titled “AsyncStreamEvents”class AsyncStreamEvents: def __init__( self, iterator: AsyncIterator[StreamEvent], *, state: StreamState | None = None, on_close: Callable[[], Awaitable[None]] | None = None, span: Span | None = None, ) -> None: ...
def __aiter__(self) -> AsyncIterator[StreamEvent]: ... async def __anext__(self) -> StreamEvent: ... async def aclose(self) -> None: ...
@property def error(self) -> BubError | None: ...
@property def usage(self) -> dict[str, Any] | None: ...bub.errors
Section titled “bub.errors”ErrorKind
Section titled “ErrorKind”class ErrorKind(StrEnum): INVALID_INPUT = "invalid_input" CONFIG = "config" PROVIDER = "provider" TOOL = "tool" TEMPORARY = "temporary" NOT_FOUND = "not_found" UNKNOWN = "unknown"BubError
Section titled “BubError”@dataclassclass BubError(Exception): kind: ErrorKind message: str details: dict[str, Any] | None = None
def as_dict(self) -> dict[str, Any]: ...bub.environment
Section titled “bub.environment”ENVIRONMENT_STATE_KEY
Section titled “ENVIRONMENT_STATE_KEY”ENVIRONMENT_STATE_KEY = "_runtime_environment"CallTool
Section titled “CallTool”CallTool = Callable[[str, dict[str, Any]], Awaitable[Any]]CodeFailed
Section titled “CodeFailed”class CodeFailed(Exception): def __init__(self, error: str, traceback: str = "") -> None: ...Process
Section titled “Process”class Process(abc.ABC): @property @abc.abstractmethod def stdout(self) -> asyncio.StreamReader: ...
@property @abc.abstractmethod def stderr(self) -> asyncio.StreamReader: ...
@property @abc.abstractmethod def returncode(self) -> int | None: ...
@abc.abstractmethod async def wait(self) -> int: ...
@abc.abstractmethod async def write_stdin(self, data: bytes) -> None: ...
@abc.abstractmethod def close_stdin(self) -> None: ...
@abc.abstractmethod def signal(self, *, kill: bool) -> None: ...
def is_running(self) -> bool: ...Environment
Section titled “Environment”class Environment(abc.ABC): workspace: str | None = None python: str = "python3"
@abc.abstractmethod async def spawn( self, command: str | Sequence[str], *, cwd: str | None = None, env: Mapping[str, str] | None = None, ) -> Process: ...
@abc.abstractmethod async def read_text(self, path: str) -> str: ... async def read_bytes(self, path: str) -> bytes: ...
@abc.abstractmethod async def write_text(self, path: str, content: str) -> None: ...
def resolve_path(self, path: str) -> str: ...
@abc.abstractmethod async def run_code( self, code: str, *, tools: Sequence[str], call_tool: CallTool, write: Callable[[str], None], ) -> None: ...
async def close(self) -> None: ...LocalEnvironment
Section titled “LocalEnvironment”class LocalEnvironment(Environment): def __init__(self, workspace: str | Path | None = None) -> None: ...
async def spawn( self, command: str | Sequence[str], *, cwd: str | None = None, env: Mapping[str, str] | None = None, ) -> LocalProcess: ...
async def read_text(self, path: str) -> str: ... async def read_bytes(self, path: str) -> bytes: ... async def write_text(self, path: str, content: str) -> None: ...
async def run_code( self, code: str, *, tools: Sequence[str], call_tool: CallTool, write: Callable[[str], None], ) -> None: ...
def resolve_path(self, path: str) -> str: ...BubFramework
Section titled “BubFramework”class BubFramework: def __init__(self, config_file: Path = DEFAULT_CONFIG_FILE) -> None: ...
@property def plugin_manager(self) -> pluggy.PluginManager: ...
def load_builtin_hooks(self) -> None: ... def load_hooks(self) -> None: ... def create_cli_app(self) -> typer.Typer: ...
async def build_prompt( self, message: Envelope, session_id: str, state: dict[str, Any], ) -> list[UserContent]: ...
async def continue_prompt( self, prompt: list[UserContent] | None, tape: Tape, state: StreamState, ) -> str | None: ...
async def build_state(self, message: Envelope, session_id: str) -> TurnState: ... async def process_inbound(self, inbound: Envelope, stream_output: bool = False) -> TurnResult: ... async def resolve_session(self, message: Envelope) -> str: ... def plugin_report(self) -> dict[str, PluginStatus]: ... def hook_report(self) -> dict[str, list[str]]: ... def bind_channel_router(self, router: ChannelRouter | None) -> None: ... async def dispatch_via_channel_router(self, message: Envelope) -> bool: ... async def quit_via_channel_router(self, session_id: str) -> None: ...
async def admit_message( self, *, session_id: str, message: Envelope, turn: TurnSnapshot, ) -> AdmitDecision | None: ...
async def get_model_options( self, *, session_id: str, workspace: str | Path | None = None, ) -> ModelOptions: ...
async def steer_message( self, *, message: Envelope, session_id: str, state: TurnState, reason: str | None = None, ) -> bool: ...
def get_channels(self, message_handler: MessageHandler) -> dict[str, Channel]: ...
@asynccontextmanager async def running(self) -> AsyncGenerator[contextlib.AsyncExitStack, None]: ...
async def get_environment(self, session_id: str) -> Environment | None: ... def get_tape_store(self) -> TapeStore | AsyncTapeStore | None: ... def get_tape_sidecars(self) -> tuple[TapeSidecar, ...]: ... def get_tools(self) -> tuple[Tool, ...]: ... def get_steering_inbox(self) -> SteeringInbox | None: ... def get_agent_hooks(self) -> PluginAgentHooks: ... def get_system_prompt(self, prompt: list[UserContent], state: dict[str, Any]) -> str: ... def build_tape_context(self) -> TapeContext: ... def collect_onboard_config(self, plugin_name: str | None = None) -> dict[str, Any]: ...bub.builtin.agent
Section titled “bub.builtin.agent”BubAgent
Section titled “BubAgent”class BubAgent(core.Agent): command_prefix: str
def __init__( self, framework: BubFramework, *, tools: Collection[Tool] | None = None, tape_store: TapeStore | AsyncTapeStore | None = None, skill_dirs: Collection[Path] | None = None, command_prefix: str | None = None, ) -> None: ...
async def run_stream( self, *, session_id: str, prompt: list[UserContent] | LegacyPrompt, state: TurnState | None = None, model: str | None = None, allowed_skills: Collection[str] | None = None, allowed_tools: Collection[str] | None = None, reasoning_effort: str | None = None, ) -> AsyncStreamEvents: ...
async def prepare_tools( self, tape: Tape, prompt: list[UserContent] | None, ) -> list[Tool]: ...bub.envelope
Section titled “bub.envelope”Envelope
Section titled “Envelope”type Envelope = Anyfield_of
Section titled “field_of”def field_of(message: Envelope, key: str, default: Any = None) -> Any: ...content_of
Section titled “content_of”def content_of(message: Envelope) -> str: ...normalize_envelope
Section titled “normalize_envelope”def normalize_envelope(message: Envelope) -> dict[str, Any]: ...unpack_batch
Section titled “unpack_batch”def unpack_batch(batch: Any) -> list[Envelope]: ...bub.turn
Section titled “bub.turn”TurnState
Section titled “TurnState”type TurnState = dict[str, Any]TurnResult
Section titled “TurnResult”@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)bub.model_selection
Section titled “bub.model_selection”ModelChoice
Section titled “ModelChoice”@dataclass(frozen=True)class ModelChoice: id: str name: str | None = None description: str | None = None meta: dict[str, Any] | None = NoneModelOptions
Section titled “ModelOptions”@dataclass(frozen=True)class ModelOptions: models: list[ModelChoice] = field(default_factory=list) current_model: str | None = Nonebub.channels
Section titled “bub.channels”MediaType
Section titled “MediaType”type MediaType = Literal["image", "audio", "video", "document"]MessageKind
Section titled “MessageKind”type MessageKind = Literal["error", "normal", "command"]MediaItem
Section titled “MediaItem”@dataclassclass MediaItem: type: MediaType mime_type: str filename: str | None = None url: str | None = None data_fetcher: Callable[[], Awaitable[bytes | None]] | None = None
async def get_url(self) -> str | None: ...ChannelMessage
Section titled “ChannelMessage”@dataclassclass ChannelMessage: session_id: str channel: str content: str chat_id: str = "default" is_active: bool = False kind: MessageKind = "normal" context: dict[str, Any] = field(default_factory=dict) media: list[MediaItem] = field(default_factory=list) lifespan: contextlib.AbstractAsyncContextManager | None = None output_channel: str = ""
@property def context_str(self) -> str: ...
@classmethod def from_batch(cls, batch: list[ChannelMessage]) -> ChannelMessage: ...Channel
Section titled “Channel”class Channel(ABC): name: ClassVar[str] = "base"
@abstractmethod async def start(self, stop_event: asyncio.Event) -> None: ...
@abstractmethod async def stop(self) -> None: ...
@property def needs_debounce(self) -> bool: ...
@property def enabled(self) -> bool: ...
async def send(self, message: ChannelMessage) -> None: ...
def stream_events( self, message: ChannelMessage, stream: AsyncIterable[StreamEvent], ) -> AsyncIterable[StreamEvent]: ...
async def admit_message( self, session_id: str, message: Envelope, turn: TurnSnapshot, ) -> AdmitDecision | None: ...Interface
Section titled “Interface”class Interface(Channel): ...Lifecycle
Section titled “Lifecycle”class Lifecycle(Channel): ...MessageHandler
Section titled “MessageHandler”type MessageHandler = Callable[[Envelope], Coroutine[Any, Any, None]]ChannelRouter
Section titled “ChannelRouter”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
Section titled “ChannelManager”class ChannelManager: def __init__( self, framework: BubFramework, enabled_channels: Collection[str] | None = None, stream_output: bool | None = None, system_prompt: str | None = CHANNEL_SYSTEM_PROMPT, ) -> None: ...
async def on_receive(self, message: ChannelMessage) -> None: ... def get_channel(self, name: str) -> Channel | None: ... 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: ... def enabled_channels(self) -> list[Channel]: ... async def listen_and_run(self) -> None: ... async def shutdown(self) -> None: ...
async def admit_channel_message( self, session_id: str, message: Envelope, turn: TurnSnapshot, ) -> AdmitDecision | None: ...TurnAdmissionAction
Section titled “TurnAdmissionAction”type TurnAdmissionAction = Literal["process", "drop", "follow_up", "steer"]AdmitDecision
Section titled “AdmitDecision”@dataclass(frozen=True)class AdmitDecision: action: TurnAdmissionAction reason: str | None = NoneTurnSnapshot
Section titled “TurnSnapshot”@dataclass(frozen=True)class TurnSnapshot: session_id: str is_running: bool running_count: int pending_count: int steering_count: int = 0SteeringInbox
Section titled “SteeringInbox”class SteeringInbox(Protocol): async def enqueue_message(self, message: Envelope, state: TurnState) -> None: ... async def drain_messages(self, state: TurnState) -> list[Envelope]: ... def message_count(self, state: TurnState) -> int: ...bub.application
Section titled “bub.application”Outcome
Section titled “Outcome”type Outcome = Literal["succeeded", "failed", "cancelled"]PromptKind
Section titled “PromptKind”type PromptKind = Literal["question", "permission", "auth"]CommandChanged
Section titled “CommandChanged”@dataclass(frozen=True)class CommandChanged(Event): outcome: Outcome | None = NoneWorkChanged
Section titled “WorkChanged”@dataclass(frozen=True)class WorkChanged(Event): busy: bool completed: Outcome | None = NonePromptChanged
Section titled “PromptChanged”@dataclass(frozen=True)class PromptChanged(Event): kind: PromptKind | Nonebub.program_status
Section titled “bub.program_status”ProgramStatus
Section titled “ProgramStatus”class ProgramStatus: def __init__(self, output: TextIO | None) -> None: ... def __call__(self, event: Event) -> None: ...bub.inquirer
Section titled “bub.inquirer”CheckboxValidator
Section titled “CheckboxValidator”CheckboxValidator = Callable[[list[str]], bool | str]ask_prompt
Section titled “ask_prompt”def ask_prompt( prompt: Callable[..., InquirerResult[Any]], message: str, *, kind: PromptKind = "question", **kwargs: Any,) -> Any: ...ask_text
Section titled “ask_text”def ask_text(message: str, default: str = "") -> str: ...ask_secret
Section titled “ask_secret”def ask_secret(message: str) -> str: ...ask_confirm
Section titled “ask_confirm”def ask_confirm( message: str, default: bool = False, *, kind: PromptKind = "question",) -> bool: ...ask_select
Section titled “ask_select”def ask_select(message: str, choices: list[str], default: str = "") -> str: ...ask_fuzzy
Section titled “ask_fuzzy”def ask_fuzzy( message: str, choices: list[str], default: str | None = None,) -> str: ...ask_checkbox
Section titled “ask_checkbox”def ask_checkbox( message: str, choices: list[str], enabled: list[str] | None = None, validate: CheckboxValidator | None = None,) -> list[str]: ...