Provider plugins

A provider plugin adds a coding agent to bb. It claims one provider key and one bridge key.

The server claim supplies provider data and session control. The host claim translates the agent protocol into the common bb bridge protocol.

This page adds an acme provider. The example assumes that acme-agent has its own line protocol.

Use this when

  • Add a coding agent to bb. Claim one bb.providers.provider key for the agent.
  • Connect a CLI or SDK agent. Translate its native protocol through the bb.providers.bridge host role.
  • Support models, health, and usage. Use the provider contract and the shared host services.
  • Report a rate limit to the UI. Send normalized limit data and a provider error that settles the turn.
  • Request approval before a command. Send an interaction/request and wait for the typed result.
  • Replace an installed provider implementation. Claim the same provider key and use the global winner picker.
  • Keep provider processes reliable. Use the shared supervisor instead of local process control.

What you build

The example has one server artifact and one host artifact.

Provider plugins are normally headless. Add an @get-bb/plugin/app factory only when you claim a separate owner surface.

  • bb.plugin.jsonc
  • icons/acme.svg
  • src/declaration.ts
  • src/server.ts
  • src/host.ts
  • src/acme-bridge.ts

The build writes dist/contract.json. It records both keyed claims and the static provider declaration.

Part Contract Kind Key
Provider service bb.providers.provider keyed acme
Bridge host role bb.providers.bridge keyed acme

Each key has one active winner. A replacement joins the same winner selection system as other replaceable contracts.

Steps

1. Write the provider declaration

The declaration contains data that bb can inspect before it starts plugin code. Keep this file free of host work.

// src/declaration.ts
import type { ProviderDeclaration } from "@bb/providers/contracts";

export const declaration = {
  id: "acme",
  displayName: "Acme Agent",
  family: "native",
  icon: "./icons/acme.svg",
  visibility: "installed",
  capabilities: {
    supportsServiceTier: false,
    supportsNativeUserQuestion: false,
    fork: "none",
    supportsManualCompaction: false,
    supportsThreadArchive: false,
    supportsThreadRename: false,
    permissionModes: ["auto", "full"],
    reasoningLevels: ["low", "high"],
  },
  composerActions: [],
  reasoningLevels: [
    { id: "low", label: "Low", description: "Use less time." },
    { id: "high", label: "High", description: "Use more time." },
  ],
  maintenance: {
    health: true,
    usage: true,
    installation: true,
  },
  models: {
    scope: "host",
    fallback: [
      {
        id: "acme-1",
        displayName: "Acme 1",
        description: "The default Acme model.",
        supportedReasoningEfforts: [
          { reasoningEffort: "low", description: "Use less time." },
          { reasoningEffort: "high", description: "Use more time." },
        ],
        defaultReasoningEffort: "low",
        isDefault: true,
      },
    ],
  },
  env: { passthrough: ["ACME_API_BASE"] },
  bridgeOptions: {},
} satisfies ProviderDeclaration;

The build copies this static contract data into contract.json. The host and server import the same value.

Do not get provider data from a live process during discovery. Use models(), health(), and usage() for live data.

2. Claim the provider and bridge keys

Create bb.plugin.jsonc at the package root.

{
  "$schema": "https://getbb.app/schemas/plugin-v2.schema.json",
  "schemaVersion": 2,
  "id": "acme.provider",
  "version": "2.0.0",
  "name": "Acme Agent",
  "description": "Run bb threads with Acme Agent.",
  "category": "providers",
  "engines": { "bb": "^2.0.0", "sdk": "^2.0.0" },

  "claims": [
    {
      "service": "bb.providers.provider",
      "version": "^1",
      "key": "acme",
      "default": true
    },
    {
      "hostRole": "bb.providers.bridge",
      "version": "^1",
      "key": "acme",
      "default": true
    }
  ],

  "surfaces": [],
  "services": [],
  "requires": [
    { "service": "bb.providers", "range": "^1" },
    { "service": "bb.providers.host.supervisor", "range": "^1" },
    { "service": "bb.providers.host.requests", "range": "^1" },
    { "service": "bb.providers.host.maintenance", "range": "^1" }
  ],
  "optional": [],
  "watched": [],

  "exports": {
    "./declaration": "./dist/declaration.mjs"
  },
  "artifacts": {
    "server": "./dist/server.mjs",
    "host": "./dist/host.mjs"
  }
}

This plugin owns no new service or surface contract. It claims contracts that the bb.providers plugin owns.

The first claim for a new key must set default to true. A later replacement cannot change that fallback.

3. Provide the keyed server service

The bb.providers.provider contract defines one implementation for each provider ID.

// src/server.ts
import { defineServerPlugin } from "@get-bb/plugin/server";
import { bbProvider } from "@bb/providers/contracts";
import { declaration } from "./declaration.js";

export default defineServerPlugin((api) => {
  api.services.provide(bbProvider, {
    key: "acme",
    implementation: {
      declaration: () => declaration,

      deriveOptions: async () => ({}),

      models: (context) =>
        api.host.roles.call("bb.providers.bridge", "model/list", context),

      health: (context) =>
        api.host.roles.call("bb.providers.bridge", "provider/health", context),

      usage: (context) =>
        api.host.roles.call("bb.providers.bridge", "provider/usage", context),

      installationStatus: (context) =>
        api.host.roles.call(
          "bb.providers.bridge",
          "provider/installation/status",
          context,
        ),

      runInstallation: (context, action) =>
        api.host.roles.call(
          "bb.providers.bridge",
          "provider/installation/run",
          { ...context, action },
        ),

      resolveNativeRoots: async () => ({ skills: [], commands: [] }),

      openSession: (context) =>
        api.host.roles.session("bb.providers.bridge", context),
    },
  });
});

