Skip to content

feat: report RFD delegations as linked session messages - #1405

Merged
benbrandt merged 1 commit into
mainfrom
rfd-parent-session-messages
Oct 23, 2026
Merged

benbrandt merged 1 commit into
mainfrom
rfd-parent-session-messages

Conversation

@benbrandt

Copy link
Copy Markdown
Member

Summary

For a client on the subagents RFD (every ACP v2 client, and v1 clients that declare subagents), session messages now replace the Agent/Task call that launches a child. The parent no longer sees that tool call. AIR's form is unchanged.

  • Prompt. The launch's prompt is a session_message in both transcripts: the parent's outgoing view and the child's incoming one. Both use the launch's tool use id, and each names the other with recipientMessageId / senderMessageId from feat(unstable): link session message views and keep identity off chunks agent-client-protocol#2465.
  • Answer. A foreground child's answer, from the launch's result, is the child's session_message to the parent (<launch id>:reply). It uses the result as AgentReporter renders it, so the model-facing agentId: … line and <usage> block are gone. A failure or cancellation comes from the child's idle stop reason, which it already reported. A background launch's result gets no reply.
  • Hand-back mode. Claude Code (seen with 2.1.303) can have the child hand its report back with a SubagentHandback call; the Agent result then only says that the report "was delivered to you as a message". That call is reported as the child's message to the parent, in both transcripts and linked the same way, and the launch's reply is skipped for that run. A failed hand-back delivered nothing, so it stays a tool call of the child.
  • Unexposed work. A launch whose child is never exposed is shown as a tool call after all, with its final status (unexposedControlFallback, the former failedControlFallback).
  • Hidden launches by id. The adapter's own report that closes an unfinished call (for example at a forced cancel) carries no toolName, so the runtime tracks hidden launches by id to keep that report from reaching the client for a call it never saw.
  • Replay sends the same messages from the child's transcript: the launch's prompt, the reply, and hand-backs. A launch without Claude Code's metadata file keeps its tool call and the missing-history notice. Replay can't tell a child's runs apart, so any hand-back in its transcript suppresses its launch's reply.
  • SendMessage calls stay tool calls of their sender, and the child gets the incoming view without references, as before.
  • README's subagent section describes the new behavior.

Do not merge before agentclientprotocol/agent-client-protocol#2465 settles the field names: a merge to main publishes a preview right away. Clients on the published schema ignore the two fields (no deny_unknown_fields in the Rust schema; the scenario validation with the SDK's zod schema passes).

Testing

  • npm run build, npm run check and npm run test:run pass (57 files, 4 skipped as before).
  • The six v2 subagent-*.jsonl scenario snapshots changed: the launch's tool-call updates are gone, the parent gets the outgoing view, the child's view carries senderMessageId, and foreground results became :reply messages. AIR snapshots are unchanged.
  • New unit tests cover hidden launches, nested launches, replies, failed and background launches, unexposed launches with both final statuses, the close report without a tool name, and hand-back success and failure, live and on replay.
  • A real run: the built adapter driven over v2 stdio with Claude Code, one foreground subagent asked to answer PONG, then session/resume with replay in a fresh process. Live and replay both showed the linked prompt (parent → child), the linked hand-back "PONG" (child → parent), and no Agent tool call.

For a client on the subagents RFD, session messages replace the Agent/Task
call that launches a child, which its parent is no longer shown:

- The launch's prompt goes to both transcripts, as the parent's outgoing
  view and the child's incoming one. Both views carry the launch's tool use
  id and name each other with the draft RFD's recipientMessageId and
  senderMessageId.
- A foreground child's answer, from the launch's result, is the child's
  session message to the parent. In Claude Code's hand-back mode, the
  child's SubagentHandback call is that message instead, in both
  transcripts and linked the same way; the launch's result then only says
  where the report went.
- A launch whose child is never exposed, and a failed hand-back, are shown
  as tool calls after all.

The runtime tracks hidden launches by id, since the adapter's own report
that closes an unfinished call has no tool name. Replay reports the same
messages from the child's transcript. SendMessage calls stay tool calls.
@benbrandt
benbrandt marked this pull request as ready for review October 23, 2026 11:12
@benbrandt
benbrandt merged commit 59e292e into main Oct 23, 2026
6 checks passed
@benbrandt
benbrandt deleted the rfd-parent-session-messages branch October 23, 2026 11:12
benbrandt pushed a commit that referenced this pull request Oct 23, 2026
🤖 I have created a release *beep* *boop*
---


##
[0.95.0](v0.94.0...v0.95.0)
(2026-10-23)


### Features

