Skip to content

feat: run subagents as tasks with background mode, events and profiles - #396

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

frostming merged 4 commits into
mainfrom
feat/subagent-tasks

Conversation

@frostming

@frostming frostming commented Nov 4, 2026 •

Copy link
Copy Markdown
Contributor

What changed and why

Bub's subagent tool was a synchronous call: run the child, concatenate its text, return. Compared with the subagent mechanisms of T3 Code, Claude Code and Codex, it lacked task identity and lifecycle, and it had three correctness bugs. This PR fixes the bugs and turns subagents into tasks.

Fixes

  • Children stole the parent's steering messages. The child copied the parent's turn state, including _runtime_thread_id, which keys the steering inbox, so a user message sent while a subagent ran was drained into the child. Children now get their own thread ID (subagent:<task_id>) and drop the parent's tool scope and running tape. A regression test fails on the old behavior.
  • session="inherit" produced out-of-order history. The child forked the committed tape, not the running turn, and merged back before the parent's turn. It is now an alias of fork, which starts from a snapshot of the current conversation, including the calling step's messages that are not yet on the tape, and is discarded afterwards.
  • output mixed every step's narration. It is now the child's final answer: the text of its last step that made no tool calls.

New

  • Subagents always run in the background: subagent returns a task_id at once, and the result reaches the parent as a <subagent-notification> message, unless the parent already read it with subagent.wait. There is no background parameter.
  • A run does not end while subagents it started are outstanding: when a step ends without tool calls, the run waits for the next task to finish and continues with its notification. Tasks that outlive their run (failed, cancelled or max_steps turns) report through the session's steering inbox.
  • New tools subagent.wait (with timeout_seconds=0 it only checks status), subagent.send (steer a running task, or continue a named session) and subagent.cancel.
  • SubagentStarted, SubagentOutput and SubagentFinished events on agent.events, so channels such as ACP can observe child runs without replacing the tool.
  • Subagent profiles in .agents/agents/*.md (workspace, then home), with model, tools, skills, reasoning effort and timeout; the body is added to the child's system prompt. The parent's prompt lists the profiles.
  • Limits: a child's tools and skills stay within the parent run's; BUB_SUBAGENT_MAX_CONCURRENCY (default 4) and BUB_SUBAGENT_TIMEOUT_SECONDS.
  • Cleanup: quit and runtime exit cancel running subagents; ending a temp or fork subagent stops the background shells it started.

Core additions

  • Agent.start/run/session accept an open Tape to fork, as well as a tape name.
  • ToolContext.step_messages and ToolContext.call_id.
  • MessagesPending.step_ended, true for the emission at the end of a step without tool calls.

Impacted modules

  • bub.core.agent, bub.core.tools
  • bub.builtin.subagents (new; bub.builtin.tools re-exports run_subagent, SubAgentInput and SubAgentResult), bub.builtin.agent, bub.builtin.settings, bub.builtin.hook_impl
  • Docs: subagents guide, tools/settings/python-api reference (en and zh-cn)

SubAgentResult gains task_id and status; the tool now returns before the child finishes. A follow-up in bub-contrib moves bub-acp-server from replacing the subagent tool to observing these events, and adds the Lody subagents capability.

Known limits

  • A task that outlives its run reports with the session's next turn; waking an idle session needs channel support.
  • A temp/fork task cannot be continued with subagent.send after it ends; use a named session for that.

Verification

  • uv run ruff check src tests: passed
  • uv run mypy src: passed
  • uv run pytest -q: 724 passed, 20 skipped
  • tests/test_tracing.py and tests/test_otlp.py with the OpenTelemetry SDK installed: 24 passed
  • Docs build (pnpm build): passed

🤖 Generated with Claude Code

frostming and others added 3 commits November 4, 2026 10:28
…tools

- `Agent.start`/`run`/`session` accept an open `Tape` as well as a tape name,
  so a run can start from another run's tape.
- `ToolContext.step_messages` holds the user messages the calling model step
  sent, which the tape records only after the step's tool calls finish.
- `ToolContext.call_id` is the model's ID of the call.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…d profiles

Fixes:
- A child run copied the parent's `_runtime_thread_id`, so it drained steering
  messages meant for the parent. Children now get their own thread ID and drop
  the parent's tool scope and running tape.
- `session="inherit"` merged the child's turn into the parent session out of
  order. It is now an alias of `fork`: the child starts from a snapshot of the
  current conversation, including the calling step's messages, and is discarded.
- `output` is the child's final answer instead of every text delta.

New:
- `background=true` returns a task ID at once; the result reaches the parent as
  a `<subagent-notification>` steering message unless read with `subagent.wait`.
- `subagent.wait`, `subagent.send` and `subagent.cancel` tools.
- `SubagentStarted`, `SubagentOutput` and `SubagentFinished` events on the
  agent's bus, so channels can observe subagents without replacing the tool.
- Subagent profiles in `.agents/agents/*.md`.
- Child tools and skills stay within the parent's; concurrency limit
  (`BUB_SUBAGENT_MAX_CONCURRENCY`) and timeout (`BUB_SUBAGENT_TIMEOUT_SECONDS`).
- Cancelling the parent call, `quit` or runtime exit cancels subagents; ending a
  temporary subagent stops its background shells.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Nov 4, 2026 •

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
bub 2e0692b Commit Preview URL

Branch Preview URL
Nov 04 2026, 02:40 AM

Drop the `background` parameter of `subagent` and `subagent.send`: every
subagent returns a task ID at once and reports with a <subagent-notification>.

So that a parent never ends its turn before its subagents report, a run that
started subagent tasks waits for the next one to finish when a step would end
the run, then continues with its notification. `MessagesPending.step_ended`
tells that emission apart from the one made while a request is built.

A notification sent through the steering inbox now marks the task delivered,
so a result is never handed to the parent twice.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
frostming added a commit to bubbuild/bub-contrib that referenced this pull request Nov 4, 2026
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 merged commit d14de71 into main Nov 4, 2026
7 checks passed
@frostming
frostming deleted the feat/subagent-tasks branch November 4, 2026 03:51
frostming added a commit to bubbuild/bub-contrib that referenced this pull request Nov 4, 2026
…agents (#98)

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

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>

* test(bub-acp-server): follow Bub subagents running in the background

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>

* chore: lock bub with subagent tasks

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <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