Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,41 @@ jobs:




core-only-install:
runs-on: ubuntu-latest
steps:
- name: Check out
uses: actions/checkout@v6

- name: Set up the environment
uses: ./.github/actions/setup-python-env

- name: Build the wheel
run: uv build --wheel -o dist

# `pip install bub` without the app extra installs only the agent kernel.
- name: Use bub.core without the app extra
run: |
uv run --isolated --no-project --with dist/bub-*.whl python - <<'EOF'
import importlib.util
from bub.core import Agent
from bub.core.store import InMemoryTapeStore
import bub.errors, bub.core.store, bub.core.tape, bub.core.tools

Agent(store=InMemoryTapeStore())
app_modules = ("typer", "pluggy", "pydantic_settings", "yaml", "rapidfuzz", "telegram", "rich")
assert not [m for m in app_modules if importlib.util.find_spec(m)], "app dependencies were installed"
EOF

- name: The bub command asks for the app extra
run: |
if output=$(uv run --isolated --no-project --with dist/bub-*.whl bub 2>&1); then
echo "bub ran without the app extra"; exit 1
fi
echo "$output"
grep -q "bub\[app\]" <<<"$output"

check-docs:
runs-on: ubuntu-latest
steps:
Expand Down
11 changes: 7 additions & 4 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,24 @@

Core code lives under `src/`:

- `src/bub/__main__.py`: Typer CLI entrypoint.
- `src/bub/__main__.py`: Typer CLI entrypoint; `main` (the `bub` script) checks the `app` extra before importing the CLI.
- `src/bub/core/`: agent kernel with no settings or plugin dependencies: agent loop over a tape store (`agent.py`), model protocol and Republic chat model adapter (`model.py`), agent events and their bus (`events.py`), the tape recorder handler (`recorder.py`), system prompt sections (`system_prompt.py`), command prompts (`commands.py`), tools, tape, store, tape context, and code mode (`codemode/`: `CodeMode`, the `run_code` subprocess runner, and the `republic` bridge).
- `src/bub/framework.py`: turn orchestration and outbound routing.
- `src/bub/hooks/`: hook specifications, execution helpers, and interception contracts.
- `src/bub/channels/`: channel bases, routing contracts, admission policy, and lifecycle manager.
- `src/bub/{envelope,turn,streaming,errors,model_selection}.py`: small kernel vocabulary owned by each concern.
- `src/bub/builtin/`: builtin runtime, CLI wiring, settings, tools, and tape services.
- `src/bub/builtin/`: builtin runtime: `BubAgent` (a `bub.core.Agent` subclass adding settings, skills, commands, steering), `BubModel`, tape archiving, CLI wiring, tools, tape services, and the fuzzy-searching file tape store.
- `src/bub/channels/`: channel abstractions plus CLI and Telegram adapters.
- `src/bub/skills.py` / `src/bub/tools.py`: skill discovery and tool registry.
- `src/bub/skills.py` / `src/bub/core/tools.py`: skill discovery and tool registry.
- `src/skills/`: bundled skills shipped with Bub.

Tests live in `tests/`. The website and docs runtime lives in `website/`.

Packaging: `pip install bub` installs only the kernel's dependencies (republic, pydantic, loguru, typing-extensions). Everything outside `bub.core` (framework, CLI, channels, hooks, skills, builtin) needs the `app` extra, and the `bub` script (`bub.__main__:main`) asks for it when it is missing. Add new non-kernel dependencies to `app`, not to `dependencies`.

## Build, Test, and Development Commands

- `uv sync`: install or update the Python environment; enough to run Bub from source or for deployment-only hosts.
- `uv sync`: install or update the Python environment, including the `app` extra through the `dev` group; enough to run Bub from source. Deployment-only hosts use `uv sync --no-dev --extra app`.
- `make install`: full local development bootstrap; sync Python deps, install website deps, and install `prek` hooks.
- `uv run bub chat`: run the interactive CLI.
- `uv run bub gateway`: start channel listener mode.
Expand Down
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ WORKDIR /app
COPY pyproject.toml uv.lock README.md LICENSE entrypoint.sh ./
# Copy the full source and install
COPY src ./src
RUN uv sync --no-dev --no-editable && uv pip install "any-llm-sdk[gemini,xai]" && \
RUN uv sync --locked --no-dev --no-editable --extra app && \
chmod +x /app/entrypoint.sh

