Skip to content

Ask each ACP agent what it supports instead of declaring one answer for five (Q21) - #2231

Closed
SawyerHood wants to merge 1 commit into
ws2b-acp-5-plugin-owns-agentsfrom
ws2b-acp-5b-probe-capabilities
Closed

Ask each ACP agent what it supports instead of declaring one answer for five (Q21)#2231
SawyerHood wants to merge 1 commit into
ws2b-acp-5-plugin-owns-agentsfrom
ws2b-acp-5b-probe-capabilities

Conversation

@SawyerHood

@SawyerHood SawyerHood commented Aug 21, 2026

Copy link
Copy Markdown
Collaborator

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_CAPABILITIES declared fork: "tip" for every acp-* provider. Both agents bb has since read the wire for support none of it, and I confirmed that today by asking them:

grok agent stdio  → {"reachable":true,"protocolVersion":1,"fork":false,"loadSession":true,"promptImage":false,"authMethods":["cached_token","grok.com"]}
cursor-agent acp  → {"reachable":true,"protocolVersion":1,"fork":false,"loadSession":true,"promptImage":true,"authMethods":["cursor_login"]}
opencode acp      → {"reachable":false,"reason":"the agent exited before it answered initialize: … spawn opencode ENOENT"}

A fork bb offers but the agent refuses is not a missing feature: the bridge refuses session/fork only 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, send initialize, read agentCapabilities, 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 -32601 to 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 as experimental_probeAcpAgent with experimental_acpAgentProbeSchema for the RPC boundary.

The plugin's host artifact gains an RPC. src/contract.ts declares probeAgent, and src/host.ts now exports both surfaces from one artifact — the named experimental_providerBridge the 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: one bb.host file, 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

  • Against the real agents, output pasted above: grok and cursor-agent both answer 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.ts covers a missing agent (reports ENOENT, 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.ts covers 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-sdk 127 tests; @bb/plugin-build 42 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 why cat will not do.

AGENT GENERATED: by Claude Opus 5

…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>
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