bb · provider plugins

Compatibility and the experimental_ policy

How bb tells you which parts of the API it has promised, which parts it is still deciding, and how long a deprecated name stays alive.

What experimental_ means #

Every new public plugin API member ships with an experimental_ prefix: an export of @get-bb/plugin-sdk/app, an app.slots.* method, a BbPluginApi property, and by the same rule the value exports of the provider-bridge, ACP and testing subpaths. Types are unprefixed. The prefix means:

The audit document #

docs/api_to_audit.md in the bb repository holds one entry per experimental member: what it does, and what has to be decided or verified before the prefix drops. Entries that stay experimental after an audit say so with the date and the open question. The first audit (dated 2026-08-22, the provider-plugin migration's stabilization) stabilized the provider declaration's target-state fields and maintenance, tool presentation, UrlLink / openUrl, fixedTabs and the shared Original delegation prop, and deleted the alias and status-label members; the thread-row-status member, experimental_setThreadRowStatus, stays experimental because the collapsed sidebar section rollup consumes its store (see below). Read the entry for a member before you build on it: it is the most honest statement of what might move.

Windows currently open (removal target: bb 0.42) #

A one-release compatibility window keeps an old name alive beside the new one so a plugin bundle compiled against an older SDK keeps working for one release. Older mobile builds are not supported: the app ships with the server, so no window protects a client on an older protocol version. The timeline wire serves grammar v3 only, with no client capability header, no legacy projection, and no activityIntents key on tool rows (only command rows carry parsed intents); ProviderInfo carries maintenance { health, usage, installation } alone, with no experimental_provider* booleans beside it (see deleted members). These are the windows open right now, every one of them for code compiled against an older SDK or for pi's presentation-less rows; everything in this list goes away in bb 0.42, except the presentation-less tool-row window, which closes on its own condition.

OldNewWho it protects
experimental_UrlLink (app)UrlLinkPlugin bundles compiled against an SDK before 0.4.16. The alias warns on its first render, then renders UrlLink.
BbNavigate.experimental_openUrlopenUrlSame bundles. Warns on its first call, then calls openUrl.
experimental_Original prop on the thread-list, file-opener, source-code renderer and diff rendererOriginalSame bundles; passed beside Original, warns on first render. The timeline renderer never carried the old name.
A presentation-less tool row, on the wire and persistedpresentation on every item.open and item.closePersisted pre-v3 history, and pi. The pi bridge's generic tool rows carry no presentation yet, so its live read/grep/find/ls calls take the same read-time adapter as the old Claude rows (upgradeLegacyToolItem, applied when a stored row is parsed, keyed on the absence of presentation and never on a provider id) and render as exploration rows, path or query and no output; Task*/Todo*/ToolSearch bookkeeping calls collapse; a childless Agent/Task/spawnAgent/resumeAgent call classifies as a delegation row. This window has no release date: it closes when pi stamps fileRead/search presentation so no live row reaches the adapter, the legacy-tool-item-backfill migration stamps the old rows, and presentation becomes required on item.open/item.close.
experimental_toConformanceMessages (/provider-bridge/testing)experimental_runBridgeConformance assembles thread/delta itself and takes the bridge's providerIdA conformance suite written against the transport shape before 0.4.16. The export still exists but throws on call with the replacement named, instead of failing as a missing export.

A bundle that never uses an alias never warns. The two app exports from before 0.4.16, experimental_ProviderModelPicker and experimental_PermissionModePicker are present and stay experimental; neither carries an alias.

Scheduled removals (next major) #

Published names the simplification pass found unreferenced by any first-party plugin. They stay because 0.4.x published them and dropping a published name is a breaking change; they go at the next major version. Do not build on them.

Deleted and renamed members #

The version numbers, in plain words #

SDK version: @get-bb/plugin-sdk 0.4.16
The package you compile against. Its bundled-types/*.d.ts files are the published contract; its /provider-bridge, /provider-bridge/acp and /provider-bridge/testing subpaths are real code that bb plugin build inlines into your host artifact, and into your server bundle when server.ts imports them: only the bare @get-bb/plugin-sdk specifier stays external there. The manifest's engines.bbPluginSdk (the echo example says ">=0.4.3") tells bb the minimum SDK your plugin was written for. Keep the SDK pinned in your plugin and bump it deliberately.
Host-daemon protocol version: 164
The number that guards the wire between the bb server and the host daemons enrolled with it. Every change that can alter what crosses that wire (a field added to a session payload, a host RPC result, a WebSocket message) bumps it, and a daemon whose number differs from its server's updates itself automatically. You never see this number from a plugin. Bump 161 renamed the maintenance facts on the wire: the server sends the daemon bridgeLaunch.capabilities.providerInstallation (the capabilities object is strict, so an older daemon rejects the new name and a newer daemon the old one), and serves clients ProviderInfo.maintenance { health, usage, installation }. The one-release window that would have served the old experimental_provider* booleans beside maintenance to a client built before 161 was withdrawn before release: every client ships with the server, so the old names are not served and no window is described in terms of a client's protocol version. The latest bump, 164, changed no bytes on the wire: it records that the server stopped accepting any interaction lifecycle record from a daemon event batch (system/interaction/lifecycle, and the legacy system/permissionGrant/lifecycle and system/userQuestion/lifecycle). Those records are the server's own account of an interaction it registered, written on registration and on every settle; no daemon, bridge or provider plugin has ever produced one, and a daemon-posted record is now dropped and logged, never stored, whatever interaction it names. The bump before it, 163, removed the absolute side from the declared native roots the server sends the daemon with its skill and command listings: nativeRoots.skills and nativeRoots.commands are { user, project }, and what your resolveNativeRoots answers arrives in nativeRoots.resolved. The roots schema is strict on both sides, so the bump is what moves an enrolled machine onto a daemon that reads the two-sided shape.
Provider Bridge Protocol version: 2
The JSON-RPC envelope and method vocabulary between the runtime and your bridge, exchanged at initialize. It bumps only for a change an older bridge or runtime cannot tolerate; additive changes ride capability tolerance instead, which is why bridges version with their plugin rather than with the daemon.
Grammar version: 3
The thread/delta vocabulary, negotiated separately as a [min, max] range so the vocabulary can grow without a protocol bump. Report [3, 3].
Bridge API version: experimental_apiVersion: 1
The contract between your artifact's experimental_providerBridge export and the daemon's bootstrap. The bootstrap accepts version 1 only and refuses anything else by name. Because a plugin artifact and a daemon update independently, the deprecation window for a future bump is one of the open audit questions; that is also why the export name and experimental_defineProviderBridge keep their prefix for now.
Removal target: bb 0.42
The bb release in which every window listed above closes.

Provider members still experimental #

As of the first audit, with the open question in one line each. The full reasoning is in the audit document.

MemberOpen question
experimental_providerBridge export name; experimental_defineProviderBridge; experimental_apiVersionAn artifact-to-daemon contract read from every installed plugin; renaming needs a dual-name acceptance window plus a protocol bump, and that deprecation policy is undecided.
experimental_bridgeOptions, experimental_visibilityOne consumer (the ACP plugin); decide whether static options survive beside deriveProviderOptions, whether 64 KiB is the right cap, and whether "always" | "installed" is enough listing policy.
experimental_nativeSkillRoots, experimental_nativeCommandRoots, experimental_resolvesNativeRoots, experimental_nativeRootsHostContract, experimental_filterResolvedNativeRootsClosed on 2026-08-22: the absolute side is gone and every first-party plugin with host-only roots resolves them per host. Still open: whether the two-bucket shape (user, project) is the right vocabulary or a root should name its own base; no third-party agent has validated the relative-path and 32-root rules, the per-root options, the shape vocabulary, the 256-root resolver cap or the ten-second cache; whether commands and skills should be one list of typed roots; whether the flag should exist or the server should detect the method; whether the filter's per-root leniency belongs in the contract's output schema, and whether dropped[].reason is a contract or free text.
Vendor plugin roots: experimental_resolveClaudePluginRoots, experimental_resolveVendorPluginRootsWhether the walk should keep taking a layout name (claude, grok) or the two facts behind it once a third vendor layout appears; whether claudeDir belongs on the answer; whether a skills-only caller should be able to ask for one side; whether the repeated-path rule should be the contract's rather than each helper's; where a plugin's name should be validated as a name prefix.
The provider maintenance toolkit (fourteen experimental_ helpers on /provider-bridge, from experimental_resolveExecutablePath to experimental_clampPercent)The timeouts are fixed (5 s for which/--version, 15 s for npm); compareVersions is semver-shaped, not semver (build metadata and four-part versions read as 0.0.0); the npm helpers model one layout, so pnpm, volta and corepack shims read as external; downloadedInstallerCommand is POSIX only.
The presentation builders (twelve experimental_ members on /provider-bridge, from experimental_presentationTitle to experimental_toolPresentation)The wording is a product decision a third-party bridge inherits (whether labels should come from the host, localized, rather than be persisted from the bridge); the 160-character headline cap is a kit convention and the 280-character detail cap is the schema's; experimental_planStepsPresentation collapses the row by default.
experimental_readBoundedLinesThe default cap is the wire's 64 MiB, which a provider child may not deserve; onOverflow is required and there is no fail-closed mode.
experimental_scanPublicSdkOnlyThe allowlist is bb's (every published SDK subpath and vitest; another runner or schema library must be named in allow); imports are found by a regular expression, not a parser, so an import()/require() whose argument is not a string literal is reported as dynamic-specifier rather than read, and a relative path that leaves the package root is outside-package unless allow names it.
experimental_BridgeRecoveryErrorStabilizes with the bridge kit; confirm the five kinds cover third-party needs and whether retryable should be per kind.
experimental_buildBridgeToolCallContentStill accepts a legacy input shape; no image MIME or size policy at the server boundary.
Record mode: experimental_recordProviderChildIo, experimental_isProviderBridgeRecordingThe recording entry shape is a de-facto fixture format to freeze together with the readers; the per-thread scope is untested against a multiplexing bridge.
The ACP kit: experimental_acpProviderBridge, experimental_probeAcpAgent, experimental_acpAgentProbeSchema, experimental_acpLaunchSpecSchemaWhat the probe owes a caller (a ten-second timeout, the bridge's own client capabilities, a -32601 to anything the agent asks); whether AcpDialect is the right shape before the dialect registry becomes public; whether the bridge should be a factory rather than a module singleton; what a plugin is owed when the stored launch spec grows a field.
The testing kit (every experimental_ export of /provider-bridge/testing)The assembled-event lane went when the kit took over assembly; still open are the calibration normalizer's defaults, the replay child's dialect set (or a pluggable child), the two shipped programs beside the bundle, the textual workspace-path restoration, and whether the kit should pin a grammar version in its exports.
bb.experimental_aiServices.register, experimental_aiServicesHostContractOne consumer; whether the BB_INFERENCE / BB_TRANSCRIPTION strings stay the chooser; the 5 MB plugin-served transcription cap (8 MiB JSON per host RPC call); the reserved-id model; host choice.
bb.branding.experimental_icons (manifest) and the namespaced "<pluginId>/<name>" presentation glyphWhether the key becomes icons outright (the strict manifest schema makes the rename breaking, so the stabilization release must accept both for one release); whether bb.branding.icon should resolve a self-referencing namespaced glyph through the map instead of refusing it (the refusal is the smaller, reversible choice); whether the stricter icon validator should also apply to bb.branding.icon and path-shaped provider icons, which today keep the older compact-icon validator (applying it can fail existing plugins at load); whether a full-colour mode is wanted, since today every value is a string and every icon is a monochrome mask; whether toolUse approval presentations are checked at ingest too (today only timeline rows are; approvals are ephemeral and both clients fall back); and whether a row persisted with a namespaced glyph should ever be rewritten when the plugin renames or removes the icon (today rows are never rewritten and simply fall back).
app.experimental_useProvidersReads the primary-host roster only; whether icon and logoUrl fold into one field when ProviderInfo stabilizes.
app.slots.experimental_timelineRendererZero consumers; body-versus-whole-row, the tool-row payload (preview or full output), legacy rows without presentation, mobile parity.
The content-script mount context's experimental_setThreadRowStatusKept: its status store feeds the collapsed sidebar section rollup. Audit: whether to stabilize the member or fold the rollup into core.
app.slots.experimental_providerIconZero consumers; id squatting (nothing checks that the registering plugin owns the provider), bundle cost, whether the slot should exist at all.
Host plugin foundation: bb.hosts.experimental_client, experimental_defineHostEntry, experimental_createHostEntryHarness, the experimental_ context membersSignals and watches have no consumer; lifetime and limit numbers measured against one plugin; artifact-contract names read by the daemon need a dual-name window to rename.
experimental_ProviderModelPicker, experimental_PermissionModePickerThe atomic controlled-value contract, routing, catalog-failure policy.

What the first audit stabilized #

These carry no prefix and are promised: bb.providers.register and the declaration's target-state fields (strings, serviceTiers, reasoningLevels, extensionKinds, models, env, deriveProviderOptions, family, icon, composerActions, capabilities) and maintenance; ProviderInfo.maintenance; tool presentation on bb.agents.registerTool; the non-prefixed bridge kit (createBridgeIo, createBridgeLineHandler, runBridgeRequest, createPendingToolCallTracker, decodeToolCallResponsePayload, addTokenUsage, ZERO_TOKEN_USAGE, the envelope schemas and parsing helpers); the protocol constants and schemas; the pending-interaction slot; and, outside the provider surface, UrlLink / openUrl, fixedTabs and the Original delegation prop.