feat(unstable): Intial RFD and schema for subagents - #1992
Ololoshechkin wants to merge 11 commits into
Conversation
1509932 to
2fc02e6
Compare
|
Implementation feedback from AIR and the reference adapter branches:
The canonical capability and lifecycle shape works for both adapters. We only needed Two edge cases still seem underspecified:
One smaller compatibility note: the current RFD requires |
0c06d25 to
4315189
Compare
|
@nikita-ashihmin please check again :) |
|
I would like to keep the RFD + protocol changes in separate PRs because it is easier to accept an RFD without having to do rounds on the Rust code at the same time to get a merge |
benbrandt
left a comment
There was a problem hiding this comment.
I think this is going in the right direction overall, but still have several questions I think we need to resolve.
Thanks for kicking this off!
| /// A short, human-readable label for the subagent. | ||
| pub name: String, | ||
| /// A human-readable summary of the work delegated to the subagent. | ||
| pub task: String, | ||
| /// Client-to-agent operations permitted for this subagent session. | ||
| pub capabilities: SubagentSessionCapabilities, |
There was a problem hiding this comment.
I would imaging several of these should be optional
There was a problem hiding this comment.
Done in ee65f9c — only subagentSessionId is required now. name/task/capabilities/state are optional with patch semantics: omitted (or null) means unchanged, and never-supplied has defined defaults (client-chosen label, no permitted operations, running).
| #[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)] | ||
| #[serde(rename_all = "snake_case")] | ||
| #[non_exhaustive] | ||
| pub enum SubagentState { |
There was a problem hiding this comment.
My inclination is we should treat this similarly to how we are treating tool calls, messages, and terminals in v2 where we continuously update a given subagent "entity" so we only have one notification type with clear upsert/update semantics
There was a problem hiding this comment.
Agreed — merged subagent_spawned + subagent_state_update into a single upsert-style subagent_update modeled on v2's TerminalUpdate/ToolCallUpdate: only the ID is required, the first update announces the child, later ones patch it, and one of them carries the terminal state (ee65f9c). Also mirrored the same type into the v2 schema — capability-free, MaybeUndefined patch semantics, open state enum — so the shape carries into v2 unchanged (a06ecf2).
| ```json | ||
| { | ||
| "clientCapabilities": { | ||
| "subagents": {} |
There was a problem hiding this comment.
I think this is good for v1, but just a note, I think for v2 we can assume support
There was a problem hiding this comment.
Why do you think so? Not all possible agents even support subagents (and not all of them plan/should do so afaik)
There was a problem hiding this comment.
Resolving my own question above after implementing it: “assume support” turned out fine — it doesn't force Agents to have subagents; an Agent without them simply never sends the update. What v2 removes is only the negotiation handshake: unknown sessionUpdate types flow into OtherSessionUpdate, which Clients preserve and otherwise ignore. The RFD now has a “Subagents in ACP v2” section, and the v2 schema carries subagent_update behind the same unstable flag (a06ecf2).
| this RFD. The field is optional and non-nullable. An omitted field means the | ||
| capability is not supported; `{}` means it is supported. | ||
|
|
||
| Add an optional `subagents` object to `AgentCapabilities.sessionCapabilities`. |
There was a problem hiding this comment.
Why does the agent need to express support? Wouldn't the notifications inherently handle it?
There was a problem hiding this comment.
| When an Agent creates a subagent that it wants to expose, it **MUST** send a | ||
| `subagent_spawned` update on the immediate parent session before sending any | ||
| updates for the child session: | ||
|
|
There was a problem hiding this comment.
Yeah this seems important. We may want to handle some things on the SDK side to better tolerate queueing messages for sessions we don't know about in case someone doesn't abide by this, but this does seem also easy enough (before the first event for the new session, emit the first subagent notification)
There was a problem hiding this comment.
Added exactly that: SDKs MAY buffer updates for unknown session IDs until the announcing update arrives, and Agents MUST NOT rely on such tolerance. Also added to the implementation plan.
| The child inherits the parent's effective working directory, additional | ||
| directories, MCP servers, and Client capabilities. The Agent may apply stricter | ||
| runtime or tool restrictions to the child, but it **MUST NOT** grant access that | ||
| was not available to the parent. A future RFD may add explicit per-child | ||
| execution-context fields if Clients need to display or negotiate them. |
There was a problem hiding this comment.
Hmm I don't know if we want to do this? The reason I say this is I see codex spawning lots of subagents in /tmp directories to do work, and may not pass along mcp or client capabilities.
We likely need a way to express these though
There was a problem hiding this comment.
Reworked (182be75): the child's execution context — cwd, MCP servers, tools — is now explicitly Agent-chosen and may differ from the parent's (scratch dirs like the Codex /tmp case). Kept only the enforceable part: child activity flows through the same connection, subject to the same Client capabilities and permission checks, and a child must not be used to bypass parent-session restrictions. The future-RFD pointer for explicit per-child context fields stays — agreed we'll likely want a way to express these; an optional workingDirectory on subagent_update would be an easy follow-up if you want it now.
| "params": { | ||
| "sessionId": "sess_parent", | ||
| "update": { | ||
| "sessionUpdate": "subagent_state_update", |
There was a problem hiding this comment.
Again, I wonder if we want a more generic subagent_update notification that captures all updates of that subagent (see v2 patterns that we can backport to v1 here)
| descendants in a local `disconnected` state: the task outcome is unknown. The | ||
| Client **MUST NOT** present such a child as `failed` or `cancelled`, **MUST | ||
| NOT** continue presenting it as running, and **MUST NOT** wait indefinitely for | ||
| a terminal update. |
There was a problem hiding this comment.
Hmm this seems odd to me... Because the client could continue to receive updates from that session, which would tell it whether it is still running or not?
There was a problem hiding this comment.
The constraint driving this: v1 ties session/update to the active prompt turn, so once session/prompt returns there's no defined channel through which the child's real outcome could still arrive — “keep waiting” becomes a spinner that can never resolve (this rule came from AIR's implementation feedback earlier in this PR). A late terminal update on a still-live connection is already allowed to supersede the local state. I added this rationale to the section. In v2 this trigger disappears entirely: prompt resolves at acceptance and updates keep flowing, so connection loss remains the only cause (covered in the new v2 section).
Address review feedback from #1992: - Merge subagent_spawned and subagent_state_update into one upsert-style subagent_update following the v2 entity pattern; add explicit running state - Make name, task, and capabilities optional; only subagentSessionId required - Drop the Agent-side capability: the Client capability alone gates the updates - Drop the per-child close capability; session/close is not valid for child IDs - Stop implying children inherit the parent's execution context - Allow SDKs to buffer updates for unannounced child sessions - Explain why the local disconnected state cannot wait for late updates - Add FAQ entries for naming, capability, and disconnected-state choices - Define v2 support: capability-free, v2 patch semantics, open state enum Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…gent_update Replace SubagentSpawnedUpdate and SubagentStateUpdate with a single SubagentUpdate where only subagentSessionId is required and omitted or null fields mean unchanged. Add a running state to SubagentState, drop the per-child close capability, and remove the agent-side subagents session capability. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Mirror the v1 subagent types into v2 with v2 conventions: no capability (unknown updates flow into OtherSessionUpdate), MaybeUndefined patch semantics, and an open SubagentState enum that preserves unknown values. Register subagent_update in the known-discriminator guards. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Thanks for the thorough review, Ben! Pushed a rework addressing all threads (182be75, ee65f9c, a06ecf2):
On splitting RFD and protocol changes: happy to do that — I'd keep this PR as the RFD and move the schema/Rust changes into a follow-up PR once you're happy with the shape. Say the word and I'll split it. @nikita-ashihmin heads-up: the wire shape changed (single |
Separate reusable child sessions from individual tool-call operations. Add non-owning session references, object cancellation capabilities, and work-state reporting for both protocol versions. Define ownership, routing, best-effort replay, and provider cost semantics in the RFD and draft guides. Regenerate unstable schemas and add serialization and compatibility tests.
Preserve main's RFD status categories and retain subagents in Draft. Regenerate schema artifacts from the merged Rust definitions to include both main's capabilities and the subagent session model.
The v1 client only uses Usage in the subagent work-state payload. Require both unstable_subagents and unstable_end_turn_token_usage so usage-only feature combinations do not fail CI with warnings denied. Validated all 95 depth-two feature configurations with CARGO_BUILD_WARNINGS=deny.
Replace tool-call session references with message upserts and chunks, with optional participant metadata and mirrored v2 child state snapshots. Align new v1 patches with tri-state semantics and refine subagent recovery and documentation.
Add subagents RFD