* end a turn at its result for AIR clients with backgroundSubagents
([#1262](#1262))
([9b3a5c8](9b3a5c8))
* report read-only subagent configuration
([#1417](#1417))
([6ab670b](6ab670b))
* report RFD delegations as linked session messages
([#1405](#1405))
([59e292e](59e292e))
* serve experimental ACP v2
([#1400](#1400))
([4d378f9](4d378f9))
* show messages typed in other Remote Control clients
([#1371](#1371))
([5b11f47](5b11f47))
* update to Claude Code 2.1.309
([#1413](#1413))
([a1521b6](a1521b6))


### Bug Fixes

* bump fast-uri from 3.1.8 to 3.1.9
([#1410](#1410))
([0fe22ae](0fe22ae))
* bump postcss from 8.5.30 to 8.5.31 in the minor group
([#1403](#1403))
([ecaa2c9](ecaa2c9))
* bump the minor group across 1 directory with 2 updates
([#1416](#1416))
([f7d6030](f7d6030))
* IJAI-3744 keep a session running while its background tasks are open
([#1399](#1399))
([e9f986b](e9f986b))
* publish the session title as soon as the CLI has it
([#1406](#1406))
([50cd76e](50cd76e))
* publish turns that Claude Code starts after end_turn as an async task
in v1
([#1390](#1390))
([a1d0eee](a1d0eee))

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).

Co-authored-by: acp-release-bot[bot] <246668977+acp-release-bot[bot]@users.noreply.github.com>
buvidk1234 pushed a commit to buvidk1234/zed that referenced this pull request Oct 24, 2026
ACP subagents now get the same inline card as native subagents. The
anchor is the parent's outgoing session message to the subagent, which
claude-agent-acp now reports when it launches one
(agentclientprotocol/claude-agent-acp#1405). The card replaces that
message's "To …" row.

- **One card per subagent for now.** The first message the parent sends
to one of its own subagents becomes the card; later messages to the same
subagent stay rows. Until session messages can name each other
(agentclientprotocol/agent-client-protocol#2465), nothing links a
message to its part of the subagent's transcript or to its outcome, so
the card covers the whole subagent. claude-agent-acp sends one such
message per subagent, at the launch.
- **Status** comes from the subagent's reported state:

  | Reported state | Card |
  |---|---|
  | running, requires action | Running |
| idle, with no stop reason, `end_turn`, or one this version doesn't
know | Completed |
  | idle, `cancelled` | Canceled |
| idle, `error`, `refusal`, `max_tokens`, `max_turn_requests` | Failed,
with the error text |
  | no state yet, `unknown` | Unknown (new status) |

- **Title:** the parent's label for the subagent wins over the
subagent's own title, as on the permission row.
- **Stop** is offered only while the subagent runs and only when its
parent advertises `cancel` for it. The subagent's own titlebar now
follows the same rule. It also showed a failed or canceled ACP subagent
with a green check, because it read the outcome from a native tool call
that ACP subagents don't have.
- **Expansion** is keyed by a new `SubagentCardId` (a tool call or a
session message), so cards no longer need a tool-call ID.
- **Thread search** skips a message drawn as a card, since the card
doesn't show its text.
- **Redraw fix:** `update_subagent_association` only notified the
subagent's thread, so views that read the association from the parent's
side (the card, the permission row's label) didn't redraw when it
changed. It now emits `AcpThreadEvent::SubagentAssociationUpdated`.

Release Notes:

- N/A
benbrandt added a commit that referenced this pull request Oct 24, 2026
In the subagents RFD form, a `SendMessage` call to a child now becomes a
message between the two sessions, as a launch's prompt already does
(#1405). Before, it was a tool call of its sender plus an unlinked
message in the child. Every delegation now has a parent-side message,
which a client can anchor a per-delegation card on, and both views name
each other with `senderMessageId`/`recipientMessageId`.

## Live

- The call is held until its final update. The SDK reports that the
message resumed a child before it reports the call's result.
- **Message reached an announced child:** the sender gets the outgoing
view and the child the incoming one (`linkedSessionMessages`). The
call's result isn't shown.
- **Message reached no announced child**, for example one sent to the
parent: the call is shown after all, with its final status
(`unexposedControlFallback`).
- **Call a permission request already showed:** it keeps its updates,
and its message goes only to the child.

## Replay

- A child's history replays right after its launch, so a call from a
parent to its child is always matched to its delivery first.
- The child's view then names the call, and the sender's view goes out
where the call sits.
- A call the replay reaches before its delivery stays a tool call,
unlinked. That can happen with a child's message to a sibling whose
history replays later.

`publishPrompt`'s `replacesLaunch` option is now `replacesCall`.

## Testing

- **Live:** the existing `SendMessage` tests in
`native-subagents.test.ts` and `acp-agent.test.ts` now expect both views
and check that the call and its completed result are hidden. A new test
checks that a call reaching no announced child is shown with its final
status.
- **Replay:** the existing test expects the linked views in place of
`root tool_call toolu_send`. A new test covers a call reached before its
delivery, and fails if delivery ignores that the call was already shown.
- `npm run check`, `npm run build` and `npm run test:run` (3301 tests)
pass locally.

Not tried against a real Claude Code run yet.
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