Agent providers as a first-class plugin surface (provider bridge protocol) - #1640
Open
ymichael wants to merge 148 commits into
Open
Agent providers as a first-class plugin surface (provider bridge protocol)#1640ymichael wants to merge 148 commits into
ymichael wants to merge 148 commits into
Conversation
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…mer gap Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…where Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…gences - 87 protocol bumps (26->121), not 92; turn-settlement fixes are #1196/#1234/#1321/#1432 - #75 is a pre-GitHub ticket id; cite commit 1a5620b - ProviderAdapter has 18 members, not 16; corrected per-provider line counts - The outbound vocabulary is a shared 7-method core with real divergences (acp lacks fork; codex maps stop/discard/compact/skills to different methods and has 4 methods no bb bridge speaks) - phase 1 must pick canonical mappings; codex bridge is a mapping layer, not a passthrough - Canonical PendingInteractionPayload union lives in @bb/domain, not shared/pending-interaction-normalization.ts (codex-only helper) - classify split is claude-vs-rest; normalizeExecutionOptions is claude-only Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ances Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…cleanup Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…rtsSessionRestore Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… resume is same-process Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
#1610 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…on 122 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…d phase 6 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ss model Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…mmar Phase 1 of plans/agent-provider-plugin-surface.md. Zod schemas for the one runtime<->bridge JSON-RPC contract: versioned initialize handshake with session-behavior capabilities (drift-proof: reported by the code that implements them), canonical request set with stop intents and checkpoint- aware fork, thread/event notifications carrying validated ThreadEvents, mandatory session/replaced, droppable provider/raw, and canonical tool-call/ interaction shapes from @bb/domain. docs/provider-bridge-protocol.md states the event grammar: turn/item lifecycles, bridge-minted id rules, ordering guarantees, and the incident lessons each rule encodes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The kit (@bb/provider-bridge-protocol/conformance) drives any bridge black-box over a lines-in/messages-out transport: JSON-RPC hygiene, the initialize handshake, then a shared session lifecycle with grammar checks (turn settlement, item-opens-before-delta, release-not-interrupted, id uniqueness across resume). Transport-agnostic so in-process handleLine bridges and spawned binaries both work; no test-framework dependency. Calibration against the UNMODIFIED acp bridge pins the phase-2a work list and surfaced a real finding: #859 gave the acp bridge request-vs-response discrimination but reply-never-drop was never implemented there - handleParsedMessage silently returns for unknown or schema-invalid requests, so the hygiene rules fail today. Aliveness-dependent probes report skipped (not false failures) on bridges that drop unknown methods. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
One adapter for every protocol-pure bridge: constant command->canonical method mapping, handshake capture via a post-initialize request (zero runtime changes), capability gating that never sends unadvertised methods, approval-policy ownership from the handshake, thread/event passthrough of already-translated ThreadEvents, session/replaced surfaced as a visible warning, canonical tool-call and interaction decode, and providerOptions packing so provider-flavored execution fields cross the wire opaquely during migration. classifyExecutionSettingsChange always reports live: options ride every command and the bridge reconciles internally. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… them Phase 2a increment 1, fixing the conformance calibration's first finding: decodeAcpBridgeJsonRpcRequest now returns discriminated failures, the known method set is derived from the schema union so it cannot drift (#853), and handleParsedMessage answers METHOD_NOT_FOUND / INVALID_PARAMS (with the zod issues) where it previously returned silently - each drop was an undebuggable 30s timeout on the runtime side. Calibration hygiene expectations flip to pass; all 64 existing acp bridge tests unchanged. Also fixes an invalid permission fixture (full mode requires permissionEscalation null) in the kit defaults and adapter tests. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The last first-party migration. packages/agent-runtime/src/acp/** moves verbatim to plugins/provider-acp/src/**, the manifest gains `bb.providerBridge: ./src/bridge/bridge.ts`, and `bb plugin build` emits dist/provider-bridge.mjs — 919 KB, self-contained (no hono reaches it from @bb/host-daemon-contract), and run standalone with an empty PATH it answers model/list with its synthetic "Agent default" list. ACP's blocker was routing, not bundling. Only the ids bb declares itself (`acp-cursor`) are plugin-registered; every known agent and every `customAcpAgents` entry is resolved from a launch spec at request time and never registered at all. `resolveBridgeLaunchForProviderId` served only registered ids, so those agents would have graduated into having no bridge whatsoever. It now resolves the ACP tier explicitly: an unregistered `acp-*` id borrows the artifact of whichever plugin declares ACP and takes its capabilities from the shared tier, the same fallback every other ACP policy accessor on the registry already uses. The launch spec keeps riding exactly as before — the `acpLaunchSpec` command field into the daemon, then the provider-scoped static options into the bridge — so configured agents, known agents, and acp-cursor (whose spec has no server-side entry and still comes from the runtime's built-in table, now `acp-launch-specs.ts` beside its fingerprint) all launch unchanged. The registry's ACP branch is gone; the generic plugin route packs the launch spec, the plugin's own bag, and the environment write roots into one static bag. Pi is now the only bundled bridge: provider-catalog is down to one entry, the daemon bundle manifest to one bridge target, and the runtime's last import of an ACP module is gone (the steer-stale check reads the canonical `BRIDGE_JSON_RPC_ERRORS.NO_ACTIVE_TURN`, which is the same -32001 the ACP constant restated). Two test-side consequences, both real coverage rather than fixture churn: - the integration harness now builds and records the first-party bridge artifacts like the plugin runtime does, because without a bridgeLaunch a graduated provider has no bridge at all. The dynamic-ACP smoke — the one test that drives a configured `acp-<slug>` agent end to end — therefore exercises the real artifact route, and it passes. - the host-daemon-contract bridge-launch round-trip was left red by the codex commit (it predates the three declared thread capabilities on the wire); it is green again. 143 acp tests run as bb-plugin-provider-acp#test. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The live-CLI integration suite calls the runtime directly, so nothing
played the server's part of attaching `bridgeLaunch`. Every graduated
provider therefore had no bridge at all and failed with "Unsupported
provider" -- 53 failures once codex, claude-code, and acp had shipped as
plugin artifacts.
The harness now does what the plugin runtime and the server do, and
nothing is stubbed: a vitest global setup builds the three plugin
bridges from source with @bb/plugin-build, captures each plugin's
declaration through the same capture stub + validation the server test
helper uses, and records {sha256, artifactPath, capabilities} in a
manifest. `createTestRuntime` reads it synchronously and injects the
launch into the five entry points that can start a provider process,
unless a test passed its own. Pi has no entry: its bridge stays in the
daemon bundle, which is exactly the `isBundledProviderId` predicate the
production registry routes on.
53 failures -> 10, and all 10 reproduce on the pre-graduation tree
(e11221d^) with the same real CLIs: nine live readonly/auto
permission scenarios where current codex and claude CLIs no longer
restrict the workspace, plus one pi resume that loses its context. One
codex readonly-deny case that fails pre-graduation now passes.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Archive and unarchive spawn the provider bridge on their own — unarchive always runs on a fresh provider-maintenance runtime, and archive has no live process once the thread's session was reaped — so a graduated provider (every first-party bridge except Pi) had no bridge to launch and `createProviderForId` threw `Unsupported provider "codex"`. Thread the launch spec end to end, exactly as thread.start does: the wire commands carry it, the server attaches it from the provider registry, the daemon resolves the cached artifact, and the runtime keys the process and builds the adapter from it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The restart shut the thread's codex process down and re-resumed from ThreadRuntimeConfig, which stored no bridge launch — so the re-resume derived an un-suffixed process key and built the adapter with no launch, which throws for a graduated provider and leaves the thread with no process at all. Store the launch on the thread's runtime config at start and resume, and re-resume with it. Archive/unarchive falls back to it too, so a live thread keeps working when the caller has none. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
`thread/openWork` is level-triggered on the runtime side, so a claim raised for a native subagent stood forever once the app-server child exited: the idle-session reaper refuses any thread with open work, and the stale tracked subagent would have re-raised the claim anyway. Child exit now drops the state that only describes a live child (raw command output, native-subagent tracking) and reports open work again, which retracts the claim. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Known ACP agents and every customAcpAgents entry are composed into the provider listing straight from config, independently of the registry — but since the ACP bridge graduated to a plugin artifact they have no bridge of their own: they borrow the registered ACP plugin's. With that plugin disabled the picker still listed them and the first turn died on the daemon with "Unsupported provider". Gate the dynamic ACP tier on a registered ACP provider plugin, which also skips the known-agent host probe when there is nothing to offer. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Providers come only from plugin registrations now, and the HTTP listener deliberately serves before plugins load. In that window the registry is empty: creating a thread threw the 409 "no provider available", the provider list came back empty, and a turn on an existing thread dispatched with no bridgeLaunch and died on the daemon as an unsupported provider. The registry now exposes a settlement gate the server resolves when plugin startup finishes (or fails), and the provider-routed paths — thread create, thread.start/turn.submit command building, provider listing and model loads — wait for it. The wait is bounded so a stuck plugin (or a plugin's own loopback SDK call during startup) cannot wedge requests. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…providers Two app-side reads of provider identity were wrong in the same way: they assumed a provider id is enough, or that a cache read re-renders. ThreadDetailView gated fork and edit-message on a render-time execution-options cache read, but it never mounts that query (its composer child does), so on a deep link the affordance stayed hidden until an unrelated query re-rendered the tree. It now subscribes to the cache. The skills library labelled providers from the icon's per-tier aria label, so every custom ACP agent rendered as "ACP provider" — in the filter menu, the scope label, and the search text. It now names them from the server's provider roster, with the icon label as fallback. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Picks up thread.archive / thread.unarchive carrying bridgeLaunch. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The provider declaration advertises fork coarsely for UI affordances and the handshake is the operative truth, but the runtime read only the declaration: a fork request was built and dispatched no matter what the bridge said. A bridge advertising fork "none" had no obligation to reject it, and a "tip" bridge got checkpoint forks it can only fail. The adapter now rejects both before dispatch with a legible error, and the start path builds its plan inside the try so a rejected fork takes the same failed-session-construction cleanup as any other start failure. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…n release Two halves of the same leak. The process key carried the artifact hash but not the declaration facts the adapter is built from at spawn — capabilities and the static provider option bag — so a plugin that edits its declaration without rebuilding its bundle kept serving new threads from the superseded adapter. Both now ride the key (and the daemon's model-list runtime cache). And the stale-hash sweep only ran when a process was ensured, so a superseded process that still owned a thread was skipped and never revisited: losing its last thread left it running until daemon shutdown. The release path, which already retired thread-scoped codex processes, now retires superseded bridge processes as well. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The conformance kit checks the grammar statically, against bridges someone ran it against. Bridges now ship as plugin artifacts and can be third-party, so nothing checked their streams at all: a delta into an item no item/started opened, an item settled twice, or a turn/completed for a turn that never started went straight into runtime state and a persisted timeline. ThreadEventGrammar is that grammar as a streaming state machine, in the protocol package so the kit's item/opens-before-delta rule is now this machine fed a log — one implementation, same rule ids. The runtime runs it at intake and drops violations with a warning naming the rule; it replaces the turn replay filter, whose completed-turn tracking it subsumes. An item that settles without opening is kept: it carries the whole item, so dropping it would lose content. Ownership: the single-thread fallback in event thread-id resolution now refuses an id that names another live thread, instead of filing one thread's work into another's timeline. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two real bounds and one deliberate non-bound. A provider bridge bundle is now third-party plugin output, and the daemon buffers it whole to hash-verify it before executing it — with no ceiling anywhere, so a large bundle was unbounded daemon memory. The cap lives on the wire contract and is enforced on both ends: the server refuses to record an oversized artifact (so it is never addressable) and the daemon refuses one before a byte is fetched. Both ends of the bridge pipe read JSON-RPC with `readline`, which has no maximum line length: a runaway or never-terminated line grew a buffer until the process died. readBoundedLines is readline with the bound it lacks — it discards the oversized line, reports it, and resumes at the next one — and it strips CR, which the stdout path never did. Left as is: the daemon event-sink queue is unbounded on purpose (it holds every host thread's events across a delivery stall and must not drop them); its tripwires already warn when it backs up. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A declaration registered a listable, thread-creatable provider on its own word. Nothing checked that anything could run it: a plugin whose bridge build failed loaded as "running" with the failure as a status detail and its provider still in the picker, and a router-kind declaration — which by contract has no bridge, and which nothing anywhere resolves — did the same by construction. Either way the first turn died on the host with "Unsupported provider". Registration now requires an implementation: this load built a bridge artifact, or the id is one the daemon bundles a bridge for (pi, now named once on the contract both the server and the runtime read). Without one the plugin load fails, naming the reason, and contributes no provider. Two dead contract fields go with it. `kind` had one usable value, since `router` had no resolver; `bridge.entry` was path-validated and then never bound to anything — the manifest's `bb.providerBridge` is the real entry, and all four first-party plugins passed the placeholder string "provider-bridge". Routers return when their resolver does. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Collision rejection only covers ids that are registered right now, so with the official plugin disabled — or failed, or not yet loaded — any plugin could claim "codex", "claude-code", "pi", or an "acp-" id. For pi that is worse than a name: the runtime refuses artifact routing for daemon-bundled ids, so a third-party "pi" would supply the metadata, capabilities, and provider options while bb executed its own bundled pi bridge underneath. Those ids are now reserved to the plugin that owns them, checked at declaration time (so a staged registration fails the factory) and in the registry (so a live one throws), whether or not the owner is loaded. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…lout The echo-provider example drops the two deleted declaration fields, and the generated plugin-SDK d.ts is regenerated (it also picks up an unrelated enum-ordering drift that was already stale on the branch). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The legacy pi adapter is gone, so the bridge's second dialect had one
driver left: the tarball smoke, which still sent `{clientInfo}`,
option-less `thread/start`, intent-less `thread/stop`, and read
`sdk/message` notifications. The smoke now drives the canonical protocol
for every bridge it touches (handshake, session construction, turn
start, release stop) and asserts canonical `thread/event`s — the pi E2E
waits for `turn/completed{status:"completed"}` rather than an SDK
`agent_end`, so an interrupted or failed turn fails loudly instead of
satisfying the wait.
With that, `PiSessionDialect` and everything it gated goes: the nullable
per-session translator, the legacy `thread/identity` re-emissions, the
bare-`error` notification arm, the duplicate legacy `handleTurnStart` /
`handleTurnSteer` / `handleThreadStop` entries, and the six legacy param
schemas. Deleting the legacy union arms also closes a real hole: the
legacy schemas were non-passthrough and sat *second* in each union, so a
canonical request that failed validation for any reason fell through to
them, got `options` stripped, and was silently served in the legacy
dialect instead of answering INVALID_PARAMS.
Session construction collapses from four layers to one.
`buildPiCanonicalSessionParams` used to emit an untyped record shaped
like the legacy wire, which was re-parsed through the legacy zod schema,
mapped again by a second `buildPiSessionParams`, and only then became SDK
options — and the env vars made a full round trip out to
`shell_environment_policy.set.*` config keys and back via
`extractEnvOverrides`. One typed `buildPiSessionParams` now produces the
session params directly, with `shellEnvOverrides` built once by the new
kit helper `buildShellEnvOverrides` (which `buildShellEnvironmentPolicyConfig`
now delegates to, so the filtering rule lives in one place). The legacy
`sessionPath` override, reachable only from the deleted resume shape,
goes with it.
Tests follow: the pi bridge suite drives canonical params through shared
`sessionParams`/`turnStartParams`/`threadRef` helpers and asserts
translated thread events. The skill-path test folds into the
`skills/configure` test, which is the only way skills reach a session now.
Nothing has driven the ACP legacy dialect since the adapter was deleted: the runtime's bridge-protocol adapter always sends canonical params, and the tarball smoke now speaks the canonical handshake. So the per-session `AcpSessionDialect`, the nullable translator, the `emitForSession` / `emitSessionError` raw-notification arms, the deferred-emit legacy branch, and every legacy union arm in `acpBridgeCommandSchema` are gone. The `acp/*` envelopes stay — they are the translator's input vocabulary, not wire traffic, and the module header now says so. Three paths die outright rather than collapse: - The `acp/permission/request` runtime request. The canonical `interaction/request` carries the same approval, and the legacy request's params schema already had no importer at all. - `thread/compact`. The handshake reports `manualCompaction: false`, so the runtime never sends it; the method existed only in the legacy dialect. `startCompaction`, the compaction notification methods, their schemas, and the translator's two compaction arms go with it, and `activePromptKind` becomes the boolean `turnActive` it always was once compaction was the only other value. - The internal params zod round-trip. `buildAcpSessionParams` and `buildAcpModelListParams` produced untyped records that were immediately re-parsed through the legacy wire schemas; they now return the typed `AcpSessionParams` / `AcpModelListParams` interfaces they always described, and the three near-identical start/resume/fork handlers collapse into one. `model/list` loses its two-branch decode: a request without a launch spec decodes to a null profile, which is the same synthetic-default degradation the old legacy branch produced. The bridge test suite drove the legacy dialect end to end, including its model-list params, so it moves to the wire the bridge now speaks: a launch spec in `options.providerOptions`, canonical turn and stop params, and assertions on translated `thread/event`s instead of `acp/*` notifications. Model discovery now runs the agent binary itself, which is what a launch-spec-derived list command always does, so the fake agent grew a `--list-models` mode. One test is new: session construction without a launch spec must fail with INVALID_PARAMS, which is the one degradation the bridge must not make.
Codex graduated first, so it carried no dialect branching to remove. What was left was the legacy probe arm on `initialize` — kept alive only by the tarball smoke, which now sends the canonical handshake — plus a set of seams whose reason to exist was the deleted adapter. `handleThreadStart`, `handleThreadResume`, and `handleThreadFork` were three copies of the same nine lines differing only in the construction request and one boolean; they become one `handleThreadConstruction` whose `resumable` reading is derived from the request kind, which is what the boolean always meant. Two modules existed only because two consumers were expected. `permission-mapping.ts` had exactly one importer (`interactive-requests.ts`) and no test of its own; merged, all seven of its exports become locals. `subagent-activity-translation.ts` had exactly one importer (`translator.ts`), no test of its own, and could never gain another — the sub-agent tracking state lives entirely in the translator's closures. `CodexSkillsExtraRootsSetParams` had no reference anywhere. Fifteen more exports were module-internal in practice and are now declared that way. Three that looked the same are deliberately kept exported: `combineWorkspaceWriteRoots`, `toCodexReasoningEffort`, and `mapCodexReasoningLevelToBb` are pure functions with real unit tests, and routing those assertions through a caller would test less, not more. The remaining changes are comments: the module headers and several inline notes still explained themselves in terms of "shared with the legacy adapter" or "mirrors the legacy path", which is now a claim about code that does not exist.
The legacy claude-code adapter is gone and the tarball smoke now speaks the canonical handshake, so `ClaudeSessionDialect` had no driver left. With it go the nullable per-session translator, the raw `sdk/message` forward in `emitForSession`, the bare-`error` notification arm, the non-canonical `emitCanonicalSessionReplacement` guard, the dual identity result shapes, and every legacy params arm in `claudeCodeCommandSchema` — including `isCanonicalDialectParams` and `selectDialectBranchIssues`, whose only job was picking which union branch's issues to report. `sdk/message` stays as the translator's input envelope; it just never reaches the wire. The interaction path collapses to one shape. A pending request's `canonicalPayload` was nullable only so legacy entries could carry Claude-native shapes; it is now the required `payload`, and the two `item/permissionRequest/approval` / `item/userQuestion/request` tails are gone — every approval and question rides `interaction/request` with a canonical payload and comes back as a canonical resolution. Two seams that only the legacy wire could reach die with it: - `inputGroups`. Canonical `turn/start` and `turn/steer` carry one flat input list and `buildClaudeTurnParams` never emitted groups, so the multi-prompt queueing path in the steer handler and `buildPromptTexts` were unreachable. - A `thread/resume` with a null `providerThreadId`. Canonical resume names the session it reopens; the bridge now answers INVALID_PARAMS instead of quietly starting a fresh session. Session construction merges: `buildClaudeCanonicalSessionParams` decoded the providerOptions bag and delegated to `buildClaudeSessionParams`, which is now that one function; the three construction handlers share `toClaudeSessionParams`, and the canonical turn handlers absorb the internal ones (`acceptance` stops being optional, so its three `!== undefined` guards go). Tests follow the wire. The bridge suite drives canonical params through shared `canonicalOptions`/`canonicalTurnParams` helpers and asserts translated payloads. Three readonly-Bash cases and one policy case drove Claude's `default`/`dontAsk` modes, which no canonical permission policy maps onto — the hook-level readonly suite in the same file already covers that rewrite in depth, so the unreachable wire cases go and the surviving cases name the policy they construct rather than a Claude mode. The two `buildClaudeCanonicalSessionParams`-vs-`buildClaudeSessionParams` equivalence assertions became tautologies when the functions merged and are replaced by absolute assertions on the knobs they were guarding.
The canonical thread/compact request had no sender: the runtime adapter never built a compaction command, so pi's and codex's handlers were dead (ACP's was already deleted). Manual compaction actually travels the prompt path — a standalone builtin /compact mention through the normal turn pipeline, which codex maps to thread/compact/start. Removed the method constant and params schema, both bridge handlers and their command-schema union entries, and the pi bridge test that existed only to exercise the method. The manualCompaction handshake fact and the declared supportsManualCompaction stay: they gate the /compact affordance, and the protocol doc now records that the trigger is prompt input and a structured request is future work. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
bb's manual compaction is a prompt-path affordance: the composer sends a standalone builtin `/compact` mention as turn input. The codex bridge intercepts it in turn/start; the pi bridge did not, so the literal text `/compact` went to the model. Live on openai/gpt-5.4-mini that produced an assistant message claiming compaction while context grew, and no `thread/compacted` event — pi's own `/compact` is an interactive-mode command the SDK path never sees, and `PiSdkSession.compact()` had no caller. turn/start now classifies the input with `isStandaloneBuiltinCompactCommand` and drives the SDK compaction. The translator already turned pi's `compaction_start`/`compaction_end` into the maintenance turn, and the prompt settle report closes the requested turn when pi refuses to compact and emits no events at all. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
bb's manual compaction is a prompt-path affordance: the composer sends a standalone builtin `/compact` mention as turn input. On origin/main the ACP adapter intercepted it and sent `thread/compact`, which the bridge ran as a provider-local maintenance prompt — `session/prompt` with `/compact`, which is how OpenCode exposes its built-in compaction over ACP. Graduation deleted the sender; the canonical-protocol simplification then deleted the handler, reasoning from the bridge's own `manualCompaction: false` handshake. That fact was stale bookkeeping, not the gate: wave 4 made ACP compaction a per-agent server-side declaration, and `providerRegistry.supportsManualCompaction` is what admits the compact action. So an `acp-opencode` `/compact` still reached the bridge and went to the model as literal text. `turn/start` now classifies its input with `isStandaloneBuiltinCompactCommand` and drives the maintenance prompt, with `activePromptKind` (turn | compaction | null), the compaction envelopes, and the translator's two arms back with it. Only an `end_turn` prompt reports `thread/compacted`; every other stop reason or a rejected prompt fails the turn with the agent's own reason. Per-agent honesty stays server-side. An agent's `available_commands_update` cannot serve as the gate: OpenCode lists only its custom commands there and never its built-in `compact`. The handshake now reports `manualCompaction: true` — a process-level fact about what this bridge implements, decided before any session exists — and the protocol doc and schema now say plainly that nothing gates on that field. Verified live on acp-opencode: contextCompaction item, `thread/compacted`, completed turn, context 10,612 -> 2,491 tokens. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
I opened #1668 to track a remaining Pi SDK bridge compatibility gap. The Pi bridge calls As a result, portable Pi extension UI calls such as The issue proposes mapping Pi’s public It does not request support for TUI-only |
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.
Agent providers as a first-class plugin surface
Implements plans/agent-provider-plugin-surface.md end to end — phases 1–6 plus graduation. Providers declare their metadata through the plugin API and run as plugin-registered provider bridges speaking one bb-owned, provider-agnostic JSON-RPC protocol. The canonical path is no longer gated: the experiment, the legacy adapters, and the core provider catalog are all deleted, so the bridge protocol is the only way a provider reaches bb.
132 commits, 622 files, +41,035 / −25,028 (
git diff --stat origin/main..HEAD, rename-aware — most of the insertions are provider sources moving intoplugins/provider-*).What's here, by phase (commits are ordered to match)
@bb/provider-bridge-protocol: versioned handshake with session-behavior capabilities, canonical requests/notifications (including end-to-endskills/configure), error codes, an event-grammar doc, and a transport-agnostic conformance kit (12 rules, incl. full streaming-delta coverage and zero-work-prompt settlement).codex app-serverchildren with the Fix provider process exit lifecycle races #1402 supervision rules. One genericBridgeProviderAdapterreplaces per-provider adapter glue. Turn-start watchdog (provider_turn_start_timeout).ProviderRegistryService(catalog-equality pinned),bb.agents.experimental_registerProvider, andapp.slots.experimental_providerIcon(plugins ship theme-aware React brand marks;logoUrlfiles remain for static color logos). Both havedocs/api_to_audit.mdentries.plugins/provider-{codex,claude-code,pi,acp}take over their core-seed entries in place (position preserved, restored on disable, takeover merge preserves flagship behaviors) and register their brand marks as app components. The app no longer depends on@bb/agent-providers.bb.providerBridgemanifest key builds a self-contained bridge bundle; server stores/serves it content-addressed; daemon caches by verified hash; a verifiedbridgeLaunch(carrying the declaration's validated execution capabilities) is its own routing authority.HOST_DAEMON_PROTOCOL_VERSION123 → 124 (the PR's only bump; main took 123 for artifact engine ranges).examples/plugins/echo-providerproves the path end to end.Graduation (waves 1–5)
Each wave ended with a full board (typecheck + all suites) before the next started, and every deletion first scanned the dying adapter's tests for uncovered shared-module invariants and moved them.
The pinned gap, then acp + pi — the codex zero-work-prompt settlement fix landed first (dispatch-ownership seam, no fabricated turns), unpinning the last non-green conformance rule; acp's and pi's legacy adapters were deleted.
claude-code, then codex — the last two legacy adapters deleted. No legacy adapter remains anywhere.
Flag and core-catalog retirement — the
providerBridgeexperiment, the whole provider-bridge-policy endpoint, and the daemon's prefix capture are gone; the registry's core seed is deleted and@bb/agent-providersno longer exists. The runtime's codex special cases were audited one by one and consciously kept (one stale comment fixed). v124 was unshipped, so no further bump was needed.Phase-6 consolidation sweep — one commit per item, each red-verified: ACP manual compaction as a per-agent declaration; the skill provider id opened and the six per-provider skill scopes collapsed to
provider-user/provider-project; ask-user-question readingsupportsNativeUserQuestion; edit-message eligibility reading the declaredsupportsNativeSessionRewind; plan mode gating on the declaredplancomposer action (plus deleting the thread-view display-name switch); and thethread/openWorknotification that finally giveshasOpenThreadWorka canonical implementation and stops the reaper killing codex native subagents.First-party artifact migration — two moves. First the bridge kit: a plugin-shipped bridge cannot import
@bb/agent-runtime, and nearly every module underagent-runtime/src/shared/turned out to have only provider-side consumers, so they moved wholesale (with their tests) to@bb/provider-bridge-protocol/bridge-kit— JSON-RPC plumbing, the stdio harness, tool-call/interaction codecs, id scoping, visibility metadata, translation helpers, and the runtime↔bridge structural types; bridge test infrastructure became@bb/provider-bridge-protocol/testing. Then the bridges themselves: codex (868 KB), acp (919 KB) and claude-code (2.44 MB) now ship as self-contained, content-addressed plugin artifacts on the same route asecho-provider, with every core carve-out removed end to end (daemon bundle target, bb-appfilesentries, launcher assertions, registry branches, catalog capability/session-restore baselines) and the tarball smoke driving the packed artifacts.Pi is the one documented exception, and the evidence is in the plan: a fully-inlined 14.8 MB artifact was built and driven through the real fixtures plus this machine's live pi config. It fails for three reasons, none of them module identity (an identity-divergent control run passed everything): pi-coding-agent's extension loader resolves itself by
__dirnamewith no fallback,import.meta.resolvefor the pi packages has nonode_modulesancestor in the daemon cache, and pi-ai's OAuth loader is deliberately bundler-hostile. The only shape proven to work is a directory artifact (bridge + a real npm tree), which is future design work. Pi's bridge therefore stays daemon-bundled — the only bundled bridge left — while its daemon coupling was still removed (createConfiguredPiSettingsManagerinlined, the@bb/agent-runtimere-export deleted).After wave 5 every provider-specific line lives in
plugins/provider-*;@bb/agent-runtimekeeps only the protocol, the generic adapter, and supervision. The deliberate exceptions still id-switched in core are listed in the plan (skill-root union normalizers,isAcpProviderId, theacp-cursorlaunch table, codex error-text classification).Verification
thread.archiveandthread.unarchivecarried nobridgeLaunch, so every graduated provider threwUnsupported provider— unarchive always (it runs on a fresh provider-maintenance runtime), archive whenever the thread's process was not already live — and the server's warn-log swallowed it. The launch spec is now threaded exactly likethread.start(wire field → server attach → daemon artifact resolve → runtime process key + adapter), and the codex account-restart re-resume carries it onThreadRuntimeConfigtoo. The other six: codex never retractingthread/openWorkon child exit (thread never idle-reaped); dynamic ACP agents offered while the ACP plugin is disabled; the boot race where the listener serves before plugins load (now a bounded registry settlement gate awaited by thread create, command building, provider listing and model loads); non-reactive execution-options reads in ThreadDetailView; and custom ACP agents labelled "ACP provider" in the skills library.none→off, claude rate-limit classification), each verified red before landing green.Follow-ups (tracked, not in this PR)
.mjsplus a pruned npm tree) that would let pi leave the daemon bundle; the empirical findings are recorded in the plan.skills/scanRoots,provider/health|install|update,provider/usage— which share one transport and should land as their own wave; and (b) the four surfaces blocked on new plugin API rather than on consolidation — provider-scoped settings toggles, onboarding/settings entries, the provider-retry banner label, and provider icon colors. Both groups want a plugin-facing provider directory, which does not exist on either SDK surface today.