Elevator pitch
What are you proposing to change?Allow an Agent to expose subagents that it creates when delegating work. Each subagent is represented by its own ACP session ID, so its messages, thoughts, plans, tool calls, and current work can be displayed independently from the parent session. The Agent associates a reusable child session with its parent through an upsert-style
subagent_update. Child events then flow automatically on the
same connection. Messages between sessions use session_message upserts or
session_message_chunk notifications with normal message content and optional
sender and recipient metadata, rather than tool-call content.
The association is not a task or a one-shot lifetime. The parent can message
the same child again after earlier work finishes. Client-initiated session
mutations remain disabled unless explicitly advertised for that child; this
proposal initially defines only cancellation of current work.
Status quo
How do things work today and what problems does this cause? Why would we change things?ACP models a conversation as a single session update stream. Agents can run work concurrently internally, but there is no portable way to tell the Client that an update came from a subagent or to describe the relationship between the two sessions. Agents therefore have to flatten subagent activity into the parent session, hide it, or encode it in custom tool calls. Clients cannot reliably:
- render concurrent work as separate activity;
- associate plans, messages, and tool calls with the worker that produced them;
- distinguish a worker’s current title from the history of tasks sent to it;
- track nested subagents;
- cancel one subagent without cancelling the parent turn; or
- distinguish an idle child from one that is still processing work.
session/prompt, or future
queueing and steering methods, even when the underlying runtime has no way to
accept user input for that worker.
What we propose to do about it
What are you proposing to improve the situation?
Capability negotiation
In v1, add an optionalsubagents object to ClientCapabilities. A non-null
object means the Client understands the association updates, session-directed
messages and chunks, and restricted-session semantics in this RFD. The field
is optional and nullable, like adjacent capability objects. Omission or null
means the capability is not supported; {} means it is supported.
subagents. It may still use
subagents internally and present their results through the parent session.
There is no Agent-side capability. Subagent visibility flows only from Agent to
Client, so the Client capability alone is enough to prevent sending updates the
other side cannot understand; a Client learns that a particular Agent exposes
subagents by receiving the first update. In ACP v2, subagent updates are part
of the baseline session model and support is assumed rather than negotiated;
see Subagents in ACP v2.
Announcing and updating an association
subagent_update notifies the Client that the enclosing parent session has
created and owns another session. The first update for an unknown
update.sessionId announces that ownership association; later updates patch
its metadata without creating a new child or transferring ownership. This is
not a message to the child or merely a link to a related session.
When an Agent creates a subagent that it wants to expose, it MUST send the
announcing subagent_update before sending any request or notification bearing
the child’s session ID, including any live session message naming it as sender
or recipient. This includes permission, elicitation, filesystem, and terminal
requests where supported by the protocol version:
update.sessionId is required:
update.sessionIdis an opaqueSessionIdunique within the ACP connection. It identifies the same child across repeated delegations and MUST NOT be reused for an unrelated session.titleis an optional human-readable label for the child in its parent’s view. It need not be unique; the Client chooses a fallback if none is supplied.descriptionis an optional human-readable description of the child’s role or purpose. It is current display metadata, not a transcript of instructions.capabilitiesoptionally describes the Client-initiated session mutations permitted for this specific child session.cancelis an optional, nullable capability object: omitted ornullmeans unsupported, while{}advertises support. The object may include optional nullable_meta; omitted ornull_metameans no capability metadata. Ifcapabilitieswas never supplied, no session mutations are permitted.stateis an optional current-work snapshot, defined in Current work state. It is not a lifecycle outcome._metaoptionally carries association metadata.
null clears the field, and a concrete value replaces the whole
previous field. Clearing title or description removes the parent’s display
override. Clearing state leaves current activity unconfirmed, not idle.
Both capabilities: null and capabilities: {} disable Client-initiated
mutations.
The parent stream carries title and description so Clients can maintain a
subagent roster without collecting metadata from every child stream. These are
the parent’s labels for its child. The child’s ordinary session_info_update
can still describe its own conversation; it does not patch the parent
association’s display metadata. Updating either title does not rewrite the
content of earlier messages or operations.
The capabilities object is replaced as a whole, not patched recursively.
For example, capabilities: { "cancel": null } disables individual cancellation
in either version. Outer capabilities: null clears the entire capability set.
Capability advertisements inside that set are not patches: omitted or null
cancel means unsupported, not unchanged.
The outer sessionId establishes the immediate parent. This supports arbitrary
nesting without adding a second parent identifier. If a child spawns another
subagent, the Agent sends subagent_update with the child’s session ID as the
outer sessionId. A child has one immediate parent; the association cannot
reparent an existing session, refer to itself, or create a cycle.
This association describes ownership and management, not the set of sessions
that may interact with the child. Session-directed messages describe communication
and may go from a child back to its parent or to another known session
without changing the ownership tree.
Session identity follows the exposed conversation, not an individual provider
task, turn, or tool call. Completing a task does not create a new conversation.
Provider IDs can be used directly when their scope and lifetime meet the
identity requirements above; otherwise adapters can derive suitable ACP IDs
or maintain a mapping.
Announcement ordering also applies to short-lived work. Clients attribute
subsequent traffic to the announced child and apply its restricted-session
semantics; routing internals and presentation are implementation choices.
SDKs may tolerate non-conforming Agents by buffering early traffic, but that
tolerance does not replace the Agent’s announcement obligation.
Provider callbacks need not arrive in announcement order. If an interaction
arrives before the corresponding spawn metadata, the adapter can announce a
minimal association as soon as it knows the child’s identity and immediate
parent, or buffer the interaction until it can do so. It MUST NOT guess
parentage or temporarily announce a nested child under the root.
Exposure is optional. If the runtime cannot supply sufficient identity and
parentage without blocking the operation, the Agent may keep that operation
unexposed and represent it through the parent instead. It MUST NOT later
move an already-issued interaction to a different session. Once a child is
exposed, new interactions known to belong to that child’s work MUST use its
session ID rather than silently falling back to the root. A parent’s own
request for permission to create or delegate to a child remains a parent
interaction; it does not require announcing a child that does not yet exist.
The child’s execution context — its working directory, MCP servers, and
available tools — is chosen by the Agent and is not guaranteed to match the
parent’s. An Agent may, for example, run a child in a scratch directory or
without the parent’s MCP servers. Whatever the context, all child activity
flows through the same ACP connection: Agent-to-Client requests made on behalf
of a child are subject to the same Client capabilities and permission checks as
the parent’s, and the Agent MUST NOT use a child to circumvent restrictions
the Client imposed on the parent session. A future RFD may add explicit
per-child execution-context fields if Clients need to display or negotiate
them.
Automatic child event delivery
Announcement registers the child for routing on the existing ACP connection. The Agent then sends child events without waiting for another Client request. This RFD does not introducesession/attach, session/subscribe, or a
per-child event opt-in. Clients MUST NOT call session/load or
session/resume on a child to begin receiving its events.
Automatic delivery is an ACP contract, not an assumption about the provider
API. Depending on the provider, adapters may need to enable child output,
attach listeners, or restore subscriptions when reconnecting. A provider that
already delivers the necessary events requires no additional mechanism.
Existing session updates are addressed to the child in the normal way. For
example, the child can report its own conversation title separately from the
parent’s roster label:
Session-directed messages
Addsession_message and session_message_chunk variants to SessionUpdate
in both versions. These report a session’s outgoing or incoming messages to or
from another session, not responses addressed to the human user and not tool
calls. They use the existing session/update notification, not a new JSON-RPC
method or a new ContentBlock type.
The enclosing params.sessionId identifies the transcript being updated, not
necessarily the sender. Each update has:
messageId: required, non-nullable, and unique within the enclosing session, just like other message IDs. It identifies the entry in that transcript, not a globally shared message or either participant.senderSessionId: optional and nullable. Identifies the sending session when supplied.recipientSessionId: optional and nullable. Identifies the receiving session when supplied.content: asession_messagemay replace the wholeContentBlock[];session_message_chunkrequires one non-nullableContentBlockto append. Normal text, images, audio, and resource content use their existing shapes._meta: optional and nullable. It updates message-scoped metadata onsession_message; it is chunk-scoped onsession_message_chunk.
null mean not supplied:
the Client retains any previously reported value. They do not use the
null-clearing semantics of the mutable content and _meta fields.
Known participant information must remain consistent with the enclosing
transcript and earlier updates for that message. When both IDs are known,
they identify different sessions and the outer params.sessionId matches one
of them. Matching the sender represents the outgoing view; matching the
recipient represents the incoming view. Supplied live participant IDs refer
to sessions known through ordinary setup or a subagent_update announcement;
missing participants do not prevent displaying the message.
Whole-message updates use the same patch convention in both versions:
omitted content and _meta leave the stored value unchanged, null clears
the field, and concrete values replace the whole field. content: [] also
clears accumulated content. New messages start with empty content and no
metadata when those fields are omitted. Chunk _meta is not a message patch:
omitted or null means no metadata for that chunk.
Clients should render outgoing entries as “To …” with a recipient link and
incoming entries as “From …” with a sender link. Both use normal message
content while remaining distinct from user messages and answers to the user.
Neither needs a tool-call card, title, execution status, or a raw-input viewer.
When participant metadata is insufficient, Clients can use a generic
inter-session presentation and add links when identities arrive. Missing
metadata is not evidence of human authorship or confirmed receipt.
For example, the parent’s outgoing view of a message to its child is:
session/prompt because it received one. Neither
form acknowledges recipient processing or establishes new ownership. Actual
delivery is handled by the Agent.
The Agent reports each side only when it can observe that side: an outgoing
entry is not sufficient evidence to manufacture an incoming entry. Clients
MUST NOT infer receipt or synthesize an Agent-reported incoming transcript
entry solely from an outgoing entry. This does not prevent a clearly labeled
projection of outgoing messages in a cross-session view. The two views have
independently scoped message IDs, and Clients
MUST NOT assume equal IDs identify the same communication across sessions.
No cross-session message-correlation ID is defined here.
An incoming entry uses this message kind, not an additional ordinary user
message that would misattribute the communication. Rendering the same entry
in multiple views or quoting it in a summary is a presentation choice. Its
content reflects what was observed in that conversation; it need not match
the outgoing view byte for byte if the Agent transformed the content during
delivery. Neither view indicates that the recipient finished processing the
message.
Normal tool calls remain available for actual tools, including tools that
perform delegation or waiting. They report those operations’ inputs, outputs,
and outcomes. Agents do not need to invent a tool call to report a directed
message, and this RFD adds no session-reference variant to ToolCallContent.
Streaming
A directed message may start with a chunk on either side; a prior whole-message notification and complete participant metadata are not required:messageId. Participant metadata can arrive with a later chunk or a
metadata-only upsert, without repeating or replacing the content:
session_message is an upsert: when it supplies a content array, that array
replaces all content accumulated for the message rather than appending. It can
also update metadata without resending content. Later chunks append to the
stored content; chunk metadata does not patch message metadata.
Why session IDs appear in different places
These IDs answer different questions; they are not competing ways to route the same event:
For example, with parent
sess_parent and child sess_child_1:
- An association update has outer
sessionId: "sess_parent"andupdate.sessionId: "sess_child_1". This registers the parent-child relationship and the child’s capabilities. - An outgoing message has outer
sessionId: "sess_parent",senderSessionId: "sess_parent", andrecipientSessionId: "sess_child_1". This updates the parent’s history with a link to the child. - The received view has outer
sessionId: "sess_child_1"and the same sender and recipient. Its independent message ID identifies the entry in the child’s history, where the Client can link back to the parent. - The child’s own messages, plans, and tool calls have outer
sessionId: "sess_child_1". They update the child’s history, not the parent session. V2 work-state notifications use this child-addressed stream and are mirrored on the parent association; v1 reports work state through the association as described below. - A reply reverses the sender and recipient, not the ownership association. The Agent can report its outgoing view in the child’s transcript and its incoming view in the parent’s transcript.
Current work state
There is no separate subagent lifecycle enum. Work can start and stop many times within the same child session. Becoming idle, completing an operation, or cancelling current work does not permanently end the association. V2 uses the child’s ordinarystate_update notifications and their existing
foreground-work semantics. Agents SHOULD also mirror each reported child
state on the immediate parent’s subagent_update.state, so both the parent’s
roster and the child’s own stream carry the information.
These are two reports of one logical child state, not independent lifecycles.
Mirrored reports use the same StateUpdate snapshot and are idempotent: a
second copy does not represent additional work, completion, or token usage.
Copies of a transition precede later transitions on either path, so a delayed
mirror cannot overwrite newer state.
For example, a v2 child reports:
running: foreground work is in progress.requires_action: foreground work is blocked on user action.unknown: the Agent cannot currently determine foreground activity, for example after losing a remote child’s event feed while the ACP connection remains live. This is a nonterminal work state, not a work outcome.idle: foreground work is not in progress; the parent may assign more work. Optional nullablestopReasonuses v1’s existingStopReason. Omitted ornullmeans no reason was reported. When the draft end-turn usage feature is supported, optional nullableusagefollows the same convention.- Each known state may contain optional nullable
_meta; omitted andnullboth mean no metadata for that snapshot.
running or idle state.
It SHOULD report requires_action while observed foreground work is
blocked on user action, and include stopReason when the reason for stopping
is known. This does not require reconstructing unobserved intermediate
transitions or guessing activity from provider callbacks. Cancellation uses
idle with stopReason: "cancelled", not a terminal child state.
When observability of child foreground work is actually lost, the Agent
MUST report unknown using the existing state payload: v1 sends
subagent_update with "state": { "state": "unknown" } on the parent stream;
v2 sends state_update with "state": "unknown" on the child stream. Silence
alone is not evidence of lost observability. V2 mirrors this snapshot on the
parent association as well. The Client MUST NOT continue
presenting an earlier running or requires_action as confirmed live activity
after unknown, though it may retain that last-known value for context. A
subsequent running, requires_action, or idle replaces unknown for the
same child ID. unknown neither closes or cancels the child nor resolves
pending requests; it does not automatically revoke mutation capabilities.
If a control is no longer available, the Agent updates capabilities
separately.
The association’s state field is a replacement snapshot, not a nested patch:
omission leaves the previous snapshot unchanged, null clears it, and a
concrete object replaces it entirely. For example, { "state": "running" }
replaces an earlier idle snapshot and its stop reason. If no state has been
reported, or the snapshot is cleared, the Client has no confirmed current-work
state; neither announcement nor clearing implies running, idle, or
cancellation. A later state report on either supported path can supply the
current snapshot again. Unrelated association updates need not repeat it.
Unrecognized state objects preserve both their discriminator and payload. Values
beginning with _ are implementation-specific; other unknown values are
reserved for future ACP states. An unknown value does not imply completion or
closure. V2 keeps its open state and stop-reason handling, with unknown added
to ordinary StateUpdate under the experimental unstable_subagents feature.
Idle is not a successful task outcome or a declaration that the session has
been released. As in ordinary v2 sessions, background activity may still emit
updates while foreground work is idle. Operation results and failures are
reported through the relevant tool calls or other ordinary session output.
Connection loss
Connection availability is separate from reported work state. After losing the ACP connection, the Client MUST NOT continue presenting a last-known running state as confirmed live activity, or infer that unfinished work succeeded, failed, or was cancelled. It may show the child as disconnected or its current activity as unknown while retaining recorded outcomes. This Client-local uncertainty differs from an Agent-reportedunknown state while
ACP is connected.
This is local presentation, not a synthesized wire state or a permanent change
to the session’s history. Fresh state reported after reconnecting is
authoritative and may be running again for the same child ID. Neither the
parent’s prompt response nor its transition to idle implies a lost connection
or a stopped child.
Reconnection and replay
The Client reconnects through the ordinary parent session. Direct childsession/load and session/resume are not supported by this proposal.
The Agent decides whether parent reconnection reattaches surviving child
runtimes, restores them, or only makes their recorded history available.
The protocol does not prescribe a runtime-recovery algorithm.
The replay entry point depends on the protocol version:
- ACP v1:
session/loadrequests history replay.session/resumerestores the parent without replaying history. - ACP v2:
session/resumewithreplayFrom: { "type": "start" }requests full history replay. Omitted ornullreplayFrommeans no history replay. v2 has nosession/loadmethod.
- Replayed child updates MUST preserve their original session identities, known parentage, and per-session ordering. Child announcements precede child traffic.
- If identity and parentage are recoverable but conversation content is not, the Agent may replay only the association. This is historical context, not evidence of a live runtime or a complete transcript.
- If an association cannot be reconstructed truthfully, the Agent MUST NOT guess its parent or issue traffic for an unannounced child. It may omit that child’s own replay.
- A recorded directed message in a known session’s history may still be replayed with its available participant metadata and content when the other participant’s association or history is unavailable. This applies to incoming as well as outgoing entries. A historical address is not an announcement. Clients MUST NOT infer ownership, live activity, or controls from it; they can show the other participant as unavailable until its session becomes known. Agents MUST NOT invent either participant ID or reinterpret the content as an ordinary message to or from the human user.
- The Agent SHOULD make known gaps apparent through a supported advisory mechanism rather than imply that child replay is complete. Missing history is not evidence that a child never existed, stopped, failed, or was cancelled.
- All history selected for this replay MUST be sent before the response. The response completes the best-effort child replay; it does not certify that every child record was available. Delayed historical child state must not be sent after that boundary as if it were fresh activity.
subagents receive ordinary parent history, not unsupported child
updates or session-directed message variants. Parent-level results and
summaries can still be replayed.
Reconnection separates routing context from current work state and controls.
When loading or resuming a parent, the Client MUST invalidate cached live
work state and mutation authorization for its children. Historical data may
remain visible, but effective child capabilities start empty and current work
state starts unconfirmed. Historical capability patches never populate that
effective set; omitting a current capability field cannot revive an earlier
cancel grant. The Client MUST NOT enable child mutations until the
load/resume succeeds and a current capability grants them. If the operation
fails, current state and authorization received during that attempt are
invalidated.
The Client need not retain an earlier session tree. Before using a child in
live traffic, the Agent establishes the child’s complete ancestor chain in
parent-before-child order during this load/resume. Replayed associations can
provide that routing context and need not be repeated merely because the
response has been sent. Missing intermediate ancestors still need announcing,
even if their runtimes were not restored; an association establishes identity,
not a live runtime.
When replay is requested, the successful response is the freshness boundary
for child work-state and mutation-capability snapshots:
- Before the response, these snapshots are historical. They MUST NOT authorize Client-initiated mutations or establish confirmed current activity.
- Current snapshots are sent after the response. Normal association and state updates suffice; no extra announcement phase is needed for already-known associations. An Agent can omit capabilities when none are enabled, because the effective set starts empty rather than inheriting historical grants.
- In v2, the response and current snapshots that depend on this boundary MUST NOT share a JSON-RPC batch.
root → coordinator → worker was previously announced
and only worker resumes, the Agent establishes coordinator under root,
then worker under coordinator, before sending fresh worker traffic.
Replay may already have supplied these associations. Without replay they can
be announced before the resume response. A routing-only ancestor can omit
capabilities and work state; it has no enabled controls or confirmed activity.
The worker is not reparented directly under root.
The Agent SHOULD report current child work state when it can determine it.
Historical running states are not proof of current activity. An Agent MUST
NOT manufacture a failure, cancellation, or permanent orphan outcome solely
because the persisted history lacks a final update or a runtime could not be
restored. A history-only child remains displayable, with unconfirmed current
activity and no enabled mutations.
Restricted session methods
Client-initiated methods that modify a child session or its runtime are disabled unless explicitly enabled by that child’s capabilities. This is an allowlist, not a list of individual prohibited methods: Clients MUST NOT invoke an unadvertised mutation, including a future method, merely because the Agent supports it for ordinary sessions. Mutations include prompting, queueing, steering, cancellation, closing, deletion, mode or configuration changes, and forking. Loading or resuming also changes runtime attachment and is not a read-only history query. The only mutation capability defined by this RFD iscancel; direct child load, resume,
and close are therefore not allowed. The parent Agent’s own communication with
its children is not a Client-initiated mutation and is not restricted by these
capabilities.
Read-only operations retain their normal protocol semantics and capability
requirements; this RFD does not grant access to additional sessions or define
a new history-query method. Responses to Agent-to-Client requests are not
Client-initiated session mutations and MUST still be delivered.
Restricted children are discovered through parent associations rather than
ordinary standalone session/list entries. This avoids offering load, resume,
or prompt controls that are not available for them.
If forking a parent copies child history, the Agent MUST consistently remap
the copied session IDs in both message participant fields, including references
to the forked root. Each copied child still has one parent and a distinct
identity. Participants outside the copied tree do not become part of the copy;
unavailable participants follow the historical replay rules above. Copying
history does not itself create live child runtimes. Parent deletion keeps its
ordinary history-management semantics; it is not a substitute for closing
active sessions.
Cancellation and resource ownership
Ifcapabilities.cancel is a non-null object, the Client may send the existing
session/cancel notification with the child’s session ID. This cancels current
work in that child and its active descendants, not its parent or siblings.
Cancelling a parent likewise cascades to its active descendants. The per-child
capability controls individual Client cancellation; it does not prevent the Agent
from cancelling its own delegated work.
An omitted or null per-child cancel capability does not waive ancestor
cancellation. Adapters should verify whether the provider already supplies
that cascade or needs additional descendant cancellation. Advertise individual
cancellation only when the adapter can target the child’s current work
correctly in that runtime mode.
Cancellation retains ordinary session semantics: abort the affected work and
send its pending updates before confirming cancellation. A child confirms
cancellation with idle and stopReason: "cancelled" through its v1 state
snapshot or ordinary v2 notification, mirrored on the v2 parent association.
There is no Client-issued child
session/prompt response to wait for. A cancellation racing with already-ended
work does not rewrite that work’s outcome.
Acceptance of a provider stop/interrupt command is not, by itself, evidence that
the work has stopped. The Agent reports cancellation from actual work-state or
completion evidence, not merely from a successful command acknowledgement. If
activity becomes unobservable, it reports unknown rather than inventing a
cancelled outcome.
The Client SHOULD optimistically mark the affected unfinished tool calls as
cancelled, as specified for v1 prompt cancellation
and v2 work cancellation. It
SHOULD still accept subsequent Agent updates for those calls. This local
presentation is not a new v1 wire-level tool status.
Cancellation does not close the child session or prevent the parent from
assigning later work to it. Completing or failing one operation also does not
close the session or automatically fail its parent.
The Agent owns child resources. Closing the ordinary parent session cancels
its active descendant work and releases the associated active resources under
the existing close contract. It does not erase child history or require a new
terminal-state notification. Whether child runtimes can later be restored is
left to the Agent.
Pending permission and elicitation requests
Optimistically updating a tool’s display does not resolve its JSON-RPC requests. When the Client cancels child work, directly or through an ancestor, it MUST respond to pendingsession/request_permission requests for the
affected work with the cancelled outcome, following the existing cancellation
rule. It SHOULD dismiss affected elicitation controls and respond with the
cancel action. The same cleanup applies when closing the parent.
Generic $/cancel_request remains optional.
Agents may use it to cancel an abandoned child request, including when the
Agent stops work on its own. Advertising subagent support does not add a
requirement to implement that generic mechanism.
If the Client learns that an interactive request’s associated work has ended
and the request no longer applies, it SHOULD dismiss the control and send
the corresponding cancellation response. Idle alone is not a cancellation of
unrelated background requests. Requests remain attributed to their original
child session and MUST NOT move to the parent or a later operation.
A late response still resolves its original request, but the Agent MUST NOT
use it to restart cancelled or otherwise abandoned work, or to authorize a new
assignment to the same child. A request MUST NOT receive more than one
response. There is no new child-termination barrier that requires every
outstanding request in a reusable session to resolve before reporting work
state.
Cost reporting without inferred aggregation
Exposing child sessions does not make existingusage_update.cost values
exclusive or additive. Cost reporting remains optional. An Agent may continue
reporting the provider’s cumulative cost for the parent session even when it
includes child work or internal helper calls. Exposing subagents does not
require changing that accounting scope, producing an exclusive breakdown, or
discarding a useful inclusive total.
Child costs remain optional and may overlap the parent’s reported cost.
Missing child cost is unknown, not zero. Agents MUST NOT fabricate an
exclusive parent or child amount from incomplete token counts or an aggregate
total.
Clients MUST NOT infer a combined tree total by adding parent and child
values, or infer exclusive costs by subtracting them, unless a separate
accounting contract establishes their coverage and how any overlap is handled.
Clients may display each session’s latest reported cumulative value without
claiming an accounting scope that has not been established. Successive
cumulative updates and repeated messages to a session are not additional costs.
For example, a parent’s reported USD 1.20 may already include a child’s USD
0.30. Both values can be shown, but the Client must not synthesize USD 1.50 as
the combined cost from the session tree alone. A missing cost for another child
does not invalidate the provider’s USD 1.20 report.
This RFD adds no cost-scope field or accounting negotiation. A future usage
extension can define explicit inclusive/exclusive breakdowns where available.
Amounts in different currencies must not be added without conversion under
such an accounting contract. Context-window usage remains local to each session
and is not summed across the tree.
Subagents in ACP v2
The two versions use the same ownership and directed-message model, with these differences:- Baseline support, no capability. v2 Clients MUST understand
subagent_update, apply child operation restrictions, understand directed message snapshots/chunks, and attribute requests and notifications to the correct sessions. Clients may choose not to render a dedicated subagent UI, but cannot merely ignore the announcement as an unknown update. ThesubagentsClient capability is v1-only. - Mirrored child work state. V2 keeps
state_updateon the child’s own stream and mirrors the same state in the parent association. V1 uses only the parent-carried snapshot. Both describe the child’s ordinary foreground work, not a separate subagent lifecycle. - Resume with replay. Available child history is replayed best-effort
through
session/resumewithreplayFrom: { "type": "start" }, notsession/load.
session/prompt at acceptance and reports foreground work through
state_update. Neither that response nor the parent’s transition to idle
implies that every child has finished.
Shiny future
How will things play out once this feature exists?A Client can render a parent session with a live tree of concurrent workers. Selecting a worker shows its own plan, messages, tool calls, and status without mixing them into the main transcript. Where supported, the user can stop a runaway worker without discarding useful work from its siblings or parent. Agents that do not implement subagents remain unchanged. Agents that use subagents internally but do not want to expose them can continue flattening or summarizing their output in the parent session.
Implementation details and plan
Tell me more about your implementation. What is your detailed implementation plan?This is a project rollout and validation plan, not a requirement for every implementation to use the named SDKs, providers, or integration techniques.
- Add the v1
SubagentCapabilitiesmarker and theSubagentUpdateassociation andSubagentSessionCapabilitiestypes in both versions. - Add
SessionMessageandSessionMessageChunkupdate variants in both versions, using normal content and optional sender and recipient session metadata for incoming and outgoing views. - Mirror the v2
StateUpdatepayload in v1 and embed it in the v1 association update. In v2, mirror ordinary child-session state notifications in the parent association as well. - Keep the additions behind
unstable_subagentsand regenerate schemas and reference documentation. - Ship SDK releases that carry the draft
subagentscapability and update types — or at minimum preserve them when deserializing and re-serializing — before adapter rollout. SDKs that strip the draft fields force temporary out-of-band negotiation bridges such as_meta-scoped capability flags. Such bridges are compatibility shims, not an alternative protocol, and are retired once SDK support ships. SDK preservation of the draft fields is an explicit prerequisite for the validation step below. - Support the freshness boundary for replayed child state and capabilities. For example, an SDK could offer an explicit responder or after-response hook for publishing current snapshots after replay completes. Non-replaying resume does not need such a hook, and other live child traffic need not be globally buffered. Any ordering mechanism should provide the required ordering rather than rely on arbitrary delays.
- Update example Clients to route child events automatically, retain reusable session identities, and render session-directed messages distinctly from user-facing responses and tool calls. Enable or restore provider-side child delivery where that provider requires it.
- Establish stable provider-to-ACP session identity, truthful interaction attribution, and cancellation of current work. Retain or reconstruct child history where practical, but do not make complete child transcripts a prerequisite for live exposure.
- Exercise the validation scenarios with captured native events, including reordered callbacks and reconnects.
- Validate this revision against both Claude Code and Codex adapter mappings and a Client with concurrent-session UI before stabilization. Schema tests and the existence of provider APIs alone are not end-to-end validation.
Provider integration notes
The following inspected versions provide concrete inputs for adapters; they are not a claim that all versions or runtime modes have equivalent behavior, or that the existing adapters already implement this revision.- Claude Agent SDK 0.3.280: the published declarations
expose forwarded child messages through
forwardSubagentTextandparent_tool_use_id, task lifecycle events, permissionagentIDandtoolUseID,stopTask(taskId), and child transcript APIs such aslistSubagentsandgetSubagentMessages. Adapters may need to correlate these IDs where their scopes differ from ACP conversation identity. Permission callbacks can precede spawn metadata. Query cost totals may already include child and internal work. - Codex 0.156.1: thread/turn identifiers, thread status and approval/input
wait flags,
turn/interrupt(threadId, turnId), and thread history support the corresponding mapping. The server attaches listeners to newly created threads; adapters should also check delivery for existing threads on reconnect. Per-thread token usage is not a monetary cost report.
requires_action, and acknowledging an interrupt is not a completed
cancellation. Likewise, a decoder preserving a new field is not sufficient
implementation of child routing or the replay freshness boundary.
Validation scenarios
- Send two assignments to the same child conversation: keep its ACP session
ID, use distinct message IDs, and allow
running → idle → running. - Mirror v2 child work state to the parent association. Verify that both streams converge on the same state and duplicate snapshots do not count work completion or usage twice.
- Send messages to a parent and a sibling from a child’s stream. Verify that each message has the correct sender, recipient, and content, and that the parent-child tree does not change.
- Report the received view in the recipient’s transcript, with its own message ID and a link to the sender. Verify that neither the Client nor the adapter invents an Agent-reported incoming entry from outgoing traffic alone. Clearly labeled UI projections remain possible, without duplicating the communication as an ordinary user message.
- Start a directed message with a chunk, interleave another message, and replace accumulated content with a whole snapshot. Start without participant metadata, add it later without replacing content, and omit it on subsequent chunks. Verify that missing metadata remains an inter-session message rather than being attributed to a human or guessed sender.
- Finish reporting a message before the child finishes processing it, and keep receiving child events after the parent’s prompt response.
- Deliver a child permission callback before spawn metadata, including a nested child: verify early truthful announcement or buffering, no guessed parent, and the unexposed-operation fallback when necessary.
- Cancel a child and then an ancestor while interactions are pending: verify native work is targeted correctly, acknowledgement is not confused with completion, and late responses cannot authorize later assignments.
- Replay after restarting the adapter with complete, partial, and unavailable child history. Preserve IDs and relationships for whatever is replayed, preserve historical senders and recipients without guessing ancestry, and do not infer an outcome from a gap. Verify that current work state and controls depend on post-response snapshots, not historical capabilities. An omitted current capability set must leave controls disabled.
- Issue a live child request during replay after announcing its ancestor chain. Verify ordinary request handling without enabling historical controls, and that replay cannot overwrite or duplicate newer live transcript content.
- Resume without history, including current announcements before the response. Verify that controls remain disabled until success, failed resume invalidates attempted live state, and routing-only ancestors are retained. No additional Client attach/subscribe request or universal after-response hook is required.
- Lose a child’s activity feed while ACP stays connected: report
unknownwithout inventing an outcome, and accept later state for the same child. - Report an inclusive parent cost with missing or overlapping child costs: retain the parent report and do not synthesize a tree total or exclusive shares from the message traffic.
Frequently asked questions
What questions have arisen over the course of authoring this document or during subsequent discussions?
Is a subagent session read-only?
Only from the Client’s conversational perspective. The Client cannot prompt, queue, or steer it. The subagent may still read and write files, run terminal commands, and request permissions using the capabilities already negotiated on the ACP connection. Making all subagents workspace-read-only would prevent many coding use cases and should instead be an agent policy or a future sandbox capability.Do Claude Code and Codex support stopping individual subagents?
Current Claude Code and Codex orchestration surfaces both have concepts for stopping or closing an individual worker. That does not mean every version, runtime mode, or ACP adapter can implement it, and other Agents may only support coarse parent-turn cancellation. Individual cancellation must therefore be a capability, not a protocol requirement implied by subagent support. The proposal uses a per-child capability because support may even vary within one Agent: a local worker might be interruptible while a delegated remote job is not.Why reuse session/cancel instead of adding subagent/stop?
Once a child has a real ACP session ID, the existing session-scoped lifecycle
methods already identify it unambiguously. Reusing them avoids two ways to
perform the same operation. The subagent capability narrows where those methods
are valid and defines the required cascade behavior.
Why one subagent_update type instead of separate spawn and state notifications?
The upsert announces an association and patches its capabilities without a
separate create method. It also carries the parent’s title and description
for the child. Ordinary session updates handle the child’s own conversation
and, in v2, foreground work. Both versions carry a state snapshot on the
association so the parent has roster information; v2 mirrors the child’s
ordinary notification. Neither version needs a second, permanently terminal
subagent lifecycle.
Why are there two sessionId fields in an association update?
The fields have different scopes: params.sessionId is the parent whose stream
receives the association update, while params.update.sessionId is the child
being associated with it. The tagged SessionUpdate payload is flattened
inside update, not into params, so these names do not collide. Directed
messages instead use senderSessionId and recipientSessionId for their
participants; the outer sessionId chooses the transcript being updated.
See Why session IDs appear in different places.
Why is cancel the only per-child capability?
Stopping current work is a useful minimal control and maps to
session/cancel. The Agent retains ownership of child resources; the Client
does not need to attach, subscribe, or close each child merely to display it.
The capability object leaves room for future controls without making those
operations available implicitly.
Why not subscribe to individual child streams?
Selective delivery could reduce bandwidth and processing costs for large session trees, so it is worth revisiting. Clients can already choose which sessions to render or expand without changing event delivery. A future subscription proposal would need to distinguish transcript delivery from ownership announcements, capabilities, current work state, cancellation, and permission or elicitation requests. Hiding a transcript must not strand an interaction or make an unobserved child appear stopped. It would also need defined catch-up or snapshot behavior when a Client attaches, including gaps in retained history and ordering relative to live events. This RFD keeps automatic delivery and does not add attach/subscribe methods.Why remove the terminal disconnected state?
The earlier draft correctly avoided inventing success, failure, or
cancellation when the outcome was unknown. It unnecessarily made that
uncertainty a permanent property of the child session. Connection availability
and recorded operation outcomes are now separate: Clients show unconfirmed
activity as unknown, and Agents may restore the same child without rewriting
history or pretending to know what happened while disconnected.
Why not model a subagent as a tool call?
The two represent different things. A session owns a conversation; a tool call reports execution of a tool. An outgoing or incoming message should be renderable as ordinary content with a recipient or sender link, without requiring a tool-call card or a raw-input viewer. The directed-message updates provide that representation and support streaming. Actual tool operations can still be reported normally, but their status does not define a child’s lifetime or acknowledge that it processed a message.Why not allow prompting or steering a subagent?
The parent Agent can message or reuse a child according to its runtime’s semantics. This proposal does not expose the same control to the Client: direct user intervention may be unsupported or interfere with orchestration. A future per-child capability could opt into it without assuming every Agent can implement it.What do the association’s title and description describe?
They describe the child in its parent’s view, so Clients can show multiple children directly from parent-session updates.title is a short display
label; description supplies its role or purpose. The child’s conversation may
also have an ordinary session title, which does not replace these parent-owned
labels.
These fields are not the contents of the latest message sent to the child.
Messages and operations retain their own content and history even when the
parent revises the child’s display metadata. There is no session-wide task
field that rewrites earlier instructions.
Is session/fork sufficient for subagents?
No. Forking is initiated by the Client and creates a normal session derived
from existing context. This RFD covers Agent-created sessions with a parent
relationship and restricted Client controls. An Agent may use a fork
internally to implement a subagent, but the wire semantics are different.
What alternative approaches did you consider, and why did you settle on this one?
- Put a
subagentIdon every existing update. This would duplicate routing information and require changing every update type. - Use only tool-call parent/child relationships. This cannot represent nested, independent session streams cleanly.
- Treat children as unrestricted ACP sessions. This advertises methods that many agent-created workers cannot accept.
- Add dedicated stop and close methods. Existing session methods already have the desired addressing and lifecycle semantics.
- Use separate
subagent_spawnedandsubagent_state_updatenotifications, as an earlier draft of this RFD did. Merged into one upsert-style update to match the v2 entity pattern. - Treat every completed task as the end of a child session. Rejected because Agents can reuse a child for later work.
- Require child subscriptions or independent load/resume. Deferred: association already gives the Client automatic event delivery on the parent connection.
Revision history
- 2026-09-24: Separated reusable session associations from tool-call operations, added whole and streamed session-directed messages with incoming and outgoing views, and distinguished parent-owned display metadata from the child’s ordinary conversation metadata. Replaced the terminal lifecycle enum with v1 work-state snapshots and normal v2 state notifications. Made event delivery automatic, left runtime restoration to the Agent, retained ordinary optimistic cancellation without requiring generic request cancellation, and added nonterminal unknown activity and routing-only ancestor recovery. Clarified the distinct session ID roles, incorporated provider integration and validation requirements, and preserved provider-reported costs without inferred cross-session aggregation. Aligned cancellation with object capabilities, allowed messages back to parents and other known sessions, and made child-history replay explicitly best-effort while retaining live identity and ordering guarantees.
- 2026-09-15: Merged
subagent_spawnedandsubagent_state_updateinto a single upsert-stylesubagent_updatefollowing the v2 entity pattern, madename,task, andcapabilitiesoptional, added an explicitrunningstate, removed the Agent-side capability and the per-childclosecapability, stopped implying that a child inherits the parent’s execution context, and allowed SDKs to buffer updates for unannounced children. Added the samesubagent_updateto the v2 schema: capability-free, with v2 patch semantics and an open state enum. - 2026-08-31: Defined the Client-local
disconnectedstate for unknown outcomes on a live connection, required pending permission and elicitation requests to resolve before the terminal lifecycle update with defined race handling, and made SDK preservation of the draft fields an explicit rollout prerequisite. - 2026-08-25: Defined child reconnection, replay, orphan, and disconnected lifecycle semantics.
- 2026-08-19: Initial draft.