bb · provider plugins

Concepts

One section per idea. Each states the rule, the shape on the wire, and who enforces it.

The bridge protocol #

The Provider Bridge Protocol is the one JSON-RPC contract between the agent runtime (inside the host daemon) and every bridge process. The rule of thumb: the bridge knows the dialect, the runtime knows the timeline.

Transport

Versioning

initialize exchanges { protocolVersion, capabilities } in both directions. The protocol version is 2; the runtime rejects any other with a legible startup error. The version bumps only for breaking changes. Everything additive rides capability tolerance: unknown methods answer -32601, unknown notifications are ignored, unknown capability fields pass through. Bridges version with their plugin, not with the daemon.

The thread/delta grammar is versioned separately. The runtime states its assembler's range in the initialize params and the bridge states its own in the result; both use the highest common version. The assembler speaks v3 only, so every bridge reports grammarVersions: [3, 3]; a bridge that omits the field reads as [2, 2] and is refused at spawn.

Handshake capabilities

These are session-behavior facts the bridge reports because it implements them. Each defaults on parse so an older bridge that omits one is read as not having it.

CapabilityDefaultMeaning
grammarVersions[2, 2]Inclusive [min, max] of the delta grammar the bridge speaks. Report [3, 3].
sessionRestorefalseA released session can be re-attached later from its persisted providerThreadId. Per-session sessionRestorable on thread-identity results refines it.
threadArchivefalseThe bridge mirrors bb archive state into the provider. When false the runtime never sends thread/archive or thread/unarchive.
threadRenamefalseThe bridge pushes bb thread titles to the provider (thread/name/set).
threadGoalClearfalseThe bridge supports thread/goal/clear.
fork"none""none" | "tip" | "checkpoint". May only narrow the declaration's capabilities.fork, never widen it.
approvalEnforcedBy"runtime""runtime": forward every approval and let the runtime apply the thread policy. "provider": the bridge enforces policy first, so every forwarded request already needs the user.
steerMode"queue""inject" feeds a turn/steer into the running model loop; "queue" holds it for the next prompt boundary. Nothing in the runtime, server or clients reads it yet: turn/steer is sent either way, and a steer whose turn is gone is dropped on your staleTurn hint or NO_ACTIVE_TURN error whatever the mode. Declare the truth anyway.
skills.configurefalseThe bridge handles skills/configure. When false the runtime never sends it and the thread runs without injected skills.

One process per provider artifact, lanes per thread

The daemon spawns one bridge process per provider artifact, and that process serves every thread of the provider(s) the artifact owns. Per-thread state is the bridge's own business: most bridges keep a map from bb threadId to a session object that holds the provider's session handle and any child process. The runtime never scopes processes per thread and never matches error text.

The request vocabulary

Runtime → bridge: initialize, model/list, thread/start, thread/resume, thread/fork, thread/stop, thread/discard, thread/name/set, thread/archive, thread/unarchive, thread/goal/clear, turn/start, turn/steer, skills/configure, and the sessionless provider/health, provider/usage, provider/installation/status, provider/installation/run. Bridge → runtime notifications: thread/delta, thread/identity, session/replaced, provider/raw, provider/recovery, error. Bridge → runtime requests: item/tool/call, interaction/request. Params and results are in the reference.

Execution options ride every session and turn command and carry no provider-named field: { model?, serviceTier?, reasoningLevel?, promptMode?, instructions?, envVars?, providerOptions? } plus the runtime permission policy. The runtime never diffs them; the bridge reconciles internally and reports a rebuild through session/replaced.

Sessions and the stop rule

Ordering

Producers guarantee that thread/identity precedes any delta for the session, that deltas within a turn are in presentation order, and that settlement deltas precede the session/replaced that made them necessary. Consumers must not assume that a request's response arrives before the notifications it caused (turn/started may precede the turn/start response), and must assume nothing about provider/raw, which is droppable at any pressure point.

