Ask each ACP agent what it supports instead of declaring one answer for five (Q21) - #2231
Closed
SawyerHood wants to merge 1 commit into
Closed
Ask each ACP agent what it supports instead of declaring one answer for five (Q21)#2231SawyerHood wants to merge 1 commit into
SawyerHood wants to merge 1 commit into
Conversation
…or five (Q21) A provider declaration states capabilities before any agent has spoken, so bb guessed: the ACP tier offered session/fork for every acp-* provider. Both agents bb has since read the wire for — cursor-agent and grok — support none of it, and a fork bb offers but the agent refuses is not a missing feature: the bridge refuses it only after bb created the fork thread, so the thread dies on start (#1833). The agent already reports the truth at initialize. The kit gains probeAcpAgent — spawn, initialize, read agentCapabilities, kill — published as experimental_probeAcpAgent, and the plugin's bb.host artifact gains an RPC that runs it where the agent is installed. The plugin registers what it declares, then re-registers any agent whose answer differs. The rule is one-directional: bb narrows a capability the agent denies and never widens one it claims, because a probe verifies the agent's own answer, not that the whole fork path works. An unreachable host or an agent that is not installed there leaves the declaration untouched. Co-Authored-By: Claude <noreply@anthropic.com>
This was referenced Aug 21, 2026
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.
Stacked on #2229.
What was wrong
A provider declaration states its capabilities before any agent has spoken, so bb guessed — once, for the whole ACP tier:
ACP_TIER_CAPABILITIESdeclaredfork: "tip"for everyacp-*provider. Both agents bb has since read the wire for support none of it, and I confirmed that today by asking them:A fork bb offers but the agent refuses is not a missing feature: the bridge refuses
session/forkonly after bb created the fork thread, so the thread dies on start (#1833). #2229 kept the hand-set values ("none"for cursor and grok, the unverified"tip"for opencode/omp/hermes-agent) and marked the guesses as guesses. This layer replaces guessing with asking.What changed
The kit gains a probe.
packages/provider-bridge-acp/src/probe.ts: spawn the agent, sendinitialize, readagentCapabilities, kill it. It never starts a session and never prompts, it advertises exactly the client capabilities the real bridge advertises (so an agent that varies its answer by client sees the same client), it answers-32601to anything the agent asks mid-probe, and it never throws — a missing agent, a broken one or a slow one is a{reachable: false, reason}answer. Published asexperimental_probeAcpAgentwithexperimental_acpAgentProbeSchemafor the RPC boundary.The plugin's host artifact gains an RPC.
src/contract.tsdeclaresprobeAgent, andsrc/host.tsnow exports both surfaces from one artifact — the namedexperimental_providerBridgethe daemon's bridge bootstrap looks for, and a default host entry whose handler runs the probe in the worker's own temp directory. This is the shape the echo canary proves: onebb.hostfile, two consumers.The plugin re-registers what the agents answer. It registers every agent from its declaration (so the picker is live from the first moment), then probes each connected host in the background and re-registers any agent whose answer differs, logging the reason. It re-probes a host after its worker exits and after a settings change.
The rule is one-directional (
src/probe-capabilities.ts): bb narrows a capability the agent denies and never widens one it claims. A probe verifies the agent's own answer, not that the whole fork path works through the bridge, the runtime and the timeline — so an agent that advertises fork against a"none"declaration stays"none"until bb has verified the rest. An unreachable host, or an agent not installed there, leaves the declaration exactly as it is.How I verified
fork: false, which is what Let the ACP plugin own its agents, including the user's own #2229 hand-set for them — the probe confirms the two values bb had verified by reading wire logs, and would have caught them had they been wrong. opencode is not installed here, and the probe says so rather than inventing a capability.test --filter=@bb/provider-bridge-acp: 16 files, 233 tests.probe.test.tscovers a missing agent (reportsENOENT, never throws), an agent that starts and answers nothing (the timeout), and a scripted agent that reports capabilities.test --filter=bb-plugin-provider-acp: 5 files, 34 tests.probe-capabilities.test.tscovers narrowing an unadvertised fork, changing nothing when the answer matches, refusing to widen (both an explicit"none"and an agent that declared nothing), and leaving an unreachable agent alone.typecheck(whole repo) 75/75;@get-bb/plugin-sdk127 tests;@bb/plugin-build42 tests (including the artifact build, which now also exercises the host entry beside the bridge export); G1 ratchet unchanged at 148/40.One test found a real mistake worth keeping: I first wrote the timeout case against
cat, which echoes the initialize request, so the probe answered its own request and returned the refusal message instead of timing out. The test now uses a genuinely silent process, and the comment says whycatwill not do.