Repository navigation
feat(bub-acp-server): observe Bub subagent tasks and support Lody subagents - #98
Conversation
…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>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
Not ready as is: two findings in agent.py, both about what the server advertises and what it keeps.
subagentEventsis still advertised when the installed Bub has nobub.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 atagent.py:155.- 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 atagent.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: |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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.
What changed and why
Depends on bubbuild/bub#396, which runs Bub subagents as tasks and publishes
SubagentStarted,SubagentOutputandSubagentFinishedon the agent's event bus. Keep this PR in draft until that one is merged into Bubmain: the tests importbub.builtin.subagents.Until now the ACP server replaced each session's
subagenttool 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 advertisecancel: trueandoutputRead: "live_tail", and snapshots carry the profile name.subagentscapability (lifecycle,list,cancel,output), advertised to Lody clients only:tool_call_updatewith_meta.lody.task(LodyTaskMeta,kind: "subagent") on thesubagenttool call that started the task._lody/subagents/list|cancel|output. ThetaskIdis the run'srunId, the same choice as acp-extension-dsh.lifecycle: true, so the lifecycle is implemented fully, not only advertised.nullin terminal snapshots'reason.message, which the contract forbids.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 passedpytest(whole repo, without the uninstalledbub-herdr): 1070 passed, 11 skippedruff check packages/bub-acp-server: no new findings; the package now has one fewer pre-existing finding🤖 Generated with Claude Code