Skip to content

feat(bub-acp-server): observe Bub subagent tasks and support Lody subagents - #98

Merged
frostming merged 3 commits into
mainfrom
feat/acp-subagent-tasks
Nov 4, 2026
Merged

frostming merged 3 commits into
mainfrom
feat/acp-subagent-tasks

Conversation

@frostming

@frostming frostming commented Nov 4, 2026 •

Copy link
Copy Markdown
Contributor

What changed and why

Depends on bubbuild/bub#396, which runs Bub subagents as tasks and publishes SubagentStarted, SubagentOutput and SubagentFinished on the agent's event bus. Keep this PR in draft until that one is merged into Bub main: the tests import bub.builtin.subagents.

Until now the ACP server replaced each session's subagent tool with a streaming copy to see the child run. It now subscribes to the events instead, so the model sees Bub's own tool and result. Bub subagents always run in the background, so the tool call returns at once and the events keep reporting the child run.

  • subagentEvents: the same snapshot, output and progress mapping, driven by events. Runs now advertise cancel: true and outputRead: "live_tail", and snapshots carry the profile name.
  • New Lody subagents capability (lifecycle, list, cancel, output), advertised to Lody clients only:
    • The task lifecycle is sent as a tool_call_update with _meta.lody.task (LodyTaskMeta, kind: "subagent") on the subagent tool call that started the task.
    • _lody/subagents/list|cancel|output. The taskId is the run's runId, the same choice as acp-extension-dsh.
    • Lody's client schema requires lifecycle: true, so the lifecycle is implemented fully, not only advertised.
  • Closing or deleting an ACP session cancels its running subagent tasks.
  • Fixes an explicit null in terminal snapshots' reason.message, which the contract forbids.
  • If the installed Bub has no bub.builtin.subagents, nothing subagent-related is advertised or published.

Impacted modules

packages/bub-acp-server: agent.py, subagents.py, README, tests.

Verification

Run against a local editable install of bubbuild/bub#396:

  • pytest packages/bub-acp-server: 215 passed
  • pytest (whole repo, without the uninstalled bub-herdr): 1070 passed, 11 skipped
  • ruff check packages/bub-acp-server: no new findings; the package now has one fewer pre-existing finding

🤖 Generated with Claude Code

frostming and others added 2 commits November 4, 2026 10:28
…agents

Bub now runs subagents as tasks and publishes SubagentStarted, SubagentOutput
and SubagentFinished on the agent's event bus (bubbuild/bub#396). The ACP
server subscribes to those events instead of replacing the `subagent` tool.

- `subagentEvents` runs advertise `cancel: true` and `outputRead: "live_tail"`.
- New Lody `subagents` capability: the task lifecycle as `_meta.lody.task` on
  the `subagent` tool call that started it, and
  `_lody/subagents/list|cancel|output` (the task ID is the run ID).
- Closing an ACP session cancels its running subagent tasks.
- Terminal snapshots no longer send an explicit null `reason.message`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Bub's `subagent` tool now always returns a task ID at once (bubbuild/bub#396),
so the tests wait for each task's result with `subagent.wait`, and a failing
child is reported as a failed task rather than a raised tool error.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@frostming
frostming marked this pull request as ready for review November 4, 2026 03:51
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not ready as is: two findings in agent.py, both about what the server advertises and what it keeps.

  1. subagentEvents is still advertised when the installed Bub has no bub.builtin.subagents, so a client opts in and never receives an event — the degradation path the PR description claims is silent-by-absence is only half implemented. See the inline comment at agent.py:155.
  2. Finished subagent runs are never dropped until the ACP session closes, and each one keeps the Bub task, the child's accumulated text and (for session="fork") a private copy of the conversation. See the inline comment at agent.py:1057.

The rest of the changed behavior matches the README: the event-driven snapshot/progress/output mapping, the _meta.lody.task lifecycle on the starting tool call, list/cancel/output with their parameter validation, and session-close cancellation all behave as documented, and the native test run (bub-acp-server: event, lifecycle, request and close-session cases) covers them.

"subagentEvents": dict(LODY_SUBAGENT_EVENTS_CAPABILITY),
"sessionTitle": {"version": 1},
}
if BUB_SUBAGENT_TASKS:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Only subagents is gated on BUB_SUBAGENT_TASKS; subagentEvents stays in the block. On an installed Bub without bub.builtin.subagents, _watch_subagents returns before registering anything, so the agent advertises subagentEvents: {"version": 1} and then never publishes a single _lody/subagents/event — the client opts in, shows the outer subagent tool call, and waits for child output that cannot arrive. That install is inside the declared dependency (bub[app]>=0.6.0; bub 0.6.0 ships no src/bub/builtin/subagents.py), and on it this PR also removes the streaming subagent replacement that used to report the child run.

Reproduction

Reloading bub_acp_server.subagents and bub_acp_server.agent with importlib.util.find_spec("bub.builtin.subagents") forced to return None, then initialize as a Lody client that negotiated subagentEvents:

BUB_SUBAGENT_TASKS: False
advertised: {'steering': {...}, 'subagentEvents': {'version': 1}, 'sessionTitle': {'version': 1}}
client opted into subagentEvents; agent will publish: False

Either move "subagentEvents": dict(LODY_SUBAGENT_EVENTS_CAPABILITY) under the same if BUB_SUBAGENT_TASKS: guard, as the PR description promises, or keep the advertisement and state the required Bub version in the README's Subagent events section the way the instance-tool API requirement is stated earlier in the file.

task=task,
emitter=emitter,
)
self._subagent_runs[run.run_id] = run

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nothing bounds this: runs are added here and only removed by _end_subagents when the ACP session closes. A session that stays open (the normal case for an editor) keeps every finished run forever, and each one holds the live SubagentTask, run.output (every text delta the child produced), and task._request, which keeps base_state and, for session="fork" tasks, the private conversation copy built by snapshot_tape in _RunRequest.fork_from. Bub prunes its own registry at MAX_RETAINED_TASKS, but these references keep the pruned objects alive.

retained runs: 3        # after three completed tasks in one session
live: False terminated: True request retained: True base_state keys: ['_runtime_agent', 'session_id']

Some history is needed — list and output answer for ended tasks — so keep the newest handful of finished runs per session, or copy the terminal metadata plus the joined output into a small record and drop task, so memory follows the number of recent tasks rather than every task the session ever ran.

@frostming
frostming merged commit 39da21a into main Nov 4, 2026
2 checks passed
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