WORKDIR /workspace
Expand Down
125 changes: 16 additions & 109 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,13 @@

<p><strong>A tiny agent runtime, composable with plugins.</strong></p>

[Website](https://bub.build) · [Docs](https://bub.build/docs/getting-started/) · [Hub](https://hub.bub.build)

</div>

Bub is a small Python runtime for building agents in shared environments. It started in group chats, where multiple humans and agents had to work in the same conversation without hidden state, hand-wavy memory, or framework-specific magic.

Built on [agents.md](https://agents.md/) and [Agent Skills](https://agentskills.io/) , Bub stays intentionally small. Every turn stage is a [pluggy](https://pluggy.readthedocs.io/) hook. Builtins are included but replaceable. The same runtime drives CLI, Telegram, and any channel you add.

[Website](https://bub.build) · [GitHub](https://github.com/bubbuild/bub)
Built on [agents.md](https://agents.md/) and [Agent Skills](https://agentskills.io/), Bub stays intentionally small. Every turn stage is a [pluggy](https://pluggy.readthedocs.io/) hook, builtins are replaceable, and the same runtime drives CLI, Telegram, and any channel you add.

## Quick Start

Expand All @@ -31,39 +31,24 @@ Windows PowerShell:
powershell -ExecutionPolicy ByPass -c "irm https://bub.build/install.ps1 | iex"
```

The interactive installer uses a colored preset picker, accepts additional plugin dependencies, and runs `bub onboard` after installation. For automation, select a preset explicitly; non-interactive installs skip onboarding:

```bash
curl -fsSL https://bub.build/install.sh | bash -s -- --preset recommended --dependency extra-plugin
```

```powershell
powershell -ExecutionPolicy ByPass -c "& ([scriptblock]::Create((irm https://bub.build/install.ps1))) --preset recommended --dependency extra-plugin"
```

Or from source:

```bash
git clone https://github.com/bubbuild/bub.git
cd bub
uv sync # enough to run Bub from source
```

For local development, use `make install` instead so the website toolchain and `prek` hooks are installed too.
Then:

```bash
bub chat # interactive session
bub run "summarize this repo" # one-shot task
bub gateway # channel listener mode
```

See [Getting Started](https://bub.build/docs/getting-started/) for installer presets and source installs, and [Configure](https://bub.build/docs/operate/configure/) for model and runtime settings.

## Why Bub

- **Composable by design.** Every turn stage is a plugin hook. Override one stage or replace the whole flow without forking the runtime.
- **Tape context.** Context is rebuilt from append-only records, not carried around as mutable session state. Easier to inspect, replay, and hand off.
- **One runtime across surfaces.** The same inbound pipeline runs across CLI, Telegram, and custom channels. Adapters change the surface, not the runtime model.
- **Batteries included.** CLI, Telegram, tools, skills, and model execution ship with the core runtime. Use the defaults first, replace them later.
- **Operator equivalence.** Humans and agents work inside the same runtime boundaries, with the same evidence trail and handoff model. No hidden operator class.
- **Operator equivalence.** Humans and agents work inside the same runtime boundaries, with the same evidence trail and handoff model.
- **Embeddable Python SDK.** `pip install bub` installs only `bub.core`, which exposes the agent loop on its own (the CLI, channels and plugins come with `bub[app]`) — bring your own model, tools, prompts, and tape store, with no settings or plugins required.

## How It Works

Expand All @@ -75,101 +60,23 @@ resolve_session → load_state → build_prompt → run_model
dispatch_outbound ← render_outbound ← save_state
```

Builtins are registered first. External plugins load after them. At runtime, later plugins take precedence. There are no framework-only shortcuts.

Key source files:

- Turn orchestrator: [`src/bub/framework.py`](https://github.com/bubbuild/bub/blob/main/src/bub/framework.py)
- Hook contract: [`src/bub/hooks/specs.py`](https://github.com/bubbuild/bub/blob/main/src/bub/hooks/specs.py)
- Builtin hooks: [`src/bub/builtin/hook_impl.py`](https://github.com/bubbuild/bub/blob/main/src/bub/builtin/hook_impl.py)
- Skill discovery: [`src/bub/skills.py`](https://github.com/bubbuild/bub/blob/main/src/bub/skills.py)

## Extend It

```python
from bub import hookimpl
from bub.envelope import content_of


class EchoPlugin:
@hookimpl
def build_prompt(self, message, session_id, state):
return f"[echo] {content_of(message)}"

@hookimpl
async def run_model(self, prompt, session_id, state):
return prompt


echo_plugin = EchoPlugin()
```

```toml
[project.entry-points."bub"]
echo = "my_package.plugin:echo_plugin"
```

See the [Build docs](https://bub.build/docs/build/) for hook guides, packaging, and plugin structure.

## CLI

| Command | Description |
| ------------------ | --------------------------------- |
| `bub chat` | Interactive REPL |
| `bub run MESSAGE` | One-shot turn |
| `bub gateway` | Channel listener (Telegram, etc.) |
| `bub install` | Install or sync Bub plugin deps |
| `bub update` | Upgrade Bub plugin deps |
| `bub login openai` | OpenAI Codex OAuth |

Lines starting with `,` enter internal command mode (`,help`, `,skill name=my-skill`, `,fs.read path=README.md`).

`bub hooks` still exists for diagnostics, but it is hidden from top-level help. `bub install` and `bub update` manage a separate uv project for Bub plugins, defaulting to `~/.bub/bub-project` or `BUB_PROJECT`.

## Configuration

| Variable | Default | Description |
| --------------------------- | ---------------------------- | ---------------------------------------------------- |
| `BUB_MODEL` | `openrouter:openrouter/free` | Model identifier |
| `BUB_API_KEY` | — | Provider key (optional with `bub login openai`) |
| `BUB_API_BASE` | — | Custom provider endpoint |
| `BUB_CLIENT_ARGS` | — | JSON object forwarded to the underlying model client |
| `BUB_COMPLETION_ARGS` | — | JSON object forwarded to each completion call |
| `BUB_MAX_STEPS` | unlimited | Tool-use loop limit; must be a positive integer |
| `BUB_MAX_TOKENS` | `16384` | Max tokens per model call |
| `BUB_MODEL_TIMEOUT_SECONDS` | — | Model call timeout (seconds) |
| `BUB_SPILL_THRESHOLD` | `4096` | Estimated tokens before tool output spills; `0` disables |

## Background

Bub is shaped by one constraint: real collaboration is messier than a solo demo. In shared environments, operators need visible boundaries, auditable history, and extension points that do not collapse into framework sprawl.

Read more:

- [Why We Rewrote Bub](https://bub.build/posts/why-rewrite-bub/)
- [Socialized Evaluation and Agent Partnership](https://bub.build/posts/socialized-evaluation/)
- [Context from Tape](https://tape.systems)
Builtins are registered first and external plugins load after them, so later plugins take precedence. There are no framework-only shortcuts.

## Docs

- [Getting Started](https://bub.build/docs/getting-started/) — install Bub and run the first turn
- [Concepts](https://bub.build/docs/concepts/) — the mental model behind the runtime
- [Channels](https://bub.build/docs/operate/channels/) — run Bub in CLI, Telegram, or your own channel
- [Skills](https://bub.build/docs/build/skills/) — discover, inspect, and author Agent Skills in Bub
- [Python SDK](https://bub.build/docs/build/sdk/) — embed Bub's agent loop in your own application
- [Build](https://bub.build/docs/build/) — write plugins, override hooks, ship tools and skills
- [Bub Hub](https://hub.bub.build) — browse community plugins, skills, and distributions
- [Channels](https://bub.build/docs/operate/channels/) — run Bub in CLI, Telegram, or your own channel
- [Deployment](https://bub.build/docs/operate/deploy/) — Docker, environment, upgrades

## Development

```bash
make install
make check
make test
make docs
make docs-test
```
## Background

See [CONTRIBUTING.md](https://github.com/bubbuild/bub/blob/main/CONTRIBUTING.md).
- [Why We Rewrote Bub](https://bub.build/posts/why-rewrite-bub/)
- [Socialized Evaluation and Agent Partnership](https://bub.build/posts/socialized-evaluation/)
- [Context from Tape](https://tape.systems)

## License

Expand Down
33 changes: 18 additions & 15 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,22 +20,11 @@ classifiers = [
"Topic :: Software Development :: Libraries :: Python Modules",
]

# The agent kernel, bub.core. Everything else (framework, CLI, channels, builtin agent) is in the `app` extra.
dependencies = [
"republic>=0.6.0a1",
"pydantic>=2.12.5",
"pydantic-settings>=2.14.2",
"pyyaml>=6.0.3",
"pluggy>=1.6.0",
"inquirer-textual>=0.8.0",
"typer>=0.24.1",
"authlib>=1.7.2",
"any-llm-sdk[anthropic]>=1.22.1",
"rich>=14.3.4",
"prompt-toolkit>=3.0.52",
"python-telegram-bot>=22.7",
"loguru>=0.7.3",
"rapidfuzz>=3.14.5",
"aiohttp>=3.14.3",
"httpx[socks]>=0.28.1",
"typing-extensions>=4.15.0",
]

Expand All @@ -45,21 +34,35 @@ Repository = "https://github.com/bubbuild/bub"
Documentation = "https://bub.build"

[project.scripts]
bub = "bub.__main__:app"
bub = "bub.__main__:main"

[project.optional-dependencies]
app = [
"pydantic-settings>=2.14.2",
"pyyaml>=6.0.3",
"pluggy>=1.6.0",
"inquirer-textual>=0.8.0",
"typer>=0.24.1",
"rich>=14.3.4",
"prompt-toolkit>=3.0.52",
"python-telegram-bot>=22.7",
"rapidfuzz>=3.14.5",
"aiohttp>=3.14.3",
"httpx[socks]>=0.28.1",
]
trace = [
"opentelemetry-api>=1.39.0",
"opentelemetry-sdk>=1.39.0",
"opentelemetry-exporter-otlp-proto-http>=1.39.0",
]
logfire = [
"bub[trace]",
"bub[app,trace]",
"logfire>=4.32.0",
]

[dependency-groups]
dev = [
"bub[app]",
"pytest>=9.0.3",
"tox-uv>=1.35.1",
"types-PyYAML>=6.0.12.20260408",
Expand Down
41 changes: 29 additions & 12 deletions src/bub/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
"""Bub framework package."""
"""Bub framework package.

The top-level names are imported on first access, so ``import bub.core`` does not load
the framework, its settings or the CLI.
"""

from __future__ import annotations

Expand All @@ -7,12 +11,15 @@
from importlib.metadata import PackageNotFoundError
from importlib.metadata import version as metadata_version
from pathlib import Path
from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, Any

from bub.configure import Settings, config, ensure_config
from bub.framework import DEFAULT_HOME, BubFramework
from bub.hooks import hookimpl
from bub.tools import tool
if TYPE_CHECKING:
from bub.configure import Settings, config, ensure_config
from bub.core.tools import tool
from bub.framework import BubFramework
from bub.hooks import hookimpl

home: Path

__all__ = [
"BubFramework",
Expand All @@ -24,6 +31,15 @@
"tool",
]

_EXPORTS = {
"BubFramework": "bub.framework",
"Settings": "bub.configure",
"config": "bub.configure",
"ensure_config": "bub.configure",
"hookimpl": "bub.hooks",
"tool": "bub.core.tools",
}

try:
__version__ = import_module("bub._version").version
except ModuleNotFoundError:
Expand All @@ -33,13 +49,14 @@
__version__ = "0.0.0"


if TYPE_CHECKING:
home: Path


def __getattr__(name: str):
def __getattr__(name: str) -> Any:
if name == "home":
# Importing bub.configure loads ``.env``, which may set ``BUB_HOME``.
from bub.configure import DEFAULT_HOME

if "BUB_HOME" in os.environ:
return Path(os.path.expanduser(os.environ["BUB_HOME"]))
return DEFAULT_HOME
raise AttributeError(f"module {__name__} has no attribute {name}")
if name not in _EXPORTS:
raise AttributeError(f"module {__name__} has no attribute {name}")
return getattr(import_module(_EXPORTS[name]), name)
Loading
Loading