The factory receives a typed api object. The loader removes this claim when the plugin generation stops.

You do not need a registration disposer. Return a disposer only for resources that the API does not own.

4. Use the common bridge protocol

bb.providers.bridge uses newline-delimited JSON-RPC 2.0. The handshake selects the highest common delta grammar.

The public protocol module is @bb/providers/bridge. It supplies schemas, types, the bridge factory, and protocol helpers.

import type { BridgeCapabilities } from "@bb/providers/bridge";

export const capabilities = {
  sessionRestore: false,
  threadArchive: false,
  threadRename: false,
  threadGoalClear: false,
  fork: "none",
  approvalEnforcedBy: "runtime",
  grammarVersions: [3, 3],
  steerMode: "queue",
  skills: { configure: false },
} satisfies BridgeCapabilities;

A bridge cannot report a capability that exceeds its static declaration.

The runtime can send these main requests:

Group Methods
Handshake initialize
Catalog and status model/list, provider/health, provider/usage
Maintenance provider/installation/status, provider/installation/run
Thread lifecycle thread/start, thread/resume, thread/fork, thread/stop, thread/discard
Turn lifecycle turn/start, turn/steer
Optional features archive, name, goal, and skill methods

The bridge can request item/tool/call and interaction/request. It can send ordered thread/delta notifications.

Use stable item keys for streamed data. Repeat the full item in every item close event.

Send unknown native events as provider/raw. Mark only confirmed noise as safe to drop.

5. Provide the keyed host role

The host claim connects the provider service to the agent process.

// src/host.ts
import { defineHostPlugin } from "@get-bb/plugin/host";
import {
  bbProviderBridgeRole,
  bbProviderHostSupervisor,
} from "@bb/providers/host";
import { createAcmeBridge } from "./acme-bridge.js";
import { declaration } from "./declaration.js";

export default defineHostPlugin(async (api) => {
  const supervisor = await api.services.use(bbProviderHostSupervisor);

  api.hostRoles.provide(
    bbProviderBridgeRole,
    createAcmeBridge({ supervisor, declaration }),
    { key: "acme" },
  );
});

createAcmeBridge contains only the Acme protocol translation. Use the bridge runtime from @bb/providers/bridge in that module.

Do not write another JSON-RPC loop. Do not add a local process registry or a local request tracker.

The exact translation depends on the native agent protocol. It must normalize native events into the published bridge types.

6. Use the shared host services

bb.providers supplies four common host services.

Service Owner work
bb.providers.host.supervisor Process identities, sessions, exits, leases, child environments, and transcript records
bb.providers.host.requests JSON-RPC requests, time limits, tool calls, interactions, and failed scopes
bb.providers.host.maintenance Executable checks, versions, install commands, and verification
bb.providers.host.declarations Dynamic declarations and native-root checks

The supervisor gives each session one process identity. It applies aborts, exit rejection, and process termination steps.

The request service rejects pending work when the process exits. It also owns tool and interaction result tracking.

The maintenance service returns an exact install command and a verification rule. Do not duplicate version and package checks.

The declaration service applies one atomic declaration change. Use it only when the provider discovers keys on the host.

7. Build and inspect the contract

Run bb plugin build after all claims have runtime implementations.

The build checks the manifest and contract.json together. It rejects a static claim without a runtime implementation.

It also rejects a runtime implementation without a static claim. Inspect the generated provider key and bridge key before distribution.

Use @bb/providers/bridge/testing for bridge protocol tests. Use @get-bb/plugin/testing for plugin factory tests.

Test at least these cases:

  • A successful handshake and one complete turn.
  • A process exit with pending requests.
  • An interaction that the user denies.
  • A rate limit that includes normalized reset data.
  • An item close event that repeats the full item.
  • A replacement that fails before the winner changes.

What happens at runtime

  1. The loader reads bb.plugin.jsonc and contract.json without plugin execution.
  2. The provider registry adds the acme claimant to the keyed winner record.
  3. The loader stages the server and host factories for one plugin generation.
  4. The host role uses the shared supervisor to start the bridge process.
  5. The runtime and bridge complete the protocol handshake.
  6. The provider service opens one supervised session for the thread.
  7. The bridge translates native events into ordered thread deltas.
  8. The runtime drains old calls before a winner change completes.

The runtime commits both plugin halves as one generation. A failed host candidate cannot remove the current acme winner.

Replacement rules

bb.providers.provider is a replaceable keyed contract. Each provider ID has its own winner and fallback.

The bridge role uses the same key. The inspector shows the server generation and host generation in one provider record.

The runtime makes a candidate ready before the change. It then stops new calls to the old winner and drains current calls.

The runtime restarts required dependents in graph order. It returns to the declared fallback after a winner failure.

Pitfalls

  • Keep both keys equal. Use acme for the provider claim, bridge claim, and runtime registrations.
  • Keep the declaration static. The registry must show the provider without a live agent process.
  • Do not exceed the declaration. The handshake can narrow capabilities, but it cannot make them wider.
  • Use the shared supervisor. Local process maps break exit rejection, leases, and replacement behavior.
  • Use the shared request service. Local trackers can leave tool calls or interactions unresolved.
  • Repeat a full item on close. This rule keeps event replay independent from bridge memory.
  • Declare maintenance gates accurately. An enabled gate requires its matching bridge method.
  • Keep secrets outside deltas. The kernel bb.secrets port stores secret interaction values.
  • Use dot contract IDs. Use bb.providers.provider, not a path-style ID.

See also

  • Design page 09 for the complete provider, bridge, delta, item, and host-service contracts.
  • Guide 02 for service claims and the four contract kinds.
  • Guide 10 for host-tier concepts and host targets.
  • Guide 13 for factory and protocol tests.