Item grammar v3 #

Everything timeline-bound rides one notification, thread/delta { threadId, deltas }. A delta is a parsed semantic unit, never a raw provider event and never a finished event. The runtime's delta assembler owns every timeline invariant: it mints turn and item ids, pairs opens with closes, accumulates streamed text, throttles progress, and constructs the canonical ThreadEvents. Deltas carry provider-native join keys (key.providerItemId, key.channel, key.parentRef, providerTurnId) and the assembler holds the maps in both directions, so a bridge does zero id translation.

Core item kinds

The kinds core acts on: message, reasoning, command, fileChange, fileRead, search, webSearch, webFetch, imageView, delegation, planSteps, compaction, tool. On the wire an item.open/item.close carries one of these shapes:

Shape typeFieldsUse it for
commandcommand, cwd, aggregatedOutput?, exitCode?, durationMs?A shell command. Stream output with item.outputDelta { channel: "command" } or command.outputSnapshot.
fileChangechanges: [{ path, kind: add|update|delete, movePath?, diff?, oldText?, newText? }]Edits. The bridge states the change kind; the assembler builds a unified diff from old/new text when no diff is given.
fileReadpath, cmd?A file the agent read (a structured tool, or a cat/sed -n command the bridge classified).
searchmode: content|path|list, query, path?, cmd?Grep, glob and directory listing as one shape.
delegationchildRef, label, background, summary?Delegated work. The child's deltas link back through parentRef; background: true outlives its turn.
planStepssteps: [{ step, status }], explanation?A plan snapshot; each supersedes the previous one.
tooltool, server?, args?, result?, error?, durationMs?Any tool with no core kind. Stamp server: "bb" and the definition's presentation on calls to bb-injected tools.
agentMessage, plantextAssistant text; streamed with item.textDelta.
reasoningsummary[], content[]Thinking; channels reasoningSummary and reasoningText.
webSearch, webFetch, imageViewqueries[] / url, prompt?, pattern / pathWeb and image tools.
compactionA context compaction the provider performed.
backgroundTaskfamilyId, taskType, description, status, taskStatus, skipTranscript, …Provider background work, re-embedded as a snapshot per event.
extensionkind: "<pluginId>/<name>", payloadYour own kind. Presentation is required on its open and close.

The delta kinds

Rules the assembler holds you to

  1. Item and stream deltas never open a turn. Only turn.open, a claiming turn.boundary and accepted-input settlement do. A turn-scoped delta with no turn to attach to surfaces its noTurnFallback as a thread-scoped provider/unhandled, or is dropped.
  2. Every item's first event is item/started. item.close always carries the full terminal shape; the assembler settles uniformly (paired close, reclassifying dual-settle, or bare close-without-open). Repeated closes for a settled provider-identified key are deduped.
  3. Ids are unique for the life of a thread, including resumes, because the assembler mints them and session.reset starts a fresh provider id space.
  4. A prompt the provider handles without doing work still produces a turn.open + turn.boundary pair, or an input.accepted + claiming boundary; zero-delta acceptance is the hung-thread class.
  5. Completion follows content: if the provider emits a completion before the output it refers to, hold the close and flush in order. Output may be delayed, never lost.
  6. Turn scoping is vouched: only turn keys the bridge itself opened may scope a delta. A provider's internal turn labels must never be forwarded as scoping.

The host also applies the grammar live at its event intake (ThreadEventGrammar): a streaming event for an item nobody opened, a second settlement, a duplicate turn start or completion, or a completion for a turn that never started is dropped with a warning naming the rule.

Extension kinds

