Repository navigation
Add the provider plugin API target-state doc - #2119
Closed
SawyerHood wants to merge 1 commit into
Closed
SawyerHood wants to merge 1 commit into
SawyerHood wants to merge 1 commit into
Conversation
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>
Collaborator
Author
|
Superseded by #2124, which adds an identical |
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.registerwith 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, onedelegationkind, split closed-approvals / open-requests interactions, server projection into oneTimelineRowshape, 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 #