Skip to content

Add the provider plugin API target-state doc - #2119

Closed
SawyerHood wants to merge 1 commit into
mainfrom
bb/provider-plugin-api-doc
Closed

SawyerHood wants to merge 1 commit into
mainfrom
bb/provider-plugin-api-doc

Conversation

@SawyerHood

Copy link
Copy Markdown
Collaborator

What was wrong

The provider plugin surface is half-migrated and non-uniform: providers own their bridge translation but not timeline projection or tool rendering, capability truth is spread across four shapes, and core still branches on specific provider ids in ~178 places. There was no single document describing the intended uniform end state, so each cleanup re-litigated the target.

What changed

Adds docs/provider-plugin-api.md, the agreed target state (settled across a 42-decision design session): bb.providers.register with one capability projection, a bridge that reports session facts at handshake, a small core item vocabulary plus plugin-declared extension kinds, presentation attached by the bridge and persisted with each item, one delegation kind, split closed-approvals / open-requests interactions, server projection into one TimelineRow shape, and lazy plugin web renderers over a declarative base that mobile also renders. Documentation only; no code changes.

The doc's code blocks reference target types that do not exist yet (bb.providers.register, extension kinds). The guard that compiles the code blocks against the real types (G10) lands with the contract PR that introduces those types.

How you verified

Docs-only change; no tests apply. Rendered and proofread. Full design, workstream plan, verification gates, performance budgets, guardrails, and the regression-confidence program are published at https://get-bb.github.io/reports/design/provider-plugin-api.html.

Fixes #

AGENT GENERATED: by Claude Opus 4.8

Describes the uniform end state of the provider plugin surface: zero
first-party privilege, a small core vocabulary plus plugin-declared
extension kinds, presentation attached by the bridge, one delegation
kind, split approvals/requests, and lazy plugin renderers over a
declarative base. No behavior change; documentation only. The code
blocks are compiled against the real types by the guard that lands with
the contract PR.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@SawyerHood

Copy link
Copy Markdown
Collaborator Author

Superseded by #2124, which adds an identical docs/provider-plugin-api.md together with the doc-type-sync guard (provider-plugin-doc.test.ts) that compiles the doc's code blocks against the real v3 contract types. The doc belongs with the types it documents, so it lands there. Closing to avoid a merge conflict on the same file.

@SawyerHood SawyerHood closed this Aug 21, 2026
SawyerHood added a commit that referenced this pull request Aug 21, 2026
…ync guardrail

- PluginProviderDeclaration gains experimental_strings,
  experimental_serviceTiers, experimental_reasoningLevels and
  experimental_extensionKinds; validatePluginProviderDeclaration checks,
  freezes and carries them (WS2a projects them). Audit entry added to
  docs/api_to_audit.md.
- @get-bb/plugin-sdk/provider-bridge exports the v3 shapes, presentation,
  handshake and recovery schemas and types.