A kind is <pluginId>/<name> in lowercase letters, digits and -. Declare each with an item schema, a state schema, or both. Only the namespace is validated on the wire; the server validates the payload against your schema at ingest (64 KiB cap) and persists a provider/unhandled in its place on a miss, never dropping it and never storing it unvalidated. Before the schema is even looked up, the kind is held to the emitter rule a namespaced presentation glyph is held to: the thread's provider names the plugin that wrote the row (its live registration's pluginId), and that must be the plugin the kind names, for an item and for a state alike. A kind another plugin owns persists as the same provider/unhandled, in the same batch slot, with a reason naming the kind, the owning plugin and the plugin behind the thread's provider; a thread whose provider has no live registration has nothing to vouch for its rows, so every extension kind on it is refused. Clients pick a kind's renderer by the kind alone, which is what the rule protects. Declare your own kinds under your own id and emit only those, as every first-party bridge and the echo example do. Codex's long-running goal (provider-codex/goal, a thread state whose cleared value is null) and the macOS permission profile beside an approval (provider-codex/macos-permission, an item) are extension kinds; nothing about them lives in core.

Presentation #

Presentation is how a client renders an item it has no special code for. It is attached by the bridge at item.open, restated on item.close (the close's value wins; the open's survives when the close carries none), and persisted with the item, so a row renders the same way after the plugin is uninstalled or upgraded, and mobile renders every kind without plugin code.

presentation: {
  label: { pending: "Reading file", completed: "Read file" },  // present / past tense
  icon: { glyph: "FileText" },        // a host glyph, or your own "<pluginId>/<name>"
  title?: "src/index.ts",             // row headline beside the label
  detail?: "…",                       // short Markdown, at most 280 characters
  suppress?: true,                    // low-value rows clients collapse by default
  tint?: { light: "#1d4ed8", dark: "#93c5fd" },   // accent per theme
}

How rows render #

The server folds every item, core or extension, into timeline rows. Core kinds render with bb's renderers on every client. Extension kinds and generic tool items render from their presentation: label, glyph, tint, headline, detail. That is the whole story on mobile, which loads no plugin JavaScript by design.

On the web, a provider plugin's frontend bundle may upgrade the body of the rows it owns with app.slots.experimental_timelineRenderer({ kind, component }), where kind is one of its extension kinds or "tool" for the generic tool items of the providers it registered. The component receives { row, payload, presentation, thread, Original }; the row header stays host-rendered; render <Original /> to keep the declarative base beside your content. With no renderer registered the base renders, so a row never goes blank, and a crash is contained to that row. Provider bundles load lazily and never enter the boot payload: everything a provider plugin's bundle registers (a timeline renderer, a settings section, a nav panel, a palette action, a provider icon, a pending-interaction form) arrives with the bundle, which loads on the first thread of one of its providers, when one of its forms is asked for (a child thread's plugin-defined request surfacing on a parent thread of another provider, say), or when its own panel route is opened. Boot-time UI belongs in a separate, non-provider plugin.

The provider directory is available to plugin frontends through experimental_useProviders() and to backends through bb.sdk.providers.list(); never re-vendor provider names or icons.

Interactions #

A bridge raises an interaction with the interaction/request request: { providerThreadId, threadId?, turnId, payload, providerNativeIds? }. The response is the canonical resolution. Provider-specific request and response dialects never cross the boundary; the bridge maps in both directions. One event type, system/interaction/lifecycle, records every status change of every interaction with the ask and the answer paired by kind; the server fabricates no placeholder items.

Approvals (closed)

An approval payload has kind: "approval" and a subject: command, file_change, permission_grant, or tool_use { itemId, tool, presentation }. A tool_use is any tool call with no core kind (an MCP tool, a provider-native tool); its presentation is the whole description of the ask. Permission modes auto-decide approvals: accept-edits approves file changes; auto approves commands, file changes and tool uses; full approves all. A bridge whose handshake says approvalEnforcedBy: "provider" applies the policy itself and forwards only what still needs a user. The resolution is { decision: "allow_once" | "allow_for_session", grantedPermissions } or { decision: "deny" }. Both sides of the boundary also agree on auto-deny: shouldAutoDenyInteractiveRequest({ permissionEscalation }) in the kit is the one statement of it.

Requests (open)

A bridge that keeps the payload it raised parses the wire resolution together with it (providerInteractionOutcomeSchema), so its response encoder narrows on the payload kind and a mismatched pair is a wire error, never a throw inside the encoder.

Recovery hints #

A hint says what went wrong in the provider's own terms and what the runtime may do about it: { kind, message, retryable }. The runtime keys on kind alone and never consults the provider id or matches text.

kindRuntime action
sessionArchivedthread/unarchive the session, then retry the rejected request once (retryable: true).
authRequiredReject the request with a typed auth_required error and forward the hint so the host can re-check provider health.
restartRecommendedStop the bridge process the thread runs on and resume the thread on a fresh one: right away when idle, otherwise before its next turn.
staleTurnDrop the steer; the turn it targeted is gone.
rateLimitedWith retryable: true on a rejected request: retry on a short bounded ladder and surface the last failure. With retryable: false: forward only; the runtime never re-runs a user's turn on its own.

One payload, two carriers. Rejecting a request? Put the hint in error.data.recovery: throw experimental_BridgeRecoveryError({ code, message, recovery }) from a handler run through runBridgeRequest, or call sendError(id, code, message, { recovery }); the JSON-RPC id is the correlation. No request to reject (a terminal 401 mid-turn)? Send the provider/recovery notification with the hint and the threadId. Never both for one event. A timeout or a bridge exit has no response and therefore no hint. A bridge that can heal itself (rebuild its own child before the next turn) does so and still sends the hint so the failure is typed.

Every rejection is acted on. The runtime's per-kind action runs on every rejection of a request, not only the first: a rate-limit ladder rung rejected with another kind is handled by that kind (a staleTurn rung reports the steer stale, a retryable sessionArchived rung unarchives, a restartRecommended rung schedules the restart), and so is the retry after an unarchive, with one bound: a second sessionArchived on that retry is reported, never unarchived again, so the protocol's one retry stays one retry. A process speaks only for the threads it hosts. A provider/recovery whose threadId names a thread the emitting process does not host (another provider's, or one that moved to a replacement process) is dropped with a stderr line, the same rule the event path applies. When a restartRecommended restart re-resumes the process's other threads, it skips a sibling that is live again or on its way (an operation in flight, a turn pending or active), so the daemon's own resume of that sibling is never followed by a second thread/resume.

Models, reasoning levels, service tiers, permission modes #

Native roots and injected skills #

Injected skills: skills/configure

bb's own skills reach a session through one request shape for every provider: skills/configure { roots: [{ id, path, skills: [{ name, description }] }] }, where path is an absolute directory holding one subdirectory per listed skill (<path>/<name>/SKILL.md). The bridge maps it to the agent's layout (an extra skills root, a local plugin, a prompt listing). The runtime sends it only to a bridge whose handshake declares skills: { configure: true }, once per process, before the first thread command. Conformance rule skills/configure-declared pins both directions.

The agent's own skills and commands

Where an agent keeps its own skills and slash commands is the plugin's fact, declared so bb can list them beside its own and offer them in the composer; core never guesses a provider's layout.

Four first-party patterns, all on the same two surfaces: Codex declares .codex/skills and .agents/skills and resolves $CODEX_HOME/skills plus every enabled codex plugin per host, feeding the plugins it reads from config.toml and the cache into experimental_resolveVendorPluginRoots; Claude Code declares .claude/skills (with the ancestor walk and the plugin marker) and .claude/commands, and answers both sides from experimental_resolveClaudePluginRoots beside the claudeDir directories; the ACP plugin's omp and grok resolvers take the readers' skills side; Pi declares its documented defaults (.pi/agent/skills, .agents/skills, .pi/skills) and resolves, per host, the directories its settings.json names and an agent directory PI_CODING_AGENT_DIR moved, every one as a user root. The absolute side Pi once re-registered across hosts is gone.

Maintenance: health, usage, installation #

These are sessionless requests (no thread, no session) the server sends through the bridge only when the declaration's maintenance turns them on. Every one takes { providerId, cwd?, providerOptions? } so one bridge can serve several provider ids, and every result may answer { supported: false } for one id.

provider/health
Cheap, host-local readiness, never a network check: { supported: true, health: { status: ready | not_installed | unauthenticated | expired | unsupported_version | unknown, statusMessage, accountEmail, planLabel, installedVersion, minimumSupportedVersion, canInstall, canUpdate, loginCommand } }. Drives the picker's availability and experimental_visibility: "installed".
provider/usage
Subscription usage windows: { status: "ok", accountEmail, planLabel, windows: [{ label, usedPercent, resetsAt, cost? }] }, or not_installed, unauthenticated, expired, error { message }. A provider that declares usage: false is omitted from the usage settings.
provider/installation/status, provider/installation/run
A deliberately split execution boundary. The bridge owns discovery, version and source comparison, and the install/update decision (for a user-installed CLI the kit's maintenance toolkit does the host-local probing, the version compare and the install-command and verification plumbing; your policy, the minimum version and the login command, stays with you; see the reference); status returns { executableName, executablePath, installed, installSource: notInstalled | npmGlobal | external, currentVersion, latestVersion, minimumSupportedVersion, npmPackageName, npmGlobalPackageVersion, installAction, needsUpdate, versionUnsupported } with a display-only command. A status request may carry a typed requirement such as "thread_rewind"; the bridge owns the minimum provider version that operation needs and reports it through the ordinary status. When the user acts, run { action: install | update } rechecks and returns { available: false, message } or { available: true, command: { command, args, displayCommand }, verification: { kind: installed | version_changed | version_at_least } }. The host daemon, not the bridge, chooses the environment and cwd, serializes installations, supervises the process, streams output and re-asks the bridge for fresh status to verify. Raw arguments never reach a product client.

AI services #

bb's helper inference (thread titles, commit messages: a prompt and a JSON Schema in, a structured value out) and voice transcription are plugin-served. Register in server.ts and implement the contract in the host entry:

// server.ts
bb.experimental_aiServices.register({ id: "acme", displayName: "Acme AI", kinds: ["inference", "voice"] });

// host.ts
import { experimental_aiServicesHostContract } from "@get-bb/plugin-sdk/ai-services";
import { experimental_defineHostEntry } from "@get-bb/plugin-sdk/host";
export default experimental_defineHostEntry({
  contract: experimental_aiServicesHostContract,
  handlers: {
    "ai.inference.complete": async (input) => {
      // input: { serviceId, model, reasoningEffort: "none", prompt, outputSchema, timeoutMs }
      return { ok: true, model: input.model, value };
    },
    "ai.voice.transcribe": async (input) => {
      // input: { serviceId, model, audioBase64, mimeType, filename, prompt, timeoutMs }
      return { ok: true, model: input.model, text };
    },
  },
});

Strings, icons, branding #

Plugin icons #

Host glyphs cover most rows. When your provider has a row no host glyph fits, ship the SVG yourself and name it. experimental

The manifest map

"bb": {
  "branding": {
    "icon": "Zap",
    "experimental_icons": { "receipt": "./icons/receipt.svg" }
  }
}

Naming an icon

Reference an entry by its namespaced glyph, "<pluginId>/<name>", the same split as an extension kind. Host glyph names are PascalCase without a /, so the two vocabularies cannot collide. The form is accepted in three places:

It is refused in one place: bb.branding.icon itself. That field is the plugin's own mark, and a self-reference would only restate a path already in the map while no reader of bb.branding.icon resolves the map, so the manifest schema rejects the namespaced form rather than carrying a name that resolves nowhere.

What ingest does

The server checks every namespaced glyph on every event that carries a full item (item/started, item/completed, and the thread-scoped item/delegation/progress, item/delegation/completed, item/backgroundTask/progress and item/backgroundTask/completed snapshots, the only place a background delegation's or task's final glyph appears, because the assembler stamps the close's presentation on them) before the event is persisted, because a client resolves the name against the plugin inventory, and a glyph that names another plugin or a name you never declared would persist a row whose icon can never be found. The rule is the one at registration: the plugin id must be the emitting plugin's and the name must be in its map. The emitting plugin is the thread's provider plugin, with one exception: a server: "bb" tool row carries the presentation the server resolved from the plugin that registered the tool, so its glyph is checked against that plugin (it passes when the tool is still registered by the plugin the glyph names, with exactly that icon), which is how a tool's own icon survives on any provider's thread.

A glyph that fails gets the same visible fate as an extension payload that fails its schema: the event is replaced, in its batch slot, by a provider/unhandled carrying rawType: "presentation/icon:<itemType>" and { itemId, itemType, glyph, reason } in the raw event, scope and parent kept, so the row is diagnosable where the original would have sat. The reason names the glyph and the plugin: presentation.icon "other-plugin/receipt" is not an icon declared by plugin "echo-provider", or, for a thread whose provider has no live registration, presentation.icon "…" names a plugin icon, but the thread's provider has no live registration to check it against. Host glyphs (no /) are never touched at ingest: whether a client can draw "Zap" is the client's call, and the per-kind fallback covers a name it cannot. The conformance rule presentation/icon-namespaced-declared inspects the same six event types.

How clients resolve it

The installed-plugin inventory (GET /api/v1/plugins, bb.sdk.plugins.list()) carries each plugin's icons: declared name → hashed SVG URL. A row persists the name and follows the plugin's current map at render time:

  1. A glyph of the form "<pluginId>/<name>" is looked up in the inventory. Found: the SVG draws. (A SKILL.md read keeps its Zap over anything the bridge named, as it does for host glyphs.)
  2. Not found, because the plugin is uninstalled or the name is no longer declared: the row's per-kind fallback glyph draws (Puzzle for an extension item, Terminal for a tool, and so on). A host glyph the client's registry knows draws itself; one it does not know falls back the same way.

This is the accepted trade-off: rows are never rewritten when a plugin renames or removes an icon, and a persisted row never depends on a file that may have moved. A disabled plugin's icons still resolve (they ride the plugin's identity, like its compact branding icon); they stop resolving only once the plugin is uninstalled. The resolution happens on the client, before any image is requested, on purpose: a mask whose URL 404s would render nothing, not the fallback.

Ship monochrome shapes: every icon is a tinted mask, and SVG colours are ignored.

The SVG validator

The bytes are served from your installed plugin directory under a strict policy (Content-Security-Policy: default-src 'none'; style-src 'unsafe-inline', X-Content-Type-Options: nosniff), and a monochrome mark never needs scripts, external references, style elements or CSS escapes, so the validator refuses all of them rather than rewriting anything: the served bytes are hashed, and the build and the load must agree on what the plugin ships. It rejects, over and above the rules bb.branding.icon already meets (valid UTF-8 SVG XML, an <svg> root, no doctype, no processing instructions):

A gradient, a clip path or a marker defined in the same file and referenced as url(#id) is fine. The echo example's icon is the whole pattern: a 24×24 viewBox, fill="none", stroke="currentColor", a few paths.

bb.branding.icon and a path-shaped provider icon keep the older, looser compact-icon validator; whether the stricter one should apply to them too is an open audit question (Compatibility).

Process topology and lifecycle #

  1. Build. bb plugin build (or install/reload) bundles bb.host into a self-contained dist/host.js; only Node builtins stay external, and any private @bb/* import is rejected. The server records the artifact by content digest.
  2. Deliver. A thread command for your provider carries bridgeLaunch { pluginId, source: { kind: "artifact", digest, byteLength }, envPassthrough } to the daemon. The daemon downloads the bytes over an internal route, verifies the digest before caching, and keeps one cache per plugin. Trust is installation trust, identical to every other plugin surface.
  3. Spawn. The runtime spawns one process per provider artifact through the bridge bootstrap (argv: <bridgeModulePath> <pluginId> <pluginDataDir>). The bootstrap imports the artifact, finds experimental_providerBridge, refuses anything but experimental_apiVersion: 1 by name, creates the process's tempDir, calls start({ pluginId, dataDir, tempDir }), wires onSigterm/onSigint, and feeds stdin lines to handleLine until stdin closes (onClose). dataDir is persistent per plugin on that daemon and survives restarts and plugin updates; tempDir is removed when the process exits. Changing experimental_bridgeOptions changes the process identity, so the next command uses a fresh process.
  4. Environment. The bridge's environment is built by the runtime from an allowlist: inherited BB_* variables are stripped except those the declaration's env.passthrough names. Build your children's environments the same way (sanitizeInheritedChildProcessEnv, withoutBridgeRuntimeEnv, buildShellEnvOverrides) and never leak your own inherited env downward.
  5. Children. Bridges may spawn provider processes underneath themselves (the Codex bridge supervises per-thread app-server children; the ACP bridge launches the agent command per session); process topology is bridge-internal and invisible to the runtime. Own the exit-race lessons: finalize on close not exit with a bounded grace, verify currency in stream callbacks, and never let a descendant holding an inherited pipe inject into a fresh session. Read a JSON-lines child with experimental_readBoundedLines, the LF-only bounded reader the daemon reads your own stdout with, rather than readline.
  6. Stop and restart. thread/stop releases per-thread resources (see the stop rule above). A restartRecommended hint makes the runtime stop the process and resume the thread on a fresh one. The runtime backstops an accepted turn that never starts with a visible watchdog event, sends a best-effort thread/stop { release } for a construction that timed out, and sweeps a bridge's process group when the bridge dies unexpectedly.
  7. The host RPC worker is the artifact's other consumer: the daemon starts it lazily for the first host RPC call, stops it after five idle minutes with no active call, watch or retained lease, and restarts it on the next call. It has its own lifecycle, its own process, and the same dataDir layout.

Server decides, plugin decides #

The bb server (product policy)Your plugin (the provider's truth)
Which permission mode a thread runs in, clamped to the host's ceiling; auto-deciding approvals by modeWhich permission modes the agent can run in at all; how the mode maps onto the agent's native settings
The instructions, tool list and injected skill roots a session getsHow instructions, tools and skill roots are handed to the agent; whether skills/configure is supported
Defaults: the default model when none is chosen, picker order, the default providerThe live model catalog (model/list), the cold-cache fallback, reasoning ladders, service tiers, catalog scope
Calling deriveProviderOptions on every command and forwarding the result untouchedWhat goes in providerOptions (memory toggles, subagent switches, a launch spec) and what the bridge does with it
Validating extension payloads against declared schemas and holding each kind to its owning plugin's threads; persisting; projecting rows; the interaction lifecycle event (the server is its only author; a daemon-posted one is dropped)The extension kinds, their schemas, and the presentation of every row
Minting every turn and item id (in the runtime's assembler); the turn and item lifecycle; grammar enforcement at intakeThe join keys that let the assembler correlate provider-native ids; when a turn opens and settles
Acting on a recovery kind: unarchive and retry, typed auth error, bridge restart, drop the steer, bounded retry ladderDiagnosing the failure in the provider's own terms and choosing the kind; healing its own child when it can
Polling maintenance through the bridge, serializing and supervising an installation run, verifying the outcomeWhether each maintenance request exists, what health means for this agent, which command installs or updates it
Routing BB_INFERENCE / BB_TRANSCRIPTION to a registered service and applying retry and fallback policyServing the inference or transcription call, and reporting failures with a typed code
Building, digesting and serving the host artifact; the daemon verifying and running itEverything inside the bridge process, including any child processes and their environments