Skip to content

Surface live subagent rosters and interaction attribution - #3041

Open
bradhallett wants to merge 2 commits into
get-bb:mainfrom
bradhallett:feat/subagent-roster
Open

Surface live subagent rosters and interaction attribution#3041
bradhallett wants to merge 2 commits into
get-bb:mainfrom
bradhallett:feat/subagent-roster

Conversation

@bradhallett

@bradhallett bradhallett commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

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 agentLabel stays 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):

  • New provider event thread/subagents/updated carrying a full roster snapshot (ThreadSubagent { id, label, state, 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; partial index widened to include thread/subagents/updated), drizzle meta regenerated, migrate.ts validator and test rewind helpers per repo convention.

Server + capability chain: agent-runtime adapter derives supportsSubagents live from the handshake (getter, mirroring approvalEnforcedBy); plugin-sdk declaration normalizes absent → false; server surfaces it through ProviderInfo.capabilities and the daemon launch payload; timeline read path gains subagents (mirrors goal) with latest-page extraction in thread-view.

ACP bridge (dormant translation): wire schema for sessionUpdate: subagents_update 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. 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), so acp-omp ships with the capability off. The fake ACP agent gains FAKE_ACP_SUBAGENTS=1 roster 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: ThreadSubagentsCard roster panel (mirrors ThreadTodoCard) — collapsible, running-count header, rows sorted running > idle > parked > aborted with state badges, summary lines; hidden when no roster; events-appended invalidation refetches so snapshots arrive live. Pending-interaction banners render Agent: {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; 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.

Depends on #3040 (stacked: only commit 853f7b5fe is new here; merge that first — or review this as a single combined diff).

Fixes #3020

AGENT GENERATED

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

No visibility into a provider session's subagent mesh: live rosters and interaction attribution

1 participant