Skip to content

Surfaces

This page explains the three extension surfaces Bub exposes and why each one is decoupled from the others.

Most agent frameworks conflate “talks to a chat platform”, “knows how to do a procedure”, and “calls an action on the model’s behalf” into a single concept. Bub keeps them separate so each can evolve and be replaced independently.

          outward I/O              operator procedures        model-callable actions
    ╭─────────────────────╮      ╭─────────────────────╮      ╭─────────────────────╮
    │      Channels       │      │       Skills        │      │        Tools        │
    ╰──────────┬──────────╯      ╰──────────┬──────────╯      ╰──────────┬──────────╯
               │                            │                            │
               ▼                            ▼                            ▼
           Envelope                     Envelope                       State
       (in & out of the             (loaded into the              (per-turn dict
        turn pipeline)               prompt as text)            with `_runtime_*`)

All three surfaces meet on two shared types from bub.types:

  • Envelope — the duck-typed payload moving through the turn pipeline. It is intentionally weakly typed (Any) and accessed through helpers (field_of, content_of).
  • State — a per-turn dict that hooks contribute to via load_state, and that survives until save_state finishes.

A channel is anything that delivers inbound messages into the turn pipeline and accepts outbound messages from it. The contract is bub.channels.base.Channel:

  • start(stop_event) — start the listener loop.
  • stop() — clean up.
  • send(message) — deliver one outbound envelope.
  • stream_events(message, stream) — optionally wrap the model’s stream for incremental rendering.

Why decoupled: the kernel does not care whether messages arrive over the CLI, Telegram, a webhook, or a hardware device. New channels are added by implementing this class and registering them via the provide_channels hook.

For specific built-in channels and how to enable them, see Operate Bub: channels.

A skill is a directory containing SKILL.md (frontmatter + body) plus optional scripts and assets. A skill is invokable by any operator — human or agent — by name.

The contract is intentionally not a Python interface: skills are markdown procedures. Bub discovers them from three roots in priority order: project (.agents/skills/), user (~/.agents/skills/), and built-in.

Why decoupled: skills migrate between agents (Bub follows the Agent Skills format). They are not tied to a specific model, channel, or runtime. The same SKILL.md works whether the operator is a teammate reading the file in code review or an agent calling it during a turn.

A tool is a typed Python function the model can call during a turn. Tools are registered via bub.tool and exposed to the model through the agent loop.

Why decoupled: tools are the only surface that the model itself invokes. Skills tell the operator what to do; tools are what the agent can actually do. Keeping them separate makes it possible to add a skill without giving the model new powers, or to add a tool that humans never invoke directly.

The split mirrors three different control flows:

  • A channel is invoked by the outside world.
  • A skill is invoked by an operator naming it.
  • A tool is invoked by the model on its own initiative.

Conflating them makes it impossible to reason about authority. Keeping them apart lets each surface have its own contract: channels have a lifecycle, skills have a discovery and naming convention, tools have a typed signature and registry.

The pipeline is the only place these surfaces interact directly:

  • A channel produces an Envelope that enters process_inbound.
  • load_state (and the built-in agent) loads applicable skills into the prompt and the active tool set.
  • The model may call tools, whose results land back on the tape.
  • render_outbound produces envelopes, and dispatch_outbound hands them to channels.

Nothing else couples the three. This is what lets a plugin add a Discord channel without touching skills, or a workspace add a repo-map skill without touching channels or tools.