- docs/provider-plugin-api.md lands (byte-identical to PR #2119) with
  guardrail G10: every ts block is parsed and each field mapped onto the
  real contract or an explicit WS-owned gap; gaps that land fail the test.

Co-Authored-By: Claude <noreply@anthropic.com>
SawyerHood added a commit that referenced this pull request Aug 21, 2026
…, interaction split (#2124)

## What was wrong

The provider-plugin migration
([docs/provider-plugin-api.md](#2119),
design thread `thr_qs25ef3tq2`) has no shared vocabulary yet. Every
later workstream — the generic assembler (WS1a), the per-bridge
migrations (WS1b), the registry and strings (WS2a/2b), projection and
renderers (WS3), runtime recovery (WS4), the interaction split (WS5) —
needs the same types: the v3 delta grammar, `presentation`, extension
kinds, the new core item kinds, the handshake range, typed recovery, and
the approval/request split. Without one contract PR they each invent
their own and the stacks conflict.

## What changed

Types and schemas, plus the two narrow runtime pieces review asked for
(presentation carry-through, grammar-range gate). Everything is ADDITIVE
and lives beside what exists (A1 additive-then-delete: v3 is accepted
next to v2). No v3 shape is assembled yet; no existing path is deleted.

**Delta grammar v3** —
`packages/provider-bridge-protocol/src/thread-delta.ts`
- New item shapes on `deltaItemShapeSchema`: `fileRead { path, cmd? }`,
`search { mode: content|path|list, query, path?, cmd? }`, `delegation {
childRef, label, background, summary? }`, `planSteps { steps,
explanation? }`, and `extension { kind: "<pluginId>/<name>", payload }`
(its lifecycle delta must carry `presentation`). Each is documented with
the corpus motivation (Claude `Read` 7,568 calls → `fileRead`; codex
`spawnAgent`/`wait` + Claude `Agent` + `local_agent` background tasks →
`delegation`; codex `update_plan` in 295 threads + Claude
`TaskCreate`/`TaskUpdate` → `planSteps`).
- Optional `presentation` on `item.open` and `item.close`
(`deltaPresentationSchema` = the domain's
`threadEventItemPresentationSchema`: `label {pending, completed}`, `icon
{glyph}` (host glyphs only — a plugin-relative asset path cannot outlive
the plugin; a durable content-addressed form is WS3's, recorded as a G10
gap), `title?`, `detail? (≤280)`, `suppress?`, `tint? {light, dark}`).
Presentation lives in ONE place — the lifecycle delta — and is REQUIRED
there when the shape is `extension`. The `tool` shape's doc names it as
the escape hatch for generic tools. The assembler persists it: the
open's value on `item/started`; on settlement the close's value wins and
the open's survives when the close carries none (close-echo). Items that
never carried one gain no key, so v2 streams are byte-identical.
- `item.progress.snapshot` widened to `backgroundTask | delegation`.
- New thread-scoped `extension.state { extensionKind, payload }` delta
kind. Only the namespace shape is validated; the payload is opaque with
a `TODO(WS1a)` pointing at server ingest validation.
- `provider/recovery` is a bridge **notification** (`notifications.ts`),
not a delta: it is a runtime signal the way `session/replaced` and
`provider/raw` are, never persisted. `{ threadId?, kind:
sessionArchived|authRequired|restartRecommended|staleTurn|rateLimited,
message, retryable }`. No consumer yet (WS4).

**Handshake** — `handshake.ts`: `grammarVersions: [min, max]` in BOTH
directions — the runtime states the range its assembler speaks in the
`initialize` params (`ASSEMBLER_GRAMMAR_VERSIONS`, `[2, 2]` until WS1a
implements the v3 shapes), the bridge states its own in the result
(default `[2, 2]`: a silent side speaks exactly the version it
negotiated), `negotiateGrammarVersion` picks the highest common version,
and a disjoint range fails startup with the same legible error a wrong
`protocolVersion` does. `steerMode: "inject" | "queue"` (default
`"queue"`: the conservative reading, consistent with this handshake's
"absence is a definite no"; nothing branches on it yet, and every
first-party bridge now declares its real mode — `inject` for
pi/claude/codex, `queue` for ACP v1's cancel-then-prompt). Nothing
removed.

**Domain** — `packages/domain/src/provider-event.ts`,
`item-presentation.ts` (new), `provider-extension-kind.ts` (new)
- Canonical items `fileRead`, `search`, `delegation`, `planSteps`,
`extension`; optional `presentation` on every existing provider-produced
variant (`userMessage` excluded: bb authors it).
- `item/delegation/progress` and `item/delegation/completed` events,
thread-scoped exactly as `item/backgroundTask/*` (rationale recorded in
`thread-event-scope.ts`). Wired through the exhaustive switches:
`thread-event-scope.ts`, `packages/db/src/stored-event-item-fields.ts`,
`apps/server/src/internal/events.ts`, plus
`packages/thread-view/src/event-decode.ts` and the grammar checker,
which were also exhaustive.
- `CORE_ITEM_KINDS` + `CoreItemKind` with a type-level exhaustiveness
check against `threadEventItemSchema` (G4).
- `provider-types.ts`: optional `strings`,
`serviceTiers`/`reasoningLevels` (`{id,label,description?}[]`),
`extensionKinds` on `providerInfoSchema`; `ProviderRecoveryKind`.
`ProviderInfo` construction is unchanged.
- `pending-interactions.ts`: `tool_use { itemId, tool, presentation }`
approval subject added to `pendingInteractionApprovalSubjectSchema`; new
`interactionRequestPayloadSchema = user_question | plan_review |
"<pluginId>/<name>"` beside the untouched
`pendingInteractionPayloadSchema`. No producers rewired.

**Plugin SDK** — `PluginProviderDeclaration` gains
`experimental_strings`, `experimental_serviceTiers`,
`experimental_reasoningLevels`, `experimental_extensionKinds`;
`validatePluginProviderDeclaration` validates, freezes and carries them
(not silently dropped; not projected — WS2a). Audit entry in
`docs/api_to_audit.md`. `@get-bb/plugin-sdk/provider-bridge` exports the
v3 shapes/types, presentation, handshake and recovery schemas.
`docs/provider-plugin-api.md` lands here byte-identical to #2119 so the
G10 test has its subject; if #2119 merges first the rebase is a no-op.

**Mobile** — `apps/mobile/src/screens/thread/timeline/item-kind-map.ts`:
`MOBILE_ITEM_KIND_MAP satisfies Record<CoreItemKind | "extension", …>`,
so a new domain kind without a mobile decision fails to typecheck. New
kinds fall through to the existing registry fallback until WS3.

**Guardrail seeds**
- G2 `provider-contract-purity.test.ts` + committed
`provider-contract-purity.allowlist.json`: no key on the
provider-agnostic contract names a provider (segment match, so `pi` does
not trip on `capabilities`). The only allowlisted offender is
`runtimeThreadExecutionOptionsSchema.claudeCodePermissionMode` (WS2b). A
new matching key or an unused entry fails.
- G3 `grammar-version.test.ts` +
`provider-bridge-grammar.v2.snapshot.json`: a structural snapshot (every
delta kind/field, item shape/field, presentation, capabilities, method
tables) paired with `PROVIDER_BRIDGE_PROTOCOL_VERSION`, which stays at
**2** — every v3 addition is a new union member or an optional field,
which `version.ts` defines as non-bumping; the v2-deletion workstream
bumps.
- G4 type-level `CORE_ITEM_KINDS` exhaustiveness + the mobile kind map.
- G10 `packages/plugin-sdk/src/__tests__/provider-plugin-doc.test.ts`:
extracts every ```ts block from the doc and maps each field onto the
real contract (zod keys at runtime, interface keys via `satisfies`) or
an explicit `{ gap: "WS…" }`; a gap that lands fails, and a doc edit
fails until the map is updated. Full compilation of the blocks is left
as a TODO for stabilization because the doc's blocks are target-state
pseudo-code (`bb.providers.register`, `app.slots.timelineRenderer`,
`TimelineRow { kind, payload, presentation }` do not exist yet). No new
dependency was added.

**"Unsupported until WS<n>" stubs** (for the coordinator to track; every
one is an explicit case, never a silent default):
- WS1a — `packages/agent-runtime/src/delta-assembler.ts`:
`UnsupportedDeltaShapeError` thrown for every v3 shape in
`shapeMatchesItem` (L717), `buildOpenedItem` (L874),
`buildClosedItemFromShape` (L1053), the delegation `item.progress`
snapshot (L1406), and the `extension.state` delta (L2028).
- WS5 — throws:
`apps/server/src/services/interactions/pending-interaction-timeline.ts`
L447 and L507; `plugins/provider-codex/src/interactive-requests.ts`
L266; `plugins/provider-claude-code/src/interactions.ts` L300;
`packages/agent-runtime/src/test/runtime-integration-harness.ts` L740.
- WS5 — declarative-base rendering of `tool_use` from its `presentation`
(explicit `case "tool_use"`, marked "Declarative base only"):
`packages/core-ui/src/pending-interaction-formatting.ts` L190,
`packages/core-ui/src/pending-interaction-presentation.ts` L54,
`apps/app/.../ThreadPendingInteractionBanner.tsx` L400,
`apps/server/src/internal/interactive-requests.ts` L45,
`apps/mobile/src/data/interactions/approval-presentation.ts` L81,
`apps/cli/src/commands/thread/interactions.ts` L137 and L258.
- WS3 — `apps/mobile/src/screens/thread/timeline/item-kind-map.ts`:
`fileRead`, `search`, `planSteps`, `delegation`, `extension` are `{
fallback: "WS3 …" }`.

**`HOST_DAEMON_PROTOCOL_VERSION` is bumped to 147.** The daemon wire
carries `threadEventSchema` and `pendingInteractionCreateSchema`, and
this PR widens both: new union members (item kinds, `item/delegation/*`
event types, the `tool_use` subject) and an optional field
(`presentation`). An older daemon never emits any of them and nothing in
this version produces them, so compatibility is believed preserved in
both directions — but the repository rule is to bump rather than ship a
widened wire on an untested assumption, and the mismatch moves enrolled
machines onto a daemon whose runtime also negotiates the delta grammar
range. `PROVIDER_BRIDGE_PROTOCOL_VERSION` stays at 2 per `version.ts`
("Additive changes … do NOT bump the version"); the grammar itself is
negotiated through `grammarVersions`. The daemon contract's
optional-field allowlist
(`packages/host-daemon-contract/test/contract.test.ts`) gained the four
`tool_use` presentation fields with their omission semantics.

Not done here, by design: no assembly of the v3 shapes, no projection,
no `ProviderInfo` projection from the new declaration fields, no
producer of `tool_use`/`plan_review`/extension requests, no
event-pruning trigger for `item/delegation/progress` (WS1a adds it with
the producer). The only runtime behavior added is the narrow
presentation carry-through and the grammar-range gate above, both raised
by review.

## How you verified

- `pnpm exec turbo run typecheck` — **74/74 packages green** (covers
provider-bridge-protocol, domain, server-contract, plugin-sdk,
agent-runtime, server, app, mobile, host-daemon-contract, cli, core-ui,
thread-view, db, and the three provider plugins).
- `pnpm exec turbo run test --force` piped to a file for every package
with new or affected tests, all green:
`@bb/domain` 148 · `@bb/provider-bridge-protocol` 114 ·
`@get-bb/plugin-sdk` 126 · `@bb/agent-runtime` 427 ·
`@bb/host-daemon-contract` 52 · `@bb/server-contract` 58 · `@bb/core-ui`
17 · `@bb/thread-view` 379 · `@bb/db` 406 · `@bb/mobile` 833 · `@bb/cli`
453 · `bb-plugin-provider-codex` 172 · `bb-plugin-provider-claude-code`
263 · `bb-plugin-provider-acp` 181. CI runs the server and app suites.
- New tests (fail before, pass after):
`packages/domain/test/provider-event-v3-items.test.ts`,
`packages/domain/test/interaction-split.test.ts`,
`packages/provider-bridge-protocol/src/thread-delta-v3.test.ts`,
`packages/provider-bridge-protocol/src/contract-tests/{provider-contract-purity,grammar-version}.test.ts`,
`packages/plugin-sdk/src/__tests__/{provider-plugin-doc,provider-declaration-v3}.test.ts`,
`apps/mobile/src/screens/thread/timeline/item-kind-map.test.ts`.
- Existing suites are untouched-green: the assembler's exhaustive
switches gained throwing v3 cases, and presentation is attached only
when a delta carries one, so every existing v2 stream assembles
byte-for-byte as before (new assembler tests cover open, close-echo,
close-wins, dual-settle, and the no-key v2 path; new adapter tests cover
the grammar-range gate).

**Review round (SlopCop, 5 findings, all addressed in commits
`acd4225`…`6ed9bf1`):** presentation persisted by the assembler; two-way
grammar negotiation with a startup gate; `HOST_DAEMON_PROTOCOL_VERSION`
→ 147; one presentation location (required on the delta for `extension`
shapes); persisted icons glyph-only with the asset form recorded as a
WS3 gap.

Rebased over #2140 (`9dff42407`, "Remove dead code and simplify module
surfaces"). Conflicts in `provider-event.ts`, `pending-interactions.ts`,
`notifications.ts` resolved by the rule "restore `export` only on
symbols another package imports; otherwise accept the de-export; keep
every v3 member". Restored export on **0** symbols: everything the v3
grammar, bridge kit and SDK surface import cross-package
(`threadEventPlanStepSchema`, `providerRateLimitStateSchema`,
`threadEventItemStatusSchema`, …) was still exported on `main`; the only
breakage was this PR's own same-package test importing two now-private
approval schemas, rewritten to parse through the public
`pendingInteractionPayloadSchema`. The new `itemPresentationField`, the
`tool_use` subject and the request family sit beside #2140's private
`threadEventWebSearchItemSchema`,
`pendingInteractionApprovalSubjectSchema` and
`AnyPendingInteractionPayload`; #2140's removal of the unused
`ProviderRawNotification` type is kept. Verified after the rebase:
typecheck green for domain, provider-bridge-protocol, plugin-sdk,
agent-runtime, server, thread-view, core-ui, mobile, app, cli,
host-daemon-contract and the three provider plugins; tests green for
domain 146, provider-bridge-protocol 114, plugin-sdk 126, agent-runtime
423, host-daemon-contract 52, core-ui 17.

`@get-bb/plugin-sdk` is bumped 0.4.10 → 0.4.11 (top commit, via
`scripts/bump-plugin-sdk.mjs --patch`, both version files together):
0.4.10 is published, and this PR widens the SDK's public surface
(`provider-bridge.ts` v3 exports, `backend-contract.ts` declaration
fields), so the npm version guard requires a new version.
`check-npm-version-guard.mjs` passes locally.

Part of the provider-plugin migration; no single issue. Target doc:
#2119.

> AGENT GENERATED: by Claude Opus 5

---------

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant