Repository navigation
feat(unstable): link session message views and keep identity off chunks - #2465
Merged
Merged
Conversation
Add optional senderMessageId on the incoming view and recipientMessageId on the outgoing view, so a Client can go from either view to the other without inferring a link from equal message IDs. Either reference can be supplied after the view starts streaming.
SessionMessage and SessionMessageChunk gain optional senderMessageId and recipientMessageId in v1 and v2, following the participant-ID rules: omitted or null means not supplied and retains a known value, and a malformed value is treated as not supplied.
benbrandt
added a commit
to agentclientprotocol/claude-agent-acp
that referenced
this pull request
Oct 23, 2026
## 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 agentclientprotocol/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.
A session_message_chunk now carries only content, as agent_message_chunk does. The participant IDs and the new message references live only on session_message upserts; an Agent that learns them while streaming sends a metadata-only upsert. This gives each field one source and keeps chunks from patching message state, which their _meta already does not.
opensource-fork-sync Bot
pushed a commit
to cla7aye15I4nd/opensource-zed-industries__zed
that referenced
this pull request
Oct 23, 2026
…es#66028) The RFD reports messages between sessions, such as a parent delegating work to its subagent, as `session_message` and `session_message_chunk` updates. Each transcript gets its own view: the parent's outgoing message, the subagent's incoming one. Zed dropped both. - **Transcript entry.** Session messages are a new `AgentThreadEntry::SessionMessage`, kept separate from user and assistant messages. They reuse the keyed-message path: an upsert replaces content, a chunk appends, and a message can start with a chunk. Participants come only from `session_message` upserts: one that an upsert omits keeps its earlier value, and a known participant cannot change. Chunks carry only content, so the participant fields that schema 1.12.0 still has on chunks are ignored, following the RFD revision in agentclientprotocol/agent-client-protocol#2465. v1 replaces message metadata where v2 patches it by key. - **Row.** It reads "To <recipient>" or "From <sender>". A subagent is named with its parent's title for it, as on its permission card, and the root session as "Main Agent". The name opens that session's thread; a session the conversation never learned about shows as "Unavailable Session" without a link. - **v1 stays off.** Like `subagent_update`, v1 session messages are only applied when Zed advertised the `subagents` client capability, which it does not do yet. - **Resume replay test.** A separate commit adds `v2_resume_replay_delivers_subagent_history`. It confirms that a subagent's history replayed during a v2 resume reaches the subagent. zed-industries#65976's description said this wasn't handled; resume registers the parent before sending `session/resume`, so it works. Follow-up: the same RFD revision adds optional `senderMessageId` and `recipientMessageId` so each view can name the other. Once a schema release includes them, the row will jump to the linked entry instead of opening the thread, and per-delegation subagent cards can build on the same links. Release Notes: - N/A
benbrandt
marked this pull request as ready for review
October 23, 2026 19:19
Open
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
The subagents RFD reports a message between sessions as two independent views: the sender's outgoing
session_messageand the recipient's incoming one. Message IDs are local to each transcript, and Clients must not link views by equal IDs, so a Client has no way to go from one view to the other. This revision lets each view name the other, adds the fields to the unstable schema, and keeps a message's identity off its chunks.RFD (
docs/rfds/subagents.mdx)senderMessageIdon the incoming view names themessageIdof the outgoing view in the sender's transcript.recipientMessageIdon the outgoing view names themessageIdof the incoming view in the recipient's transcript.nullmeans not supplied and keeps a known value, and a known value doesn't change. Either can arrive in a later chunk or update, so an Agent can start streaming a view before it knows the other one's ID.session_message_chunk, and the references are only onsession_message, as message-level fields are foragent_message, whose chunks also carry only content. An Agent that learns a message's identity while streaming sends a metadata-only upsert. Before this, the participant IDs had two sources that could disagree, and a chunk patched message state even though its_metadoesn't. I found no recorded reason for putting them on chunks (feat(unstable): Intial RFD and schema for subagents #1992 and its review), and no implementation sends them there: claude-agent-acp only sends upserts. A new FAQ entry, "Why do chunks carry only content?", records this.Schema
sender_message_idandrecipient_message_idonSessionMessage, in v1 and v2, behindunstable_subagents, with builders.SessionMessageChunklosessender_session_idandrecipient_session_id(and their builders) in v1 and v2. This is a breaking change to unstable types: identity fields sent on a chunk are now ignored. A malformed value is treated as not supplied (DefaultOnError), as for the participant IDs.schema.unstable.jsonfor v1 and v2 and the draft schema docs. The draft prompt-turn (v1) and prompt-lifecycle (v2) pages mention the references, and their example now names the participants with an upsert before streaming chunks.Users
Testing
nullor malformed references mean not supplied.npm run generateandnpm run check(clippy, fmt, prettier, spellcheck,cargo test --all-features) pass.