From 7f1d6c2aa7f8e2932e3c98815e9f5747c9ccaa7f Mon Sep 17 00:00:00 2001 From: Chojan Shang Date: Thu, 16 Jul 2026 01:57:52 +0800 Subject: [PATCH 1/2] refactor: align with Bub contract boundaries --- .../src/bub_acp_server/plugin.py | 42 +++++++++---------- packages/bub-acp-server/tests/test_plugin.py | 42 +++++++++---------- packages/bub-codex/src/bub_codex/plugin.py | 18 ++++---- packages/bub-codex/tests/test_plugin.py | 2 +- packages/bub-cursor/src/bub_cursor/plugin.py | 20 ++++----- packages/bub-cursor/tests/test_plugin.py | 2 +- .../bub-dingtalk/src/bub_dingtalk/channel.py | 2 +- .../bub-dingtalk/src/bub_dingtalk/plugin.py | 2 +- .../bub-dingtalk/tests/test_inbound_flow.py | 12 +++--- .../bub-discord/src/bub_discord/channel.py | 2 +- .../bub-discord/src/bub_discord/plugin.py | 2 +- packages/bub-extism/src/bub_extism/channel.py | 5 ++- packages/bub-extism/src/bub_extism/codec.py | 2 +- packages/bub-extism/src/bub_extism/plugin.py | 28 +++++++------ packages/bub-extism/src/bub_extism/stream.py | 2 +- packages/bub-extism/tests/test_bridge.py | 4 +- packages/bub-extism/tests/test_examples.py | 4 +- packages/bub-feishu/src/bub_feishu/channel.py | 2 +- packages/bub-feishu/src/bub_feishu/plugin.py | 2 +- .../src/bub_github_copilot/plugin.py | 14 +++---- .../bub-github-copilot/tests/test_plugin.py | 2 +- packages/bub-kimi/src/bub_kimi/plugin.py | 18 ++++---- packages/bub-kimi/tests/test_plugin.py | 2 +- .../src/bub_mcp_server/plugin.py | 2 +- packages/bub-mcp/src/bub_mcp/plugin.py | 6 ++- packages/bub-qq/src/bub_qq/channel.py | 2 +- packages/bub-qq/src/bub_qq/plugin.py | 2 +- .../bub-schedule/src/bub_schedule/plugin.py | 6 ++- .../src/bub_session_prompt/plugin.py | 4 +- packages/bub-slack/src/bub_slack/channel.py | 4 +- .../bub-tapestore-otel/tests/test_plugin.py | 2 +- .../src/bub_tapestore_redis/store.py | 2 +- .../bub-tapestore-redis/tests/test_store.py | 2 +- .../tests/test_store.py | 2 +- .../src/bub_tapestore_sqlite/store.py | 2 +- .../bub-tapestore-sqlite/tests/test_store.py | 2 +- packages/bub-wechat/src/bub_wechat/channel.py | 2 +- packages/bub-wechat/src/bub_wechat/plugin.py | 2 +- packages/bub-wecom/src/bub_wecom/channel.py | 2 +- packages/bub-wecom/src/bub_wecom/plugin.py | 2 +- 40 files changed, 143 insertions(+), 134 deletions(-) diff --git a/packages/bub-acp-server/src/bub_acp_server/plugin.py b/packages/bub-acp-server/src/bub_acp_server/plugin.py index a9950da..2f30236 100644 --- a/packages/bub-acp-server/src/bub_acp_server/plugin.py +++ b/packages/bub-acp-server/src/bub_acp_server/plugin.py @@ -54,12 +54,14 @@ ResumeSessionResponse, ) from acp.helpers import start_tool_call, tool_content, update_tool_call -from bub import RuntimeChoice, RuntimeOptions, hookimpl +from bub import hookimpl +from bub.channels.contracts import ChannelRouter from bub.channels.message import ChannelMessage, MediaItem, MediaType -from bub.envelope import content_of, field_of -from bub.runtime import StreamEvent +from bub.envelope import Envelope, content_of, field_of +from bub.model_selection import ModelChoice, ModelOptions +from bub.streaming import StreamEvent from bub.tape import TapeEntry, TapeQuery -from bub.types import Envelope, OutboundChannelRouter, TurnResult +from bub.turn import TurnResult from bub_acp_server.config import ACPServerSettings @@ -362,7 +364,7 @@ async def close_session( async def cancel(self, session_id: str, **kwargs: Any) -> None: del kwargs - await self.framework.quit_via_router(session_id) + await self.framework.quit_via_channel_router(session_id) async def set_config_option( self, @@ -374,9 +376,7 @@ async def set_config_option( del kwargs session = self._sessions.get(session_id) or self._adopt_session(session_id) session.touch() - config_options = await self._set_session_runtime_option( - session, config_id, value - ) + config_options = await self._set_session_model_option(session, config_id, value) self._save_sessions() return SetSessionConfigOptionResponse(config_options=config_options) @@ -551,14 +551,14 @@ async def _load_tape_entries(self, session: ACPSession) -> list[TapeEntry]: async def _session_config_options( self, session: ACPSession ) -> list[SessionConfigOptionSelect] | None: - runtime_options = await self.framework.get_runtime_options( + model_options = await self.framework.get_model_options( session_id=session.session_id, workspace=session.cwd, ) - acp_options = _runtime_options_to_acp_config_options(runtime_options, session) + acp_options = _model_options_to_acp_config_options(model_options, session) return acp_options or None - async def _set_session_runtime_option( + async def _set_session_model_option( self, session: ACPSession, config_id: str, @@ -603,18 +603,18 @@ async def _process_inbound_with_streaming( async with self._prompt_lock: router = ACPStreamRouter(client, session.session_id) previous_router = cast( - OutboundChannelRouter | None, - getattr(self.framework, "_outbound_router", None), + ChannelRouter | None, + getattr(self.framework, "_channel_router", None), ) previous_workspace = self.framework.workspace self.framework.workspace = session.cwd - self.framework.bind_outbound_router(router) + self.framework.bind_channel_router(router) try: result = await self.framework.process_inbound( inbound, stream_output=True ) finally: - self.framework.bind_outbound_router(previous_router) + self.framework.bind_channel_router(previous_router) self.framework.workspace = previous_workspace if result.model_output and not router.sent_text: await client.session_update( @@ -754,17 +754,17 @@ def _framework_tape_store(framework: BubFramework) -> object | None: return store if hasattr(store, "fetch_all") else None -def _runtime_options_to_acp_config_options( - runtime_options: RuntimeOptions, session: ACPSession +def _model_options_to_acp_config_options( + model_options: ModelOptions, session: ACPSession ) -> list[SessionConfigOptionSelect]: - choices = runtime_options.models + choices = model_options.models if not choices: return [] choice_ids = {choice.id for choice in choices} current_value = session.runtime.get("model") if current_value not in choice_ids: - current_value = runtime_options.current_model + current_value = model_options.current_model if current_value not in choice_ids: current_value = choices[0].id return [ @@ -773,13 +773,13 @@ def _runtime_options_to_acp_config_options( id="model", name="Model", current_value=current_value, - options=[_runtime_choice_to_acp_option(choice) for choice in choices], + options=[_model_choice_to_acp_option(choice) for choice in choices], category="model", ) ] -def _runtime_choice_to_acp_option(choice: RuntimeChoice) -> SessionConfigSelectOption: +def _model_choice_to_acp_option(choice: ModelChoice) -> SessionConfigSelectOption: return SessionConfigSelectOption( value=choice.id, name=choice.name or choice.id, diff --git a/packages/bub-acp-server/tests/test_plugin.py b/packages/bub-acp-server/tests/test_plugin.py index f95e432..41ccb94 100644 --- a/packages/bub-acp-server/tests/test_plugin.py +++ b/packages/bub-acp-server/tests/test_plugin.py @@ -5,10 +5,10 @@ import pytest from acp.schema import TextContentBlock -from bub import RuntimeChoice, RuntimeOptions -from bub.types import TurnResult -from bub.runtime import StreamEvent +from bub.model_selection import ModelChoice, ModelOptions +from bub.streaming import StreamEvent from bub.tape import TapeEntry, TapeQuery +from bub.turn import TurnResult from bub_acp_server import plugin from bub_acp_server.plugin import BubACPAgent @@ -37,17 +37,17 @@ def __init__(self) -> None: self.messages: list[object] = [] self.stream_output_values: list[bool] = [] - def bind_outbound_router(self, router: object) -> None: + def bind_channel_router(self, router: object) -> None: self.previous_routers.append(router) self.router = router - async def quit_via_router(self, session_id: str) -> None: + async def quit_via_channel_router(self, session_id: str) -> None: return None - async def get_runtime_options( + async def get_model_options( self, *, session_id: str, workspace: Path - ) -> RuntimeOptions: - return RuntimeOptions() + ) -> ModelOptions: + return ModelOptions() async def process_inbound( self, inbound: object, stream_output: bool = False @@ -114,20 +114,20 @@ async def stream(): class ConfigFramework(FakeFramework): def __init__(self) -> None: super().__init__() - self.runtime_queries: list[tuple[str, Path]] = [] + self.model_queries: list[tuple[str, Path]] = [] - async def get_runtime_options( + async def get_model_options( self, *, session_id: str, workspace: Path - ) -> RuntimeOptions: - self.runtime_queries.append((session_id, workspace)) - return RuntimeOptions( + ) -> ModelOptions: + self.model_queries.append((session_id, workspace)) + return ModelOptions( models=[ - RuntimeChoice( + ModelChoice( id="openai:gpt-5", name="GPT-5", description="OpenAI model", ), - RuntimeChoice( + ModelChoice( id="anthropic:claude-sonnet-4-5", name="Claude Sonnet", ), @@ -253,7 +253,7 @@ async def test_session_lifecycle_returns_config_options(tmp_path: Path) -> None: assert loaded.config_options[0].id == "model" assert resumed.config_options is not None assert resumed.config_options[0].id == "model" - assert framework.runtime_queries == [ + assert framework.model_queries == [ (created.session_id, tmp_path), (created.session_id, tmp_path), (created.session_id, tmp_path), @@ -279,13 +279,13 @@ async def test_set_config_option_updates_session_runtime_and_returns_config_opti } assert response.config_options[0].id == "model" assert response.config_options[0].current_value == "anthropic:claude-sonnet-4-5" - assert framework.runtime_queries == [ + assert framework.model_queries == [ (created.session_id, tmp_path), (created.session_id, tmp_path), ] -def test_runtime_options_fall_back_when_persisted_model_is_unavailable( +def test_model_options_fall_back_when_persisted_model_is_unavailable( tmp_path: Path, ) -> None: session = plugin.ACPSession( @@ -293,12 +293,12 @@ def test_runtime_options_fall_back_when_persisted_model_is_unavailable( cwd=tmp_path, runtime={"model": "removed:model"}, ) - options = RuntimeOptions( - models=[RuntimeChoice(id="available:model")], + options = ModelOptions( + models=[ModelChoice(id="available:model")], current_model="available:model", ) - config_options = plugin._runtime_options_to_acp_config_options(options, session) + config_options = plugin._model_options_to_acp_config_options(options, session) assert config_options[0].current_value == "available:model" diff --git a/packages/bub-codex/src/bub_codex/plugin.py b/packages/bub-codex/src/bub_codex/plugin.py index 1c7e3d8..239c151 100644 --- a/packages/bub-codex/src/bub_codex/plugin.py +++ b/packages/bub-codex/src/bub_codex/plugin.py @@ -9,8 +9,8 @@ import bub from bub import hookimpl -from bub.runtime import StreamEvent -from bub.types import State +from bub.streaming import StreamEvent +from bub.turn import TurnState from pydantic import Field from pydantic_settings import SettingsConfigDict @@ -21,11 +21,11 @@ class RuntimeAgent(Protocol): async def run_stream( - self, *, session_id: str, prompt: str | list[dict], state: State + self, *, session_id: str, prompt: str | list[dict], state: TurnState ) -> AsyncIterable[StreamEvent]: ... -def _load_thread_id(session_id: str, state: State) -> str | None: +def _load_thread_id(session_id: str, state: TurnState) -> str | None: workpace = workspace_from_state(state) threads_file = workpace / THREADS_FILE with contextlib.suppress(FileNotFoundError): @@ -34,7 +34,7 @@ def _load_thread_id(session_id: str, state: State) -> str | None: return threads.get(session_id) -def _save_thread_id(session_id: str, thread_id: str, state: State) -> None: +def _save_thread_id(session_id: str, thread_id: str, state: TurnState) -> None: workpace = workspace_from_state(state) threads_file = workpace / THREADS_FILE if threads_file.exists(): @@ -47,7 +47,7 @@ def _save_thread_id(session_id: str, thread_id: str, state: State) -> None: json.dump(threads, f, indent=2) -def workspace_from_state(state: State) -> Path: +def workspace_from_state(state: TurnState) -> Path: raw = state.get("_runtime_workspace") if isinstance(raw, str) and raw.strip(): return Path(raw).expanduser().resolve() @@ -69,7 +69,7 @@ def _settings() -> CodexSettings: return bub.ensure_config(CodexSettings) -def _runtime_agent_from_state(state: State) -> RuntimeAgent | None: +def _runtime_agent_from_state(state: TurnState) -> RuntimeAgent | None: agent = state.get("_runtime_agent") if agent is None: return None @@ -77,7 +77,7 @@ def _runtime_agent_from_state(state: State) -> RuntimeAgent | None: async def _run_internal_command( - prompt: str, session_id: str, state: State + prompt: str, session_id: str, state: TurnState ) -> str | None: if not prompt.strip().startswith(","): return None @@ -93,7 +93,7 @@ async def _run_internal_command( @hookimpl -async def run_model(prompt: str, session_id: str, state: State) -> str: +async def run_model(prompt: str, session_id: str, state: TurnState) -> str: internal_command_result = await _run_internal_command(prompt, session_id, state) if internal_command_result is not None: return internal_command_result diff --git a/packages/bub-codex/tests/test_plugin.py b/packages/bub-codex/tests/test_plugin.py index 7d4ea00..8593b5a 100644 --- a/packages/bub-codex/tests/test_plugin.py +++ b/packages/bub-codex/tests/test_plugin.py @@ -6,7 +6,7 @@ from pathlib import Path import pytest -from bub.runtime import AsyncStreamEvents, StreamEvent +from bub.streaming import AsyncStreamEvents, StreamEvent from bub_codex import plugin diff --git a/packages/bub-cursor/src/bub_cursor/plugin.py b/packages/bub-cursor/src/bub_cursor/plugin.py index 4597511..4922e33 100644 --- a/packages/bub-cursor/src/bub_cursor/plugin.py +++ b/packages/bub-cursor/src/bub_cursor/plugin.py @@ -12,8 +12,8 @@ import typer from bub import BubFramework, hookimpl from bub.builtin.auth import app as auth_app -from bub.runtime import StreamEvent -from bub.types import State +from bub.streaming import StreamEvent +from bub.turn import TurnState from pydantic import Field from pydantic_settings import SettingsConfigDict @@ -37,7 +37,7 @@ class RuntimeAgent(Protocol): async def run_stream( - self, *, session_id: str, prompt: str | list[dict], state: State + self, *, session_id: str, prompt: str | list[dict], state: TurnState ) -> AsyncIterable[StreamEvent]: ... @@ -60,14 +60,14 @@ def _settings() -> CursorSettings: return bub.ensure_config(CursorSettings) -def workspace_from_state(state: State) -> Path: +def workspace_from_state(state: TurnState) -> Path: raw = state.get("_runtime_workspace") if isinstance(raw, str) and raw.strip(): return Path(raw).expanduser().resolve() return Path.cwd().resolve() -def _load_thread_id(session_id: str, state: State) -> str | None: +def _load_thread_id(session_id: str, state: TurnState) -> str | None: threads_file = workspace_from_state(state) / THREADS_FILE with contextlib.suppress(FileNotFoundError, json.JSONDecodeError): with threads_file.open() as f: @@ -78,7 +78,7 @@ def _load_thread_id(session_id: str, state: State) -> str | None: return None -def _save_thread_id(session_id: str, thread_id: str, state: State) -> None: +def _save_thread_id(session_id: str, thread_id: str, state: TurnState) -> None: threads_file = workspace_from_state(state) / THREADS_FILE if threads_file.exists(): with threads_file.open() as f: @@ -90,7 +90,7 @@ def _save_thread_id(session_id: str, thread_id: str, state: State) -> None: json.dump(threads, f, indent=2) -def _runtime_agent_from_state(state: State) -> RuntimeAgent | None: +def _runtime_agent_from_state(state: TurnState) -> RuntimeAgent | None: agent = state.get("_runtime_agent") if agent is None: return None @@ -108,7 +108,7 @@ def _prompt_to_text(prompt: str | list[dict[str, Any]]) -> str: async def _run_internal_command( - prompt: str, session_id: str, state: State + prompt: str, session_id: str, state: TurnState ) -> str | None: if not prompt.strip().startswith(","): return None @@ -156,7 +156,7 @@ def _cursor_command( return command -def _result_from_stdout(stdout_text: str, session_id: str, state: State) -> str: +def _result_from_stdout(stdout_text: str, session_id: str, state: TurnState) -> str: try: data = json.loads(stdout_text) except json.JSONDecodeError: @@ -176,7 +176,7 @@ def _result_from_stdout(stdout_text: str, session_id: str, state: State) -> str: @hookimpl async def run_model( - prompt: str | list[dict[str, Any]], session_id: str, state: State + prompt: str | list[dict[str, Any]], session_id: str, state: TurnState ) -> str: prompt_text = _prompt_to_text(prompt) internal_command_result = await _run_internal_command( diff --git a/packages/bub-cursor/tests/test_plugin.py b/packages/bub-cursor/tests/test_plugin.py index 9381ae9..d91504c 100644 --- a/packages/bub-cursor/tests/test_plugin.py +++ b/packages/bub-cursor/tests/test_plugin.py @@ -6,7 +6,7 @@ from pathlib import Path import pytest -from bub.runtime import AsyncStreamEvents, StreamEvent +from bub.streaming import AsyncStreamEvents, StreamEvent from typer.testing import CliRunner from bub.builtin.auth import app as auth_app diff --git a/packages/bub-dingtalk/src/bub_dingtalk/channel.py b/packages/bub-dingtalk/src/bub_dingtalk/channel.py index 4d75cb1..72f020b 100644 --- a/packages/bub-dingtalk/src/bub_dingtalk/channel.py +++ b/packages/bub-dingtalk/src/bub_dingtalk/channel.py @@ -9,7 +9,7 @@ import bub from bub.channels import Channel from bub.channels.message import ChannelMessage -from bub.types import MessageHandler +from bub.channels.contracts import MessageHandler from dingtalk_stream import ( AckMessage, CallbackHandler, diff --git a/packages/bub-dingtalk/src/bub_dingtalk/plugin.py b/packages/bub-dingtalk/src/bub_dingtalk/plugin.py index 74eb5b5..35cda82 100644 --- a/packages/bub-dingtalk/src/bub_dingtalk/plugin.py +++ b/packages/bub-dingtalk/src/bub_dingtalk/plugin.py @@ -5,7 +5,7 @@ from bub import hookimpl from bub import inquirer as bub_inquirer from bub.channels import Channel -from bub.types import MessageHandler +from bub.channels.contracts import MessageHandler from .channel import DingTalkChannel diff --git a/packages/bub-dingtalk/tests/test_inbound_flow.py b/packages/bub-dingtalk/tests/test_inbound_flow.py index 998dbc6..ed00dc3 100644 --- a/packages/bub-dingtalk/tests/test_inbound_flow.py +++ b/packages/bub-dingtalk/tests/test_inbound_flow.py @@ -76,11 +76,11 @@ async def dispatch_output(self, message) -> bool: outbounds_captured.append(message) return True - framework.bind_outbound_router(CaptureRouter()) + framework.bind_channel_router(CaptureRouter()) try: result = await framework.process_inbound(inbound) finally: - framework.bind_outbound_router(None) + framework.bind_channel_router(None) assert result.session_id == "dingtalk:204818006723348842" assert len(result.outbounds) >= 1 @@ -124,7 +124,7 @@ async def dispatch_output(self, message) -> bool: ) return True - framework.bind_outbound_router(CaptureRouter()) + framework.bind_channel_router(CaptureRouter()) try: result = asyncio.run(framework.process_inbound(inbound)) print( @@ -133,7 +133,7 @@ async def dispatch_output(self, message) -> bool: for i, o in enumerate(outbounds): print(f" outbound[{i}]: content={(o.content or '')[:100]!r}") finally: - framework.bind_outbound_router(None) + framework.bind_channel_router(None) def test_channel_manager_on_receive_to_process_inbound() -> None: @@ -163,7 +163,7 @@ async def dispatch_output(self, message) -> bool: outbounds.append(message) return True - framework.bind_outbound_router(CaptureRouter()) + framework.bind_channel_router(CaptureRouter()) await manager.on_receive(inbound) msg = await asyncio.wait_for(manager._messages.get(), timeout=2.0) @@ -171,7 +171,7 @@ async def dispatch_output(self, message) -> bool: assert msg.content == "test" await framework.process_inbound(msg) - framework.bind_outbound_router(None) + framework.bind_channel_router(None) assert len(outbounds) >= 1 assert outbounds[0].channel == "dingtalk" diff --git a/packages/bub-discord/src/bub_discord/channel.py b/packages/bub-discord/src/bub_discord/channel.py index f3ba716..8ba11d0 100644 --- a/packages/bub-discord/src/bub_discord/channel.py +++ b/packages/bub-discord/src/bub_discord/channel.py @@ -11,7 +11,7 @@ import discord from bub.channels import Channel from bub.channels.message import ChannelMessage -from bub.types import MessageHandler +from bub.channels.contracts import MessageHandler from discord.ext import commands from loguru import logger from pydantic_settings import SettingsConfigDict diff --git a/packages/bub-discord/src/bub_discord/plugin.py b/packages/bub-discord/src/bub_discord/plugin.py index 2f2132a..b317fa0 100644 --- a/packages/bub-discord/src/bub_discord/plugin.py +++ b/packages/bub-discord/src/bub_discord/plugin.py @@ -3,7 +3,7 @@ from bub import hookimpl from bub import inquirer as bub_inquirer from bub.channels import Channel -from bub.types import MessageHandler +from bub.channels.contracts import MessageHandler CHANNEL_NAME = "discord" diff --git a/packages/bub-extism/src/bub_extism/channel.py b/packages/bub-extism/src/bub_extism/channel.py index 60715fe..553c2b8 100644 --- a/packages/bub-extism/src/bub_extism/channel.py +++ b/packages/bub-extism/src/bub_extism/channel.py @@ -5,8 +5,9 @@ from typing import Any from bub.channels import Channel -from bub.runtime import StreamEvent -from bub.types import Envelope, MessageHandler +from bub.channels.contracts import MessageHandler +from bub.envelope import Envelope +from bub.streaming import StreamEvent from bub_extism.bridge import ExtismBridge from bub_extism.codec import message_to_json diff --git a/packages/bub-extism/src/bub_extism/codec.py b/packages/bub-extism/src/bub_extism/codec.py index e10d932..af9104e 100644 --- a/packages/bub-extism/src/bub_extism/codec.py +++ b/packages/bub-extism/src/bub_extism/codec.py @@ -6,7 +6,7 @@ from typing import Any from bub.envelope import normalize_envelope -from bub.runtime import StreamEvent +from bub.streaming import StreamEvent from bub.tape import TapeEntry, utc_now BUB_EXTISM_ABI_VERSION = "bub.extism.v1" diff --git a/packages/bub-extism/src/bub_extism/plugin.py b/packages/bub-extism/src/bub_extism/plugin.py index 0be03e1..e4640ee 100644 --- a/packages/bub-extism/src/bub_extism/plugin.py +++ b/packages/bub-extism/src/bub_extism/plugin.py @@ -3,7 +3,7 @@ from typing import TYPE_CHECKING, Any from bub import hookimpl -from bub.runtime import AsyncStreamEvents +from bub.streaming import AsyncStreamEvents from bub.tape import LAST_ANCHOR, TapeContext from bub_extism.bridge import ExtismBridge from bub_extism.channel import channels_from_value @@ -22,7 +22,9 @@ import typer from bub.channels import Channel from bub.framework import BubFramework - from bub.types import Envelope, MessageHandler, State + from bub.channels.contracts import MessageHandler + from bub.envelope import Envelope + from bub.turn import TurnState from bub.tape import TapeStore @@ -37,12 +39,12 @@ def _message_session_args(message: Envelope, session_id: str) -> dict[str, Any]: } -def _state_args(state: State) -> dict[str, Any]: +def _state_args(state: TurnState) -> dict[str, Any]: return {"state": state_to_json(state)} def _message_session_state_args( - message: Envelope, session_id: str, state: State + message: Envelope, session_id: str, state: TurnState ) -> dict[str, Any]: return { **_message_session_args(message, session_id), @@ -53,7 +55,7 @@ def _message_session_state_args( def _prompt_session_state_args( prompt: str | list[dict[str, Any]], session_id: str, - state: State, + state: TurnState, ) -> dict[str, Any]: return { "prompt": prompt, @@ -171,7 +173,7 @@ async def hook_build_prompt( self, message: Envelope, session_id: str, - state: State, + state: TurnState, ) -> str | list[dict[str, Any]] | None: return _prompt_value( await self._call( @@ -180,7 +182,9 @@ async def hook_build_prompt( ) ) - async def hook_load_state(self, message: Envelope, session_id: str) -> State | None: + async def hook_load_state( + self, message: Envelope, session_id: str + ) -> TurnState | None: return _optional_mapping( await self._call( "load_state", **_message_session_args(message, session_id) @@ -191,7 +195,7 @@ async def hook_load_state(self, message: Envelope, session_id: str) -> State | N async def hook_save_state( self, session_id: str, - state: State, + state: TurnState, message: Envelope, model_output: str, ) -> None: @@ -205,7 +209,7 @@ def hook_render_outbound( self, message: Envelope, session_id: str, - state: State, + state: TurnState, model_output: str, ) -> list[Envelope]: return _outbound_messages( @@ -243,7 +247,7 @@ async def hook_on_error( ) def hook_system_prompt( - self, prompt: str | list[dict[str, Any]], state: State + self, prompt: str | list[dict[str, Any]], state: TurnState ) -> str | None: return _optional_string( self._call_sync("system_prompt", prompt=prompt, **_state_args(state)), @@ -272,7 +276,7 @@ async def hook_run_model( self, prompt: str | list[dict[str, Any]], session_id: str, - state: State, + state: TurnState, ) -> str | None: return _optional_string( await self._call( @@ -286,7 +290,7 @@ async def hook_run_model_stream( self, prompt: str | list[dict[str, Any]], session_id: str, - state: State, + state: TurnState, ) -> AsyncStreamEvents | None: value = await self._call( "run_model_stream", diff --git a/packages/bub-extism/src/bub_extism/stream.py b/packages/bub-extism/src/bub_extism/stream.py index 821f5d7..a394e07 100644 --- a/packages/bub-extism/src/bub_extism/stream.py +++ b/packages/bub-extism/src/bub_extism/stream.py @@ -3,7 +3,7 @@ from collections.abc import AsyncIterator from typing import Any -from bub.runtime import AsyncStreamEvents, StreamEvent, StreamState +from bub.streaming import AsyncStreamEvents, StreamEvent, StreamState def stream_events_from_value(value: Any) -> AsyncStreamEvents | None: diff --git a/packages/bub-extism/tests/test_bridge.py b/packages/bub-extism/tests/test_bridge.py index 1fd332c..1d7a272 100644 --- a/packages/bub-extism/tests/test_bridge.py +++ b/packages/bub-extism/tests/test_bridge.py @@ -11,8 +11,8 @@ import pytest from bub.tape import TapeEntry, TapeQuery -from bub.hook_runtime import HookRuntime -from bub.hookspecs import BUB_HOOK_NAMESPACE, BubHookSpecs +from bub.hooks import BUB_HOOK_NAMESPACE, BubHookSpecs +from bub.hooks.runtime import HookRuntime from bub_extism.config import ExtismSettings from bub_extism.plugin import ExtismPlugin diff --git a/packages/bub-extism/tests/test_examples.py b/packages/bub-extism/tests/test_examples.py index 92e0d3b..8e4ba0e 100644 --- a/packages/bub-extism/tests/test_examples.py +++ b/packages/bub-extism/tests/test_examples.py @@ -12,8 +12,8 @@ import pluggy import pytest -from bub.hook_runtime import HookRuntime -from bub.hookspecs import BUB_HOOK_NAMESPACE, BubHookSpecs +from bub.hooks import BUB_HOOK_NAMESPACE, BubHookSpecs +from bub.hooks.runtime import HookRuntime from bub_extism.config import ExtismSettings from bub_extism.plugin import ExtismPlugin diff --git a/packages/bub-feishu/src/bub_feishu/channel.py b/packages/bub-feishu/src/bub_feishu/channel.py index b2c3531..bc8130f 100644 --- a/packages/bub-feishu/src/bub_feishu/channel.py +++ b/packages/bub-feishu/src/bub_feishu/channel.py @@ -17,7 +17,7 @@ import lark_oapi as lark from bub.channels import Channel from bub.channels.message import ChannelMessage, MediaItem -from bub.types import MessageHandler +from bub.channels.contracts import MessageHandler from lark_oapi.api.im.v1 import ( CreateMessageRequest, CreateMessageRequestBody, diff --git a/packages/bub-feishu/src/bub_feishu/plugin.py b/packages/bub-feishu/src/bub_feishu/plugin.py index 4016474..e0526ed 100644 --- a/packages/bub-feishu/src/bub_feishu/plugin.py +++ b/packages/bub-feishu/src/bub_feishu/plugin.py @@ -3,7 +3,7 @@ from bub import hookimpl from bub import inquirer as bub_inquirer from bub.channels import Channel -from bub.types import MessageHandler +from bub.channels.contracts import MessageHandler CHANNEL_NAME = "feishu" diff --git a/packages/bub-github-copilot/src/bub_github_copilot/plugin.py b/packages/bub-github-copilot/src/bub_github_copilot/plugin.py index c08a39e..55cffbd 100644 --- a/packages/bub-github-copilot/src/bub_github_copilot/plugin.py +++ b/packages/bub-github-copilot/src/bub_github_copilot/plugin.py @@ -10,8 +10,8 @@ import bub from bub import BubFramework, hookimpl from bub.builtin.auth import app as auth_app -from bub.runtime import StreamEvent -from bub.types import State +from bub.streaming import StreamEvent +from bub.turn import TurnState from copilot import CopilotClient, SubprocessConfig from copilot.session import PermissionHandler from pydantic import Field @@ -52,7 +52,7 @@ class RuntimeAgent(Protocol): async def run_stream( - self, *, session_id: str, prompt: str | list[dict], state: State + self, *, session_id: str, prompt: str | list[dict], state: TurnState ) -> AsyncIterable[StreamEvent]: ... @@ -77,14 +77,14 @@ def _settings() -> GitHubCopilotSettings: return bub.ensure_config(GitHubCopilotSettings) -def workspace_from_state(state: State) -> Path: +def workspace_from_state(state: TurnState) -> Path: raw = state.get("_runtime_workspace") if isinstance(raw, str) and raw.strip(): return Path(raw).expanduser().resolve() return Path.cwd().resolve() -def _runtime_agent_from_state(state: State) -> RuntimeAgent | None: +def _runtime_agent_from_state(state: TurnState) -> RuntimeAgent | None: agent = state.get("_runtime_agent") if agent is None: return None @@ -192,7 +192,7 @@ def _assistant_message_from_history(messages: list[object]) -> str | None: async def _run_internal_command( - prompt: str, session_id: str, state: State + prompt: str, session_id: str, state: TurnState ) -> str | None: if not prompt.strip().startswith(","): return None @@ -324,7 +324,7 @@ def github_copilot_login( @hookimpl -async def run_model(prompt: str | list[dict], session_id: str, state: State) -> str: +async def run_model(prompt: str | list[dict], session_id: str, state: TurnState) -> str: prompt_text = _prompt_to_text(prompt) internal_command_result = await _run_internal_command( prompt_text, session_id, state diff --git a/packages/bub-github-copilot/tests/test_plugin.py b/packages/bub-github-copilot/tests/test_plugin.py index 4cdad5b..9f4c276 100644 --- a/packages/bub-github-copilot/tests/test_plugin.py +++ b/packages/bub-github-copilot/tests/test_plugin.py @@ -4,7 +4,7 @@ from pathlib import Path import pytest -from bub.runtime import AsyncStreamEvents, StreamEvent +from bub.streaming import AsyncStreamEvents, StreamEvent from typer.testing import CliRunner from bub.builtin.auth import app as auth_app diff --git a/packages/bub-kimi/src/bub_kimi/plugin.py b/packages/bub-kimi/src/bub_kimi/plugin.py index 6bcfabc..f47714d 100644 --- a/packages/bub-kimi/src/bub_kimi/plugin.py +++ b/packages/bub-kimi/src/bub_kimi/plugin.py @@ -10,8 +10,8 @@ import bub from bub import hookimpl -from bub.runtime import StreamEvent -from bub.types import State +from bub.streaming import StreamEvent +from bub.turn import TurnState from pydantic import Field from pydantic_settings import SettingsConfigDict @@ -23,11 +23,11 @@ class RuntimeAgent(Protocol): async def run_stream( - self, *, session_id: str, prompt: str | list[dict], state: State + self, *, session_id: str, prompt: str | list[dict], state: TurnState ) -> AsyncIterable[StreamEvent]: ... -def _load_thread_id(session_id: str, state: State) -> str | None: +def _load_thread_id(session_id: str, state: TurnState) -> str | None: workspace = workspace_from_state(state) threads_file = workspace / THREADS_FILE with contextlib.suppress(FileNotFoundError): @@ -36,7 +36,7 @@ def _load_thread_id(session_id: str, state: State) -> str | None: return threads.get(session_id) -def _save_thread_id(session_id: str, thread_id: str, state: State) -> None: +def _save_thread_id(session_id: str, thread_id: str, state: TurnState) -> None: workspace = workspace_from_state(state) threads_file = workspace / THREADS_FILE if threads_file.exists(): @@ -49,7 +49,7 @@ def _save_thread_id(session_id: str, thread_id: str, state: State) -> None: json.dump(threads, f, indent=2) -def workspace_from_state(state: State) -> Path: +def workspace_from_state(state: TurnState) -> Path: raw = state.get("_runtime_workspace") if isinstance(raw, str) and raw.strip(): return Path(raw).expanduser().resolve() @@ -72,7 +72,7 @@ def _settings() -> KimiSettings: return bub.ensure_config(KimiSettings) -def _runtime_agent_from_state(state: State) -> RuntimeAgent | None: +def _runtime_agent_from_state(state: TurnState) -> RuntimeAgent | None: agent = state.get("_runtime_agent") if agent is None: return None @@ -80,7 +80,7 @@ def _runtime_agent_from_state(state: State) -> RuntimeAgent | None: async def _run_internal_command( - prompt: str, session_id: str, state: State + prompt: str, session_id: str, state: TurnState ) -> str | None: if not prompt.strip().startswith(","): return None @@ -96,7 +96,7 @@ async def _run_internal_command( @hookimpl -async def run_model(prompt: str, session_id: str, state: State) -> str: +async def run_model(prompt: str, session_id: str, state: TurnState) -> str: internal_command_result = await _run_internal_command(prompt, session_id, state) if internal_command_result is not None: return internal_command_result diff --git a/packages/bub-kimi/tests/test_plugin.py b/packages/bub-kimi/tests/test_plugin.py index cba17ad..c49326c 100644 --- a/packages/bub-kimi/tests/test_plugin.py +++ b/packages/bub-kimi/tests/test_plugin.py @@ -6,7 +6,7 @@ from pathlib import Path import pytest -from bub.runtime import AsyncStreamEvents, StreamEvent +from bub.streaming import AsyncStreamEvents, StreamEvent from bub_kimi import plugin diff --git a/packages/bub-mcp-server/src/bub_mcp_server/plugin.py b/packages/bub-mcp-server/src/bub_mcp_server/plugin.py index 06552d5..6d8a0a0 100644 --- a/packages/bub-mcp-server/src/bub_mcp_server/plugin.py +++ b/packages/bub-mcp-server/src/bub_mcp_server/plugin.py @@ -8,7 +8,7 @@ from bub import hookimpl from bub.channels import Channel, Lifecycle from bub.channels.message import ChannelMessage -from bub.types import MessageHandler +from bub.channels.contracts import MessageHandler from fastmcp import FastMCP from loguru import logger diff --git a/packages/bub-mcp/src/bub_mcp/plugin.py b/packages/bub-mcp/src/bub_mcp/plugin.py index 4af5ab9..3945e32 100644 --- a/packages/bub-mcp/src/bub_mcp/plugin.py +++ b/packages/bub-mcp/src/bub_mcp/plugin.py @@ -13,7 +13,9 @@ from bub import hookimpl, tool from bub.channels import Channel, Lifecycle from bub.tools import REGISTRY, Tool, ToolContext -from bub.types import Envelope, MessageHandler, State +from bub.channels.contracts import MessageHandler +from bub.envelope import Envelope +from bub.turn import TurnState from loguru import logger from bub_mcp.config import MCPSettings @@ -308,7 +310,7 @@ def __init__(self, framework: Any) -> None: self._manager = MCPChannel() @hookimpl - def load_state(self, message: Envelope, session_id: str) -> State: + def load_state(self, message: Envelope, session_id: str) -> TurnState: return {"mcp": self._manager} @hookimpl diff --git a/packages/bub-qq/src/bub_qq/channel.py b/packages/bub-qq/src/bub_qq/channel.py index 14363e7..101aaa8 100644 --- a/packages/bub-qq/src/bub_qq/channel.py +++ b/packages/bub-qq/src/bub_qq/channel.py @@ -8,7 +8,7 @@ import bub from bub.channels import Channel from bub.channels.message import ChannelMessage -from bub.types import MessageHandler +from bub.channels.contracts import MessageHandler from loguru import logger from .auth import QQTokenProvider diff --git a/packages/bub-qq/src/bub_qq/plugin.py b/packages/bub-qq/src/bub_qq/plugin.py index a56b3e8..2af93fe 100644 --- a/packages/bub-qq/src/bub_qq/plugin.py +++ b/packages/bub-qq/src/bub_qq/plugin.py @@ -3,7 +3,7 @@ from bub import hookimpl from bub import inquirer as bub_inquirer from bub.channels import Channel -from bub.types import MessageHandler +from bub.channels.contracts import MessageHandler CHANNEL_NAME = "qq" RECEIVE_MODES = ["webhook", "websocket"] diff --git a/packages/bub-schedule/src/bub_schedule/plugin.py b/packages/bub-schedule/src/bub_schedule/plugin.py index ec660f8..5deae8b 100644 --- a/packages/bub-schedule/src/bub_schedule/plugin.py +++ b/packages/bub-schedule/src/bub_schedule/plugin.py @@ -4,7 +4,9 @@ from bub import hookimpl from bub.channels import Channel from bub.framework import BubFramework -from bub.types import Envelope, MessageHandler, State +from bub.channels.contracts import MessageHandler +from bub.envelope import Envelope +from bub.turn import TurnState from bub_schedule.jobstore import JSONJobStore @@ -23,7 +25,7 @@ def __init__(self, framework: BubFramework | None = None) -> None: self.scheduler = default_scheduler() @hookimpl - def load_state(self, message: Envelope, session_id: str) -> State: + def load_state(self, message: Envelope, session_id: str) -> TurnState: return {"scheduler": self.scheduler} @hookimpl diff --git a/packages/bub-session-prompt/src/bub_session_prompt/plugin.py b/packages/bub-session-prompt/src/bub_session_prompt/plugin.py index 331a8ad..6b27c7c 100644 --- a/packages/bub-session-prompt/src/bub_session_prompt/plugin.py +++ b/packages/bub-session-prompt/src/bub_session_prompt/plugin.py @@ -2,11 +2,11 @@ import bub from bub import hookimpl -from bub.types import State +from bub.turn import TurnState @hookimpl -def system_prompt(prompt: str, state: State) -> str: +def system_prompt(prompt: str, state: TurnState) -> str: session_id = state.get("session_id", "default") session_dir = bub.home / "sessions" prompt_file = session_dir / session_id / "AGENTS.md" diff --git a/packages/bub-slack/src/bub_slack/channel.py b/packages/bub-slack/src/bub_slack/channel.py index de64387..11ccc31 100644 --- a/packages/bub-slack/src/bub_slack/channel.py +++ b/packages/bub-slack/src/bub_slack/channel.py @@ -13,7 +13,7 @@ * Messages from bots (including itself) and message subtypes (joins, edits, deletes, file shares, ...) are ignored to avoid echo loops and noise. -Replies are delivered through Bub's normal outbound router: the framework +Replies are delivered through Bub's normal channel router: the framework renders the model's text into a :class:`ChannelMessage` targeted at this channel and calls :meth:`send`; we leave ``output_channel`` at its default so routing falls back to ``slack``. @@ -32,7 +32,7 @@ import bub from bub.channels import Channel from bub.channels.message import ChannelMessage -from bub.types import MessageHandler +from bub.channels.contracts import MessageHandler from loguru import logger from slack_sdk.socket_mode.aiohttp import SocketModeClient from slack_sdk.socket_mode.request import SocketModeRequest diff --git a/packages/bub-tapestore-otel/tests/test_plugin.py b/packages/bub-tapestore-otel/tests/test_plugin.py index 700ff88..4d7b259 100644 --- a/packages/bub-tapestore-otel/tests/test_plugin.py +++ b/packages/bub-tapestore-otel/tests/test_plugin.py @@ -2,7 +2,7 @@ import bub_tapestore_otel.plugin as plugin import pluggy -from bub.hookspecs import BUB_HOOK_NAMESPACE, BubHookSpecs, hookimpl +from bub.hooks import BUB_HOOK_NAMESPACE, BubHookSpecs, hookimpl from bub_tapestore_otel.plugin import OTelTapeStorePlugin, OTelTapeStoreSettings from bub_tapestore_otel.store import OTelTapeStore diff --git a/packages/bub-tapestore-redis/src/bub_tapestore_redis/store.py b/packages/bub-tapestore-redis/src/bub_tapestore_redis/store.py index 246f1fa..b44e81b 100644 --- a/packages/bub-tapestore-redis/src/bub_tapestore_redis/store.py +++ b/packages/bub-tapestore-redis/src/bub_tapestore_redis/store.py @@ -11,7 +11,7 @@ from typing import Any import redis.asyncio as redis -from bub.runtime import BubError, ErrorKind +from bub.errors import BubError, ErrorKind from bub.tape import TapeEntry, TapeQuery from redis.exceptions import ResponseError diff --git a/packages/bub-tapestore-redis/tests/test_store.py b/packages/bub-tapestore-redis/tests/test_store.py index feff06b..1f890f6 100644 --- a/packages/bub-tapestore-redis/tests/test_store.py +++ b/packages/bub-tapestore-redis/tests/test_store.py @@ -5,7 +5,7 @@ import pytest import pytest_asyncio -from bub.runtime import BubError, ErrorKind +from bub.errors import BubError, ErrorKind from bub.tape import LAST_ANCHOR, TapeContext, TapeEntry, TapeQuery, build_messages from bub_tapestore_redis import RedisTapeStore from fakeredis import FakeAsyncRedis diff --git a/packages/bub-tapestore-sqlalchemy/tests/test_store.py b/packages/bub-tapestore-sqlalchemy/tests/test_store.py index 6ee0d48..adde9fb 100644 --- a/packages/bub-tapestore-sqlalchemy/tests/test_store.py +++ b/packages/bub-tapestore-sqlalchemy/tests/test_store.py @@ -7,7 +7,7 @@ import pytest from bub_tapestore_sqlalchemy.store import SQLAlchemyTapeStore -from bub.runtime import BubError +from bub.errors import BubError from bub.tape import TapeEntry, TapeQuery diff --git a/packages/bub-tapestore-sqlite/src/bub_tapestore_sqlite/store.py b/packages/bub-tapestore-sqlite/src/bub_tapestore_sqlite/store.py index a5e4095..1f48273 100644 --- a/packages/bub-tapestore-sqlite/src/bub_tapestore_sqlite/store.py +++ b/packages/bub-tapestore-sqlite/src/bub_tapestore_sqlite/store.py @@ -9,7 +9,7 @@ import aiosqlite import sqlite_vec from any_llm import AnyLLM -from bub.runtime import BubError, ErrorKind +from bub.errors import BubError, ErrorKind from bub.tape import TapeEntry, TapeQuery ALLOWED_JOURNAL_MODES = {"DELETE", "TRUNCATE", "PERSIST", "MEMORY", "WAL", "OFF"} diff --git a/packages/bub-tapestore-sqlite/tests/test_store.py b/packages/bub-tapestore-sqlite/tests/test_store.py index 8464a7c..df3b932 100644 --- a/packages/bub-tapestore-sqlite/tests/test_store.py +++ b/packages/bub-tapestore-sqlite/tests/test_store.py @@ -6,7 +6,7 @@ from types import SimpleNamespace import pytest -from bub.runtime import BubError +from bub.errors import BubError from bub.tape import TapeEntry, TapeQuery from bub_tapestore_sqlite import store as store_module diff --git a/packages/bub-wechat/src/bub_wechat/channel.py b/packages/bub-wechat/src/bub_wechat/channel.py index beeeed8..96327ad 100644 --- a/packages/bub-wechat/src/bub_wechat/channel.py +++ b/packages/bub-wechat/src/bub_wechat/channel.py @@ -11,7 +11,7 @@ import bub from bub.channels import Channel, ChannelMessage from bub.channels.message import MediaItem -from bub.types import MessageHandler +from bub.channels.contracts import MessageHandler from loguru import logger from weixin_bot import IncomingMessage, WeixinBot from weixin_bot.api import MessageItemType, send_message diff --git a/packages/bub-wechat/src/bub_wechat/plugin.py b/packages/bub-wechat/src/bub_wechat/plugin.py index 22eb01a..7bcec4d 100644 --- a/packages/bub-wechat/src/bub_wechat/plugin.py +++ b/packages/bub-wechat/src/bub_wechat/plugin.py @@ -3,7 +3,7 @@ from bub.builtin.auth import app as auth_app from bub.channels import Channel from bub.tools import ToolContext -from bub.types import MessageHandler +from bub.channels.contracts import MessageHandler from bub_wechat.channel import OutgoingMessage, WeChatChannel, get_token_path diff --git a/packages/bub-wecom/src/bub_wecom/channel.py b/packages/bub-wecom/src/bub_wecom/channel.py index 03fadf0..2316754 100644 --- a/packages/bub-wecom/src/bub_wecom/channel.py +++ b/packages/bub-wecom/src/bub_wecom/channel.py @@ -12,7 +12,7 @@ import bub from bub.channels import Channel from bub.channels.message import ChannelMessage -from bub.types import MessageHandler +from bub.channels.contracts import MessageHandler from loguru import logger from pydantic import Field from pydantic_settings import SettingsConfigDict diff --git a/packages/bub-wecom/src/bub_wecom/plugin.py b/packages/bub-wecom/src/bub_wecom/plugin.py index 4ef4af5..698bd14 100644 --- a/packages/bub-wecom/src/bub_wecom/plugin.py +++ b/packages/bub-wecom/src/bub_wecom/plugin.py @@ -5,7 +5,7 @@ from bub import hookimpl from bub import inquirer as bub_inquirer from bub.channels import Channel -from bub.types import MessageHandler +from bub.channels.contracts import MessageHandler CHANNEL_NAME = "wecom" POLICIES = ["open", "disabled", "allowlist"] From f9e4efebd72e146f89e74a72c57605d4004f94ce Mon Sep 17 00:00:00 2001 From: Chojan Shang Date: Fri, 17 Jul 2026 10:09:35 +0800 Subject: [PATCH 2/2] chore: refresh Bub dependency --- uv.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/uv.lock b/uv.lock index 237a39f..c9414a6 100644 --- a/uv.lock +++ b/uv.lock @@ -344,8 +344,8 @@ wheels = [ [[package]] name = "bub" -version = "0.3.10.dev16+g7769cd401" -source = { git = "https://github.com/bubbuild/bub.git#7769cd401f02feeda0d78c1748bda6d9ea1f3c57" } +version = "0.3.10.dev20+g2dd63a0c0" +source = { git = "https://github.com/bubbuild/bub.git#2dd63a0c0a58a4202306bf9c93df5d4dac315833" } dependencies = [ { name = "aiohttp" }, { name = "any-llm-sdk" },