Skip to content

Commit 64819c7

Browse files
authored
feat(unstable): report subagent working directories (#2593)
Add optional `cwd` and `additionalDirectories` fields to the v1 and v2 `SubagentUpdate` association so Agents can report where a child runs. Unset values are inherited from the immediate parent; `null` clears a reported value back to inherited. Update the subagents RFD with the inheritance rules, a child's root set (v1), the terminal `cwd` default (v1), a child session context overview, and related FAQ entries.
1 parent dcf969c commit 64819c7

9 files changed

Lines changed: 496 additions & 53 deletions

File tree

‎agent-client-protocol-schema/src/v1/client.rs‎

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -950,6 +950,8 @@ impl CompactionSummaryChunk {
950950
/// state replaces the entire previous state object, not the session.
951951
/// The title and description provide the parent's display metadata for the
952952
/// child. They do not replace the content of individual messages or operations.
953+
/// `cwd` and `additionalDirectories` report where the Agent placed the child;
954+
/// while unset, each inherits the immediate parent's effective value.
953955
#[cfg(feature = "unstable_subagents")]
954956
#[serde_as]
955957
#[skip_serializing_none]
@@ -997,6 +999,23 @@ pub struct SubagentUpdate {
997999
#[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
9981000
#[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
9991001
pub state: MaybeUndefined<StateUpdate>,
1002+
/// The child's working directory. Must be an absolute path.
1003+
///
1004+
/// Optional and nullable. Omitted means unchanged; `null` clears the
1005+
/// reported value. While unset, the child inherits its immediate parent's
1006+
/// effective working directory. Clients use this value for the child's
1007+
/// root set and as the default terminal working directory.
1008+
#[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
1009+
pub cwd: MaybeUndefined<PathBuf>,
1010+
/// Additional workspace roots for the child. Each path must be absolute.
1011+
///
1012+
/// Optional and nullable. Omitted means unchanged; `null` clears the
1013+
/// reported value. While unset, the child inherits its immediate parent's
1014+
/// effective additional directories, independently of `cwd`. A concrete
1015+
/// array replaces the whole list; `[]` reports no additional directories.
1016+
/// Clients use these values for the child's root set.
1017+
#[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
1018+
pub additional_directories: MaybeUndefined<Vec<PathBuf>>,
10001019
/// The _meta property is reserved by ACP to allow clients and agents to attach additional
10011020
/// metadata to their interactions. Implementations MUST NOT make assumptions about values at
10021021
/// these keys.
@@ -1024,6 +1043,8 @@ impl SubagentUpdate {
10241043
description: MaybeUndefined::Undefined,
10251044
capabilities: MaybeUndefined::Undefined,
10261045
state: MaybeUndefined::Undefined,
1046+
cwd: MaybeUndefined::Undefined,
1047+
additional_directories: MaybeUndefined::Undefined,
10271048
meta: MaybeUndefined::Undefined,
10281049
}
10291050
}
@@ -1059,6 +1080,27 @@ impl SubagentUpdate {
10591080
self
10601081
}
10611082

1083+
/// Sets, clears, or leaves unchanged the child's working directory.
1084+
///
1085+
/// While unset, the child inherits its parent's working directory.
1086+
#[must_use]
1087+
pub fn cwd(mut self, cwd: impl IntoMaybeUndefined<PathBuf>) -> Self {
1088+
self.cwd = cwd.into_maybe_undefined();
1089+
self
1090+
}
1091+
1092+
/// Replaces, clears, or leaves unchanged the child's additional directories.
1093+
///
1094+
/// While unset, the child inherits its parent's additional directories.
1095+
#[must_use]
1096+
pub fn additional_directories(
1097+
mut self,
1098+
additional_directories: impl IntoMaybeUndefined<Vec<PathBuf>>,
1099+
) -> Self {
1100+
self.additional_directories = additional_directories.into_maybe_undefined();
1101+
self
1102+
}
1103+
10621104
/// The _meta property is reserved by ACP to allow clients and agents to attach additional
10631105
/// metadata to their interactions. Implementations MUST NOT make assumptions about values at
10641106
/// these keys. Sets, clears, or omits this metadata patch.
@@ -4522,6 +4564,50 @@ mod tests {
45224564
assert!(title_only.description.is_undefined());
45234565
}
45244566

4567+
#[cfg(feature = "unstable_subagents")]
4568+
#[test]
4569+
fn subagent_directories_use_strict_patch_semantics() {
4570+
use serde_json::json;
4571+
4572+
let update = SubagentUpdate::new("child")
4573+
.cwd("/home/user/project/.worktrees/investigator")
4574+
.additional_directories(vec![]);
4575+
let wire = json!({
4576+
"sessionId": "child",
4577+
"cwd": "/home/user/project/.worktrees/investigator",
4578+
"additionalDirectories": []
4579+
});
4580+
assert_eq!(serde_json::to_value(&update).unwrap(), wire);
4581+
assert_eq!(
4582+
serde_json::from_value::<SubagentUpdate>(wire).unwrap(),
4583+
update
4584+
);
4585+
4586+
let inherited: SubagentUpdate =
4587+
serde_json::from_value(json!({"sessionId": "child"})).unwrap();
4588+
assert!(inherited.cwd.is_undefined());
4589+
assert!(inherited.additional_directories.is_undefined());
4590+
4591+
let cleared_wire = json!({
4592+
"sessionId": "child", "cwd": null, "additionalDirectories": null
4593+
});
4594+
let cleared: SubagentUpdate = serde_json::from_value(cleared_wire.clone()).unwrap();
4595+
assert!(cleared.cwd.is_null());
4596+
assert!(cleared.additional_directories.is_null());
4597+
assert_eq!(serde_json::to_value(cleared).unwrap(), cleared_wire);
4598+
4599+
for malformed in [
4600+
json!({"sessionId": "child", "cwd": false}),
4601+
json!({"sessionId": "child", "additionalDirectories": "/tmp/wt"}),
4602+
json!({"sessionId": "child", "additionalDirectories": ["/tmp/wt", false]}),
4603+
] {
4604+
assert!(
4605+
serde_json::from_value::<SubagentUpdate>(malformed.clone()).is_err(),
4606+
"{malformed}"
4607+
);
4608+
}
4609+
}
4610+
45254611
#[cfg(feature = "unstable_subagents")]
45264612
#[test]
45274613
fn subagent_patch_fields_preserve_omitted_null_and_concrete() {

‎agent-client-protocol-schema/src/v2/client.rs‎

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -952,6 +952,8 @@ impl CompactionSummaryChunk {
952952
/// unconfirmed; it does not report idle or cancellation. A child whose capabilities are
953953
/// unset permits no Client-initiated session mutations.
954954
///
955+
/// `cwd` and `additionalDirectories` report where the Agent placed the child;
956+
/// while unset, each inherits the immediate parent's effective value.
955957
/// The title and description provide the parent's display metadata for the
956958
/// child. Agents SHOULD mirror known child state changes in the parent's
957959
/// `state` using the same `StateUpdate` snapshot as the
@@ -1004,6 +1006,21 @@ pub struct SubagentUpdate {
10041006
#[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
10051007
#[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
10061008
pub state: MaybeUndefined<StateUpdate>,
1009+
/// The child's working directory. Must be an absolute path.
1010+
///
1011+
/// Optional and nullable. Omitted means unchanged; `null` clears the
1012+
/// reported value. While unset, the child inherits its immediate parent's
1013+
/// effective working directory.
1014+
#[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
1015+
pub cwd: MaybeUndefined<std::path::PathBuf>,
1016+
/// Additional workspace roots for the child. Each path must be absolute.
1017+
///
1018+
/// Optional and nullable. Omitted means unchanged; `null` clears the
1019+
/// reported value. While unset, the child inherits its immediate parent's
1020+
/// effective additional directories, independently of `cwd`. A concrete
1021+
/// array replaces the whole list; `[]` reports no additional directories.
1022+
#[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
1023+
pub additional_directories: MaybeUndefined<Vec<std::path::PathBuf>>,
10071024
/// The _meta property is reserved by ACP to allow clients and agents to attach additional
10081025
/// metadata to their interactions. Omitted leaves the stored metadata unchanged; `null`
10091026
/// clears it. An object patches the stored metadata by top-level key: keys it omits are
@@ -1033,6 +1050,8 @@ impl SubagentUpdate {
10331050
description: MaybeUndefined::Undefined,
10341051
capabilities: MaybeUndefined::Undefined,
10351052
state: MaybeUndefined::Undefined,
1053+
cwd: MaybeUndefined::Undefined,
1054+
additional_directories: MaybeUndefined::Undefined,
10361055
meta: MaybeUndefined::Undefined,
10371056
}
10381057
}
@@ -1068,6 +1087,27 @@ impl SubagentUpdate {
10681087
self
10691088
}
10701089

1090+
/// Sets, clears, or leaves unchanged the child's working directory.
1091+
///
1092+
/// While unset, the child inherits its parent's working directory.
1093+
#[must_use]
1094+
pub fn cwd(mut self, cwd: impl IntoMaybeUndefined<std::path::PathBuf>) -> Self {
1095+
self.cwd = cwd.into_maybe_undefined();
1096+
self
1097+
}
1098+
1099+
/// Replaces, clears, or leaves unchanged the child's additional directories.
1100+
///
1101+
/// While unset, the child inherits its parent's additional directories.
1102+
#[must_use]
1103+
pub fn additional_directories(
1104+
mut self,
1105+
additional_directories: impl IntoMaybeUndefined<Vec<std::path::PathBuf>>,
1106+
) -> Self {
1107+
self.additional_directories = additional_directories.into_maybe_undefined();
1108+
self
1109+
}
1110+
10711111
/// Patches, clears, or leaves unchanged subagent metadata.
10721112
///
10731113
/// A concrete object patches stored metadata by top-level key; see
@@ -3926,6 +3966,50 @@ mod tests {
39263966
assert_eq!(malformed, minimal);
39273967
}
39283968

3969+
#[cfg(feature = "unstable_subagents")]
3970+
#[test]
3971+
fn subagent_directories_use_strict_patch_semantics() {
3972+
use serde_json::json;
3973+
3974+
let update = SubagentUpdate::new("child")
3975+
.cwd("/home/user/project/.worktrees/investigator")
3976+
.additional_directories(vec![]);
3977+
let wire = json!({
3978+
"sessionId": "child",
3979+
"cwd": "/home/user/project/.worktrees/investigator",
3980+
"additionalDirectories": []
3981+
});
3982+
assert_eq!(serde_json::to_value(&update).unwrap(), wire);
3983+
assert_eq!(
3984+
serde_json::from_value::<SubagentUpdate>(wire).unwrap(),
3985+
update
3986+
);
3987+
3988+
let inherited: SubagentUpdate =
3989+
serde_json::from_value(json!({"sessionId": "child"})).unwrap();
3990+
assert!(inherited.cwd.is_undefined());
3991+
assert!(inherited.additional_directories.is_undefined());
3992+
3993+
let cleared_wire = json!({
3994+
"sessionId": "child", "cwd": null, "additionalDirectories": null
3995+
});
3996+
let cleared: SubagentUpdate = serde_json::from_value(cleared_wire.clone()).unwrap();
3997+
assert!(cleared.cwd.is_null());
3998+
assert!(cleared.additional_directories.is_null());
3999+
assert_eq!(serde_json::to_value(cleared).unwrap(), cleared_wire);
4000+
4001+
for malformed in [
4002+
json!({"sessionId": "child", "cwd": false}),
4003+
json!({"sessionId": "child", "additionalDirectories": "/tmp/wt"}),
4004+
json!({"sessionId": "child", "additionalDirectories": ["/tmp/wt", false]}),
4005+
] {
4006+
assert!(
4007+
serde_json::from_value::<SubagentUpdate>(malformed.clone()).is_err(),
4008+
"{malformed}"
4009+
);
4010+
}
4011+
}
4012+
39294013
#[cfg(feature = "unstable_subagents")]
39304014
#[test]
39314015
fn subagent_notification_keeps_parent_and_child_ids_nested() {

‎docs/protocol/v1/draft/prompt-turn.mdx‎

Lines changed: 21 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -11,27 +11,27 @@ Before sending prompts, Clients **MUST** first complete the [initialization](/pr
1111

1212
Agents send `session/update` notifications to report streamed content, tool activity, and changes to session state. The `sessionUpdate` field inside `params.update` identifies the variant:
1313

14-
| `sessionUpdate` value | Description |
15-
| --------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- |
16-
| `user_message_chunk` | A streamed chunk of a user message. |
17-
| `agent_message_chunk` | A streamed chunk of the Agent's response. |
18-
| `agent_thought_chunk` | A streamed chunk of the Agent's reasoning. |
19-
| `subagent_update` | **Draft only.** Announces an owned child session or updates its parent-owned metadata and work state. Requires the Client's `subagents` capability. |
20-
| `session_message` | **Draft only.** Creates or updates a [message addressed to another session](#session-directed-messages-unstable). Requires the Client's `subagents` capability. |
21-
| `session_message_chunk` | **Draft only.** Appends content to a [session-directed message](#session-directed-messages-unstable). Requires the Client's `subagents` capability. |
22-
| `tool_call` | A new [tool call](/protocol/v1/draft/tool-calls#creating). |
23-
| `tool_call_update` | A change to a [tool call's status or results](/protocol/v1/draft/tool-calls#updating). |
24-
| `plan` | The Agent's [execution plan](/protocol/v1/draft/agent-plan). |
25-
| `plan_update` | **Draft only.** A [content update for a plan](/protocol/v1/draft/agent-plan#plan-operations) identified by ID. Requires the Client's `plan` capability. |
26-
| `plan_removed` | **Draft only.** [Removal of a plan](/protocol/v1/draft/agent-plan#removing-plans) identified by ID. Requires the Client's `plan` capability. |
27-
| `available_commands_update` | The set of [available slash commands](/protocol/v1/draft/slash-commands) is ready or has changed. |
28-
| `current_mode_update` | The current [session mode](/protocol/v1/draft/session-modes#from-the-agent) has changed. |
29-
| `config_option_update` | The session's [configuration options](/protocol/v1/draft/session-config-options#from-the-agent) have changed. |
30-
| `session_info_update` | [Session metadata](/protocol/v1/draft/session-list#updating-session-metadata), such as the title or last update time, has changed. |
31-
| `usage_update` | The session's [context window usage and cumulative cost](#session-usage-updates). |
32-
| `notice` | A live [user-facing notice](#session-notices) that is not part of session history. Requires the Client's `session.notices` capability. |
33-
| `compaction_update` | Creates or updates a [context compaction](#session-compaction). Requires the Client's `session.compaction` capability. |
34-
| `compaction_summary_chunk` | Appends content to a [compaction's retained summary](#session-compaction). Requires the Client's `session.compaction` capability. |
14+
| `sessionUpdate` value | Description |
15+
| --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
16+
| `user_message_chunk` | A streamed chunk of a user message. |
17+
| `agent_message_chunk` | A streamed chunk of the Agent's response. |
18+
| `agent_thought_chunk` | A streamed chunk of the Agent's reasoning. |
19+
| `subagent_update` | **Draft only.** Announces an owned child session or updates its parent-owned metadata, working directories, and work state. Requires the Client's `subagents` capability. |
20+
| `session_message` | **Draft only.** Creates or updates a [message addressed to another session](#session-directed-messages-unstable). Requires the Client's `subagents` capability. |
21+
| `session_message_chunk` | **Draft only.** Appends content to a [session-directed message](#session-directed-messages-unstable). Requires the Client's `subagents` capability. |
22+
| `tool_call` | A new [tool call](/protocol/v1/draft/tool-calls#creating). |
23+
| `tool_call_update` | A change to a [tool call's status or results](/protocol/v1/draft/tool-calls#updating). |
24+
| `plan` | The Agent's [execution plan](/protocol/v1/draft/agent-plan). |
25+
| `plan_update` | **Draft only.** A [content update for a plan](/protocol/v1/draft/agent-plan#plan-operations) identified by ID. Requires the Client's `plan` capability. |
26+
| `plan_removed` | **Draft only.** [Removal of a plan](/protocol/v1/draft/agent-plan#removing-plans) identified by ID. Requires the Client's `plan` capability. |
27+
| `available_commands_update` | The set of [available slash commands](/protocol/v1/draft/slash-commands) is ready or has changed. |
28+
| `current_mode_update` | The current [session mode](/protocol/v1/draft/session-modes#from-the-agent) has changed. |
29+
| `config_option_update` | The session's [configuration options](/protocol/v1/draft/session-config-options#from-the-agent) have changed. |
30+
| `session_info_update` | [Session metadata](/protocol/v1/draft/session-list#updating-session-metadata), such as the title or last update time, has changed. |
31+
| `usage_update` | The session's [context window usage and cumulative cost](#session-usage-updates). |
32+
| `notice` | A live [user-facing notice](#session-notices) that is not part of session history. Requires the Client's `session.notices` capability. |
33+
| `compaction_update` | Creates or updates a [context compaction](#session-compaction). Requires the Client's `session.compaction` capability. |
34+
| `compaction_summary_chunk` | Appends content to a [compaction's retained summary](#session-compaction). Requires the Client's `session.compaction` capability. |
3535

3636
This is the complete set of variants defined in the current draft [`SessionUpdate` schema](/protocol/v1/draft/schema#sessionupdate), which describes each payload in detail. Draft-only variants are unstable and may change or be removed. The walkthrough below illustrates common uses of these updates.
3737

0 commit comments

Comments
 (0)