Extension Guide¶
This guide explains how to implement Bub hooks with @hookimpl, and how those implementations are executed in the current runtime.
1) Import And Basic Shape¶
Use the marker exported by Bub:
Implement hooks on a plugin object:
from __future__ import annotations
from bub import hookimpl
class MyPlugin:
@hookimpl
def build_prompt(self, message, session_id, state):
return "custom prompt"
my_plugin = MyPlugin()
2) Register Plugin Via Entry Points¶
Expose your plugin in pyproject.toml:
BubFramework.load_hooks() loads builtin first, then entry points in group="bub".
3) Expose Tools By Importing The Module¶
Tools are registered through the @tool decorator's import-time side effect.
Your plugin must import the module that contains the @tool definitions before the agent starts using them.
Example:
from __future__ import annotations
from bub import hookimpl
from . import tools # noqa: F401
class MyPlugin:
@hookimpl
def system_prompt(self, prompt, state):
return "extension prompt"
If that import is missing, the tool module never runs, nothing is inserted into bub.tools.REGISTRY, and the tool will not be available to the agent or CLI completion.
4) Ship Skills In Extension Packages¶
Extension packages can also ship skills by including a top-level bub_skills/ directory in the distribution.
Example layout:
my-extension/
├─ src/
│ ├─ my_extension/
│ │ └─ plugin.py
│ └─ bub_skills/
│ └─ my-skill/
│ └─ SKILL.md
└─ pyproject.toml
Configure your build backend to include the bub_skills/ directory in the package data. For example, with pdm-backend:
At runtime, Bub discovers builtin skills from <site-packages>/bub_skills, so packaged skills in that location are loaded automatically.
These skills use normal precedence rules and can still be overridden by workspace (.agents/skills) or user (~/.agents/skills) skills.
5) Hook Execution Semantics¶
HookRuntime drives most framework hooks:
call_first(...): execute by priority, return first non-Nonecall_many(...): execute all, collect all return values (includingNone)call_first_sync(...)/call_many_sync(...): sync-only bootstrap paths
Current process_inbound() hook usage:
resolve_session(call_first)load_state(call_many, then merged by framework)build_prompt(call_first)run_model(call_first)save_state(call_many, always executed infinally)render_outbound(call_many)dispatch_outbound(call_many, per outbound)
Other hook consumers:
register_cli_commands: called bycall_many_syncprovide_channels: called bycall_many_syncinBubFramework.get_channels()system_prompt,provide_tape_store: consumed byBubFrameworkand the builtinAgent
6) Priority And Override Rules¶
- Builtin plugin is registered first.
- Later plugins have higher runtime precedence.
HookRuntimereverses pluggy implementation order so later registration runs first.- For
load_state, framework re-reverses before merge so high-priority values overwrite low-priority values.
7) Sync vs Async Rules¶
- Async hook calls can run both sync and async implementations.
- Sync hook calls skip awaitable return values and log a warning.
- Therefore, keep bootstrap hooks synchronous:
register_cli_commandsprovide_channelsprovide_tape_store
8) Signature Matching¶
HookRuntime passes only parameters declared in your function signature.
You can safely omit unused hook arguments.
Example:
from bub import hookimpl
class SessionPlugin:
@hookimpl
def resolve_session(self, message):
return "my-session"
9) Minimal End-To-End Example¶
from __future__ import annotations
from bub import hookimpl
class EchoPlugin:
@hookimpl
def build_prompt(self, message, session_id, state):
return f"[echo] {message['content']}"
@hookimpl
async def run_model(self, prompt, session_id, state):
return prompt
Run and verify:
Check that your plugin is listed for build_prompt / run_model, and output reflects your override.
10) Common Pitfalls¶
- Defining
@toolfunctions without importing the module from your plugin means the tools never register. - Returning awaitables from hooks invoked via sync paths (
call_many_sync/call_first_sync) causes skip. - Assuming hook failures are isolated: non-
on_errorhook exceptions propagate and can fail the turn. - Using stale hook names: always confirm against
src/bub/hookspecs.py.