Surface live subagent rosters and interaction attribution - #3041
Open
bradhallett wants to merge 2 commits into
Open
Surface live subagent rosters and interaction attribution#3041bradhallett wants to merge 2 commits into
bradhallett wants to merge 2 commits into
Conversation
What was wrong: the plan action and edit-past-message gates were already capability-driven (composerActions "plan"; capabilities.fork), but ACP agents could not declare either: the ACP provider hardcoded composerActions to [] and typed AcpAgentDefinition.fork as none|tip, so every ACP agent was excluded by declaration shape rather than by capability. What was changed: AcpAgentDefinition gains supportsPlan (mirroring supportsManualCompaction), threaded through the customAgents setting and mapped to composerActions ["plan"] in the declaration; acp-omp declares it. The ACP bridge now reads the session modes agents advertise (session/new, session/load, session/fork results and current_mode_update notifications) and, when the agent advertises a plan mode, drives it with session/set_mode on plan turns, strips the /plan command mention from the prompt, and returns the session to its initial mode afterward; agents that report no modes keep byte-identical text passthrough. Session rewind stays undeclarable for ACP agents: ACP v1 session/fork is tip-only with no fork-point selector and reports no per-turn checkpoints, so no shipped or custom ACP agent can honestly declare checkpoint rewind today. What was verified: pnpm exec turbo run typecheck test lint --continue plus pnpm format:check. provider-bridge-acp typecheck is clean and all 317 tests pass, including the new ones: declaration mapping (composerActions ["plan"] only for agents declaring it), wire modes/current_mode_update parsing, and bridge plan-turn translation (set_mode "plan" precedes session/prompt with the mention stripped; a no-modes agent gets no set_mode and untouched text; the mode resets to the initial mode after the turn). The server suites that consume provider declarations pass solo (first-party-provider-plugins, plugin-install, plugin-update). Remaining failures are pre-existing on main f6868ad, each verified by stash-rerun on a clean tree: secret-storage concurrent-creators, server install-machine-script (macOS Abort trap: 6 on the spawned host-daemon), host-workspace workspace-diff/provisioning, and 183 files flagged by format:check from the pinned oxfmt 0.64.0 at head (formatter default drift); the new code matches the dominant committed style of the files it touches. AGENT GENERATED
## What was wrong
BB had no visibility into a provider session's subagent mesh. A thread's
child agents were invisible to clients, and pending interactions raised
inside a subagent could not be attributed to the agent that asked. OMP
threads running orchestrator workloads (parallel scouts, reviewers, task
fan-out) looked like opaque single-agent sessions, and the desktop/CLI
surfaces had no roster to inspect. The bridge protocol, daemon contract,
and persistence layer had no vocabulary for any of this.
## What changed
Contract layer (domain, provider-bridge-protocol, host-daemon-contract):
- New provider event `thread/subagents/updated` carrying a full roster
snapshot: `ThreadSubagent { id, label, state (running|idle|parked|
aborted), summary, transcriptRef }`, thread-scoped, snapshot-replace
semantics (latest event wins, mirrors `thread/goal/updated`).
- Delta grammar v3 extended in place (additive, capability-gated — same
precedent as `thread.goal.clear`): delta kind `thread.subagents`, new
bridge handshake capability `subagents` (default false). Snapshot
regenerated.
- `HOST_DAEMON_PROTOCOL_VERSION` 178 → 179: `HostDaemonBridgeLaunch`
capabilities gain required `supportsSubagents`.
- `pendingInteractionBaseSchema` gains `agentLabel: string | null`
(default null) — a single source inherited by every provider and
plugin interaction variant; legacy payloads parse unchanged.
- Domain `providerCapabilitiesSchema` gains `supportsSubagents`
(default false) so clients can see which providers report rosters.
Persistence:
- Migration `0113_subagent_roster_state`: `pending_interactions.
agent_label` column; partial index `events_thread_state_thread_
sequence_idx` widened to include `thread/subagents/updated`.
Drizzle meta snapshot regenerated; migrate.ts schema validator and
test rewind helpers updated per repo convention.
Server + capability chain:
- Agent-runtime adapter derives `supportsSubagents` live from the
handshake (getter, mirroring approvalEnforcedBy); plugin-sdk
`PluginProviderCapabilities` gains an optional declaration that
normalizes to false when absent; server surfaces it through
`ProviderInfo.capabilities` and the daemon launch payload.
- Timeline read path: `threadTimelineResponseSchema.subagents` mirrors
`goal`; latest-page extraction in thread-view; server passes through.
(The `pendingTodos` field is unchanged alongside the new field.)
- Interaction attribution: `agentLabel` flows from the raise path
through persistence into every list/detail/resolve response. The
daemon wire does not carry a label yet, so plugin-origin rows stay
null until the bridge protocol grows the field — dormant plumbing,
same honesty rule as the roster capability.
ACP bridge (dormant translation):
- Wire schema for `sessionUpdate: subagents_update` (vendor roster
kind) reusing the domain `ThreadSubagent` schema; translation to the
`thread.subagents` delta (thread-scoped, safe outside turns —
usage_update precedent); emission gated per-provider by the
`acpSubagents` bridge option so nothing emits unless a custom agent
declares it.
- Honest declaration: no shipped agent enables it. Binary-verified that
omp 18.1.7 emits no roster over ACP (subagent state exists only on
its internal persistence format; the ACP session-update validator is
a closed switch), so `acp-omp` ships with the capability off — the
surface is ready for omp-side emission as a follow-up.
- Fake ACP agent gains `FAKE_ACP_SUBAGENTS=1` roster emission for
tests.
CLI + SDK + docs parity:
- `bb thread subagents <threadId> [--json]`: roster table, honest
empty states ("Provider has not reported a subagent roster" vs "No
subagents"). SDK method, guide template chapter, bb-cli skill entry,
and configuration.md document the capability and its dormant status.
- Plugin SDK 0.4.40 → 0.4.41 (`bump-plugin-sdk.mjs --patch`): the
shipped artifacts (provider-bridge entry points, host-policy,
bundled d.ts) change with this PR, and the npm version guard
requires an unpublished version for them. Bundled d.ts regenerated
from the updated SDK.
Desktop app:
- `ThreadSubagentsCard` roster panel (PromptStackCard, mirroring
ThreadTodoCard): collapsible, running-count header, rows sorted
running > idle > parked > aborted with state badges reusing the
activity-row mapping, summary line when present; hidden when no
roster. Mounts in the prompt area and stays mounted alongside a
pending interaction.
- Pending-interaction banners render `Agent: {label}` (mirroring the
"From child thread:" line) when attributed.
- Liveness: `events-appended` invalidation refetches the timeline, so
roster snapshots arrive live with no extra app machinery.
## Verification
- Full CI parity locally: turbo run typecheck test lint --concurrency=4
(CI's concurrency) + turbo run build (19/19) + app bundle budget +
provider-literal ratchet + plugin-SDK npm version guard — all green.
- Every residual test failure reproduced identically on a clean
worktree of main (tasks/theme-preview jsdom localStorage,
integration-harness esbuild, desktop electron binary, host-workspace,
server install-machine-script) — local-environment baseline, none in
this PR's blast radius; plugin-build failures dropped from 4 to 2 vs
main after the bundled-d.ts regen.
- oxfmt --check: zero files from this diff in the failing set (the
192-file head drift is un-gated pre-existing main state).
- Migration round-trip incl. fabricated-history replays; conformance
`subagents/roster-state`; grammar snapshot; bridge tests cover
emission → assembler thread scope, snapshot replace, and silent drop
without the capability; UI tests cover render, sort, badges, hidden
states, and attribution.
AGENT GENERATED
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.
Human comments
This is the workflow I actually miss: I run orchestrator fan-outs through one acp-omp thread (scouts, reviewers, implementers), and today that session is a black box — I can't see which children exist or which child is asking me to approve something. This PR is the visibility layer for that.
The judgment calls I'd most like maintainer eyes on are called out in the deviations section, but to surface them: the capability ships dark on purpose (no shipped provider emits rosters today — I verified omp 18.1.7 emits nothing over ACP — and I'd rather reviewers trust a capability bit than see empty rosters), snapshot-replace instead of per-agent deltas, and the daemon-wire
agentLabelstays dormant until the bridge protocol grows it. If any of those read wrong to you, the contract layer is where to push back — everything downstream follows those choices.It's stacked on #3040 because the ACP translation sits on the plan-capability plumbing; happy to rebase independently or trim the ACP half entirely if you'd rather land the core slice first. Agent-built with the same local gate discipline as #3040 (clean-main worktree classification for every residual failure); I reviewed the schema and migration work myself.
What was wrong
BB had no visibility into a provider session's subagent mesh: a thread's child agents were invisible to clients, and pending interactions raised inside a subagent could not be attributed to the agent that asked. OMP threads running orchestrator workloads (parallel scouts, reviewers, task fan-out) looked like opaque single-agent sessions, and the desktop/CLI surfaces had no roster to inspect. The bridge protocol, daemon contract, and persistence layer had no vocabulary for any of this. Fixes #3020.
What changed
Contract layer (domain, provider-bridge-protocol, host-daemon-contract):
thread/subagents/updatedcarrying a full roster snapshot (ThreadSubagent { id, label, state, summary, transcriptRef }), thread-scoped, snapshot-replace semantics (latest event wins, mirrorsthread/goal/updated).thread.goal.clear): delta kindthread.subagents, new bridge handshake capabilitysubagents(default false). Snapshot regenerated.HOST_DAEMON_PROTOCOL_VERSION178 → 179:HostDaemonBridgeLaunchcapabilities gain requiredsupportsSubagents.pendingInteractionBaseSchemagainsagentLabel: string | null(default null) — a single source inherited by every provider and plugin interaction variant; legacy payloads parse unchanged.providerCapabilitiesSchemagainssupportsSubagents(default false) so clients can see which providers report rosters.Persistence: migration
0113_subagent_roster_state(pending_interactions.agent_label; partial index widened to includethread/subagents/updated), drizzle meta regenerated, migrate.ts validator and test rewind helpers per repo convention.Server + capability chain: agent-runtime adapter derives
supportsSubagentslive from the handshake (getter, mirroringapprovalEnforcedBy); plugin-sdk declaration normalizes absent → false; server surfaces it throughProviderInfo.capabilitiesand the daemon launch payload; timeline read path gainssubagents(mirrorsgoal) with latest-page extraction in thread-view.ACP bridge (dormant translation): wire schema for
sessionUpdate: subagents_updatereusing the domainThreadSubagentschema; translation to thethread.subagentsdelta (thread-scoped, safe outside turns — usage_update precedent); emission gated per-provider by theacpSubagentsbridge option. Honesty rule: no shipped agent enables it — binary-verified that omp 18.1.7 emits no roster over ACP (subagent state exists only on its internal persistence format; the ACP session-update validator is a closed switch), soacp-ompships with the capability off. The fake ACP agent gainsFAKE_ACP_SUBAGENTS=1roster emission for tests.CLI + SDK + docs parity:
bb thread subagents <threadId> [--json]with honest empty states; SDK method, guide template chapter, bb-cli skill entry, configuration.md. Plugin SDK 0.4.40 → 0.4.41 (bump-plugin-sdk.mjs --patch, mandated by the npm version guard since the shipped artifacts change); bundled d.ts regenerated.Desktop app:
ThreadSubagentsCardroster panel (mirrors ThreadTodoCard) — collapsible, running-count header, rows sorted running > idle > parked > aborted with state badges, summary lines; hidden when no roster;events-appendedinvalidation refetches so snapshots arrive live. Pending-interaction banners renderAgent: {label}when attributed.Deviations from the issue's proposal: none material — the daemon wire does not carry an agent label yet, so plugin-origin interactions stay null until the bridge protocol grows the field (dormant plumbing, same honesty rule as the roster capability).
How you verified
Full CI parity locally:
turbo run typecheck test lint --concurrency=4+turbo run build(19/19) + app bundle budget + provider-literal ratchet + plugin-SDK npm version guard — all green. Every residual test failure reproduced identically on a clean worktree of main (tasks/theme-preview jsdom localStorage, integration-harness esbuild, desktop electron binary, host-workspace, server install-machine-script) — local-environment baseline, none in this PR's blast radius; plugin-build failures dropped from 4 to 2 vs main after the bundled-d.ts regen.oxfmt --check: zero files from this diff in the failing set (192-file head drift is un-gated pre-existing main state). Migration round-trip incl. fabricated-history replays; conformancesubagents/roster-state; grammar snapshot; bridge tests cover emission → assembler thread scope, snapshot replace, and silent drop without the capability; UI tests cover render, sort, badges, hidden states, and attribution.Depends on #3040 (stacked: only commit
853f7b5feis new here; merge that first — or review this as a single combined diff).Fixes #3020