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 member works and is supported today; first-party plugins build on it.
- Its shape may still change, and a change does not wait for a major version. Alias it on import so a rename is a one-line fix:
import { experimental_runBridgeConformance as runBridgeConformance }. - Dropping the prefix is a deliberate stabilization step: the entry is audited, the name is changed project-wide, and the audit entry is deleted in the same change. Renamed fields are then rejected at registration with a message naming the new name, so a stale name fails loudly instead of being silently ignored.
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.
| Old | New | Who it protects |
|---|---|---|
experimental_UrlLink (app) | UrlLink | Plugin bundles compiled against an SDK before 0.4.16. The alias warns on its first render, then renders UrlLink. |
BbNavigate.experimental_openUrl | openUrl | Same bundles. Warns on its first call, then calls openUrl. |
experimental_Original prop on the thread-list, file-opener, source-code renderer and diff renderer | Original | Same 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 persisted | presentation on every item.open and item.close | Persisted 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 providerId | A 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.
- Eleven unprefixed value exports of
@get-bb/plugin-sdk/provider-bridge, each still re-exported from bb's domain package where it has core consumers:acpNativeReasoningSchema,acpPermissionCliSchema,acpReasoningCliSchema,extensionKindSchema,interactionRequestPayloadSchema,isExtensionKind,isUserQuestionPendingInteractionPayload,isUserQuestionPendingInteractionResolution,providerRecoveryKindValues,threadEventItemPresentationSchema,threadEventSearchModeSchema. - Four more unprefixed values and two types of
@get-bb/plugin-sdk/provider-bridgethat 0.4.15 published and the migration had dropped, restored in 0.4.16 as aliases so a bridge built against 0.4.15 rebuilds:hostDaemonAcpLaunchSpecSchemaandnormalizeHostDaemonAcpLaunchSpec(aliases of the ACP kit'sexperimental_acpLaunchSpecSchemaand its normalizer on/provider-bridge/acp, where a plugin that declares an ACP agent should read them),claudeTaskToolNameSchemaandclaudeTaskToolOutputSchema(the Claude Code task-tool vocabulary exactly as shipped; nothing in bb reads it and there is no replacement), and the typesHostDaemonAcpLaunchSpecandClaudeTaskToolOutput. Fifteen values and two types in all, unreferenced by any first-party plugin; an SDK test holds the facade to the list. AcpAgentProfileon/provider-bridge/acp, now a deprecated alias ofAcpLaunchSpec. The bridge reads the parsed launch spec directly; the profile it used to derive carried the same fields under other names, and nothing outside the bridge produced or consumed it.CONFORMANCE_ASSEMBLED_EVENT_METHODon/provider-bridge/testing: the kit assemblesthread/deltaitself and reads nothing under that method, so the constant names a lane that no longer exists.
Deleted and renamed members #
- The deleted member throws with the removal named on first read:
bb.agents.experimental_registerProvider(usebb.providers.register). The content-script mount context'sexperimental_setThreadRowStatusis kept, not deleted: it is listed under still experimental. - Renamed declaration, tool and navPanel fields (
experimental_strings,experimental_presentation,experimental_fixedTabs, and the like) are rejected at registration with a message naming the new field, from SDK 0.4.16 on. The new names arestrings,presentation,fixedTabs. - A native-root declaration with an
absoluteside is refused at registration by name: the side protocol 157 added for Pi is gone in protocol 163, and a host-absolute directory is always the resolver's answer. With it theExperimentalResolvedNativeRoottype left/host; a resolved root isExperimentalNativeRootsResolveOutput["skills"][number]. - Removed outright from a published subpath, with no alias and no throwing stub (an import fails to resolve), and this is the whole list between 0.4.15 and 0.4.16:
ProviderInfo.experimental_providerHealth,experimental_providerUsageandexperimental_providerInstallation(/app; properties of theProviderInforowsexperimental_useProvidersserves. A typed read no longer compiles and the served row has no such key; readmaintenance.health,maintenance.usageandmaintenance.installation. The server no longer serves the three booleans besidemaintenance, and every client ships with the server, so no reader is left behind),experimental_aiServiceKindSchemaandExperimentalAiServiceKind(/ai-services; the declaration'skindsare typed byPluginAiServiceKindon the root entry, and there is no schema for it),ExperimentalAiJsonValue(/ai-services; the JSON value vocabulary is theJsonValuetype on/provider-bridge),ExperimentalResolvedNativeRoot(/host; see the previous bullet), and from/provider-bridge/testingthe three kit internals no suite used:experimental_ConformanceClientandexperimental_checkItemOpensBeforeDelta(experimental_runBridgeConformancedrives the client and applies the opens-before-delta rule itself) andexperimental_diffCumulativeText(the cumulative-text differ is the assembler's own). Every other 0.4.15 name of every subpath is still exported, renamed as in the next bullet, or listed under scheduled removals. - The remaining 0.4.15 → 0.4.16 renames are mechanical. On
/provider-bridgethe sixteenexperimental_provider*Schemavalues and their sixteenExperimentalProvider*types dropped the prefix (experimental_providerHealthSchema→providerHealthSchema,ExperimentalProviderHealth→ProviderHealth, …), as did theBRIDGE_REQUEST_METHODS.experimentalProvider*keys; the method strings on the wire are unchanged. On the root entry the tool typePluginAgentToolExperimentalStatusLabelsisPluginAgentToolLabels, the type ofpresentation.label. A TS2305 on a 0.4.15 name resolves against these three lists. - On the wire, the v2 streaming and usage dialects (
message.delta/message.close,usage.turn/usage.exact), the turn-levelturn.plandelta and thethread/openWorknotification are gone; the runtime's assembler speaks grammar v3 only.
The version numbers, in plain words #
- SDK version:
@get-bb/plugin-sdk0.4.16 - The package you compile against. Its
bundled-types/*.d.tsfiles are the published contract; its/provider-bridge,/provider-bridge/acpand/provider-bridge/testingsubpaths are real code thatbb plugin buildinlines into your host artifact, and into your server bundle whenserver.tsimports them: only the bare@get-bb/plugin-sdkspecifier stays external there. The manifest'sengines.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 clientsProviderInfo.maintenance { health, usage, installation }. The one-release window that would have served the oldexperimental_provider*booleans besidemaintenanceto 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 legacysystem/permissionGrant/lifecycleandsystem/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 theabsoluteside from the declared native roots the server sends the daemon with its skill and command listings:nativeRoots.skillsandnativeRoots.commandsare{ user, project }, and what yourresolveNativeRootsanswers arrives innativeRoots.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/deltavocabulary, 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_providerBridgeexport 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 andexperimental_defineProviderBridgekeep 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.
| Member | Open question |
|---|---|
experimental_providerBridge export name; experimental_defineProviderBridge; experimental_apiVersion | An 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_visibility | One 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_filterResolvedNativeRoots | Closed 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_resolveVendorPluginRoots | Whether 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_readBoundedLines | The 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_scanPublicSdkOnly | The 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_BridgeRecoveryError | Stabilizes with the bridge kit; confirm the five kinds cover third-party needs and whether retryable should be per kind. |
experimental_buildBridgeToolCallContent | Still accepts a legacy input shape; no image MIME or size policy at the server boundary. |
Record mode: experimental_recordProviderChildIo, experimental_isProviderBridgeRecording | The 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_acpLaunchSpecSchema | What 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_aiServicesHostContract | One 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 glyph | Whether 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_useProviders | Reads the primary-host roster only; whether icon and logoUrl fold into one field when ProviderInfo stabilizes. |
app.slots.experimental_timelineRenderer | Zero 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_setThreadRowStatus | Kept: its status store feeds the collapsed sidebar section rollup. Audit: whether to stabilize the member or fold the rollup into core. |
app.slots.experimental_providerIcon | Zero 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 members | Signals 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_PermissionModePicker | The 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.